blob: d5165c1bfa2d79d222d5406e52d3598923eb81a9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.javascript2.editor;
import java.util.ArrayList;
import org.netbeans.modules.javascript2.editor.spi.CompletionContext;
import java.util.Arrays;
import java.util.List;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
/**
*
* @author Petr Pisl
*/
public class CompletionContextFinder {
private static final List<JsTokenId> WHITESPACES_TOKENS = Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.EOL);
private static final List<JsTokenId> CHANGE_CONTEXT_TOKENS = Arrays.asList(
JsTokenId.OPERATOR_SEMICOLON, JsTokenId.BRACKET_LEFT_CURLY, JsTokenId.BRACKET_RIGHT_CURLY);
private static final List<Object[]> OBJECT_PROPERTY_TOKENCHAINS = Arrays.asList(
new Object[]{JsTokenId.OPERATOR_DOT},
new Object[]{JsTokenId.OPERATOR_DOT, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.OPERATOR_DOT, JsTokenId.PRIVATE_IDENTIFIER},
new Object[]{JsTokenId.OPERATOR_OPTIONAL_ACCESS},
new Object[]{JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.PRIVATE_IDENTIFIER}
);
private static final List<Object[]> OBJECT_THIS_TOKENCHAINS = Arrays.asList(
new Object[]{JsTokenId.KEYWORD_THIS, JsTokenId.OPERATOR_DOT},
new Object[]{JsTokenId.KEYWORD_THIS, JsTokenId.OPERATOR_DOT, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.KEYWORD_THIS, JsTokenId.OPERATOR_DOT, JsTokenId.PRIVATE_IDENTIFIER},
new Object[]{JsTokenId.KEYWORD_THIS, JsTokenId.OPERATOR_OPTIONAL_ACCESS},
new Object[]{JsTokenId.KEYWORD_THIS, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.KEYWORD_THIS, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.PRIVATE_IDENTIFIER}
);
private static final List<Object[]> NUMBER_TOKENCHAINS = Arrays.asList(
new Object[]{JsTokenId.NUMBER, JsTokenId.OPERATOR_DOT},
new Object[]{JsTokenId.NUMBER, JsTokenId.OPERATOR_DOT, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.NUMBER, JsTokenId.OPERATOR_DOT, JsTokenId.PRIVATE_IDENTIFIER},
new Object[]{JsTokenId.NUMBER, JsTokenId.OPERATOR_OPTIONAL_ACCESS},
new Object[]{JsTokenId.NUMBER, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.NUMBER, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.PRIVATE_IDENTIFIER}
);
private static final List<Object[]> STRING_TOKENCHAINS = Arrays.asList(
new Object[]{JsTokenId.STRING_END, JsTokenId.OPERATOR_DOT},
new Object[]{JsTokenId.STRING_END, JsTokenId.OPERATOR_DOT, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.STRING_END, JsTokenId.OPERATOR_DOT, JsTokenId.PRIVATE_IDENTIFIER},
new Object[]{JsTokenId.STRING_END, JsTokenId.OPERATOR_OPTIONAL_ACCESS},
new Object[]{JsTokenId.STRING_END, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.STRING_END, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.PRIVATE_IDENTIFIER}
);
private static final List<Object[]> REGEXP_TOKENCHAINS = Arrays.asList(
new Object[]{JsTokenId.REGEXP_END, JsTokenId.OPERATOR_DOT},
new Object[]{JsTokenId.REGEXP_END, JsTokenId.OPERATOR_DOT, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.REGEXP_END, JsTokenId.OPERATOR_DOT, JsTokenId.PRIVATE_IDENTIFIER},
new Object[]{JsTokenId.REGEXP_END, JsTokenId.OPERATOR_OPTIONAL_ACCESS},
new Object[]{JsTokenId.REGEXP_END, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.IDENTIFIER},
new Object[]{JsTokenId.REGEXP_END, JsTokenId.OPERATOR_OPTIONAL_ACCESS, JsTokenId.PRIVATE_IDENTIFIER}
);
@NonNull
static CompletionContext findCompletionContext(ParserResult info, int offset){
TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy();
if (th == null) {
return CompletionContext.NONE;
}
TokenSequence<JsTokenId> ts = th.tokenSequence(JsTokenId.javascriptLanguage());
if (ts == null) {
return CompletionContext.NONE;
}
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()){
return CompletionContext.NONE;
}
Token<? extends JsTokenId> token = ts.token();
JsTokenId tokenId =token.id();
if (tokenId == JsTokenId.DOC_COMMENT) {
return CompletionContext.DOCUMENTATION;
}
if ((tokenId == JsTokenId.OPERATOR_DOT || tokenId == JsTokenId.OPERATOR_OPTIONAL_ACCESS) && ts.moveNext()) {
ts.movePrevious();
ts.movePrevious();
token = ts.token();
tokenId =token.id();
}
if (tokenId == JsTokenId.STRING || tokenId == JsTokenId.STRING_END) {
token = LexUtilities.findPrevious(ts, Arrays.asList(JsTokenId.STRING, JsTokenId.STRING_BEGIN, JsTokenId.STRING_END));
if (token == null) {
return CompletionContext.IN_STRING;
}
token = LexUtilities.findPreviousNonWsNonComment(ts);
if (token.id() == JsTokenId.BRACKET_LEFT_PAREN && ts.movePrevious()) {
token = LexUtilities.findPreviousNonWsNonComment(ts);
tokenId = token.id();
if (tokenId == JsTokenId.IDENTIFIER) {
if ("getElementById".equals(token.text().toString())) {
return CompletionContext.STRING_ELEMENTS_BY_ID;
} else if ("getElementsByClassName".equals(token.text().toString())) {
return CompletionContext.STRING_ELEMENTS_BY_CLASS_NAME;
}
}
}
token = LexUtilities.findPreviousToken(ts, Utils.LOOK_FOR_IMPORT_EXPORT_TOKENS);
if (token.id() == JsTokenId.KEYWORD_EXPORT || token.id() == JsTokenId.KEYWORD_IMPORT) {
return CompletionContext.IMPORT_EXPORT_MODULE;
}
return CompletionContext.IN_STRING;
}
if (acceptTokenChains(ts, OBJECT_THIS_TOKENCHAINS, true)) {
return CompletionContext.OBJECT_MEMBERS;
}
if (acceptTokenChains(ts, NUMBER_TOKENCHAINS, tokenId != JsTokenId.OPERATOR_DOT && tokenId != JsTokenId.OPERATOR_OPTIONAL_ACCESS)) {
return CompletionContext.NUMBER;
}
if (acceptTokenChains(ts, STRING_TOKENCHAINS, tokenId != JsTokenId.OPERATOR_DOT && tokenId != JsTokenId.OPERATOR_OPTIONAL_ACCESS)) {
return CompletionContext.STRING;
}
if (acceptTokenChains(ts, REGEXP_TOKENCHAINS, tokenId != JsTokenId.OPERATOR_DOT && tokenId != JsTokenId.OPERATOR_OPTIONAL_ACCESS)) {
return CompletionContext.REGEXP;
}
if (acceptTokenChains(ts, OBJECT_PROPERTY_TOKENCHAINS, tokenId != JsTokenId.OPERATOR_DOT && tokenId != JsTokenId.OPERATOR_OPTIONAL_ACCESS)) {
return CompletionContext.OBJECT_PROPERTY;
}
ts.move(offset);
if (ts.moveNext()) {
if (isCallArgumentContext(ts)) {
return CompletionContext.CALL_ARGUMENT;
} else {
ts.move(offset);
ts.moveNext();
}
if (isPropertyNameContext(ts)) {
return CompletionContext.OBJECT_PROPERTY_NAME;
}
}
ts.move(offset);
if (!ts.moveNext()) {
if (!ts.movePrevious()) {
return CompletionContext.GLOBAL;
}
}
token = ts.token(); tokenId = token.id();
if (tokenId == JsTokenId.EOL && ts.movePrevious()) {
token = ts.token(); tokenId = token.id();
}
if (tokenId == JsTokenId.IDENTIFIER || tokenId == JsTokenId.PRIVATE_IDENTIFIER || WHITESPACES_TOKENS.contains(tokenId)) {
if (!ts.movePrevious()) {
return CompletionContext.GLOBAL;
}
token = LexUtilities.findPrevious(ts, WHITESPACES_TOKENS);
}
if (CHANGE_CONTEXT_TOKENS.contains(token.id())
|| (WHITESPACES_TOKENS.contains(token.id()) && !ts.movePrevious())) {
return CompletionContext.GLOBAL;
}
token = LexUtilities.findPreviousToken(ts, Utils.LOOK_FOR_IMPORT_EXPORT_TOKENS);
if (token.id() == JsTokenId.KEYWORD_EXPORT || token.id() == JsTokenId.KEYWORD_IMPORT) {
return CompletionContext.IMPORT_EXPORT_SPECIAL_TOKENS;
}
if (tokenId == JsTokenId.DOC_COMMENT) {
return CompletionContext.DOCUMENTATION;
}
return CompletionContext.EXPRESSION;
}
protected static boolean isCallArgumentContext(TokenSequence<JsTokenId> ts) {
if (ts.movePrevious()) {
Token<? extends JsTokenId> token = LexUtilities.findPreviousNonWsNonComment(ts);
if (token != null
&& (token.id() == JsTokenId.BRACKET_LEFT_PAREN || token.id() == JsTokenId.OPERATOR_COMMA)) {
int balanceParen = token.id() == JsTokenId.BRACKET_LEFT_PAREN ? 0 : 1;
int balanceCurly = 0;
int balanceBracket = 0;
while (balanceParen != 0 && ts.movePrevious()) {
token = ts.token();
if (token.id() == JsTokenId.BRACKET_LEFT_PAREN) {
balanceParen--;
} else if (token.id() == JsTokenId.BRACKET_RIGHT_PAREN) {
balanceParen++;
} else if (token.id() == JsTokenId.BRACKET_LEFT_CURLY) {
balanceCurly--;
} else if (token.id() == JsTokenId.BRACKET_RIGHT_CURLY) {
balanceCurly++;
} else if (token.id() == JsTokenId.BRACKET_LEFT_BRACKET) {
balanceBracket--;
} else if (token.id() == JsTokenId.BRACKET_RIGHT_BRACKET) {
balanceBracket++;
}
}
if (balanceParen == 0 && balanceCurly == 0 && balanceBracket == 0) {
if (ts.movePrevious() && token.id() == JsTokenId.BRACKET_LEFT_PAREN) {
token = LexUtilities.findPreviousNonWsNonComment(ts);
if (token.id() == JsTokenId.IDENTIFIER || token.id() == JsTokenId.PRIVATE_IDENTIFIER) {
return true;
}
}
}
}
}
return false;
}
protected static boolean isPropertyNameContext(TokenSequence<JsTokenId> ts) {
//find the begining of the object literal
JsTokenId tokenId = ts.token().id();
if (tokenId == JsTokenId.OPERATOR_COMMA) {
ts.movePrevious();
}
List<JsTokenId> listIds = Arrays.asList(JsTokenId.OPERATOR_COMMA, JsTokenId.OPERATOR_COLON, JsTokenId.BRACKET_LEFT_CURLY, JsTokenId.OPERATOR_SEMICOLON);
// find previous , or : or { or ;
Token<? extends JsTokenId> token = LexUtilities.findPreviousToken(ts, listIds);
tokenId = token.id();
boolean commaFirst = false;
if (tokenId == JsTokenId.OPERATOR_COMMA && ts.movePrevious()) {
List<JsTokenId> checkParentList = new ArrayList(listIds);
List<JsTokenId> parentList = Arrays.asList(JsTokenId.BRACKET_LEFT_PAREN, JsTokenId.BRACKET_RIGHT_PAREN, JsTokenId.BRACKET_RIGHT_CURLY);
checkParentList.addAll(parentList);
token = LexUtilities.findPreviousToken(ts, checkParentList);
tokenId = token.id();
commaFirst = true;
if (tokenId == JsTokenId.BRACKET_RIGHT_PAREN || tokenId == JsTokenId.BRACKET_RIGHT_CURLY) {
balanceBracketBack(ts);
token = ts.token();
tokenId = token.id();
} else if (tokenId == JsTokenId.BRACKET_LEFT_PAREN) {
return false;
} else {
if (tokenId == JsTokenId.OPERATOR_COLON) {
// we are in the previous property definition
return true;
}
}
}
if (tokenId == JsTokenId.BRACKET_LEFT_CURLY && ts.movePrevious()) {
List<JsTokenId> emptyIds = Arrays.asList(JsTokenId.WHITESPACE, JsTokenId.EOL, JsTokenId.BLOCK_COMMENT);
// check whether it's the first property in the object literal definion
token = LexUtilities.findPrevious(ts, emptyIds);
tokenId = token.id();
if (tokenId == JsTokenId.BRACKET_LEFT_PAREN || tokenId == JsTokenId.OPERATOR_COMMA || tokenId == JsTokenId.OPERATOR_EQUALS || tokenId == JsTokenId.OPERATOR_COLON) {
return true;
} else if (tokenId == JsTokenId.BRACKET_RIGHT_PAREN) {
// it can be a method definition
balanceBracketBack(ts);
token = ts.token();
tokenId = token.id();
if (tokenId == JsTokenId.BRACKET_LEFT_PAREN && ts.movePrevious()) {
token = LexUtilities.findPrevious(ts, emptyIds);
tokenId = token.id();
if (tokenId == JsTokenId.KEYWORD_FUNCTION && ts.movePrevious()) {
// we found a method definition, now we need to check, whether its in an object literal
token = LexUtilities.findPrevious(ts, emptyIds);
tokenId = token.id();
if (tokenId == JsTokenId.OPERATOR_COLON) {
return commaFirst;
}
}
}
}
}
return false;
}
private static boolean acceptTokenChains(TokenSequence tokenSequence, List<Object[]> tokenIdChains, boolean movePrevious) {
for (Object[] tokenIDChain : tokenIdChains){
if (acceptTokenChain(tokenSequence, tokenIDChain, movePrevious)){
return true;
}
}
return false;
}
private static boolean acceptTokenChain(TokenSequence tokenSequence, Object[] tokenIdChain, boolean movePrevious) {
int orgTokenSequencePos = tokenSequence.offset();
boolean accept = true;
boolean moreTokens = movePrevious ? tokenSequence.movePrevious() : true;
for (int i = tokenIdChain.length - 1; i >= 0; i --){
Object tokenID = tokenIdChain[i];
if (!moreTokens){
accept = false;
break;
}
if (tokenID instanceof JsTokenId) {
if (tokenSequence.token().id() == tokenID){
moreTokens = tokenSequence.movePrevious();
} else {
// NO MATCH
accept = false;
break;
}
} else {
assert false : "Unsupported token type: " + tokenID.getClass().getName();
}
}
tokenSequence.move(orgTokenSequencePos);
tokenSequence.moveNext();
return accept;
}
private static void balanceBracketBack(TokenSequence<JsTokenId> ts) {
JsTokenId tokenId = ts.token().id();
JsTokenId tokenIdOriginal = ts.token().id();
List<JsTokenId> lookingFor;
if (tokenId == JsTokenId.BRACKET_RIGHT_CURLY) {
lookingFor = Arrays.asList(JsTokenId.BRACKET_LEFT_CURLY);
} else if (tokenId == JsTokenId.BRACKET_RIGHT_PAREN) {
lookingFor = Arrays.asList(JsTokenId.BRACKET_LEFT_PAREN);
} else {
return;
}
int balance = -1;
while (balance != 0 && ts.movePrevious()) {
Token<? extends JsTokenId> token = LexUtilities.findPreviousToken(ts, lookingFor);
tokenId = token.id();
if (lookingFor.contains(tokenIdOriginal)) {
balance --;
} else if (lookingFor.contains(tokenId)) {
balance++;
}
}
}
}