blob: 942d5f6f13eaa371bb69bb451457f0abaf5e3eb7 [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.lib.editor.codetemplates;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.settings.SimpleValueNames;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.lsp.Completion;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.lib.editor.codetemplates.api.CodeTemplate;
import org.netbeans.lib.editor.codetemplates.spi.CodeTemplateFilter;
import org.netbeans.lib.editor.codetemplates.spi.CodeTemplateParameter;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.netbeans.spi.editor.completion.CompletionProvider;
import org.netbeans.spi.editor.completion.CompletionResultSet;
import org.netbeans.spi.editor.completion.CompletionTask;
import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery;
import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
import org.netbeans.spi.lsp.CompletionCollector;
/**
* Implemenation of the code template description.
*
* @author Miloslav Metelka
*/
@MimeRegistration(mimeType = "text/x-java", service = CompletionProvider.class, position = 300) //NOI18N
public final class CodeTemplateCompletionProvider implements CompletionProvider {
public CompletionTask createTask(int type, JTextComponent component) {
return (type & COMPLETION_QUERY_TYPE) == 0 || isAbbrevDisabled(component) ? null : new AsyncCompletionTask(new Query(), component);
}
public int getAutoQueryTypes(JTextComponent component, String typedText) {
return 0;
}
private static boolean isAbbrevDisabled(JTextComponent component) {
return org.netbeans.editor.Abbrev.isAbbrevDisabled(component);
}
@MimeRegistration(mimeType = "text/x-java", service = CompletionCollector.class)
public static class Collector implements CompletionCollector {
private static final String SELECTED_TEXT_VAR = "${0:$TM_SELECTED_TEXT}";
private static final Pattern SNIPPET_VAR_PATTERN = Pattern.compile("\\$\\{\\s*([-\\w]++)((?:\\s*[-\\w]++(?:\\s*=\\s*(?:\\\"[^\\\"]*\\\"|[-\\w]++))?)*\\s*)?}");
private static final Pattern SNIPPET_HINT_PATTERN = Pattern.compile("([-\\w]++)(?:\\s*=\\s*(\\\"([^\\\"]*)\\\"|[-\\w]++))?");
private static final String UNCAUGHT_EXCEPTION_CATCH_STATEMENTS = "uncaughtExceptionCatchStatements";
@Override
public boolean collectCompletions(Document doc, int offset, Completion.Context context, Consumer<Completion> consumer) {
new Query().query(doc, offset, (ct, abbrevBased) -> {
String description = ct.getDescription();
if (description == null) {
description = CodeTemplateApiPackageAccessor.get().getSingleLineText(ct);
}
String label = html2text(description.trim());
String sortText = String.format("%04d%s", 1650, "fore".equals(ct.getAbbreviation()) ? label.substring(0, 3) : label);
consumer.accept(CompletionCollector.newBuilder(label)
.sortText(sortText)
.documentation(() -> {
StringBuffer sb = new StringBuffer("<html><pre>");
ParametrizedTextParser.parseToHtml(sb, ct.getParametrizedText());
sb.append("</pre>");
return sb.toString();
}).insertText(convert(ct.getParametrizedText()))
.insertTextFormat(Completion.TextFormat.Snippet)
.build());
});
return true;
}
private static String convert(String s) {
StringBuilder sb = new StringBuilder();
Matcher matcher = SNIPPET_VAR_PATTERN.matcher(s);
int idx = 0;
Map<String, Integer> placeholders = new HashMap<>();
Map<String, String> values = new HashMap<>();
Set<String> nonEditables = new HashSet<>();
AtomicInteger last = new AtomicInteger();
while (matcher.find(idx)) {
int start = matcher.start();
sb.append(s.substring(idx, start));
String name = matcher.group(1);
switch (name) {
case CodeTemplateParameter.CURSOR_PARAMETER_NAME:
sb.append("$0");
break;
case CodeTemplateParameter.NO_FORMAT_PARAMETER_NAME:
case CodeTemplateParameter.NO_INDENT_PARAMETER_NAME:
break;
case CodeTemplateParameter.SELECTION_PARAMETER_NAME:
sb.append(SELECTED_TEXT_VAR);
break;
default:
String params = matcher.groupCount() > 1 ? matcher.group(2) : null;
if (params == null || params.isEmpty()) {
if (nonEditables.contains(name)) {
sb.append(values.getOrDefault(name, ""));
} else {
sb.append('$').append(placeholders.computeIfAbsent(name, n -> last.incrementAndGet()));
}
} else {
Map<String, String> hints = getHints(params);
String defaultValue = hints.get(CodeTemplateParameter.DEFAULT_VALUE_HINT_NAME);
if (defaultValue != null) {
values.put(name, defaultValue);
}
if (hints.containsKey(UNCAUGHT_EXCEPTION_CATCH_STATEMENTS)) {
sb.append("catch (${").append(last.incrementAndGet()).append(":Exception} ${").append(last.incrementAndGet()).append(":e}) {\n}");
} else if ("false".equalsIgnoreCase(hints.get(CodeTemplateParameter.EDITABLE_HINT_NAME))) {
nonEditables.add(name);
sb.append(values.getOrDefault(name, ""));
} else {
if (defaultValue != null) {
sb.append("${").append(placeholders.computeIfAbsent(name, n -> last.incrementAndGet())).append(':').append(defaultValue).append('}');
} else {
sb.append('$').append(placeholders.computeIfAbsent(name, n -> last.incrementAndGet()));
}
}
}
}
idx = matcher.end();
}
String tail = s.substring(idx);
return sb.append(tail.endsWith("\n") ? tail.substring(0, tail.length() - 1) : tail).toString();
}
private static Map<String, String> getHints(String text) {
Map<String, String> hint2Values = new HashMap<>();
Matcher matcher = SNIPPET_HINT_PATTERN.matcher(text);
int idx = 0;
while (matcher.find(idx)) {
String insideString = matcher.groupCount() > 2 ? matcher.group(3) : null;
String value = matcher.groupCount() > 1 ? matcher.group(2) : null;
hint2Values.put(matcher.group(1), insideString!= null ? insideString : value);
idx = matcher.end();
}
return hint2Values;
}
private static String html2text(String html) {
try {
StringBuilder sb = new StringBuilder();
new ParserDelegator().parse(new StringReader(html), new HTMLEditorKit.ParserCallback() {
@Override
public void handleText(char[] text, int pos) {
sb.append(text);
}
}, Boolean.TRUE);
return sb.toString();
} catch (IOException ex) {
return html;
}
}
}
private static final class Query extends AsyncCompletionQuery
implements ChangeListener {
private JTextComponent component;
private int queryCaretOffset;
private int queryAnchorOffset;
private List<CodeTemplateCompletionItem> queryResult;
private String filterPrefix;
protected @Override void prepareQuery(JTextComponent component) {
this.component = component;
}
protected @Override boolean canFilter(JTextComponent component) {
if (component.getCaret() == null) {
return false;
}
int caretOffset = component.getSelectionStart();
Document doc = component.getDocument();
filterPrefix = null;
if (caretOffset >= queryCaretOffset) {
if (queryAnchorOffset < queryCaretOffset) {
try {
filterPrefix = doc.getText(queryAnchorOffset, caretOffset - queryAnchorOffset);
if (!isJavaIdentifierPart(filterPrefix)) {
filterPrefix = null;
}
} catch (BadLocationException e) {
// filterPrefix stays null -> no filtering
}
}
}
return (filterPrefix != null);
}
protected @Override void filter(CompletionResultSet resultSet) {
if (filterPrefix != null && queryResult != null) {
resultSet.addAllItems(getFilteredData(queryResult, filterPrefix));
}
resultSet.finish();
}
private boolean isJavaIdentifierPart(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
if (!(Character.isJavaIdentifierPart(text.charAt(i))) ) {
return false;
}
}
return true;
}
private Collection<? extends CompletionItem> getFilteredData(
Collection<? extends CompletionItem> data,
String prefix
) {
List<CompletionItem> ret = new ArrayList<CompletionItem>();
for (CompletionItem itm : data) {
if (itm.getInsertPrefix().toString().startsWith(prefix)) {
ret.add(itm);
}
}
return ret;
}
protected void query(CompletionResultSet resultSet, Document doc, int caretOffset) {
query(doc, caretOffset, (ct, abbrevBased) -> {
if (queryResult != null) {
queryResult.add(new CodeTemplateCompletionItem(ct, abbrevBased));
}
});
if (queryResult != null) {
resultSet.addAllItems(queryResult);
}
resultSet.setAnchorOffset(queryAnchorOffset);
resultSet.finish();
}
private void query(Document doc, int caretOffset, BiConsumer<CodeTemplate, Boolean> consumer) {
String langPath = null;
String identifierBeforeCursor = null;
if (doc instanceof AbstractDocument) {
AbstractDocument adoc = (AbstractDocument)doc;
adoc.readLock();
try {
try {
if (adoc instanceof BaseDocument) {
identifierBeforeCursor = Utilities.getIdentifierBefore((BaseDocument)adoc, caretOffset);
}
} catch (BadLocationException e) {
// leave identifierBeforeCursor null
}
List<TokenSequence<?>> list = TokenHierarchy.get(doc).embeddedTokenSequences(caretOffset, true);
if (list.size() > 1) {
langPath = list.get(list.size() - 1).languagePath().mimePath();
}
} finally {
adoc.readUnlock();
}
}
if (identifierBeforeCursor == null) {
identifierBeforeCursor = ""; //NOI18N
}
if (langPath == null) {
langPath = NbEditorUtilities.getMimeType(doc);
}
queryCaretOffset = caretOffset;
queryAnchorOffset = caretOffset - identifierBeforeCursor.length();
if (langPath != null) {
String mimeType = DocumentUtilities.getMimeType(doc);
MimePath mimePath = mimeType == null ? MimePath.EMPTY : MimePath.get(mimeType);
Preferences prefs = MimeLookup.getLookup(mimePath).lookup(Preferences.class);
boolean ignoreCase = prefs.getBoolean(SimpleValueNames.COMPLETION_CASE_SENSITIVE, false);
CodeTemplateManagerOperation op = CodeTemplateManagerOperation.get(MimePath.parse(langPath));
op.waitLoaded();
Collection<? extends CodeTemplate> ctsPT = op.findByParametrizedText(identifierBeforeCursor, ignoreCase);
Collection<? extends CodeTemplate> ctsAb = op.findByAbbreviationPrefix(identifierBeforeCursor, ignoreCase);
Collection<? extends CodeTemplateFilter> filters = CodeTemplateManagerOperation.getTemplateFilters(doc, queryAnchorOffset, queryAnchorOffset);
queryResult = new ArrayList<CodeTemplateCompletionItem>(ctsPT.size() + ctsAb.size());
Set<String> abbrevs = new HashSet<String>(ctsPT.size() + ctsAb.size());
for (CodeTemplate ct : ctsPT) {
if (ct.getContexts() != null && ct.getContexts().size() > 0 && accept(ct, filters) && abbrevs.add(ct.getAbbreviation())) {
consumer.accept(ct, false);
}
}
for (CodeTemplate ct : ctsAb) {
if (ct.getContexts() != null && ct.getContexts().size() > 0 && accept(ct, filters) && abbrevs.add(ct.getAbbreviation())) {
consumer.accept(ct, true);
}
}
}
}
public void stateChanged(ChangeEvent evt) {
synchronized (this) {
notify();
}
}
private static boolean accept(CodeTemplate template, Collection/*<CodeTemplateFilter>*/ filters) {
for(Iterator<CodeTemplateFilter> it = filters.iterator(); it.hasNext();) {
CodeTemplateFilter filter = it.next();
if (!filter.accept(template))
return false;
}
return true;
}
}
}