/*
* 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.spi.editor.completion.CompletionController;
import com.tvl.spi.editor.completion.CompletionItem;
import com.tvl.spi.editor.completion.CompletionResultSet;
import com.tvl.spi.editor.completion.CompletionTask;
import com.tvl.spi.editor.completion.support.AsyncCompletionQuery;
import com.tvl.spi.editor.completion.support.AsyncCompletionTask;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
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.TrackingPositionRegion;
import org.antlr.netbeans.editor.text.VersionedDocument;
import org.antlr.netbeans.editor.text.VersionedDocumentUtilities;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.openide.util.Parameters;
/**
*
* @author Sam Harwell
*/
public class BaseCompletionController implements CompletionController {
// -J-Dorg.antlr.works.editor.antlr4.completion.BaseCompletionController.level=FINE
private static final Logger LOGGER = Logger.getLogger(BaseCompletionController.class.getName());
/** ^([A-Z][a-z]*){2,}$ */
private static final Pattern WORD_BOUNDARY_PREFIX =
Pattern.compile("^([A-Z][a-z]*){2,}$");
/** [A-Z][a-z]* */
private static final Pattern WORD_PATTERN =
Pattern.compile("[A-Z][a-z]*");
private static final List<String> recentCompletions = new ArrayList<>();
private final JTextComponent component;
private final List<? extends CompletionTask> tasks;
private final List<AsyncCompletionQuery> queries;
public BaseCompletionController(@NonNull JTextComponent component, @NonNull List<? extends CompletionTask> tasks, @NonNull List<Integer> queryTypes) {
this.component = component;
this.tasks = tasks;
this.queries = new ArrayList<>();
for (CompletionTask task : tasks) {
if (!(task instanceof AsyncCompletionTask)) {
continue;
}
AsyncCompletionQuery query = ((AsyncCompletionTask)task).getQuery();
queries.add(query);
}
}
static void addRecentCompletion(String completion) {
recentCompletions.remove(completion);
if (recentCompletions.size() == 20) {
recentCompletions.remove(0);
}
recentCompletions.add(completion);
}
static int getRecentCompletionWeight(String completion, Collator collator) {
for (int i = recentCompletions.size() - 1; i >= 0; i--) {
if (collator.compare(completion, recentCompletions.get(i)) == 0) {
return i + 1;
}
}
return 0;
}
public JTextComponent getComponent() {
return component;
}
public Document getDocument() {
return getComponent().getDocument();
}
protected List<? extends AsyncCompletionQuery> getQueries() {
return queries;
}
protected CompletionMatchEvaluator getCompletionMatchEvaluator(String evaluatedText) {
return new CompletionMatchEvaluator(evaluatedText);
}
public TrackingPositionRegion getApplicableTo() {
AbstractCompletionQuery appliedQuery = null;
for (AsyncCompletionQuery query : queries) {
if (query instanceof AbstractCompletionQuery) {
TrackingPositionRegion span = ((AbstractCompletionQuery)query).getApplicableTo();
if (span != null) {
return span;
}
}
}
return null;
}
@Override
public void sortItems(List<? extends CompletionItem> items, int sortType) {
Collections.sort(items, getComparator(sortType));
}
@Override
public Selection getSelection(List<? extends CompletionItem> items, List<? extends CompletionItem> declarationItems) {
Comparator<CompletionItem> comparator = getComparator(CompletionResultSet.TEXT_SORT_TYPE);
String completionPrefix = getCompletionPrefix();
String evaluatedText = completionPrefix;
while (true) {
CompletionItem bestMatch = null;
int bestMatchValue = 0;
int prefixMatch = 0;
CompletionMatchEvaluator evaluator = getCompletionMatchEvaluator(evaluatedText);
for (CompletionItem item : items) {
int matchValue = evaluator.getMatchStrength(item);
if (matchValue > 0) {
if ((matchValue & (CompletionMatchEvaluator.PREFIX_CASE_SENSITIVE | CompletionMatchEvaluator.PREFIX)) != 0) {
prefixMatch++;
}
boolean improved = matchValue > bestMatchValue
|| (matchValue == bestMatchValue && comparator.compare(item, bestMatch) < 0);
if (improved) {
bestMatch = item;
bestMatchValue = matchValue;
}
}
}
if (bestMatch != null) {
int index = items.indexOf(bestMatch);
boolean selected =
declarationItems.isEmpty()
&& (!(bestMatch instanceof AbstractCompletionItem) || ((AbstractCompletionItem)bestMatch).allowInitialSelection())
&& evaluatedText != null && !evaluatedText.isEmpty();
boolean unique = !completionPrefix.isEmpty()
&& (prefixMatch == 1)
&& evaluatedText.length() == completionPrefix.length();
return new Selection(index, selected, unique);
}
if (evaluatedText.length() == 0) {
break;
}
evaluatedText = evaluatedText.substring(0, evaluatedText.length() - 1);
}
return Selection.DEFAULT;
}
@Override
public void defaultAction(CompletionItem bestMatch, boolean isSelected) {
if (bestMatch instanceof AbstractCompletionItem) {
((AbstractCompletionItem)bestMatch).defaultAction(component, this, isSelected);
} else {
bestMatch.defaultAction(component);
}
}
@Override
public void processKeyEvent(KeyEvent evt, CompletionItem bestMatch, boolean isSelected) {
if (bestMatch instanceof AbstractCompletionItem) {
((AbstractCompletionItem)bestMatch).processKeyEvent(evt, this, isSelected);
} else {
bestMatch.processKeyEvent(evt);
}
}
@Override
public void render(Graphics g, Font defaultFont, Color foregroundColor, Color backgroundColor, Color selectedForegroundColor, Color selectedBackgroundColor, int width, int height, CompletionItem item, boolean isBestMatch, boolean isSelected) {
boolean selected = item instanceof AbstractCompletionItem ? isSelected : isBestMatch;
if (selected) {
// Clear the background
g.setColor(selectedBackgroundColor);
g.fillRect(0, 0, width, height);
g.setColor(selectedForegroundColor);
if (item instanceof AbstractCompletionItem) {
((AbstractCompletionItem)item).render(this, g, defaultFont, foregroundColor, backgroundColor, selectedForegroundColor, selectedBackgroundColor, width, height, isBestMatch, isSelected);
} else {
item.render(g, defaultFont, selectedForegroundColor, selectedBackgroundColor, width, height, isBestMatch);
}
} else {
// Clear the background
g.setColor(backgroundColor);
g.fillRect(0, 0, width, height);
g.setColor(foregroundColor);
if (item instanceof AbstractCompletionItem) {
((AbstractCompletionItem)item).render(this, g, defaultFont, foregroundColor, backgroundColor, selectedForegroundColor, selectedBackgroundColor, width, height, isBestMatch, isSelected);
} else {
item.render(g, defaultFont, foregroundColor, backgroundColor, width, height, isBestMatch);
}
}
}
@Override
public boolean instantSubstitution(CompletionItem uniqueMatch) {
if (uniqueMatch instanceof AbstractCompletionItem) {
return ((AbstractCompletionItem)uniqueMatch).instantSubstitution(component, this);
} else {
return uniqueMatch.instantSubstitution(component);
}
}
protected @NonNull String getCompletionPrefix() {
TrackingPositionRegion span = null;
AbstractCompletionQuery appliedQuery = null;
for (AsyncCompletionQuery query : queries) {
if (query instanceof AbstractCompletionQuery) {
span = ((AbstractCompletionQuery)query).getApplicableTo();
if (span != null) {
appliedQuery = (AbstractCompletionQuery)query;
break;
}
}
}
if (span != null) {
VersionedDocument textBuffer = VersionedDocumentUtilities.getVersionedDocument(getDocument());
return span.getText(textBuffer.getCurrentSnapshot());
}
if(getDocument() instanceof BaseDocument) {
BaseDocument doc = (BaseDocument)getDocument();
int caretOffset = getComponent().getSelectionStart();
try {
int[] block = Utilities.getIdentifierBlock(doc, caretOffset);
if (block != null) {
// if appliedQuery is null, then the provider doesn't support
// the new API so we use the old expected behavior and do not extend.
if (appliedQuery == null || !appliedQuery.isExtend()) {
block[1] = caretOffset;
}
return doc.getText(block);
}
} catch (BadLocationException ble) {
LOGGER.log(Level.WARNING, ble.getMessage(), ble);
}
}
return "";
}
protected @NonNull Comparator<CompletionItem> getComparator(int sortType) {
if (sortType == CompletionResultSet.PRIORITY_SORT_TYPE) {
return BaseCompletionItemComparator.PRIORITY_COMPARATOR;
}
if (sortType == CompletionResultSet.TEXT_SORT_TYPE) {
return BaseCompletionItemComparator.TEXT_COMPARATOR;
}
throw new IllegalArgumentException("Invalid sort type.");
}
public static @CheckForNull Pattern getPrefixBoundaryPattern(@NonNull String prefix, boolean caseSensitive) {
Parameters.notNull("prefix", prefix);
if (prefix.isEmpty()) {
return null;
}
Matcher matcher = WORD_BOUNDARY_PREFIX.matcher(prefix);
if (matcher.matches()) {
StringBuilder pattern = new StringBuilder("^");
for (Matcher wordMatcher = WORD_PATTERN.matcher(prefix); wordMatcher.find(); ) {
String group = wordMatcher.group();
if (caseSensitive) {
if (Character.isUpperCase(group.charAt(0))) {
pattern.append("(?:\\w*[a-z0-9_])?").append(group.charAt(0));
} else if (Character.isLowerCase(group.charAt(0))) {
pattern.append("(?:\\w*[0-9_])?").append(group.charAt(0));
} else {
pattern.append("\\w*").append(Pattern.quote(group.substring(0, 1)));
}
pattern.append(Pattern.quote(group.substring(1)));
} else {
pattern.append("(?:(?:\\w*[a-z0-9_])?")
.append(Character.toUpperCase(group.charAt(0)))
.append("|(?:\\w*[0-9_])?")
.append(Character.toLowerCase(group.charAt(0)))
.append(")");
for (int j = 1; j < group.length(); j++) {
char ch = group.charAt(j);
if (Character.isLetter(ch)) {
pattern.append("[")
.append(Character.toUpperCase(ch))
.append(Character.toLowerCase(ch))
.append("]");
} else {
pattern.append(ch);
}
}
}
}
pattern.append("\\w*$");
return Pattern.compile(pattern.toString());
}
return null;
}
public static @CheckForNull Pattern getLetterOrderPattern(@NonNull String prefix, boolean caseSensitive) {
Parameters.notNull("prefix", prefix);
if (prefix.isEmpty()) {
return null;
}
StringBuilder pattern = new StringBuilder();
for (int i = 0; i < prefix.length(); i++) {
if (i > 0) {
pattern.append(".*");
}
char ch = prefix.charAt(i);
if (Character.isLetterOrDigit(ch)) {
pattern.append(ch);
} else {
pattern.append('\\').append(ch);
}
}
return Pattern.compile(pattern.toString(), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
}
}