diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java index 8c33cb0ff..52cd8e03c 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentEdits.java @@ -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 (tOffsettStart) { - 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 edits = new ArrayList(); 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); } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentState.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentState.java new file mode 100644 index 000000000..4e092dead --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/DocumentState.java @@ -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 (tOffsettStart) { + 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; + } +} \ No newline at end of file diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/IDocumentState.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/IDocumentState.java new file mode 100644 index 000000000..dfaad1f14 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/IDocumentState.java @@ -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; + +} \ No newline at end of file diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/LspCompletionInterpreter.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/LspCompletionInterpreter.java new file mode 100644 index 000000000..6c230f100 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/LspCompletionInterpreter.java @@ -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 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 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; + } +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java index 4e6cf0e04..229cd64ee 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java @@ -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 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 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 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 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) { diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java index aad758a86..967d830f8 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleLanguageServer.java @@ -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 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 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 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); diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java index 015bb237f..7ceb8b142 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java @@ -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 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); + } + } + } \ No newline at end of file diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentUtil.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentUtil.java index 4e8aef546..47ef27bd6 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentUtil.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentUtil.java @@ -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); + } + } } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/IDocument.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/IDocument.java index bfa565644..07d45664d 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/IDocument.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/IDocument.java @@ -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; } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java index 69d1a31d2..54eeed1cd 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/TextDocument.java @@ -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); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java index 3c5643647..3ef9e5292 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/completion/YamlCompletionEngine.java @@ -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 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(); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java index 0b2c8b406..0e1b2c635 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/util/YamlIndentUtil.java @@ -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(' '); diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 85351652f..4cb0e1dbf 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -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 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 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) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java index b2e901135..6735c3d5b 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationYamlEditorTest.java @@ -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(