PT 155476005 - Support for jumpy completions

This commit is contained in:
nsingh
2018-06-06 18:11:41 -07:00
parent 1a8f536b60
commit 0c09c7d423
14 changed files with 548 additions and 226 deletions

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015 Pivotal, Inc.
* Copyright (c) 2015, 2018 Pivotal, 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
@@ -142,7 +142,7 @@ public class DocumentEdits implements ProposalApplier {
}
@Override
void apply(DocumentState doc) throws BadLocationException {
void apply(IDocumentState doc) throws BadLocationException {
doc.insert(grabCursor, offset, text);
}
@@ -169,7 +169,7 @@ public class DocumentEdits implements ProposalApplier {
}
public abstract int getStart();
public abstract int getEnd();
abstract void apply(DocumentState doc) throws BadLocationException;
abstract void apply(IDocumentState doc) throws BadLocationException;
@Override
public abstract String toString();
}
@@ -186,13 +186,17 @@ public class DocumentEdits implements ProposalApplier {
}
@Override
void apply(DocumentState doc) throws BadLocationException {
void apply(IDocumentState doc) throws BadLocationException {
doc.delete(grabCursor, start, end);
}
@Override
public String toString() {
return "del("+start+"->"+end+")";
try {
return "del("+start+"->"+end+", ["+ doc.textBetween(start, end)+"])";
} catch (BadLocationException e) {
return "del("+start+"->"+end+")";
}
}
@Override
@@ -207,112 +211,17 @@ public class DocumentEdits implements ProposalApplier {
}
private interface OffsetTransformer {
public interface OffsetTransformer {
int transform(int offset, Direction dir);
}
private static final OffsetTransformer NULL_TRANSFORM = new OffsetTransformer() {
static final OffsetTransformer NULL_TRANSFORM = new OffsetTransformer() {
@Override
public int transform(int offset, Direction dir) {
return offset;
}
};
/**
* DocumentState provides methods to modify a document, its methods accept
* offsets expressed relative to the original document contents and keeps track
* of a OffsetTransformer that maps them to offsets in the current document.
*/
private static class DocumentState {
private IDocument doc; //may be null, in which case no actual modifications are performed
private OffsetTransformer org2new = NULL_TRANSFORM;
private int selection = -1; //-1 Means no edits where applied that change selection so
// the current selection is unknown
public DocumentState(IDocument doc) {
this.doc = doc;
}
public void insert(boolean grabCursor, int start, final String text) throws BadLocationException {
final int tStart = org2new.transform(start, Direction.AFTER);
if (!text.isEmpty()) {
if (doc!=null) {
doc.replace(tStart, 0, text);
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
@Override
public int transform(int org, Direction dir) {
int tOffset = parent.transform(org, dir);
if (tOffset<tStart) {
return tOffset;
} else if (tOffset>tStart) {
return tOffset + text.length();
} else /* tOffset==tStart*/ {
if (dir==Direction.BEFORE) {
return tOffset;
} else {
return tOffset + text.length();
}
}
}
};
}
if (grabCursor) {
selection = tStart+text.length();
} else if (selection > tStart) {
selection += text.length();
}
}
public void delete(boolean grabCursor, final int start, final int end) throws BadLocationException {
final int tStart = org2new.transform(start, Direction.AFTER);
if (end>start) { // skip work for 'delete nothing' op
final int tEnd = org2new.transform(end, Direction.AFTER);
if (tEnd>tStart) { // skip work for 'delete nothing' op
if (doc!=null) {
doc.replace(tStart, tEnd-tStart, "");
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
@Override
public int transform(int org, Direction dir) {
int tOffset = parent.transform(org, dir);
if (tOffset<=tStart) {
return tOffset;
} else if (tOffset>=tEnd) {
return tOffset - tEnd + tStart;
} else {
return start;
}
}
};
}
}
if (grabCursor) {
selection = tStart;
} else if (selection>tStart) {
int len = end - start;
if (len > 0) {
selection = Math.max(tStart, selection-len);
}
}
}
@Override
public String toString() {
if (doc==null) {
return super.toString();
}
StringBuilder buf = new StringBuilder();
buf.append("DocumentState(\n");
buf.append(doc.get()+"\n");
buf.append(")\n");
return buf.toString();
}
}
private List<Edit> edits = new ArrayList<Edit>();
private IDocument doc;
@@ -372,7 +281,11 @@ public class DocumentEdits implements ProposalApplier {
@Override
public void apply(IDocument _doc) throws Exception {
DocumentState doc = new DocumentState(_doc);
IDocumentState doc = new DocumentState(_doc);
apply(doc);
}
public void apply(IDocumentState doc) throws Exception {
for (Edit edit : edits) {
edit.apply(doc);
}

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.completion;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.Direction;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.OffsetTransformer;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* DocumentState provides methods to modify a document, its methods accept
* offsets expressed relative to the original document contents and keeps track
* of a OffsetTransformer that maps them to offsets in the current document.
*/
public class DocumentState implements IDocumentState {
IDocument doc; //may be null, in which case no actual modifications are performed
OffsetTransformer org2new = DocumentEdits.NULL_TRANSFORM;
int selection = -1; //-1 Means no edits where applied that change selection so
// the current selection is unknown
public DocumentState(IDocument doc) {
this.doc = doc;
}
public OffsetTransformer getOrg2New() {
return this.org2new;
}
public int getCursor() {
return selection;
}
/* (non-Javadoc)
* @see org.springframework.ide.vscode.commons.languageserver.completion.IDocumentState#insert(boolean, int, java.lang.String)
*/
@Override
public void insert(boolean grabCursor, int start, final String text) throws BadLocationException {
final int tStart = org2new.transform(start, Direction.AFTER);
if (!text.isEmpty()) {
if (doc!=null) {
doc.replace(tStart, 0, text);
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
@Override
public int transform(int org, Direction dir) {
int tOffset = parent.transform(org, dir);
if (tOffset<tStart) {
return tOffset;
} else if (tOffset>tStart) {
return tOffset + text.length();
} else /* tOffset==tStart*/ {
if (dir==Direction.BEFORE) {
return tOffset;
} else {
return tOffset + text.length();
}
}
}
};
}
if (grabCursor) {
selection = tStart+text.length();
} else if (selection > tStart) {
selection += text.length();
}
}
/* (non-Javadoc)
* @see org.springframework.ide.vscode.commons.languageserver.completion.IDocumentState#delete(boolean, int, int)
*/
@Override
public void delete(boolean grabCursor, final int start, final int end) throws BadLocationException {
final int tStart = org2new.transform(start, Direction.AFTER);
if (end>start) { // skip work for 'delete nothing' op
final int tEnd = org2new.transform(end, Direction.AFTER);
if (tEnd>tStart) { // skip work for 'delete nothing' op
if (doc!=null) {
doc.replace(tStart, tEnd-tStart, "");
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
@Override
public int transform(int org, Direction dir) {
int tOffset = parent.transform(org, dir);
if (tOffset<=tStart) {
return tOffset;
} else if (tOffset>=tEnd) {
return tOffset - tEnd + tStart;
} else {
return start;
}
}
};
}
}
if (grabCursor) {
selection = tStart;
} else if (selection>tStart) {
int len = end - start;
if (len > 0) {
selection = Math.max(tStart, selection-len);
}
}
}
@Override
public String toString() {
if (doc==null) {
return super.toString();
}
StringBuilder buf = new StringBuilder();
buf.append("DocumentState(\n");
buf.append(doc.get()+"\n");
buf.append(")\n");
return buf.toString();
}
public IDocument getDocument() {
return this.doc;
}
}

View File

@@ -0,0 +1,21 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.completion;
import org.springframework.ide.vscode.commons.util.BadLocationException;
public interface IDocumentState {
void insert(boolean grabCursor, int start, String text) throws BadLocationException;
void delete(boolean grabCursor, int start, int end) throws BadLocationException;
}

View File

@@ -0,0 +1,167 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.completion;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextEdit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.Direction;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.OffsetTransformer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableList;
public class LspCompletionInterpreter implements IDocumentState {
private DocumentState docState;
private DocumentRegion mainLine;
private DocumentRegion mainEditRegion;
private DocumentRegion beforeMainEditRegion;
private DocumentRegion afterMainEditRegion;
private boolean needsCursorMove = false;
private TextDocument originalDoc;
private SimpleLanguageServer server;
private static final Logger logger = LoggerFactory.getLogger(LspCompletionInterpreter.class);
public LspCompletionInterpreter(TextDocument originalDoc, int cursor, SimpleLanguageServer server) {
this.originalDoc = originalDoc;
this.server = server;
this.docState = new DocumentState(originalDoc.copy());
this.mainLine = new DocumentRegion(originalDoc, originalDoc.getLineInformationOfOffset(cursor));
}
@Override
public void insert(boolean grabCursor, int start, String text) throws BadLocationException {
trackEditRegions(start, start);
docState.insert(grabCursor, start, text);
}
@Override
public void delete(boolean grabCursor, int start, int end) throws BadLocationException {
trackEditRegions(start, end);
docState.delete(grabCursor, start, end);
}
protected void trackEditRegions(int start, int end) {
DocumentRegion editRegion = new DocumentRegion(originalDoc, start, end);
this.needsCursorMove = true;
if (isMainEdit(start, end)) {
mainEditRegion = editRegion.merge(mainEditRegion);
this.needsCursorMove = false;
} else if (allBefore(start, end)) {
beforeMainEditRegion = editRegion.merge(beforeMainEditRegion);
} else if (allAfter(start, end)){
afterMainEditRegion = editRegion.merge(afterMainEditRegion);
} else {
if (start < mainLine.getStart()) {
DocumentRegion beforePiece = new DocumentRegion(originalDoc, start, mainLine.getStart());
beforeMainEditRegion = beforePiece.merge(beforeMainEditRegion);
editRegion = new DocumentRegion(originalDoc, mainLine.getStart(), editRegion.getEnd());
}
if (end > mainLine.getEnd()) {
DocumentRegion afterPiece = new DocumentRegion(originalDoc, mainLine.getEnd(), end);
afterMainEditRegion = afterPiece.merge(afterMainEditRegion);
editRegion = new DocumentRegion(originalDoc, start, mainLine.getEnd());
}
mainEditRegion = editRegion.merge(mainEditRegion);
}
}
public void resolveEdits(CompletionItem item) throws BadLocationException {
if (mainEditRegion != null) {
TextEdit mainEdit = new TextEdit();
mainEdit.setRange(originalDoc.toRange(mainEditRegion));
OffsetTransformer org2New = docState.getOrg2New();
String newText = docState.getDocument().textBetween(
org2New.transform(mainEditRegion.getStart(), Direction.BEFORE),
org2New.transform(mainEditRegion.getEnd(), Direction.AFTER)
);
item.setTextEdit(mainEdit);
item.setInsertTextFormat(InsertTextFormat.Snippet);
if (Boolean.getBoolean("lsp.completions.indentation.enable")) {
mainEdit.setNewText(newText);
} else {
mainEdit.setNewText(vscodeIndentFix(originalDoc, originalDoc.toPosition(mainEditRegion.getStart()), newText));
}
} else {
item.setInsertText("");
}
ImmutableList.Builder<TextEdit> additionalEdits = ImmutableList.builder();
resolveAdditionalEdit(beforeMainEditRegion, additionalEdits);
resolveAdditionalEdit(afterMainEditRegion, additionalEdits);
item.setAdditionalTextEdits(additionalEdits.build());
if (needsCursorMove) {
Position position = docState.getDocument().toPosition(docState.getCursor());
item.setCommand(new Command("Move Cursor", server.MOVE_CURSOR_COMMAND_ID, ImmutableList.of(originalDoc.getUri(), position)));
}
}
protected void resolveAdditionalEdit(DocumentRegion editRegion, ImmutableList.Builder<TextEdit> builder)
throws BadLocationException {
if (editRegion != null) {
TextEdit edit = new TextEdit();
edit.setRange(originalDoc.toRange(editRegion));
OffsetTransformer org2New = docState.getOrg2New();
String newText = docState.getDocument().textBetween(
org2New.transform(editRegion.getStart(), Direction.BEFORE),
org2New.transform(editRegion.getEnd(), Direction.AFTER));
edit.setNewText(newText);
builder.add(edit);
}
}
private boolean isMainEdit(int start, int end) {
return mainLine.containsOffset(start) && mainLine.containsOffset(end);
}
private boolean allBefore(int start, int end) {
return end <= mainLine.getStart();
}
private boolean allAfter(int start, int end) {
return start >= mainLine.getEnd();
}
private static String vscodeIndentFix(TextDocument doc, Position start, String newText) {
//Vscode applies some magic indent to a multi-line edit text. We do everything ourself so we have adjust for the magic
// and do some kind of 'inverse magic' here.
//See here: https://github.com/Microsoft/language-server-protocol/issues/83
IndentUtil indenter = new IndentUtil(doc);
try {
String refIndent = indenter.getReferenceIndent(doc.toOffset(start), doc);
if (!refIndent.isEmpty()) {
return StringUtil.stripIndentation(refIndent, newText);
}
} catch (BadLocationException e) {
logger.error("", e);
}
return newText;
}
}

View File

@@ -20,26 +20,19 @@ import java.util.function.Consumer;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.TextReplace;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.SortKeys;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonPrimitive;
import reactor.core.publisher.Mono;
@@ -153,7 +146,7 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
break;
}
try {
items.add(adaptItem(doc, c, sortkeys));
items.add(adaptItem(doc, offset, c, sortkeys));
} catch (Exception e) {
logger.error("error computing completion", e);
}
@@ -166,14 +159,14 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
return Mono.just(SimpleTextDocumentService.NO_COMPLETIONS);
}
private CompletionItem adaptItem(TextDocument doc, ICompletionProposal completion, SortKeys sortkeys) throws Exception {
private CompletionItem adaptItem(TextDocument doc, int cursor, ICompletionProposal completion, SortKeys sortkeys) throws Exception {
CompletionItem item = new CompletionItem();
item.setLabel(completion.getLabel());
item.setKind(completion.getKind());
item.setSortText(sortkeys.next());
item.setFilterText(completion.getFilterText());
item.setDetail(completion.getDetail());
resolveEdits(doc, completion, item); //Warning. Its not allowed by LSP spec to resolveEdits
resolveEdits(doc, cursor, completion, item); //Warning. Its not allowed by LSP spec to resolveEdits
//lazy as we used to do in the past.
if (resolver!=null) {
item.setData(resolver.resolveLater(completion, doc));
@@ -190,20 +183,28 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
item.setDocumentation(content);
}
private static void resolveEdits(TextDocument doc, ICompletionProposal completion, CompletionItem item) {
Optional<TextEdit> mainEdit = adaptEdits(doc, completion.getTextEdit());
if (mainEdit.isPresent()) {
item.setTextEdit(mainEdit.get());
item.setInsertTextFormat(InsertTextFormat.Snippet);
} else {
item.setInsertText("");
}
/**
* Fill in the text edit related fields in completion Item: textEdit, additionalTextEdits, command
*
*/
private void resolveEdits(TextDocument doc, int cursor, ICompletionProposal completion, CompletionItem item) {
completion.getAdditionalEdit().ifPresent(edit -> {
adaptEdits(doc, edit).ifPresent(extraEdit -> {
item.setAdditionalTextEdits(ImmutableList.of(extraEdit));
});
});
try {
LspCompletionInterpreter lspCompletionInterpreter = new LspCompletionInterpreter(doc, cursor, server);
Optional<DocumentEdits> additionalEdit = completion.getAdditionalEdit();
if (additionalEdit.isPresent()) {
additionalEdit.get().apply(lspCompletionInterpreter);
}
DocumentEdits mainEdit = completion.getTextEdit();
mainEdit.apply(lspCompletionInterpreter);
lspCompletionInterpreter.resolveEdits(item);
} catch (Exception e) {
LOG.get().error("", e);
}
}
private static String toMarkdown(Renderable r) {
@@ -213,46 +214,33 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
return null;
}
private static Optional<TextEdit> adaptEdits(TextDocument doc, DocumentEdits edits) {
try {
TextReplace replaceEdit = edits.asReplacement(doc);
if (replaceEdit==null) {
//The original edit does nothing.
return Optional.empty();
} else {
TextDocument newDoc = doc.copy();
edits.apply(newDoc);
TextEdit vscodeEdit = new TextEdit();
vscodeEdit.setRange(doc.toRange(replaceEdit.start, replaceEdit.end-replaceEdit.start));
if (Boolean.getBoolean("lsp.completions.indentation.enable")) {
vscodeEdit.setNewText(replaceEdit.newText);
} else {
vscodeEdit.setNewText(vscodeIndentFix(doc, vscodeEdit.getRange().getStart(), replaceEdit.newText));
}
//TODO: cursor offset within newText? for now we assume its always at the end.
return Optional.of(vscodeEdit);
}
} catch (Exception e) {
LOG.get().error("{}", e);
return Optional.empty();
}
}
private static String vscodeIndentFix(TextDocument doc, Position start, String newText) {
//Vscode applies some magic indent to a multi-line edit text. We do everything ourself so we have adjust for the magic
// and do some kind of 'inverse magic' here.
//See here: https://github.com/Microsoft/language-server-protocol/issues/83
IndentUtil indenter = new IndentUtil(doc);
try {
String refIndent = indenter.getReferenceIndent(doc.toOffset(start), doc);
if (!refIndent.isEmpty()) {
return StringUtil.stripIndentation(refIndent, newText);
}
} catch (BadLocationException e) {
LOG.get().error("{}", e);
}
return newText;
}
// private static Optional<TextEdit> adaptEdits(TextDocument doc, DocumentEdits edits) {
// try {
// TextReplace replaceEdit = edits.asReplacement(doc);
// if (replaceEdit==null) {
// //The original edit does nothing.
// return Optional.empty();
// } else {
// TextDocument newDoc = doc.copy();
// edits.apply(newDoc);
// TextEdit vscodeEdit = new TextEdit();
// vscodeEdit.setRange(doc.toRange(replaceEdit.start, replaceEdit.end-replaceEdit.start));
// if (Boolean.getBoolean("lsp.completions.indentation.enable")) {
// vscodeEdit.setNewText(replaceEdit.newText);
// } else {
// vscodeEdit.setNewText(vscodeIndentFix(doc, vscodeEdit.getRange().getStart(), replaceEdit.newText));
// }
// //TODO: cursor offset within newText? for now we assume its always at the end.
// return Optional.of(vscodeEdit);
// }
// } catch (Exception e) {
// LOG.get().error("{}", e);
// return Optional.empty();
// }
// }
@Override
public CompletionItem resolveCompletion(CompletionItem unresolved) {

View File

@@ -38,6 +38,7 @@ import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.Registration;
import org.eclipse.lsp4j.RegistrationParams;
@@ -64,6 +65,7 @@ import org.springframework.ide.vscode.commons.languageserver.jdt.ls.ClasspathLis
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix;
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixEdit.CursorMovement;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixResolveParams;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
@@ -77,6 +79,8 @@ import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -102,8 +106,11 @@ public class SimpleLanguageServer implements Sts4LanguageServer, LanguageClientA
private static final Scheduler RECONCILER_SCHEDULER = Schedulers.newSingle("Reconciler");
public final String EXTENSION_ID;
private final String CODE_ACTION_COMMAND_ID;
public final String MOVE_CURSOR_COMMAND_ID;
protected final LazyCompletionResolver completionResolver = createCompletionResolver();
private SimpleTextDocumentService tds;
@@ -140,6 +147,10 @@ public class SimpleLanguageServer implements Sts4LanguageServer, LanguageClientA
private Map<String, ExecuteCommandHandler> commands = new HashMap<>();
private Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.create();
private AsyncRunner async = new AsyncRunner();
private ClasspathListenerManager classpathListenerManager;
@@ -177,6 +188,7 @@ public class SimpleLanguageServer implements Sts4LanguageServer, LanguageClientA
public SimpleLanguageServer(String extensionId) {
this.EXTENSION_ID = extensionId;
this.CODE_ACTION_COMMAND_ID = "sts."+EXTENSION_ID+".codeAction";
this.MOVE_CURSOR_COMMAND_ID = "sts."+EXTENSION_ID+".moveCursor";
}
protected CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
@@ -198,6 +210,12 @@ public class SimpleLanguageServer implements Sts4LanguageServer, LanguageClientA
return applyEdit.flatMap(r -> r.isApplied() ? moveCursor : Mono.just(new ApplyWorkspaceEditResponse(true)));
})
.toFuture();
} else if (MOVE_CURSOR_COMMAND_ID.equals(params.getCommand())) {
Assert.isLegal(params.getArguments().size()==2);
String uri = ((JsonPrimitive)params.getArguments().get(0)).getAsString();
Position position = gson.fromJson((JsonObject)params.getArguments().get(1), Position.class);
return client.moveCursor(new CursorMovement(uri, position));
}
Log.warn("Unknown command ignored: "+params.getCommand());
return CompletableFuture.completedFuture(false);
@@ -372,10 +390,15 @@ public class SimpleLanguageServer implements Sts4LanguageServer, LanguageClientA
codeLensOptions.setResolveProvider(hasCodeLensResolveProvider());
c.setCodeLensProvider(codeLensOptions );
}
if (hasExecuteCommandSupport && hasQuickFixes()) {
c.setExecuteCommandProvider(new ExecuteCommandOptions(ImmutableList.of(
CODE_ACTION_COMMAND_ID
)));
if (hasExecuteCommandSupport ) {
ImmutableList.Builder<String> builder = ImmutableList.builder();
if (hasQuickFixes()) {
builder.add(CODE_ACTION_COMMAND_ID);
}
builder.add(MOVE_CURSOR_COMMAND_ID);
c.setExecuteCommandProvider(new ExecuteCommandOptions(builder.build()));
}
if (hasWorkspaceSymbolHandler()) {
c.setWorkspaceSymbolProvider(true);

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2017 Pivotal, Inc.
* Copyright (c) 2016, 2018 Pivotal, 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
@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.util.text;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -139,6 +140,16 @@ public class DocumentRegion implements CharSequence, IRegion {
public boolean containsOffset(int absoluteOffset) {
return absoluteOffset>=start && absoluteOffset <= end;
}
public Optional<DocumentRegion> intersection(DocumentRegion other) {
int newStart = Math.max(this.start, other.start);
int newEnd = Math.min(this.end, other.end);
if (newEnd >= newStart) {
return Optional.of(new DocumentRegion(doc, newStart, newEnd));
} else {
return Optional.empty();
}
}
@Override
public int length() {
@@ -339,4 +350,17 @@ public class DocumentRegion implements CharSequence, IRegion {
return getStart();
}
/**
* Computes smallest document region that encompasses this region and the other region.
*/
public DocumentRegion merge(DocumentRegion otherEdit) {
if (otherEdit == null) {
return this;
} else {
int newStart = Math.min(otherEdit.getStart(), this.getStart());
int newEnd = Math.max(otherEdit.getEnd(), this.getEnd());
return new DocumentRegion(doc, newStart, newEnd);
}
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2017 Pivotal, Inc.
* Copyright (c) 2016, 2018 Pivotal, 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
@@ -45,5 +45,19 @@ public class DocumentUtil {
throw new IllegalStateException("Bug!", e);
}
}
/**
* Determine the 'known minimum' of two document offsets. Correctly handle
* when either one or both are '-1' (unknown).
*/
public static int min(int a, int b) {
if (a==-1) {
return b;
} else if (b==-1) {
return a;
} else {
return Math.min(a, b);
}
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2017 Pivotal, Inc.
* Copyright (c) 2016-2018 Pivotal, 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
@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.commons.util.text;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.springframework.ide.vscode.commons.util.BadLocationException;
@@ -32,5 +33,6 @@ public interface IDocument {
LanguageId getLanguageId();
int getVersion();
Range toRange(IRegion asRegion) throws BadLocationException;
Position toPosition(int offset) throws BadLocationException;
}

View File

@@ -118,6 +118,7 @@ public class TextDocument implements IDocument {
}
@Override
public Position toPosition(int offset) throws BadLocationException {
int line = lineNumber(offset);
int startOfLine = startOfLine(line);

View File

@@ -30,6 +30,7 @@ import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.Unicodes;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.DocumentUtil;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
@@ -87,7 +88,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
SNode current = root.find(offset);
int cursorIndent = doc.getColumn(offset);
int nodeIndent = current.getIndent();
int baseIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
int baseIndent = DocumentUtil.min(cursorIndent, nodeIndent);
List<SNode> contextNodes = getContextNodes(doc, current, offset, baseIndent);
if (current.getNodeType()==SNodeType.RAW) {
//relaxed indentation
@@ -460,7 +461,7 @@ public class YamlCompletionEngine implements ICompletionEngine {
// rather than the structur-tree to determine the 'context' node.
int cursorIndent = doc.getColumn(offset);
int nodeIndent = node.getIndent();
int currentIndent = YamlIndentUtil.minIndent(cursorIndent, nodeIndent);
int currentIndent = DocumentUtil.min(cursorIndent, nodeIndent);
while (node.getIndent()==-1 || (node.getIndent()>=currentIndent && node.getNodeType()!=SNodeType.DOC)) {
node = node.getParent();
}

View File

@@ -53,20 +53,6 @@ public class YamlIndentUtil {
this(doc.getDefaultLineDelimiter());
}
/**
* Determine the 'known minimum' of two indentation levels. Correctly handle
* when either one or both indent levels are '-1' (unknown).
*/
public static int minIndent(int a, int b) {
if (a==-1) {
return b;
} else if (b==-1) {
return a;
} else {
return Math.min(a, b);
}
}
public static void addIndent(int indent, StringBuilder buf) {
for (int i = 0; i < indent; i++) {
buf.append(' ');

View File

@@ -14,6 +14,7 @@ package org.springframework.ide.vscode.languageserver.testharness;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness.getDocString;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertDoesNotContain;
@@ -31,6 +32,7 @@ import java.util.stream.Collectors;
import javax.swing.text.BadLocationException;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.Diagnostic;
@@ -48,16 +50,16 @@ import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.Assert;
import org.springframework.ide.vscode.commons.languageserver.HighlightParams;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentState;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.Unicodes;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableList;
import reactor.core.publisher.Flux;
import static org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness.*;
public class Editor {
public static final Predicate<CompletionItem> RELAXED_COMPLETION
@@ -445,7 +447,9 @@ public class Editor {
public void apply(CompletionItem completion) throws Exception {
completion = harness.resolveCompletionItem(completion);
TextEdit edit = completion.getTextEdit();
String docText = doc.getText();
TextDocument textDoc = new TextDocument(getUri(), languageId);
textDoc.setText(doc.getText());
DocumentState docState = new DocumentState(textDoc);
if (edit!=null) {
String replaceWith = edit.getNewText();
int cursorReplaceOffset = 0;
@@ -474,9 +478,32 @@ public class Editor {
Range rng = edit.getRange();
int start = doc.toOffset(rng.getStart());
int end = doc.toOffset(rng.getEnd());
replaceText(start, end, replaceWith);
selectionStart = selectionEnd = start+cursorReplaceOffset;
docState.delete(true, start, end);
docState.insert(true, start, replaceWith.substring(0, cursorReplaceOffset));
docState.insert(false, start, replaceWith.substring(cursorReplaceOffset));
List<TextEdit> additionalTextEdits = completion.getAdditionalTextEdits();
if (additionalTextEdits != null) {
for (TextEdit textEdit : additionalTextEdits) {
int textEditStart = doc.toOffset(textEdit.getRange().getStart());
docState.delete(false, textEditStart, doc.toOffset(textEdit.getRange().getEnd()));
docState.insert(false, textEditStart, textEdit.getNewText());
}
}
setRawText(docState.getDocument().get());
int cursor = docState.getCursor();
if (cursor >= 0) {
this.selectionStart = this.selectionEnd = cursor;
}
Command command = completion.getCommand();
if (command != null) {
harness.perform(command);
}
} else {
String docText = doc.getText();
String insertText = getInsertText(completion);
String newText = docText.substring(0, selectionStart) + insertText + docText.substring(selectionStart);
@@ -486,6 +513,7 @@ public class Editor {
}
}
private String getInsertText(CompletionItem completion) {
String s = completion.getInsertText();
if (s==null) {

View File

@@ -1401,23 +1401,6 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest {
" bar: <*>"
);
assertCompletion(
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
"other:\n" +
"foo.nested.nested.b<*>"
,
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
" nested:\n" +
" bar: <*>\n"+
"other:\n"
);
assertCompletion(
"foo:\n" +
" nested:\n" +
@@ -1433,22 +1416,6 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest {
" bar: <*>\n"
);
assertCompletion(
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
"other:\n" +
"foo.nested.bar.b<*>"
,
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
" bar: <*>\n" +
"other:\n"
);
assertCompletion(
"foo:\n" +
" nested:\n" +
@@ -1465,6 +1432,62 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest {
}
@Test public void testJumpyInsertion() throws Exception {
String[] names = {"foo", "nested", "bar"};
int levels = 4;
generateNestedProperties(levels, names, "");
assertCompletion(
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
"other:\n" +
"foo.nested.bar.b<*>"
,
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
" bar: <*>\n" +
"other:"
);
assertCompletion(
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
"other:\n" +
"foo.nested.nested.b<*>"
,
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
" nested:\n" +
" bar: <*>\n"+
"other:"
);
assertCompletion(
"foo.nested.nested.b<*>\n" +
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
"other:"
,
"foo:\n" +
" nested:\n" +
" bar:\n" +
" foo:\n" +
" nested:\n" +
" bar: <*>\n"+
"other:"
);
}
@Test public void testBooleanValueCompletion() throws Exception {
defaultTestData();
assertCompletions(