Skip to content

Commit

Permalink
Compute TextEdits during textDocument/completion call
Browse files Browse the repository at this point in the history
TextEdits need to be computed during the completion call, but this can be extremely slow
when a large number of results is returned. This change tries to mitigate the issue by returning
a partial result list and marking the results as incomplete. Smaller payload means computing
Textedits is now more affordable.
Hopefully the UX should be improved as well since completion queries will return faster too.
The number of completion results is configurable with the java.completion.maxResults preference
(defaults to 50).
Setting 0 will disable the limit and return all results. Be aware the performance will be very
negatively impacted as textEdits for all results will be computed eagerly (can be thousands of results).

Fixes eclipse-jdtls#465

Signed-off-by: Fred Bricon <[email protected]>
  • Loading branch information
fbricon committed Dec 16, 2019
1 parent cf6358b commit e04188e
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 206 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.eclipse.jdt.ls.core.internal.contentassist;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -43,6 +44,7 @@
import org.eclipse.jdt.ls.core.internal.handlers.CompletionResolveHandler;
import org.eclipse.jdt.ls.core.internal.handlers.CompletionResponse;
import org.eclipse.jdt.ls.core.internal.handlers.CompletionResponses;
import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager;
import org.eclipse.jface.text.Region;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
Expand All @@ -57,6 +59,54 @@ public final class CompletionProposalRequestor extends CompletionRequestor {
private CompletionResponse response;
private boolean fIsTestCodeExcluded;
private CompletionContext context;
private boolean isComplete = true;
private PreferenceManager preferenceManager;

static class ProposalComparator implements Comparator<CompletionProposal> {

private Map<CompletionProposal, char[]> completionCache;

ProposalComparator(int cacheSize) {
completionCache = new HashMap<>(cacheSize + 1, 1f);//avoid resizing the cache
}

@Override
public int compare(CompletionProposal p1, CompletionProposal p2) {
int res = p2.getRelevance() - p1.getRelevance();
if (res == 0) {
res = p1.getKind() - p2.getKind();
}
if (res == 0) {
char[] completion1 = getCompletion(p1);
char[] completion2 = getCompletion(p2);

int p1Length = completion1.length;
int p2Length = completion2.length;
for (int i = 0; i < p1Length; i++) {
if (i >= p2Length) {
return -1;
}
res = Character.compare(completion1[i], completion2[i]);
if (res != 0) {
return res;
}
}
res = p2Length - p1Length;
}
return res;
}

private char[] getCompletion(CompletionProposal cp) {
// Implementation of CompletionProposal#getCompletion() can be non-trivial,
// so we cache the results to speed things up
return completionCache.computeIfAbsent(cp, p -> p.getCompletion());
}

};

public boolean isComplete() {
return isComplete;
}

// Update SUPPORTED_KINDS when mapKind changes
// @formatter:off
Expand All @@ -75,8 +125,9 @@ public final class CompletionProposalRequestor extends CompletionRequestor {
CompletionItemKind.Text);
// @formatter:on

public CompletionProposalRequestor(ICompilationUnit aUnit, int offset) {
public CompletionProposalRequestor(ICompilationUnit aUnit, int offset, PreferenceManager preferenceManager) {
this.unit = aUnit;
this.preferenceManager = preferenceManager;
response = new CompletionResponse();
response.setOffset(offset);
fIsTestCodeExcluded = !isTestSource(unit.getJavaProject(), unit);
Expand Down Expand Up @@ -124,11 +175,27 @@ public void accept(CompletionProposal proposal) {
}

public List<CompletionItem> getCompletionItems() {
response.setProposals(proposals);
CompletionResponses.store(response);
//Sort the results by relevance 1st
proposals.sort(new ProposalComparator(proposals.size()));
List<CompletionItem> completionItems = new ArrayList<>(proposals.size());
for (int i = 0; i < proposals.size(); i++) {
completionItems.add(toCompletionItem(proposals.get(i), i));
int maxCompletions = preferenceManager.getPreferences().getMaxCompletionResults();
int limit = Math.min(proposals.size(), maxCompletions);
if (proposals.size() > maxCompletions) {
//we keep receiving completions past our capacity so that makes the whole result incomplete
isComplete = false;
response.setProposals(proposals.subList(0, limit));
} else {
response.setProposals(proposals);
}
CompletionResponses.store(response);

//Let's compute replacement texts for the most relevant results only
CompletionProposalReplacementProvider proposalProvider = new CompletionProposalReplacementProvider(unit, getContext(), response.getOffset(), preferenceManager.getClientPreferences());
for (int i = 0; i < limit; i++) {
CompletionProposal proposal = proposals.get(i);
CompletionItem item = toCompletionItem(proposal, i);
proposalProvider.updateReplacement(proposal, item, '\0');
completionItems.add(item);
}
return completionItems;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*
*/
public final class SortTextHelper {
private static final int CEILING = 999_999_999;
public static final int CEILING = 999_999_999;

public static final int MAX_RELEVANCE_VALUE = 99_999_999;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

import com.google.common.collect.Sets;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.ProgressMonitorWrapper;
Expand All @@ -32,24 +32,48 @@
import org.eclipse.jdt.ls.core.internal.contentassist.CompletionProposalRequestor;
import org.eclipse.jdt.ls.core.internal.contentassist.JavadocCompletionProposal;
import org.eclipse.jdt.ls.core.internal.contentassist.SnippetCompletionProposal;
import org.eclipse.jdt.ls.core.internal.contentassist.SortTextHelper;
import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;

import com.google.common.collect.Sets;

public class CompletionHandler{

public final static CompletionOptions DEFAULT_COMPLETION_OPTIONS = new CompletionOptions(Boolean.TRUE, Arrays.asList(".", "@", "#", "*"));
private static final Set<String> UNSUPPORTED_RESOURCES = Sets.newHashSet("module-info.java", "package-info.java");

static final Comparator<CompletionItem> PROPOSAL_COMPARATOR = new Comparator<CompletionItem>() {

private final String DEFAULT_SORT_TEXT = String.valueOf(SortTextHelper.MAX_RELEVANCE_VALUE);

@Override
public int compare(CompletionItem o1, CompletionItem o2) {
return getSortText(o1).compareTo(getSortText(o2));
}

private String getSortText(CompletionItem ci) {
return StringUtils.defaultString(ci.getSortText(), DEFAULT_SORT_TEXT);
}

};

private PreferenceManager manager;

public CompletionHandler(PreferenceManager manager) {
this.manager = manager;
}

Either<List<CompletionItem>, CompletionList> completion(CompletionParams position,
IProgressMonitor monitor) {
List<CompletionItem> completionItems = null;
CompletionList $ = null;
try {
ICompilationUnit unit = JDTUtils.resolveCompilationUnit(position.getTextDocument().getUri());
completionItems = this.computeContentAssist(unit,
$ = this.computeContentAssist(unit,
position.getPosition().getLine(),
position.getPosition().getCharacter(), monitor);
} catch (OperationCanceledException ignorable) {
Expand All @@ -59,27 +83,30 @@ Either<List<CompletionItem>, CompletionList> completion(CompletionParams positio
JavaLanguageServerPlugin.logException("Problem with codeComplete for " + position.getTextDocument().getUri(), e);
monitor.setCanceled(true);
}
CompletionList $ = new CompletionList();
if ($ == null) {
$ = new CompletionList();
}
if ($.getItems() == null) {
$.setItems(Collections.emptyList());
}
if (monitor.isCanceled()) {
$.setIsIncomplete(true);
completionItems = null;
JavaLanguageServerPlugin.logInfo("Completion request cancelled");
} else {
JavaLanguageServerPlugin.logInfo("Completion request completed");
}
$.setItems(completionItems == null ? Collections.emptyList() : completionItems);
return Either.forRight($);
}

private List<CompletionItem> computeContentAssist(ICompilationUnit unit, int line, int column, IProgressMonitor monitor) throws JavaModelException {
private CompletionList computeContentAssist(ICompilationUnit unit, int line, int column, IProgressMonitor monitor) throws JavaModelException {
CompletionResponses.clear();
if (unit == null) {
return Collections.emptyList();
return null;
}
List<CompletionItem> proposals = new ArrayList<>();

final int offset = JsonRpcHelpers.toOffset(unit.getBuffer(), line, column);
CompletionProposalRequestor collector = new CompletionProposalRequestor(unit, offset);
CompletionProposalRequestor collector = new CompletionProposalRequestor(unit, offset, manager);
// Allow completions for unresolved types - since 3.3
collector.setAllowsRequiredProposals(CompletionProposal.FIELD_REF, CompletionProposal.TYPE_REF, true);
collector.setAllowsRequiredProposals(CompletionProposal.FIELD_REF, CompletionProposal.TYPE_IMPORT, true);
Expand All @@ -95,7 +122,6 @@ private List<CompletionItem> computeContentAssist(ICompilationUnit unit, int lin
collector.setAllowsRequiredProposals(CompletionProposal.ANONYMOUS_CLASS_DECLARATION, CompletionProposal.TYPE_REF, true);

collector.setAllowsRequiredProposals(CompletionProposal.TYPE_REF, CompletionProposal.TYPE_REF, true);

collector.setFavoriteReferences(getFavoriteStaticMembers());

if (offset >-1 && !monitor.isCanceled()) {
Expand Down Expand Up @@ -128,7 +154,10 @@ public boolean isCanceled() {
}
}
}
return proposals;
proposals.sort(PROPOSAL_COMPARATOR);
CompletionList list = new CompletionList(proposals);
list.setIsIncomplete(!collector.isComplete());
return list;
}

private String[] getFavoriteStaticMembers() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.JSONUtility;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.contentassist.CompletionProposalReplacementProvider;
import org.eclipse.jdt.ls.core.internal.contentassist.CompletionProposalRequestor;
import org.eclipse.jdt.ls.core.internal.javadoc.JavadocContentAccess;
import org.eclipse.jdt.ls.core.internal.javadoc.JavadocContentAccess2;
Expand Down Expand Up @@ -94,11 +93,6 @@ public CompletionItem resolve(CompletionItem param, IProgressMonitor monitor) {
if (unit == null) {
throw new IllegalStateException(NLS.bind("Unable to match Compilation Unit from {0} ", uri));
}
CompletionProposalReplacementProvider proposalProvider = new CompletionProposalReplacementProvider(unit,
completionResponse.getContext(),
completionResponse.getOffset(),
this.manager.getClientPreferences());
proposalProvider.updateReplacement(completionResponse.getProposals().get(proposalId), param, '\0');
if (monitor.isCanceled()) {
param.setData(null);
return param;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) {
logInfo(">> document/completion");
CompletionHandler handler = new CompletionHandler();
CompletionHandler handler = new CompletionHandler(preferenceManager);
final IProgressMonitor[] monitors = new IProgressMonitor[1];
CompletableFuture<Either<List<CompletionItem>, CompletionList>> result = computeAsync((monitor) -> {
monitors[0] = monitor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ public class Preferences {
public static final String JAVA_COMPLETION_FAVORITE_MEMBERS_KEY = "java.completion.favoriteStaticMembers";
public static final List<String> JAVA_COMPLETION_FAVORITE_MEMBERS_DEFAULT;

/**
* Preference key for maximum number of completion results to be returned.
* Defaults to 50.
*/
public static final String JAVA_COMPLETION_MAX_RESULTS_KEY = "java.completion.maxResults";
public static final int JAVA_COMPLETION_MAX_RESULTS_DEFAULT = 50;

/**
* A named preference that controls if the Java code assist only inserts
* completions. When set to true, code completion overwrites the current text.
Expand Down Expand Up @@ -361,8 +368,8 @@ public class Preferences {
private String formatterProfileName;
private Collection<IPath> rootPaths;
private Collection<IPath> triggerFiles;

private int parallelBuildsCount;
private int maxCompletionResults;

static {
JAVA_IMPORT_EXCLUSIONS_DEFAULT = new LinkedList<>();
Expand Down Expand Up @@ -473,6 +480,7 @@ public Preferences() {
importOrder = JAVA_IMPORT_ORDER_DEFAULT;
filteredTypes = JAVA_COMPLETION_FILTERED_TYPES_DEFAULT;
parallelBuildsCount = PreferenceInitializer.PREF_MAX_CONCURRENT_BUILDS_DEFAULT;
maxCompletionResults = JAVA_COMPLETION_MAX_RESULTS_DEFAULT;
}

/**
Expand Down Expand Up @@ -612,6 +620,9 @@ public static Preferences createFrom(Map<String, Object> configuration) {
maxConcurrentBuilds = maxConcurrentBuilds >= 1 ? maxConcurrentBuilds : 1;
prefs.setMaxBuildCount(maxConcurrentBuilds);

int maxCompletions = getInt(configuration, JAVA_COMPLETION_MAX_RESULTS_KEY, JAVA_COMPLETION_MAX_RESULTS_DEFAULT);
prefs.setMaxCompletionResults(maxCompletions);

return prefs;
}

Expand Down Expand Up @@ -1057,7 +1068,29 @@ public boolean isJavaFormatOnTypeEnabled() {
return javaFormatOnTypeEnabled;
}

public void setJavaFormatOnTypeEnabled(boolean javaFormatOnTypeEnabled) {
public Preferences setJavaFormatOnTypeEnabled(boolean javaFormatOnTypeEnabled) {
this.javaFormatOnTypeEnabled = javaFormatOnTypeEnabled;
return this;
}

public int getMaxCompletionResults() {
return maxCompletionResults;
}

/**
* Sets the maximum number of completion results (excluding snippets and Javadoc
* proposals). If maxCompletions is set to 0 or lower, then the completion limit
* is considered disabled, which could certainly severly impact performance in a
* negative way.
*
* @param maxCompletions
*/
public Preferences setMaxCompletionResults(int maxCompletions) {
if (maxCompletions < 1) {
this.maxCompletionResults = Integer.MAX_VALUE;
} else {
this.maxCompletionResults = maxCompletions;
}
return this;
}
}
Loading

0 comments on commit e04188e

Please sign in to comment.