| /* |
| * 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; |
| } |
| } |