/*
* Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC
* All rights reserved.
*
* The source code of this document is proprietary work, and is not licensed for
* distribution. For information about licensing, contact Sam Harwell at:
* sam@tunnelvisionlabs.com
*/
package org.antlr.works.editor.antlr4.completion;
import com.tvl.api.editor.completion.Completion;
import com.tvl.spi.editor.completion.CompletionDocumentation;
import com.tvl.spi.editor.completion.CompletionItem;
import com.tvl.spi.editor.completion.CompletionProvider;
import com.tvl.spi.editor.completion.CompletionResultSet;
import com.tvl.spi.editor.completion.support.AsyncCompletionQuery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.antlr.netbeans.editor.text.DocumentSnapshot;
import org.antlr.netbeans.editor.text.OffsetRegion;
import org.antlr.netbeans.editor.text.SnapshotPositionRegion;
import org.antlr.netbeans.editor.text.TrackingPositionRegion;
import org.antlr.netbeans.editor.text.VersionedDocument;
import org.antlr.netbeans.editor.text.VersionedDocumentUtilities;
import org.antlr.netbeans.parsing.spi.ParserTaskManager;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.editor.BaseDocument;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
/**
*
* @author Sam Harwell
*/
public abstract class AbstractCompletionQuery extends AsyncCompletionQuery {
// -J-Dorg.antlr.works.editor.antlr4.completion.AbstractCompletionQuery.level=FINE
private static final Logger LOGGER = Logger.getLogger(AbstractCompletionQuery.class.getName());
private static final int NO_ADDITIONAL_ITEMS = 0;
private static final int ADDITIONAL_IMPORTED_ITEMS = 1;
private static final int ADDITIONAL_MEMBER_ITEMS = 2;
/** ^[\\$A-Za-z_][A-Za-z0-9_]*$ */
protected static final Pattern WORD_PATTERN = Pattern.compile("^[\\$A-Za-z_][A-Za-z0-9_]*$");
private static final String EMPTY = "";
private final AbstractCompletionProvider completionProvider;
private final int queryType;
private final boolean hasTask;
private final boolean extend;
private int caretOffset;
private JTextComponent component;
protected List<CompletionItem> results;
protected boolean possibleDeclaration;
private CompletionDocumentation documentation;
private CompletionToolTip toolTip;
private int toolTipOffset;
private String filterPrefix;
private byte hasAdditionalItems;
protected TrackingPositionRegion applicableTo;
protected AbstractCompletionQuery(AbstractCompletionProvider completionProvider, int queryType, int caretOffset, boolean hasTask, boolean extend) {
this.completionProvider = completionProvider;
this.queryType = queryType;
this.caretOffset = caretOffset;
this.hasTask = hasTask;
this.extend = extend;
}
public AbstractCompletionProvider getCompletionProvider() {
return completionProvider;
}
public final int getQueryType() {
return queryType;
}
public final boolean isExtend() {
return extend;
}
public final int getCaretOffset() {
return caretOffset;
}
public final JTextComponent getComponent() {
return component;
}
public final boolean isExplicitQuery() {
return (queryType & AbstractCompletionProvider.AUTO_QUERY_TYPE) == 0;
}
public final CompletionDocumentation getDocumentation() {
return documentation;
}
public final CompletionToolTip getToolTip() {
return toolTip;
}
public final int getToolTipOffset() {
return toolTipOffset;
}
public final TrackingPositionRegion getApplicableTo() {
return applicableTo;
}
@Override
protected void preQueryUpdate(JTextComponent component) {
if (applicableTo != null) {
int newCaretOffset = component.getSelectionStart();
Document document = component.getDocument();
VersionedDocument textBuffer = VersionedDocumentUtilities.getVersionedDocument(document);
DocumentSnapshot textSnapshot = textBuffer.getCurrentSnapshot();
SnapshotPositionRegion span = applicableTo.getRegion(textSnapshot);
if (span.contains(newCaretOffset)) {
String text = span.getText();
if (text.isEmpty() || WORD_PATTERN.matcher(text).matches()) {
return;
}
}
LOGGER.log(Level.FINE, "Hiding the completion query in AbstractCompletionQuery.preQueryUpdate().");
Completion.get().hideCompletion();
}
}
@Override
protected void prepareQuery(JTextComponent component) {
this.component = component;
if ((queryType & CompletionProvider.TOOLTIP_QUERY_TYPE) == CompletionProvider.TOOLTIP_QUERY_TYPE) {
this.toolTip = new CompletionToolTip(component);
}
}
protected ParserTaskManager getParserTaskManager() {
return Lookup.getDefault().lookup(ParserTaskManager.class);
}
@Override
@NbBundle.Messages({
"scanning_in_progress=Scanning in progress..."
})
protected void query(CompletionResultSet resultSet, Document doc, int caretOffset) {
try {
Future<Void> value = query(doc, caretOffset);
if (value != null) {
if (!value.isDone()) {
component.putClientProperty("completion-active", Boolean.FALSE);
resultSet.setWaitText(Bundle.scanning_in_progress());
value.get();
}
if ((queryType & CompletionProvider.COMPLETION_QUERY_TYPE) != 0) {
if (results != null) {
resultSet.addAllItems(results);
}
handleDeclarationItem(resultSet);
resultSet.setHasAdditionalItems(hasAdditionalItems != NO_ADDITIONAL_ITEMS);
if (hasAdditionalItems == ADDITIONAL_IMPORTED_ITEMS) {
resultSet.setHasAdditionalItemsText(Bundle.GCP_imported_items());
} else if (hasAdditionalItems == ADDITIONAL_MEMBER_ITEMS) {
resultSet.setHasAdditionalItemsText(Bundle.GCP_instance_members());
}
} else if ((queryType & CompletionProvider.TOOLTIP_QUERY_TYPE) == CompletionProvider.TOOLTIP_QUERY_TYPE) {
if (toolTip != null && toolTip.hasData()) {
resultSet.setToolTip(toolTip);
}
} else if ((queryType & CompletionProvider.DOCUMENTATION_QUERY_TYPE) == CompletionProvider.DOCUMENTATION_QUERY_TYPE) {
throw new UnsupportedOperationException("Not implemented yet.");
}
if (applicableTo != null) {
VersionedDocument textBuffer = VersionedDocumentUtilities.getVersionedDocument(doc);
resultSet.setAnchorOffset(applicableTo.getStartPosition(textBuffer.getCurrentSnapshot()).getOffset());
}
}
} catch (InterruptedException | ExecutionException | RuntimeException | Error ex) {
LOGGER.log(Level.WARNING, "An exception occurred while processing a completion query.", ex);
} finally {
resultSet.finish();
}
}
@CheckForNull
protected Future<Void> query(Document doc, int caretOffset) {
this.caretOffset = caretOffset;
if ((queryType & CompletionProvider.TOOLTIP_QUERY_TYPE) == CompletionProvider.TOOLTIP_QUERY_TYPE || isQueryContext(doc, caretOffset)) {
results = null;
documentation = null;
if (toolTip != null) {
toolTip.clearData();
}
applicableTo = null;
if ((queryType & CompletionProvider.DOCUMENTATION_QUERY_TYPE) == CompletionProvider.DOCUMENTATION_QUERY_TYPE) {
LOGGER.log(Level.WARNING, "Documentation query support is not yet implemented.");
return null;
}
Future<Void> value = getParserTaskManager().scheduleHighPriority(getTask((BaseDocument)doc));
return value;
}
return null;
}
protected boolean isQueryContext(Document doc, int caretOffset) {
return getCompletionProvider().isContext(getComponent(), caretOffset, queryType);
}
@Override
protected boolean canFilter(JTextComponent component) {
filterPrefix = null;
int newOffset = component.getSelectionStart();
if ((queryType & CompletionProvider.COMPLETION_QUERY_TYPE) != 0) {
if (applicableTo != null) {
VersionedDocument textBuffer = VersionedDocumentUtilities.getVersionedDocument(component.getDocument());
DocumentSnapshot snapshot = textBuffer.getCurrentSnapshot();
SnapshotPositionRegion applicableSpan = getApplicableTo().getRegion(snapshot);
int caretPosition = component.getCaretPosition();
// not using SnapshotPositionRegion.contains because we need to use an inclusive check at the end of the span
if (applicableSpan.getStart().getOffset() <= caretPosition && applicableSpan.getEnd().getOffset() >= caretPosition) {
OffsetRegion filterSpan = OffsetRegion.fromBounds(applicableSpan.getStart().getOffset(), component.getCaretPosition());
filterPrefix = snapshot.subSequence(filterSpan.getStart(), filterSpan.getEnd()).toString();
if (!filterPrefix.isEmpty() && !WORD_PATTERN.matcher(filterPrefix).matches()) {
filterPrefix = null;
}
}
return true;
}
} else if ((queryType & CompletionProvider.RESERVED_QUERY_MASK) == CompletionProvider.TOOLTIP_QUERY_TYPE) {
try {
if (newOffset == caretOffset) {
filterPrefix = EMPTY;
} else if (newOffset - caretOffset > 0) {
filterPrefix = component.getDocument().getText(caretOffset, newOffset - caretOffset);
} else if (newOffset - caretOffset < 0) {
filterPrefix = newOffset > toolTipOffset ? component.getDocument().getText(newOffset, caretOffset - newOffset) : null;
}
} catch (BadLocationException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
return (filterPrefix != null && filterPrefix.indexOf(',') == -1 && filterPrefix.indexOf('(') == -1 && filterPrefix.indexOf(')') == -1);
}
return false;
}
@Override
protected void filter(CompletionResultSet resultSet) {
try {
if ((queryType & CompletionProvider.COMPLETION_QUERY_TYPE) != 0) {
if (results != null) {
if (filterPrefix != null) {
Collection<? extends CompletionItem> filtered = getFilteredData(results, filterPrefix);
resultSet.addAllItems(filtered);
handleDeclarationItem(resultSet);
resultSet.setHasAdditionalItems(hasAdditionalItems > 0);
} else {
LOGGER.log(Level.FINE, "Hiding the completion query in AbstractCompletionQuery.filter().");
Completion.get().hideDocumentation();
Completion.get().hideCompletion();
}
}
} else if ((queryType & CompletionProvider.RESERVED_QUERY_MASK) == CompletionProvider.TOOLTIP_QUERY_TYPE) {
resultSet.setToolTip(toolTip != null ? toolTip : null);
}
if (applicableTo != null) {
VersionedDocument textBuffer = VersionedDocumentUtilities.getVersionedDocument(component.getDocument());
resultSet.setAnchorOffset(applicableTo.getStartPosition(textBuffer.getCurrentSnapshot()).getOffset());
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, ex.getMessage(), ex);
}
resultSet.finish();
}
protected void handleDeclarationItem(CompletionResultSet resultSet) {
if (possibleDeclaration && !isExplicitQuery() && getApplicableTo() != null) {
VersionedDocument textBuffer = VersionedDocumentUtilities.getVersionedDocument(component.getDocument());
DocumentSnapshot snapshot = textBuffer.getCurrentSnapshot();
SnapshotPositionRegion applicableSpan = getApplicableTo().getRegion(snapshot);
if (applicableSpan.getLength() > 0) {
resultSet.addDeclarationItem(createDeclarationCompletionItem(component.getDocument(), getApplicableTo()));
}
}
}
protected abstract CompletionItem createDeclarationCompletionItem(Document document, TrackingPositionRegion applicableTo);
protected abstract Task getTask(BaseDocument document);
protected Collection<? extends CompletionItem> getFilteredData(List<CompletionItem> data, String prefix) {
if (prefix.length() == 0) {
return data;
}
Pattern prefixBoundaryPattern = BaseCompletionController.getPrefixBoundaryPattern(prefix, false);
Pattern letterOrderPattern = BaseCompletionController.getLetterOrderPattern(prefix, false);
String lowercasePrefix = prefix.toLowerCase(Locale.getDefault());
List<CompletionItem> result = new ArrayList<>();
for (CompletionItem item : data) {
String insertPrefix = item.getInsertPrefix().toString();
if (insertPrefix.toLowerCase(Locale.getDefault()).contains(lowercasePrefix)) {
result.add(item);
} else if (prefixBoundaryPattern != null && prefixBoundaryPattern.matcher(insertPrefix).matches()) {
result.add(item);
} else if (letterOrderPattern != null && letterOrderPattern.matcher(insertPrefix).find()) {
result.add(item);
}
}
return result;
}
protected abstract class Task implements Callable<Void> {
private final BaseDocument document;
public Task(@NonNull BaseDocument document) {
Parameters.notNull("document", document);
this.document = document;
}
@NonNull
public BaseDocument getDocument() {
return document;
}
@Override
public Void call() {
runImpl(document);
return null;
}
protected abstract void runImpl(BaseDocument document);
}
}