Skip to content

Commit

Permalink
Performance improvements and other fixes in code completion (#3146)
Browse files Browse the repository at this point in the history
* Avoid overloading the DOM tree of the ContentAssistWidget

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>

* Ensure the code assist widget is closed when applying proposal

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>

* Respect isIncomplete flag in the completion result

If the isIncomplete flag is false, i.e. the completion result is
complete, then additional typing for the same word should not trigger a
new completion request. The latest completion result should be reused.

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>

* Fix flickering between keystrokes during code completion

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>

* Fine tune the rules for making new completion request

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>

* Set the correct offset when applying code completion

* Force a completion request message on Ctrl+Space

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>

* Fixed retrieval of CompletionItem document via Resolve request

Signed-off-by: Kaloyan Raev <kaloyan.r@zend.com>
  • Loading branch information
kaloyan-raev authored and Vitalii Parfonov committed Nov 30, 2016
1 parent 1652aef commit f1710e6
Show file tree
Hide file tree
Showing 20 changed files with 609 additions and 228 deletions.
Expand Up @@ -25,17 +25,22 @@
public interface CodeAssistProcessor {

/**
* Returns a list of completion proposals based on the
* specified location within the document that corresponds
* to the current cursor position within the text view.
* Returns a list of completion proposals based on the specified location
* within the document that corresponds to the current cursor position
* within the text view.
*
* @param editor
* the editor whose document is used to compute the proposals
* the editor whose document is used to compute the proposals
* @param offset
* an offset within the document for which completions should be computed
* @return an array of completion proposals or <code>null</code> if no proposals are possible
* an offset within the document for which completions should be
* computed
* @param triggered
* if triggered by the content assist key binding
*
* @return an array of completion proposals or <code>null</code> if no
* proposals are possible
*/
void computeCompletionProposals(TextEditor editor, int offset, CodeAssistCallback callback);
void computeCompletionProposals(TextEditor editor, int offset, boolean triggered, CodeAssistCallback callback);

/**
* Returns the reason why this content assist processor
Expand Down
Expand Up @@ -50,8 +50,9 @@ public interface CodeAssistant {
* appropriate content assist processor to invoke.
*
* @param offset a document offset
* @param triggered if triggered by the content assist key binding
* @param callback the callback to use once completions are ready
*/
void computeCompletionProposals(int offset, CodeAssistCallback callback);
void computeCompletionProposals(int offset, boolean triggered, CodeAssistCallback callback);

}
Expand Up @@ -52,12 +52,12 @@ public CodeAssistantImpl(@Assisted final DocumentPartitioner partitioner,
}

@Override
public void computeCompletionProposals(final int offset, final CodeAssistCallback callback) {
public void computeCompletionProposals(final int offset, final boolean triggered, final CodeAssistCallback callback) {
this.lastErrorMessage = "processing";

final CodeAssistProcessor processor = getProcessor(offset);
if (processor != null) {
processor.computeCompletionProposals(textEditor, offset, callback);
processor.computeCompletionProposals(textEditor, offset, triggered, callback);
this.lastErrorMessage = processor.getErrorMessage();
if (this.lastErrorMessage != null) {
notificationManager.notify("", lastErrorMessage, FAIL, EMERGE_MODE);
Expand All @@ -66,7 +66,7 @@ public void computeCompletionProposals(final int offset, final CodeAssistCallbac
} else {
final CodeAssistProcessor fallbackProcessor = getFallbackProcessor();
if (fallbackProcessor != null) {
fallbackProcessor.computeCompletionProposals(textEditor, offset, callback);
fallbackProcessor.computeCompletionProposals(textEditor, offset, triggered, callback);
this.lastErrorMessage = fallbackProcessor.getErrorMessage();
if (this.lastErrorMessage != null) {
this.textEditor.showMessage(this.lastErrorMessage);
Expand Down
Expand Up @@ -10,6 +10,7 @@
*******************************************************************************/
package org.eclipse.che.ide.api.editor.codeassist;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Widget;

import org.eclipse.che.ide.api.icon.Icon;
Expand All @@ -28,10 +29,10 @@ public interface CompletionProposal {
/**
* Returns optional additional information about the proposal. The additional information will be presented to assist the user
* in deciding if the selected proposal is the desired choice.
*
* @return the additional information or <code>null</code>
*
* @param callback a callback to return a widget with additional information
*/
Widget getAdditionalProposalInfo();
void getAdditionalProposalInfo(AsyncCallback<Widget> callback);

/**
* Returns the string to be displayed in the list of completion proposals.
Expand Down
Expand Up @@ -36,13 +36,13 @@ protected void setProcessors(final Set<? extends CodeAssistProcessor> codeAssist
}

@Override
public void computeCompletionProposals(final TextEditor textEditor, final int offset, final CodeAssistCallback callback) {
public void computeCompletionProposals(final TextEditor textEditor, final int offset, final boolean triggered, final CodeAssistCallback callback) {
if (!this.codeAssistProcessors.isEmpty()) {
final List<CompletionProposal> proposalList = new ArrayList<>();
final List<CodeAssistProcessor> expected = new ArrayList<>();
for (final CodeAssistProcessor processor : this.codeAssistProcessors) {
expected.add(processor);
processor.computeCompletionProposals(textEditor, offset, new CodeAssistCallback() {
processor.computeCompletionProposals(textEditor, offset, triggered, new CodeAssistCallback() {
@Override
public void proposalComputed(final List<CompletionProposal> processorProposals) {
expected.remove(processor);
Expand Down
Expand Up @@ -191,7 +191,7 @@ private void configureCodeAssist(final DocumentHandle documentHandle) {
final KeyBindingAction action = new KeyBindingAction() {
@Override
public boolean action() {
showCompletion(codeAssistant);
showCompletion(codeAssistant, true);
return true;
}
};
Expand All @@ -202,7 +202,7 @@ public boolean action() {
documentHandle.getDocEventBus().addHandler(CompletionRequestEvent.TYPE, new CompletionRequestHandler() {
@Override
public void onCompletionRequest(final CompletionRequestEvent event) {
showCompletion(codeAssistant);
showCompletion(codeAssistant, false);
}
});
} else {
Expand Down Expand Up @@ -234,8 +234,9 @@ public void onCompletionRequest(final CompletionRequestEvent event) {
* Show the available completions.
*
* @param codeAssistant the code assistant
* @param triggered if triggered by the content assist key binding
*/
private void showCompletion(final CodeAssistant codeAssistant) {
private void showCompletion(final CodeAssistant codeAssistant, final boolean triggered) {
final int cursor = textEditor.getCursorOffset();
if (cursor < 0) {
return;
Expand All @@ -248,7 +249,7 @@ public void computeCompletions(final CompletionReadyCallback callback) {
// cursor must be computed here again so it's original value is not baked in
// the SMI instance closure - important for completion update when typing
final int cursor = textEditor.getCursorOffset();
codeAssistant.computeCompletionProposals(cursor, new CodeAssistCallback() {
codeAssistant.computeCompletionProposals(cursor, triggered, new CodeAssistCallback() {
@Override
public void proposalComputed(final List<CompletionProposal> proposals) {
callback.onCompletionReady(proposals);
Expand Down
Expand Up @@ -10,11 +10,12 @@
*******************************************************************************/
package org.eclipse.che.ide.ext.java.client.editor;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Widget;

import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
import org.eclipse.che.ide.api.icon.Icon;
import org.eclipse.che.ide.ext.java.client.action.ProposalAction;
import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
import org.eclipse.che.ide.util.loging.Log;

/**
Expand All @@ -35,8 +36,8 @@ public ActionCompletionProposal(String display, String actionId, ProposalAction
}

@Override
public Widget getAdditionalProposalInfo() {
return null;
public void getAdditionalProposalInfo(AsyncCallback<Widget> callback) {
callback.onSuccess(null);
}

@Override
Expand Down
Expand Up @@ -155,7 +155,9 @@ public static Icon getIcon(final String image) {
}

@Override
public void computeCompletionProposals(final TextEditor textEditor, final int offset,
public void computeCompletionProposals(final TextEditor textEditor,
final int offset,
final boolean triggered,
final CodeAssistCallback callback) {
if (errorMessage != null) {
return;
Expand Down
Expand Up @@ -79,14 +79,14 @@ public JavaCompletionProposal(final int id,

/** {@inheritDoc} */
@Override
public Widget getAdditionalProposalInfo() {
public void getAdditionalProposalInfo(AsyncCallback<Widget> callback) {
Frame frame = new Frame();
frame.setSize("100%", "100%");
frame.getElement().getStyle().setBorderStyle(Style.BorderStyle.NONE);
frame.getElement().setAttribute("sandbox", ""); // empty value, not null
frame.getElement().getStyle().setProperty("resize", "both");
frame.setUrl(client.getProposalDocUrl(id, sessionId));
return frame;
callback.onSuccess(frame);
}

/** {@inheritDoc} */
Expand Down
Expand Up @@ -120,6 +120,15 @@
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/org/eclipse/che/plugin/languageserver/ide/editor/codeassist/LatestCompletionResult.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
Expand Down
Expand Up @@ -14,66 +14,97 @@

import com.google.gwt.dom.client.Style;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;

import java.util.List;

import org.eclipse.che.api.languageserver.shared.lsapi.CompletionItemDTO;
import org.eclipse.che.api.languageserver.shared.lsapi.RangeDTO;
import org.eclipse.che.api.languageserver.shared.lsapi.TextDocumentIdentifierDTO;
import org.eclipse.che.api.promises.client.Operation;
import org.eclipse.che.api.promises.client.OperationException;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.PromiseError;
import org.eclipse.che.ide.api.editor.codeassist.Completion;
import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
import org.eclipse.che.ide.api.editor.document.Document;
import org.eclipse.che.ide.api.editor.text.LinearRange;
import org.eclipse.che.ide.api.editor.text.TextPosition;
import org.eclipse.che.ide.api.icon.Icon;
import org.eclipse.che.ide.util.loging.Log;
import org.eclipse.che.plugin.languageserver.ide.LanguageServerResources;
import org.eclipse.che.plugin.languageserver.ide.filters.Match;
import org.eclipse.che.plugin.languageserver.ide.service.TextDocumentServiceClient;

import java.util.List;

/**
* @author Anatolii Bazko
* @author Kaloyan Raev
*/
public class CompletionItemBasedCompletionProposal implements CompletionProposal {

private final CompletionItemDTO completionItem;
private CompletionItemDTO completionItem;
private final TextDocumentServiceClient documentServiceClient;
private final TextDocumentIdentifierDTO documentId;
private final LanguageServerResources resources;
private final Icon icon;
private final ServerCapabilities serverCapabilities;
private final List<Match> highlights;
private final int offset;
private boolean resolved;

CompletionItemBasedCompletionProposal(CompletionItemDTO completionItem,
TextDocumentServiceClient documentServiceClient,
TextDocumentIdentifierDTO documentId,
LanguageServerResources resources, Icon icon,
LanguageServerResources resources,
Icon icon,
ServerCapabilities serverCapabilities,
List<Match> highlights) {
List<Match> highlights,
int offset) {
this.completionItem = completionItem;
this.documentServiceClient = documentServiceClient;
this.documentId = documentId;
this.resources = resources;
this.icon = icon;
this.serverCapabilities = serverCapabilities;
this.highlights = highlights;
this.offset = offset;
this.resolved = false;
}

@Override
public Widget getAdditionalProposalInfo() {
if (completionItem.getDocumentation() != null && !completionItem.getDocumentation().isEmpty()) {
Label label = new Label(completionItem.getDocumentation());
label.setWordWrap(true);
label.getElement().getStyle().setFontSize(13, Style.Unit.PX);
label.setSize("100%", "100%");
return label;
public void getAdditionalProposalInfo(final AsyncCallback<Widget> callback) {
if (completionItem.getDocumentation() == null && canResolve()) {
resolve().then(new Operation<CompletionItemDTO>() {
@Override
public void apply(CompletionItemDTO item) throws OperationException {
completionItem = item;
resolved = true;
callback.onSuccess(createAdditionalInfoWidget());
}
}).catchError(new Operation<PromiseError>() {
@Override
public void apply(PromiseError e) throws OperationException {
callback.onFailure(e.getCause());
}
});
} else {
callback.onSuccess(createAdditionalInfoWidget());
}
}

private Widget createAdditionalInfoWidget() {
String documentation = completionItem.getDocumentation();
if (documentation == null || documentation.trim().isEmpty()) {
documentation = "No documentation found.";
}
return null;

Label label = new Label(documentation);
label.setWordWrap(true);
label.getElement().getStyle().setFontSize(13, Style.Unit.PX);
label.getElement().getStyle().setMarginLeft(4, Style.Unit.PX);
label.setSize("100%", "100%");
return label;
}

@Override
Expand Down Expand Up @@ -129,50 +160,43 @@ public Icon getIcon() {

@Override
public void getCompletion(final CompletionCallback callback) {
callback.onCompletion(new CompletionImpl(completionItem, offset));
}

if (serverCapabilities.getCompletionProvider() != null &&
serverCapabilities.getCompletionProvider().getResolveProvider() != null &&
serverCapabilities.getCompletionProvider().getResolveProvider()) {
completionItem.setTextDocumentIdentifier(documentId);
documentServiceClient.resolveCompletionItem(completionItem).then(new Operation<CompletionItemDTO>() {
@Override
public void apply(CompletionItemDTO arg) throws OperationException {
callback.onCompletion(new CompletionImpl(arg));
}
}).catchError(new Operation<PromiseError>() {
@Override
public void apply(PromiseError arg) throws OperationException {
Log.error(getClass(), arg);
//try to apply with default text
callback.onCompletion(new CompletionImpl(completionItem));
}
});
} else {
callback.onCompletion(new CompletionImpl(completionItem));
}
private boolean canResolve() {
return !resolved &&
serverCapabilities.getCompletionProvider() != null &&
serverCapabilities.getCompletionProvider().getResolveProvider() != null &&
serverCapabilities.getCompletionProvider().getResolveProvider();
}

private Promise<CompletionItemDTO> resolve() {
completionItem.setTextDocumentIdentifier(documentId);
return documentServiceClient.resolveCompletionItem(completionItem);
}

private static class CompletionImpl implements Completion {

private CompletionItemDTO completionItem;
private int offset;

public CompletionImpl(CompletionItemDTO completionItem) {
public CompletionImpl(CompletionItemDTO completionItem, int offset) {
this.completionItem = completionItem;
this.offset = offset;
}

@Override
public void apply(Document document) {
//TODO in general resolve completion item may not provide getTextEdit, need to add checks
if (completionItem.getTextEdit() != null) {
RangeDTO range = completionItem.getTextEdit().getRange();
int startOffset = document.getIndexFromPosition(
new TextPosition(range.getStart().getLine(), range.getStart().getCharacter()));
int endOffset = document
.getIndexFromPosition(new TextPosition(range.getEnd().getLine(), range.getEnd().getCharacter()));
int endOffset = offset + document.getIndexFromPosition(
new TextPosition(range.getEnd().getLine(), range.getEnd().getCharacter()));
document.replace(startOffset, endOffset - startOffset, completionItem.getTextEdit().getNewText());
} else {
String insertText = completionItem.getInsertText() == null ? completionItem.getLabel() : completionItem.getInsertText();
document.replace(document.getCursorOffset(), 0, insertText);
document.replace(document.getCursorOffset() - offset, offset, insertText);
}
}

Expand Down

0 comments on commit f1710e6

Please sign in to comment.