Skip to content

Commit

Permalink
Add support to VS Code style snippets in code completion (#6339)
Browse files Browse the repository at this point in the history
Added support for code completion snippets.

Signed-off-by: Thomas Mäder <tmader@redhat.com>
  • Loading branch information
tsmaeder committed Oct 5, 2017
1 parent 47da5d3 commit af0376d
Show file tree
Hide file tree
Showing 21 changed files with 1,403 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,34 @@

import static org.eclipse.che.ide.api.theme.Style.theme;

import com.google.common.annotations.VisibleForTesting;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Widget;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.che.api.languageserver.shared.model.ExtendedCompletionItem;
import org.eclipse.che.api.promises.client.Promise;
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.link.HasLinkedMode;
import org.eclipse.che.ide.api.editor.link.LinkedModel;
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.filters.Match;
import org.eclipse.che.ide.util.Pair;
import org.eclipse.che.plugin.languageserver.ide.LanguageServerResources;
import org.eclipse.che.plugin.languageserver.ide.editor.codeassist.snippet.SnippetResolver;
import org.eclipse.che.plugin.languageserver.ide.editor.quickassist.ApplyWorkspaceEditAction;
import org.eclipse.che.plugin.languageserver.ide.service.TextDocumentServiceClient;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.TextEdit;
Expand All @@ -50,8 +59,10 @@ public class CompletionItemBasedCompletionProposal implements CompletionProposal
private final int offset;
private ExtendedCompletionItem completionItem;
private boolean resolved;
private HasLinkedMode editor;

CompletionItemBasedCompletionProposal(
HasLinkedMode editor,
ExtendedCompletionItem completionItem,
String currentWord,
TextDocumentServiceClient documentServiceClient,
Expand All @@ -60,6 +71,7 @@ public class CompletionItemBasedCompletionProposal implements CompletionProposal
ServerCapabilities serverCapabilities,
List<Match> highlights,
int offset) {
this.editor = editor;
this.completionItem = completionItem;
this.currentWord = currentWord;
this.documentServiceClient = documentServiceClient;
Expand Down Expand Up @@ -165,10 +177,11 @@ public void getCompletion(final CompletionCallback callback) {
.then(
completionItem -> {
callback.onCompletion(
new CompletionImpl(completionItem.getItem(), currentWord, offset));
new CompletionImpl(editor, completionItem.getItem(), currentWord, offset));
});
} else {
callback.onCompletion(new CompletionImpl(completionItem.getItem(), currentWord, offset));
callback.onCompletion(
new CompletionImpl(editor, completionItem.getItem(), currentWord, offset));
}
}

Expand All @@ -183,59 +196,116 @@ private Promise<ExtendedCompletionItem> resolve() {
return documentServiceClient.resolveCompletionItem(completionItem);
}

private static class CompletionImpl implements Completion {

@VisibleForTesting
static class CompletionImpl implements Completion {
private CompletionItem completionItem;
private String currentWord;
private String insertedText;
private int offset;
private LinearRange lastSelection;
private HasLinkedMode editor;

public CompletionImpl(CompletionItem completionItem, String currentWord, int offset) {
public CompletionImpl(
HasLinkedMode editor, CompletionItem completionItem, String currentWord, int offset) {
this.editor = editor;
this.completionItem = completionItem;
this.currentWord = currentWord;
this.offset = offset;
}

@Override
public void apply(Document document) {
List<TextEdit> edits = new ArrayList<>();
TextPosition cursorPosition = document.getCursorPosition();
if (completionItem.getTextEdit() != null) {
Range range = completionItem.getTextEdit().getRange();
int startOffset =
document.getIndexFromPosition(
new TextPosition(range.getStart().getLine(), range.getStart().getCharacter()));
int endOffset =
offset
+ document.getIndexFromPosition(
new TextPosition(range.getEnd().getLine(), range.getEnd().getCharacter()));
document.replace(
startOffset, endOffset - startOffset, completionItem.getTextEdit().getNewText());
edits.add(adjustForOffset(completionItem.getTextEdit(), cursorPosition, offset));
} else if (completionItem.getInsertText() == null) {
edits.add(
new TextEdit(
newRange(
cursorPosition.getLine(),
cursorPosition.getCharacter() - currentWord.length(),
cursorPosition.getLine(),
cursorPosition.getCharacter()),
completionItem.getLabel()));
} else {
int currentWordLength = currentWord.length();
int cursorOffset = document.getCursorOffset();
if (completionItem.getInsertText() == null) {
document.replace(
cursorOffset - currentWordLength, currentWordLength, completionItem.getLabel());
insertedText = completionItem.getLabel();
edits.add(
new TextEdit(
newRange(
cursorPosition.getLine(),
cursorPosition.getCharacter() - offset,
cursorPosition.getLine(),
cursorPosition.getCharacter()),
completionItem.getInsertText()));
}
if (completionItem.getAdditionalTextEdits() != null) {
completionItem
.getAdditionalTextEdits()
.forEach(e -> edits.add(adjustForOffset(e, cursorPosition, offset)));
}
TextEdit firstEdit = edits.get(0);
if (completionItem.getInsertTextFormat() == InsertTextFormat.Snippet) {
Position startPos = firstEdit.getRange().getStart();
TextPosition startTextPosition =
new TextPosition(startPos.getLine(), startPos.getCharacter());
int startOffset = document.getIndexFromPosition(startTextPosition);
Pair<String, LinkedModel> resolved =
new SnippetResolver(new DocumentVariableResolver(document, startTextPosition))
.resolve(firstEdit.getNewText(), editor, startOffset);
firstEdit.setNewText(resolved.first);
ApplyWorkspaceEditAction.applyTextEdits(document, edits);
if (resolved.second != null) {
editor.getLinkedMode().enterLinkedMode(resolved.second);
lastSelection = null;
} else {
document.replace(cursorOffset - offset, offset, completionItem.getInsertText());
insertedText = completionItem.getInsertText();
lastSelection = computeLastSelection(document, firstEdit);
}
} else {
ApplyWorkspaceEditAction.applyTextEdits(document, edits);
lastSelection = computeLastSelection(document, firstEdit);
}
}

@Override
public LinearRange getSelection(Document document) {
final TextEdit textEdit = completionItem.getTextEdit();
if (textEdit == null) {
return LinearRange.createWithStart(document.getCursorOffset() + insertedText.length())
.andLength(0);
}
private LinearRange computeLastSelection(Document document, TextEdit textEdit) {
Range range = textEdit.getRange();
TextPosition textPosition =
new TextPosition(range.getStart().getLine(), range.getStart().getCharacter());
int startOffset =
document.getIndexFromPosition(textPosition) + textEdit.getNewText().length();
return LinearRange.createWithStart(startOffset).andLength(0);
}

private Range newRange(int startLine, int startChar, int endLine, int endChar) {
return new Range(new Position(startLine, startChar), new Position(endLine, endChar));
}

private TextEdit adjustForOffset(TextEdit textEdit, TextPosition pos, int delta) {
Range range = textEdit.getRange();
int originalStart = pos.getCharacter() - delta;
if (range.getStart().getLine() != pos.getLine()
|| textEdit.getRange().getEnd().getCharacter() < originalStart) {
return textEdit;
} else if (originalStart < range.getStart().getCharacter()) {
return new TextEdit(
newRange(
range.getStart().getLine(),
range.getStart().getCharacter() + delta,
range.getEnd().getLine(),
range.getEnd().getCharacter() + delta),
textEdit.getNewText());
} else {
return new TextEdit(
newRange(
range.getStart().getLine(),
range.getStart().getCharacter(),
range.getEnd().getLine(),
range.getEnd().getCharacter() + delta),
textEdit.getNewText());
}
}

@Override
public LinearRange getSelection(Document document) {
return lastSelection;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) 2012-2017 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.plugin.languageserver.ide.editor.codeassist;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import org.eclipse.che.ide.api.editor.document.Document;
import org.eclipse.che.ide.api.editor.text.TextPosition;
import org.eclipse.che.plugin.languageserver.ide.editor.codeassist.snippet.VariableResolver;

/**
* Resolves snippet variables against an editor document.
*
* @author thomas
*/
public class DocumentVariableResolver implements VariableResolver {
// variables are functions from a document and position to a value.
private static final Map<String, BiFunction<Document, TextPosition, String>> VARIABLES;

static {
// well known variables according to https://github.com/Microsoft/vscode/blob/0ebd01213a65231f0af8187acaf264243629e4dc/src/vs/editor/contrib/snippet/browser/snippet.md
VARIABLES = new HashMap<>();
VARIABLES.put("TM_SELECTED_TEXT", DocumentVariableResolver::getSelectedText);
VARIABLES.put("TM_CURRENT_LINE", DocumentVariableResolver::getCurrentLine);
VARIABLES.put("TM_CURRENT_WORD", DocumentVariableResolver::getCurrentWord);
VARIABLES.put("TM_LINE_INDEX", DocumentVariableResolver::getCurrentLineIndex);
VARIABLES.put("TM_LINE_NUMBER", DocumentVariableResolver::getCurrentLineNumber);
VARIABLES.put("TM_FILENAME", DocumentVariableResolver::getFileName);
VARIABLES.put("TM_DIRECTORY", DocumentVariableResolver::getDirectory);
VARIABLES.put("TM_FILEPATH", DocumentVariableResolver::getPath);
}

private Document document;
private TextPosition position;

public DocumentVariableResolver(Document document, TextPosition position) {
this.document = document;
this.position = position;
}

@Override
public boolean isVar(String name) {
return VARIABLES.containsKey(name);
}

@Override
public String resolve(String name) {
return VARIABLES.get(name).apply(document, position);
}

private static String getSelectedText(Document doc, TextPosition pos) {
return doc.getContentRange(doc.getSelectedTextRange());
}

private static String getCurrentLine(Document doc, TextPosition pos) {
return doc.getLineContent(pos.getLine());
}

private static String getCurrentLineIndex(Document doc, TextPosition pos) {
return String.valueOf(pos.getLine());
}

private static String getCurrentLineNumber(Document doc, TextPosition pos) {
return String.valueOf(pos.getLine() + 1);
}

private static String getFileName(Document doc, TextPosition pos) {
return doc.getFile().getName();
}

private static String getDirectory(Document doc, TextPosition pos) {
return doc.getFile().getLocation().parent().toString();
}

private static String getPath(Document doc, TextPosition pos) {
return doc.getFile().getLocation().toString();
}

private static String getCurrentWord(Document doc, TextPosition pos) {
String line = doc.getLineContent(pos.getLine());
if (line.length() == 0) {
return "";
}
int start = pos.getCharacter();
int end = start;
while (start > 0 && !Character.isWhitespace(line.charAt(start - 1))) {
start--;
}
while (end < line.length() && !Character.isWhitespace(line.charAt(end))) {
end++;
}
return line.substring(start, end);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.che.ide.api.editor.codeassist.CodeAssistCallback;
import org.eclipse.che.ide.api.editor.codeassist.CodeAssistProcessor;
import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
import org.eclipse.che.ide.api.editor.link.HasLinkedMode;
import org.eclipse.che.ide.api.editor.texteditor.TextEditor;
import org.eclipse.che.ide.filters.FuzzyMatches;
import org.eclipse.che.ide.filters.Match;
Expand All @@ -40,7 +41,7 @@ public class LanguageServerCodeAssistProcessor implements CodeAssistProcessor {
private final ServerCapabilities serverCapabilities;
private final TextDocumentServiceClient documentServiceClient;
private final FuzzyMatches fuzzyMatches;
private final LatestCompletionResult latestCompletionResult;
private LatestCompletionResult latestCompletionResult;
private String lastErrorMessage;

@Inject
Expand All @@ -57,7 +58,7 @@ public LanguageServerCodeAssistProcessor(
this.imageProvider = imageProvider;
this.serverCapabilities = serverCapabilities;
this.fuzzyMatches = fuzzyMatches;
this.latestCompletionResult = new LatestCompletionResult();
this.latestCompletionResult = LatestCompletionResult.NO_RESULT;
}

@Override
Expand All @@ -78,14 +79,19 @@ public void computeCompletionProposals(

if (!triggered && latestCompletionResult.isGoodFor(documentId, offset, currentWord)) {
// no need to send new completion request
computeProposals(currentWord, offset - latestCompletionResult.getOffset(), callback);
computeProposals(
(HasLinkedMode) editor,
currentWord,
offset - latestCompletionResult.getOffset(),
callback);
} else {
documentServiceClient
.completion(documentPosition)
.then(
list -> {
latestCompletionResult.update(documentId, offset, currentWord, list);
computeProposals(currentWord, 0, callback);
latestCompletionResult =
new LatestCompletionResult(documentId, offset, currentWord, list);
computeProposals((HasLinkedMode) editor, currentWord, 0, callback);
})
.catchError(
error -> {
Expand Down Expand Up @@ -137,13 +143,15 @@ private List<Match> filter(String word, String label, String filterText) {
return null;
}

private void computeProposals(String currentWord, int offset, CodeAssistCallback callback) {
private void computeProposals(
HasLinkedMode editor, String currentWord, int offset, CodeAssistCallback callback) {
List<CompletionProposal> proposals = newArrayList();
for (ExtendedCompletionItem item : latestCompletionResult.getCompletionList().getItems()) {
List<Match> highlights = filter(currentWord, item.getItem());
if (highlights != null) {
proposals.add(
new CompletionItemBasedCompletionProposal(
editor,
item,
currentWord,
documentServiceClient,
Expand Down
Loading

0 comments on commit af0376d

Please sign in to comment.