Skip to content

Commit af0376d

Browse files
authored
Add support to VS Code style snippets in code completion (eclipse-che#6339)
Added support for code completion snippets. Signed-off-by: Thomas Mäder <tmader@redhat.com>
1 parent 47da5d3 commit af0376d

21 files changed

Lines changed: 1403 additions & 89 deletions

plugins/plugin-languageserver/che-plugin-languageserver-ide/src/main/java/org/eclipse/che/plugin/languageserver/ide/editor/codeassist/CompletionItemBasedCompletionProposal.java

Lines changed: 101 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,34 @@
1212

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

15+
import com.google.common.annotations.VisibleForTesting;
1516
import com.google.gwt.dom.client.Style;
1617
import com.google.gwt.dom.client.Style.Overflow;
1718
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
1819
import com.google.gwt.user.client.rpc.AsyncCallback;
1920
import com.google.gwt.user.client.ui.HTML;
2021
import com.google.gwt.user.client.ui.Widget;
22+
import java.util.ArrayList;
2123
import java.util.List;
2224
import org.eclipse.che.api.languageserver.shared.model.ExtendedCompletionItem;
2325
import org.eclipse.che.api.promises.client.Promise;
2426
import org.eclipse.che.ide.api.editor.codeassist.Completion;
2527
import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
2628
import org.eclipse.che.ide.api.editor.document.Document;
29+
import org.eclipse.che.ide.api.editor.link.HasLinkedMode;
30+
import org.eclipse.che.ide.api.editor.link.LinkedModel;
2731
import org.eclipse.che.ide.api.editor.text.LinearRange;
2832
import org.eclipse.che.ide.api.editor.text.TextPosition;
2933
import org.eclipse.che.ide.api.icon.Icon;
3034
import org.eclipse.che.ide.filters.Match;
35+
import org.eclipse.che.ide.util.Pair;
3136
import org.eclipse.che.plugin.languageserver.ide.LanguageServerResources;
37+
import org.eclipse.che.plugin.languageserver.ide.editor.codeassist.snippet.SnippetResolver;
38+
import org.eclipse.che.plugin.languageserver.ide.editor.quickassist.ApplyWorkspaceEditAction;
3239
import org.eclipse.che.plugin.languageserver.ide.service.TextDocumentServiceClient;
3340
import org.eclipse.lsp4j.CompletionItem;
41+
import org.eclipse.lsp4j.InsertTextFormat;
42+
import org.eclipse.lsp4j.Position;
3443
import org.eclipse.lsp4j.Range;
3544
import org.eclipse.lsp4j.ServerCapabilities;
3645
import org.eclipse.lsp4j.TextEdit;
@@ -50,8 +59,10 @@ public class CompletionItemBasedCompletionProposal implements CompletionProposal
5059
private final int offset;
5160
private ExtendedCompletionItem completionItem;
5261
private boolean resolved;
62+
private HasLinkedMode editor;
5363

5464
CompletionItemBasedCompletionProposal(
65+
HasLinkedMode editor,
5566
ExtendedCompletionItem completionItem,
5667
String currentWord,
5768
TextDocumentServiceClient documentServiceClient,
@@ -60,6 +71,7 @@ public class CompletionItemBasedCompletionProposal implements CompletionProposal
6071
ServerCapabilities serverCapabilities,
6172
List<Match> highlights,
6273
int offset) {
74+
this.editor = editor;
6375
this.completionItem = completionItem;
6476
this.currentWord = currentWord;
6577
this.documentServiceClient = documentServiceClient;
@@ -165,10 +177,11 @@ public void getCompletion(final CompletionCallback callback) {
165177
.then(
166178
completionItem -> {
167179
callback.onCompletion(
168-
new CompletionImpl(completionItem.getItem(), currentWord, offset));
180+
new CompletionImpl(editor, completionItem.getItem(), currentWord, offset));
169181
});
170182
} else {
171-
callback.onCompletion(new CompletionImpl(completionItem.getItem(), currentWord, offset));
183+
callback.onCompletion(
184+
new CompletionImpl(editor, completionItem.getItem(), currentWord, offset));
172185
}
173186
}
174187

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

186-
private static class CompletionImpl implements Completion {
187-
199+
@VisibleForTesting
200+
static class CompletionImpl implements Completion {
188201
private CompletionItem completionItem;
189202
private String currentWord;
190-
private String insertedText;
191203
private int offset;
204+
private LinearRange lastSelection;
205+
private HasLinkedMode editor;
192206

193-
public CompletionImpl(CompletionItem completionItem, String currentWord, int offset) {
207+
public CompletionImpl(
208+
HasLinkedMode editor, CompletionItem completionItem, String currentWord, int offset) {
209+
this.editor = editor;
194210
this.completionItem = completionItem;
195211
this.currentWord = currentWord;
196212
this.offset = offset;
197213
}
198214

199215
@Override
200216
public void apply(Document document) {
217+
List<TextEdit> edits = new ArrayList<>();
218+
TextPosition cursorPosition = document.getCursorPosition();
201219
if (completionItem.getTextEdit() != null) {
202-
Range range = completionItem.getTextEdit().getRange();
203-
int startOffset =
204-
document.getIndexFromPosition(
205-
new TextPosition(range.getStart().getLine(), range.getStart().getCharacter()));
206-
int endOffset =
207-
offset
208-
+ document.getIndexFromPosition(
209-
new TextPosition(range.getEnd().getLine(), range.getEnd().getCharacter()));
210-
document.replace(
211-
startOffset, endOffset - startOffset, completionItem.getTextEdit().getNewText());
220+
edits.add(adjustForOffset(completionItem.getTextEdit(), cursorPosition, offset));
221+
} else if (completionItem.getInsertText() == null) {
222+
edits.add(
223+
new TextEdit(
224+
newRange(
225+
cursorPosition.getLine(),
226+
cursorPosition.getCharacter() - currentWord.length(),
227+
cursorPosition.getLine(),
228+
cursorPosition.getCharacter()),
229+
completionItem.getLabel()));
212230
} else {
213-
int currentWordLength = currentWord.length();
214-
int cursorOffset = document.getCursorOffset();
215-
if (completionItem.getInsertText() == null) {
216-
document.replace(
217-
cursorOffset - currentWordLength, currentWordLength, completionItem.getLabel());
218-
insertedText = completionItem.getLabel();
231+
edits.add(
232+
new TextEdit(
233+
newRange(
234+
cursorPosition.getLine(),
235+
cursorPosition.getCharacter() - offset,
236+
cursorPosition.getLine(),
237+
cursorPosition.getCharacter()),
238+
completionItem.getInsertText()));
239+
}
240+
if (completionItem.getAdditionalTextEdits() != null) {
241+
completionItem
242+
.getAdditionalTextEdits()
243+
.forEach(e -> edits.add(adjustForOffset(e, cursorPosition, offset)));
244+
}
245+
TextEdit firstEdit = edits.get(0);
246+
if (completionItem.getInsertTextFormat() == InsertTextFormat.Snippet) {
247+
Position startPos = firstEdit.getRange().getStart();
248+
TextPosition startTextPosition =
249+
new TextPosition(startPos.getLine(), startPos.getCharacter());
250+
int startOffset = document.getIndexFromPosition(startTextPosition);
251+
Pair<String, LinkedModel> resolved =
252+
new SnippetResolver(new DocumentVariableResolver(document, startTextPosition))
253+
.resolve(firstEdit.getNewText(), editor, startOffset);
254+
firstEdit.setNewText(resolved.first);
255+
ApplyWorkspaceEditAction.applyTextEdits(document, edits);
256+
if (resolved.second != null) {
257+
editor.getLinkedMode().enterLinkedMode(resolved.second);
258+
lastSelection = null;
219259
} else {
220-
document.replace(cursorOffset - offset, offset, completionItem.getInsertText());
221-
insertedText = completionItem.getInsertText();
260+
lastSelection = computeLastSelection(document, firstEdit);
222261
}
262+
} else {
263+
ApplyWorkspaceEditAction.applyTextEdits(document, edits);
264+
lastSelection = computeLastSelection(document, firstEdit);
223265
}
224266
}
225267

226-
@Override
227-
public LinearRange getSelection(Document document) {
228-
final TextEdit textEdit = completionItem.getTextEdit();
229-
if (textEdit == null) {
230-
return LinearRange.createWithStart(document.getCursorOffset() + insertedText.length())
231-
.andLength(0);
232-
}
268+
private LinearRange computeLastSelection(Document document, TextEdit textEdit) {
233269
Range range = textEdit.getRange();
234270
TextPosition textPosition =
235271
new TextPosition(range.getStart().getLine(), range.getStart().getCharacter());
236272
int startOffset =
237273
document.getIndexFromPosition(textPosition) + textEdit.getNewText().length();
238274
return LinearRange.createWithStart(startOffset).andLength(0);
239275
}
276+
277+
private Range newRange(int startLine, int startChar, int endLine, int endChar) {
278+
return new Range(new Position(startLine, startChar), new Position(endLine, endChar));
279+
}
280+
281+
private TextEdit adjustForOffset(TextEdit textEdit, TextPosition pos, int delta) {
282+
Range range = textEdit.getRange();
283+
int originalStart = pos.getCharacter() - delta;
284+
if (range.getStart().getLine() != pos.getLine()
285+
|| textEdit.getRange().getEnd().getCharacter() < originalStart) {
286+
return textEdit;
287+
} else if (originalStart < range.getStart().getCharacter()) {
288+
return new TextEdit(
289+
newRange(
290+
range.getStart().getLine(),
291+
range.getStart().getCharacter() + delta,
292+
range.getEnd().getLine(),
293+
range.getEnd().getCharacter() + delta),
294+
textEdit.getNewText());
295+
} else {
296+
return new TextEdit(
297+
newRange(
298+
range.getStart().getLine(),
299+
range.getStart().getCharacter(),
300+
range.getEnd().getLine(),
301+
range.getEnd().getCharacter() + delta),
302+
textEdit.getNewText());
303+
}
304+
}
305+
306+
@Override
307+
public LinearRange getSelection(Document document) {
308+
return lastSelection;
309+
}
240310
}
241311
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2012-2017 Red Hat, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
*/
11+
package org.eclipse.che.plugin.languageserver.ide.editor.codeassist;
12+
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
import java.util.function.BiFunction;
16+
import org.eclipse.che.ide.api.editor.document.Document;
17+
import org.eclipse.che.ide.api.editor.text.TextPosition;
18+
import org.eclipse.che.plugin.languageserver.ide.editor.codeassist.snippet.VariableResolver;
19+
20+
/**
21+
* Resolves snippet variables against an editor document.
22+
*
23+
* @author thomas
24+
*/
25+
public class DocumentVariableResolver implements VariableResolver {
26+
// variables are functions from a document and position to a value.
27+
private static final Map<String, BiFunction<Document, TextPosition, String>> VARIABLES;
28+
29+
static {
30+
// well known variables according to https://github.com/Microsoft/vscode/blob/0ebd01213a65231f0af8187acaf264243629e4dc/src/vs/editor/contrib/snippet/browser/snippet.md
31+
VARIABLES = new HashMap<>();
32+
VARIABLES.put("TM_SELECTED_TEXT", DocumentVariableResolver::getSelectedText);
33+
VARIABLES.put("TM_CURRENT_LINE", DocumentVariableResolver::getCurrentLine);
34+
VARIABLES.put("TM_CURRENT_WORD", DocumentVariableResolver::getCurrentWord);
35+
VARIABLES.put("TM_LINE_INDEX", DocumentVariableResolver::getCurrentLineIndex);
36+
VARIABLES.put("TM_LINE_NUMBER", DocumentVariableResolver::getCurrentLineNumber);
37+
VARIABLES.put("TM_FILENAME", DocumentVariableResolver::getFileName);
38+
VARIABLES.put("TM_DIRECTORY", DocumentVariableResolver::getDirectory);
39+
VARIABLES.put("TM_FILEPATH", DocumentVariableResolver::getPath);
40+
}
41+
42+
private Document document;
43+
private TextPosition position;
44+
45+
public DocumentVariableResolver(Document document, TextPosition position) {
46+
this.document = document;
47+
this.position = position;
48+
}
49+
50+
@Override
51+
public boolean isVar(String name) {
52+
return VARIABLES.containsKey(name);
53+
}
54+
55+
@Override
56+
public String resolve(String name) {
57+
return VARIABLES.get(name).apply(document, position);
58+
}
59+
60+
private static String getSelectedText(Document doc, TextPosition pos) {
61+
return doc.getContentRange(doc.getSelectedTextRange());
62+
}
63+
64+
private static String getCurrentLine(Document doc, TextPosition pos) {
65+
return doc.getLineContent(pos.getLine());
66+
}
67+
68+
private static String getCurrentLineIndex(Document doc, TextPosition pos) {
69+
return String.valueOf(pos.getLine());
70+
}
71+
72+
private static String getCurrentLineNumber(Document doc, TextPosition pos) {
73+
return String.valueOf(pos.getLine() + 1);
74+
}
75+
76+
private static String getFileName(Document doc, TextPosition pos) {
77+
return doc.getFile().getName();
78+
}
79+
80+
private static String getDirectory(Document doc, TextPosition pos) {
81+
return doc.getFile().getLocation().parent().toString();
82+
}
83+
84+
private static String getPath(Document doc, TextPosition pos) {
85+
return doc.getFile().getLocation().toString();
86+
}
87+
88+
private static String getCurrentWord(Document doc, TextPosition pos) {
89+
String line = doc.getLineContent(pos.getLine());
90+
if (line.length() == 0) {
91+
return "";
92+
}
93+
int start = pos.getCharacter();
94+
int end = start;
95+
while (start > 0 && !Character.isWhitespace(line.charAt(start - 1))) {
96+
start--;
97+
}
98+
while (end < line.length() && !Character.isWhitespace(line.charAt(end))) {
99+
end++;
100+
}
101+
return line.substring(start, end);
102+
}
103+
}

plugins/plugin-languageserver/che-plugin-languageserver-ide/src/main/java/org/eclipse/che/plugin/languageserver/ide/editor/codeassist/LanguageServerCodeAssistProcessor.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.eclipse.che.ide.api.editor.codeassist.CodeAssistCallback;
2121
import org.eclipse.che.ide.api.editor.codeassist.CodeAssistProcessor;
2222
import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal;
23+
import org.eclipse.che.ide.api.editor.link.HasLinkedMode;
2324
import org.eclipse.che.ide.api.editor.texteditor.TextEditor;
2425
import org.eclipse.che.ide.filters.FuzzyMatches;
2526
import org.eclipse.che.ide.filters.Match;
@@ -40,7 +41,7 @@ public class LanguageServerCodeAssistProcessor implements CodeAssistProcessor {
4041
private final ServerCapabilities serverCapabilities;
4142
private final TextDocumentServiceClient documentServiceClient;
4243
private final FuzzyMatches fuzzyMatches;
43-
private final LatestCompletionResult latestCompletionResult;
44+
private LatestCompletionResult latestCompletionResult;
4445
private String lastErrorMessage;
4546

4647
@Inject
@@ -57,7 +58,7 @@ public LanguageServerCodeAssistProcessor(
5758
this.imageProvider = imageProvider;
5859
this.serverCapabilities = serverCapabilities;
5960
this.fuzzyMatches = fuzzyMatches;
60-
this.latestCompletionResult = new LatestCompletionResult();
61+
this.latestCompletionResult = LatestCompletionResult.NO_RESULT;
6162
}
6263

6364
@Override
@@ -78,14 +79,19 @@ public void computeCompletionProposals(
7879

7980
if (!triggered && latestCompletionResult.isGoodFor(documentId, offset, currentWord)) {
8081
// no need to send new completion request
81-
computeProposals(currentWord, offset - latestCompletionResult.getOffset(), callback);
82+
computeProposals(
83+
(HasLinkedMode) editor,
84+
currentWord,
85+
offset - latestCompletionResult.getOffset(),
86+
callback);
8287
} else {
8388
documentServiceClient
8489
.completion(documentPosition)
8590
.then(
8691
list -> {
87-
latestCompletionResult.update(documentId, offset, currentWord, list);
88-
computeProposals(currentWord, 0, callback);
92+
latestCompletionResult =
93+
new LatestCompletionResult(documentId, offset, currentWord, list);
94+
computeProposals((HasLinkedMode) editor, currentWord, 0, callback);
8995
})
9096
.catchError(
9197
error -> {
@@ -137,13 +143,15 @@ private List<Match> filter(String word, String label, String filterText) {
137143
return null;
138144
}
139145

140-
private void computeProposals(String currentWord, int offset, CodeAssistCallback callback) {
146+
private void computeProposals(
147+
HasLinkedMode editor, String currentWord, int offset, CodeAssistCallback callback) {
141148
List<CompletionProposal> proposals = newArrayList();
142149
for (ExtendedCompletionItem item : latestCompletionResult.getCompletionList().getItems()) {
143150
List<Match> highlights = filter(currentWord, item.getItem());
144151
if (highlights != null) {
145152
proposals.add(
146153
new CompletionItemBasedCompletionProposal(
154+
editor,
147155
item,
148156
currentWord,
149157
documentServiceClient,

0 commit comments

Comments
 (0)