blob: f44533b1b5fc4a348d54a87c0005e7e09e40cdda [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 java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.CodeCompletionContext;
import org.netbeans.modules.csl.api.CodeCompletionHandler;
import org.netbeans.modules.csl.api.CodeCompletionResult;
import org.netbeans.modules.csl.api.CompletionProposal;
import org.netbeans.modules.csl.api.ElementHandle;
import org.netbeans.modules.csl.api.ElementKind;
import org.netbeans.modules.csl.api.HtmlFormatter;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.ParameterInfo;
import org.netbeans.modules.csl.spi.DefaultCompletionResult;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.javascript2.model.api.JsObject;
import org.netbeans.modules.javascript2.model.api.Model;
import org.netbeans.modules.javascript2.types.spi.ParserResult;
/**
*
* @author Dusan Balek
*/
public class JsonCodeCompletion implements CodeCompletionHandler {
private static final Logger LOGGER = Logger.getLogger(JsonCodeCompletion.class.getName());
private boolean caseSensitive;
@Override
public CodeCompletionResult complete(CodeCompletionContext context) {
final CancelSupport cancelSupport = CancelSupport.getDefault();
if (cancelSupport.isCancelled()) {
return CodeCompletionResult.NONE;
}
long start = System.currentTimeMillis();
BaseDocument doc = (BaseDocument) context.getParserResult().getSnapshot().getSource().getDocument(false);
if (doc == null) {
return CodeCompletionResult.NONE;
}
ParserResult info = (ParserResult) context.getParserResult();
TokenHierarchy<?> th = info.getSnapshot().getTokenHierarchy();
if (th == null) {
return CodeCompletionResult.NONE;
}
TokenSequence<JsTokenId> ts = th.tokenSequence(JsTokenId.jsonLanguage());
if (ts == null) {
return CodeCompletionResult.NONE;
}
String pref = context.getPrefix();
int caretOffset = context.getParserResult().getSnapshot().getEmbeddedOffset(context.getCaretOffset());
final int anchor = caretOffset - pref.length();
ts.move(anchor);
if (!ts.moveNext() && !ts.movePrevious()){
return CodeCompletionResult.NONE;
}
final Model model = Model.getModel(info, false);
if (model == null) {
return CodeCompletionResult.NONE;
}
this.caseSensitive = context.isCaseSensitive();
final List<CompletionProposal> resultList = new ArrayList<>();
final Token<? extends JsTokenId> token = ts.token();
final JsTokenId tokenId = token.id();
ts.movePrevious();
final Token<? extends JsTokenId> prevToken = LexUtilities.findPreviousNonWsNonComment(ts);
final JsTokenId prevTokenId = prevToken.id();
if (prevTokenId == JsTokenId.BRACKET_LEFT_CURLY || prevTokenId == JsTokenId.OPERATOR_COMMA) {
if (tokenId == JsTokenId.STRING || tokenId == JsTokenId.WHITESPACE) {
Set<String> keys = new HashSet<>();
findKeys(model.getGlobalObject(), caretOffset, keys);
for (final String key : keys) {
if (startsWith(key, pref)) {
resultList.add(new CompletionProposal() {
@Override
public int getAnchorOffset() {
return anchor;
}
@Override
public ElementHandle getElement() {
return null;
}
@Override
public String getName() {
return key;
}
@Override
public String getInsertPrefix() {
return tokenId == JsTokenId.STRING ? key : '\"' + key + '\"';
}
@Override
public String getSortText() {
return key;
}
@Override
public String getLhsHtml(HtmlFormatter formatter) {
formatter.appendText(key);
return formatter.getText();
}
@Override
public String getRhsHtml(HtmlFormatter formatter) {
return null;
}
@Override
public ElementKind getKind() {
return ElementKind.PROPERTY;
}
@Override
public ImageIcon getIcon() {
return null;
}
@Override
public Set<Modifier> getModifiers() {
return Collections.emptySet();
}
@Override
public boolean isSmart() {
return false;
}
@Override
public int getSortPrioOverride() {
return 1;
}
@Override
public String getCustomInsertTemplate() {
return null;
}
});
}
}
}
}
long end = System.currentTimeMillis();
LOGGER.log(Level.FINE, "Counting JSON CC took {0}ms ", (end - start));
if (!resultList.isEmpty()) {
return new DefaultCompletionResult(resultList, false);
}
return CodeCompletionResult.NONE;
}
@Override
public String document(
org.netbeans.modules.csl.spi.ParserResult info,
ElementHandle element) {
return null;
}
@Override
public ElementHandle resolveLink(String link, ElementHandle originalHandle) {
return null;
}
@Override
public String getPrefix(
org.netbeans.modules.csl.spi.ParserResult info,
int caretOffset,
boolean upToOffset) {
String prefix = "";
BaseDocument doc = (BaseDocument) info.getSnapshot().getSource().getDocument(false);
if (doc == null) {
return null;
}
TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence(info.getSnapshot(), caretOffset, JsTokenId.jsonLanguage());
if (ts == null) {
return null;
}
int offset = info.getSnapshot().getEmbeddedOffset(caretOffset);
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
if (ts.offset() == offset) {
// We're looking at the offset to the RIGHT of the caret
// and here I care about what's on the left
ts.movePrevious();
}
Token<? extends JsTokenId> token = ts.token();
if (token != null && token.id() != JsTokenId.EOL) {
JsTokenId id = token.id();
if (id == JsTokenId.STRING && offset < ts.offset() + token.length()) {
prefix = token.text().toString();
prefix = prefix.substring(1, prefix.length() - 1);
if (upToOffset) {
int prefixIndex = getPrefixIndexFromSequence(prefix.substring(0, offset - ts.offset() - 1));
prefix = prefix.substring(prefixIndex, offset - ts.offset() - 1);
}
}
if (id == JsTokenId.KEYWORD_FALSE || id == JsTokenId.KEYWORD_TRUE || id == JsTokenId.KEYWORD_NULL) {
prefix = token.text().toString();
if (upToOffset) {
if (offset - ts.offset() >= 0) {
prefix = prefix.substring(0, offset - ts.offset());
}
}
}
if (id.isError()) {
prefix = token.text().toString();
//if (upToOffset) {
prefix = prefix.substring(0, offset - ts.offset());
//}
}
}
LOGGER.log(Level.FINE, String.format("Prefix for cc: %s", prefix));
return prefix;
}
@Override
public QueryType getAutoQuery(JTextComponent component, String typedText) {
return QueryType.NONE;
}
@Override
public String resolveTemplateVariable(
String variable,
org.netbeans.modules.csl.spi.ParserResult info,
int caretOffset,
String name,
Map parameters) {
return null;
}
@Override
public Set<String> getApplicableTemplates(Document doc, int selectionBegin, int selectionEnd) {
return null;
}
@Override
public ParameterInfo parameters(
org.netbeans.modules.csl.spi.ParserResult info,
int caretOffset,
CompletionProposal proposal) {
return ParameterInfo.NONE;
}
/** XXX - Once the JS framework support becomes plugable, should be moved to jQueryCompletionHandler getPrefix() */
private int getPrefixIndexFromSequence(String prefix) {
int spaceIndex = prefix.lastIndexOf(" ") + 1; //NOI18N
int dotIndex = prefix.lastIndexOf(".") + 1; //NOI18N
int hashIndex = prefix.lastIndexOf("#") + 1; //NOI18N
int bracketIndex = prefix.lastIndexOf("[") + 1; //NOI18N
int columnIndex = prefix.lastIndexOf(":") + 1; //NOI18N
int parenIndex = prefix.lastIndexOf("(") + 1; //NOI18N
// for file code completion
int slashIndex = prefix.lastIndexOf('/') + 1; //NOI18N
return (Math.max(0, Math.max(hashIndex, Math.max(dotIndex, Math.max(parenIndex,Math.max(columnIndex, Math.max(bracketIndex, Math.max(spaceIndex, slashIndex))))))));
}
private boolean startsWith(String theString, String prefix) {
if (prefix == null || prefix.length() == 0) {
return true;
}
return caseSensitive ? theString.startsWith(prefix)
: theString.toLowerCase().startsWith(prefix.toLowerCase());
}
private boolean findKeys(JsObject object, int offset, Set<String> keys) {
boolean containsOffset = false;
for (Map.Entry<String, ? extends JsObject> entry : object.getProperties().entrySet()) {
containsOffset = findKeys(entry.getValue(), offset, keys);
if (entry.getValue().containsOffset(offset) && !containsOffset) {
containsOffset = true;
continue;
}
if (!entry.getKey().isEmpty() && !entry.getValue().isAnonymous()) {
keys.add(entry.getKey());
}
}
return containsOffset;
}
}