diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/DocumentEdits.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/DocumentEdits.java new file mode 100644 index 000000000..b82d75a8f --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/DocumentEdits.java @@ -0,0 +1,325 @@ +/******************************************************************************* + * Copyright (c) 2015 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.completion; + +import java.util.ArrayList; + +import org.springframework.ide.vscode.util.Assert; +import org.springframework.ide.vscode.util.BadLocationException; +import org.springframework.ide.vscode.util.IDocument; +import org.springframework.ide.vscode.util.IRegion; +import org.springframework.ide.vscode.util.Region; + +import io.typefox.lsapi.TextEdit; + +/** + * Helper to make it easier to create composite modifications to IDocument. + *

+ * It allows building up a sequence of edits which are all expressed in terms of + * offsets in the unmodified document. (So, when computing edits based on a + * some kind of AST its is not necessary to recompute the AST or update its + * position information after each small modification). + *

+ * Similar functionality to Eclipse's {@link TextEdit} but unlike {@link TextEdit} + * it is not as finicky with respect to overlapping edits. We consider the + * order in which edits are created meaningful and give a logical semantics + * to edits that 'overlap'. + *

+ * Also, each edit affects the cursor position placing it at the end of + * that edit. This will mostly do what you would want it to, provided that + * you save the edit where you want the cursor to end-up at for last. + * + * @author Kris De Volder + */ +public class DocumentEdits implements ProposalApplier { + + // Note: for small number of edits this implementation is okay. + // for large number of edits it is potentially slow because of the + // way it transforms edit coordinates (a growing chain of + // OffsetTransformer is created so every extra edit added + // will take O(n) to preform the transform on its coordinates. + // So applying 'n' edits is O(n^2). + // + // A smarter way of doing this is possible. Here's a possible idea: + // + // For simplicity sake assume that all edits are 'independent' (i.e. + // changing their executing order doesn't matter. + // + // It is advantageous to sort the edits by position and execute them + // high to low because... we can then guarantee that the transform + // function that will apply to each edit does nothing on the coordinates + // that it cares about (since all prior edits only affect higher offets) + // + // Unfortunately the simplifying assumption does not allways hold. + // There are two problems: + // + // 1) updating the selection is order dependent. + // => this can be solved by observing that only the last + // edit operation need update the selection since it cancels + // all prior selections. + // => Mark the last operation with a 'flag' 'setSelection=true' + // and do not update the selection in any other operations. + // + // 2) some edits may not be independent + // => When the edits are sorted in descending order based on their 'end' + // coordinate them 'conflicting' edits should be adjacent and we can + // 'group them' together into 'cluster' where we can preserve their + // relative execution order. + // => While executing a 'cluster' we shall keep track of the offset + // transform function just like the current implementation does. + // => When the cluster of 'conflicting' operations has been dealt with + // the offset transform function no longer matters for the + // remaining edits who's offesets are all strictly 'smaller'. + // Thus the trasnform function can be discarded. + // + // Assuming most edits are independent and only a few of them conflict, then + // this algorithm can provide equivalent functinonality to the current one + // but for an 'average' performance which is O(n*log(n)) + // Of course worst-case is still O(n^2) but we wouldn't expect to hit that case + // assuming we mostly have lots of small edits to disjoint sections of the document. + // + // So... edit operations could be sorted based on their position + // and executed in decreasing order of their 'start'. + // + // The tricky part would be to preserve the order-dependent semantics. + + private class Insertion extends Edit { + private int offset; + private String text; + + public Insertion(int offset, String insert) { + this.offset = offset; + this.text = insert; + } + + @Override + void apply(DocumentState doc) throws BadLocationException { + doc.insert(offset, text); + } + + @Override + public String toString() { + return "ins("+text+"@"+offset+")"; + } + } + + private abstract class Edit { + abstract void apply(DocumentState doc) throws BadLocationException; + public abstract String toString(); + } + + private class Deletion extends Edit { + + private int start; + private int end; + + public Deletion(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + void apply(DocumentState doc) throws BadLocationException { + doc.delete(start, end); + } + + @Override + public String toString() { + return "del("+start+"->"+end+")"; + } + + } + + private interface OffsetTransformer { + int trasform(int offset); + } + + private static final OffsetTransformer NULL_TRANSFORM = new OffsetTransformer() { + public int trasform(int offset) { + 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(int start, final String text) throws BadLocationException { + final int tStart = org2new.trasform(start); + if (!text.isEmpty()) { + if (doc!=null) { + doc.replace(tStart, 0, text); + } + final OffsetTransformer parent = org2new; + org2new = new OffsetTransformer() { + public int trasform(int org) { + int tOffset = parent.trasform(org); + if (tOffsetstart) { // skip work for 'delete nothing' op + final int tEnd = org2new.trasform(end); + if (tEnd>tStart) { // skip work for 'delete nothing' op + if (doc!=null) { + doc.replace(tStart, tEnd-tStart, ""); + } + + final OffsetTransformer parent = org2new; + org2new = new OffsetTransformer() { + public int trasform(int org) { + int tOffset = parent.trasform(org); + if (tOffset<=tStart) { + return tOffset; + } else if (tOffset>=tEnd) { + return tOffset - tEnd + tStart; + } else { + return start; + } + } + }; + } + } + selection = tStart; + } + + @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 ArrayList edits = new ArrayList(); + private IDocument doc; + + public DocumentEdits(IDocument doc) { + this.doc = doc; + } + + public void delete(int start, int end) { + Assert.isLegal(start<=end); + edits.add(new Deletion(start, end)); + } + + public void delete(int offset, String text) { + delete(offset, offset+text.length()); + } + + public void insert(int offset, String insert) { + edits.add(new Insertion(offset, insert)); + } + + @Override + public IRegion getSelection(IDocument doc) throws Exception { + DocumentState selectionState = new DocumentState(null); + for (Edit edit : edits) { + edit.apply(selectionState); + } + if (selectionState.selection>=0) { + return new Region(selectionState.selection, 0); + } + return null; + } + + @Override + public void apply(IDocument _doc) throws Exception { + DocumentState doc = new DocumentState(_doc); + for (Edit edit : edits) { + edit.apply(doc); + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("DocumentModifier(\n"); + for (Edit edit : edits) { + buf.append(" "+edit); + } + buf.append(")\n"); + return buf.toString(); + } + + public void moveCursorTo(int newCursor) { + insert(newCursor, ""); + } + + public void deleteLineBackwardAtOffset(int offset) throws Exception { + int line = doc.getLineOfOffset(offset); + deleteLineBackward(line); + } + + /** + * Deletes the line of text with given line number, including either the following or + * preceding newline. If there is a choice between the preceding or following newline, + * the preceding newline is deleted. This will leave the cursor at the end of + * the preceding line. + *

+ * Note: a similar operation 'deleteLineForward' could be implemented prefering to + * delete the following newline. This would be equivalent except that it will leave the + * cursor at the start of the following line. + */ + public void deleteLineBackward(int lineNumber) throws BadLocationException { + IRegion line = doc.getLineInformation(lineNumber); + int startOfDeletion; + int endOfDeletion; + if (lineNumber>0) { + IRegion previousLine = doc.getLineInformation(lineNumber-1); + startOfDeletion = endOf(previousLine); + endOfDeletion = endOf(line); + } else if (lineNumber getCompletions(IDocument document, int offset) throws Exception; + +} diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/ICompletionProposal.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/ICompletionProposal.java new file mode 100644 index 000000000..60e9273b1 --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/ICompletionProposal.java @@ -0,0 +1,19 @@ +package org.springframework.ide.vscode.commons.completion; + +import io.typefox.lsapi.CompletionItemKind; + +/** + * Replaces STS/Eclipse's ICompletionProposal + */ +public interface ICompletionProposal { + + /** + * Transforms a proposal to make it standout less somehow. + */ + ICompletionProposal deemphasize(); + + String getLabel(); + CompletionItemKind getKind(); + DocumentEdits getTextEdit(); + +} diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/ProposalApplier.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/ProposalApplier.java new file mode 100644 index 000000000..fd1b783d7 --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/commons/completion/ProposalApplier.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2015 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.completion; + +import org.springframework.ide.vscode.util.IDocument; +import org.springframework.ide.vscode.util.IRegion; + +/** + * Interface that represents the methods that one needs to implement in order + * to define how content assist proposal is applied to a IDocument + * + * @author Kris De Volder + */ +public interface ProposalApplier { + + /** + * {@link ProposalApplier} that does nothing whatsoever. + */ + static ProposalApplier NULL = new ProposalApplier() { + @Override public IRegion getSelection(IDocument document) { return null; } + @Override public void apply(IDocument doc) {} + @Override public String toString() { return "NULL";}; + }; + + IRegion getSelection(IDocument document) throws Exception; + void apply(IDocument doc) throws Exception; + +} diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java index e6e0a4667..ef8ad8ff1 100644 --- a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/BadLocationException.java @@ -9,4 +9,7 @@ public class BadLocationException extends Exception { super(e); } + public BadLocationException() { + } + } diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/IDocument.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/IDocument.java index 9338e6e84..5addca46c 100644 --- a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/IDocument.java +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/IDocument.java @@ -12,5 +12,6 @@ public interface IDocument { int getLineOfOffset(int offset); IRegion getLineInformation(int line); int getLineOffset(int line); + void replace(int start, int len, String text); } diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/PrefixFinder.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/PrefixFinder.java new file mode 100644 index 000000000..18b29753b --- /dev/null +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/PrefixFinder.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +public abstract class PrefixFinder { + public String getPrefix(IDocument doc, int offset, int lowerBound) { + try { + if (doc == null || offset > doc.getLength()) + return null; + int prefixStart = offset; + while (prefixStart > lowerBound && isPrefixChar(doc.getChar(prefixStart-1))) { + prefixStart--; + } + return doc.get(prefixStart, offset-prefixStart); + } catch (BadLocationException e) { + return null; + } + } + + public String getPrefix(IDocument doc, int offset) { + return getPrefix(doc, offset, 0); + } + protected abstract boolean isPrefixChar(char c); +} \ No newline at end of file diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/SimpleTextDocumentService.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/SimpleTextDocumentService.java index 909b6ea07..50317b2ea 100644 --- a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/SimpleTextDocumentService.java +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/SimpleTextDocumentService.java @@ -2,6 +2,7 @@ package org.springframework.ide.vscode.util; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,6 +38,7 @@ import io.typefox.lsapi.TextDocumentPositionParams; import io.typefox.lsapi.TextEdit; import io.typefox.lsapi.VersionedTextDocumentIdentifier; import io.typefox.lsapi.WorkspaceEdit; +import io.typefox.lsapi.impl.CompletionListImpl; import io.typefox.lsapi.impl.DiagnosticImpl; import io.typefox.lsapi.impl.PublishDiagnosticsParamsImpl; import io.typefox.lsapi.services.TextDocumentService; @@ -70,7 +72,6 @@ public class SimpleTextDocumentService implements TextDocumentService { return new ArrayList<>(documents.values()); } - @Override public final void didChange(DidChangeTextDocumentParams params) { VersionedTextDocumentIdentifier docId = params.getTextDocument(); @@ -152,13 +153,17 @@ public class SimpleTextDocumentService implements TextDocumentService { return doc; } + public final static CompletableFuture NO_COMPLETIONS = Futures.of( + new CompletionListImpl(false, Collections.emptyList())); + + @Override public CompletableFuture completion(TextDocumentPositionParams position) { CompletionHandler h = completionHandler; if (h!=null) { return completionHandler.handle(position); } - return null; //TODO: does caller handle nulls? Or do we need to provide something that create a empty completion list? + return NO_COMPLETIONS; } @Override @@ -268,4 +273,8 @@ public class SimpleTextDocumentService implements TextDocumentService { } } + public synchronized TextDocument get(TextDocumentPositionParams params) { + return documents.get(params.getTextDocument().getUri()); + } + } diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/TextDocument.java b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/TextDocument.java index 80c6f3536..5bcbe440f 100644 --- a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/TextDocument.java +++ b/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/TextDocument.java @@ -5,13 +5,20 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.typefox.lsapi.Position; import io.typefox.lsapi.Range; import io.typefox.lsapi.TextDocumentContentChangeEvent; import io.typefox.lsapi.impl.PositionImpl; import io.typefox.lsapi.impl.RangeImpl; +import io.typefox.lsapi.impl.TextDocumentItemImpl; public class TextDocument implements IDocument { + //TODO: This representiation of 'document content' is simplistic and inefficient + // for large documents. + // Making any change, such as inserting a character a user typed, works by copying the String that represents the + // contents. This could really become problematic for largish-documents when there are frequent changes. + Pattern NEWLINE = Pattern.compile("\\r|\\n|\\r\\n|\\n\\r"); private int[] _lineStarts; @@ -22,6 +29,12 @@ public class TextDocument implements IDocument { this.uri = uri; } + private TextDocument(TextDocument other) { + this.uri = other.uri; + this.text = other.text; + this._lineStarts = other._lineStarts; //no need to reparse lines. + } + public String getUri() { return uri; } @@ -150,16 +163,15 @@ public class TextDocument implements IDocument { if (newlineFinder.find()) { return text.substring(newlineFinder.start(), newlineFinder.end()); } - return System.getProperty(System.getProperty("line.separator")); + return System.getProperty("line.separator"); } @Override public char getChar(int offset) throws BadLocationException { - try { + if (offset>=0 && offset PROBLEM_COMPARATOR = new Comparator() { @Override @@ -208,16 +209,29 @@ public class Editor { private void apply(CompletionItem completion) throws Exception { TextEdit edit = completion.getTextEdit(); - if (edit!=null) { - fail("Support for completions with TextEdit's not yet implemented in this test harness."); - } - String insertText = getInsertText(completion); String docText = document.getText(); - String newText = docText.substring(0, selectionStart) + insertText + docText.substring(selectionStart); - - selectionStart+= insertText.length(); - selectionEnd += insertText.length(); - setRawText(newText); + if (edit!=null) { + String replaceWith = edit.getNewText(); + int cursorReplaceOffset = replaceWith.indexOf(VS_CODE_CURSOR_MARKER); + if (cursorReplaceOffset>=0) { + replaceWith = replaceWith.substring(0, cursorReplaceOffset) + replaceWith.substring(cursorReplaceOffset+VS_CODE_CURSOR_MARKER.length()); + } else { + cursorReplaceOffset = replaceWith.length(); + } + Range rng = edit.getRange(); + int start = document.toOffset(rng.getStart()); + int end = document.toOffset(rng.getEnd()); + String newText = docText.substring(0, start) + replaceWith + docText.substring(end); + setRawText(newText); + selectionStart = selectionEnd = start+cursorReplaceOffset; + } else { + String insertText = getInsertText(completion); + String newText = docText.substring(0, selectionStart) + insertText + docText.substring(selectionStart); + + selectionStart+= insertText.length(); + selectionEnd += insertText.length(); + setRawText(newText); + } } private String getInsertText(CompletionItem completion) { @@ -239,7 +253,23 @@ public class Editor { private List getCompletions() throws Exception { CompletionList cl = harness.getCompletions(this.document, this.getCursor()); - return cl.getItems(); + ArrayList items = new ArrayList<>(cl.getItems()); + Collections.sort(items, new Comparator() { + + @Override + public int compare(CompletionItem o1, CompletionItem o2) { + return sortKey(o1).compareTo(sortKey(o2)); + } + + private String sortKey(CompletionItem item) { + String k = item.getSortText(); + if (k==null) { + k = item.getLabel(); + } + return k; + } + }); + return items; } private Position getCursor() { diff --git a/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/testharness/LanguageServerHarness.java b/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/testharness/LanguageServerHarness.java index aae69241c..f5b93f752 100644 --- a/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/testharness/LanguageServerHarness.java +++ b/vscode-extensions/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/testharness/LanguageServerHarness.java @@ -21,7 +21,6 @@ import io.typefox.lsapi.InitializeResult; import io.typefox.lsapi.Position; import io.typefox.lsapi.PublishDiagnosticsParams; import io.typefox.lsapi.Range; -import io.typefox.lsapi.ServerCapabilities; import io.typefox.lsapi.TextDocumentSyncKind; import io.typefox.lsapi.impl.ClientCapabilitiesImpl; import io.typefox.lsapi.impl.DidChangeTextDocumentParamsImpl; diff --git a/vscode-extensions/commons/pom.xml b/vscode-extensions/commons/pom.xml index fd56e653b..2c1163850 100644 --- a/vscode-extensions/commons/pom.xml +++ b/vscode-extensions/commons/pom.xml @@ -45,6 +45,11 @@ slf4j-api ${slf4j-version} + + org.slf4j + slf4j-simple + ${slf4j-version} + junit junit diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java index 8b3f95d13..8d42941a2 100644 --- a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/Assert.java @@ -20,4 +20,10 @@ public class Assert { } } + public static void isNotNull(Object it) { + if (it==null) { + throw new NullPointerException(); + } + } + } diff --git a/vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java similarity index 100% rename from vscode-extensions/commons/language-server-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java rename to vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/CollectionUtil.java diff --git a/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/FuzzyMatcher.java b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/FuzzyMatcher.java new file mode 100644 index 000000000..7e354b931 --- /dev/null +++ b/vscode-extensions/commons/util-commons/src/main/java/org/springframework/ide/vscode/util/FuzzyMatcher.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2015 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.util; + +/** + * @author Kris De Volder + */ +public class FuzzyMatcher { + + /** + * Match given pattern with a given data. The data is considered a 'match' for the + * pattern if all characters in the pattern can be found in the data, in the + * same order but with possible 'gaps' in between. + *

+ * The function returns 0. when the pattern doesn't match the data and a non-zero + * 'score' when it does. The higher the score, the better the match is considered to + * be. + */ + public static double matchScore(String pattern, String data) { + int ppos = 0; //pos of next char in pattern to look for + int dpos = 0; //pos of next char in data not yet matched + int gaps = 0; //number of 'gaps' in the match. A gap is any non-empty run of consecutive characters in the data that are not used by the match + int skips = 0; //number of skipped characters. This is the sum of the length of all the gaps. + int plen = pattern.length(); + int dlen = data.length(); + if (plen>dlen) { + return 0.0; + } + while (ppos=dlen) { + //still chars left in pattern but no more data + return 0.0; + } + char c = pattern.charAt(ppos++); + int foundCharAt = data.indexOf(c, dpos); + if (foundCharAt>=0) { + if (foundCharAt>dpos) { + gaps++; + skips+=foundCharAt-dpos; + } + dpos = foundCharAt+1; + } else { + return 0.0; + } + } + //end of pattern reached. All matched. + if (dpos more extra = worse score + } + return score(gaps, skips); + } + + private static double score(int gaps, int skips) { + if (gaps==0) { + //gaps == 0 means a prefix match, ignore 'skips' at end of String and just sort + // alphabetic (see STS-4049) + double badness = 0.1; // all scored equally, assumes using a 'stable' sorter. + return -badness; //higher is better + } else { + double badness = 1+gaps + skips/10000.0; // higher is worse + return -badness; //higher is better + } + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/AbstractYamlAssistContext.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/AbstractYamlAssistContext.java new file mode 100644 index 000000000..3c8899d80 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/AbstractYamlAssistContext.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2016 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.yaml.completion; + +import org.springframework.ide.vscode.util.PrefixFinder; +import org.springframework.ide.vscode.yaml.path.YamlPath; +import org.springframework.ide.vscode.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SDocNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SKeyNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNodeType; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SRootNode; + +/** + * @author Kris De Volder + */ +public abstract class AbstractYamlAssistContext implements YamlAssistContext { + + /** + * Delete a content assist query from the document, and also the line of + * text in the document that contains it, if that line of text contains just the + * query surrounded by whitespace. + */ + public static void deleteQueryAndLine(YamlDocument doc, String query, int queryOffset, YamlPathEdits edits) throws Exception { + edits.delete(queryOffset, query); + String wholeLine = doc.getLineTextAtOffset(queryOffset); + if (wholeLine.trim().equals(query.trim())) { + edits.deleteLineBackwardAtOffset(queryOffset); + } + } + + public final int documentSelector; + public final YamlPath contextPath; + + private static PrefixFinder prefixfinder = new PrefixFinder() { + protected boolean isPrefixChar(char c) { + return !Character.isWhitespace(c); + } + }; + + + protected final String getPrefix(YamlDocument doc, SNode node, int offset) { + //For value completions... in general we would like to determine the whole text + // corresponding to the value, so a simplistic backwards scan isn't good enough. + // instead we should use offset in current node / structure to determine the + // the start of the current value. + if (node.getNodeType()==SNodeType.KEY) { + SKeyNode keyNode = (SKeyNode) node; + if (keyNode.isInValue(offset)) { + int valueStart = keyNode.getColonOffset()+1; + while (valueStart<=offset && Character.isWhitespace(doc.getChar(valueStart))) { + valueStart++; + } + if (offset>=valueStart) { + return doc.textBetween(valueStart, offset); + } else { + //only whitespace, or nothing found upto the cursor + return ""; + } + } +// } else if (node.getNodeType()==SNodeType.RAW) { +// TODO: Handle this as we could be in a value that's on the next line instead of right behind the node + } + + //If not one of the special cases where we try to be more precise... + // we use simplistic backward scan to determine 'CA query'. + return prefixfinder.getPrefix(doc.getDocument(), offset); + } + + + public AbstractYamlAssistContext(int documentSelector, YamlPath contextPath) { + this.documentSelector = documentSelector; + this.contextPath = contextPath; + } + + protected SNode getContextNode(YamlDocument file) throws Exception { + return contextPath.traverse((SNode)getContextRoot(file)); + } + + protected SDocNode getContextRoot(YamlDocument file) throws Exception { + SRootNode root = file.getStructure(); + return (SDocNode) root.getChildren().get(documentSelector); + } + + protected CompletionFactory completionFactory() { + return CompletionFactory.DEFAULT; + } + +// @Override +// public HoverInfo getHoverInfo() { +// return null; +// } +// +// @Override +// public HoverInfo getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion) { +// //By default we don't provide value-specific hover, so just show the same hover +// // as the assistContext the value is in. This is likely more interesting than showing nothing at all. +// return getHoverInfo(); +// } + + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/CompletionFactory.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/CompletionFactory.java new file mode 100644 index 000000000..bd629d431 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/CompletionFactory.java @@ -0,0 +1,17 @@ +package org.springframework.ide.vscode.yaml.completion; + +import org.springframework.ide.vscode.commons.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.completion.ICompletionProposal; +import org.springframework.ide.vscode.util.IDocument; +import org.springframework.ide.vscode.yaml.schema.YType; +import org.springframework.ide.vscode.yaml.schema.YTypeUtil; +import org.springframework.ide.vscode.yaml.schema.YTypedProperty; + +public interface CompletionFactory { + + CompletionFactory DEFAULT = new DefaultCompletionFactory(); + + ICompletionProposal beanProperty(IDocument doc, String contextProperty, YType contextType, String query, YTypedProperty p, double score, DocumentEdits edits, YTypeUtil typeUtil); + ICompletionProposal valueProposal(String value, String query, String label, YType type, double score, DocumentEdits edits); + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/DefaultCompletionFactory.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/DefaultCompletionFactory.java new file mode 100644 index 000000000..ad2222161 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/DefaultCompletionFactory.java @@ -0,0 +1,218 @@ +package org.springframework.ide.vscode.yaml.completion; + +import java.util.Comparator; + +import org.springframework.ide.vscode.commons.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.completion.ICompletionProposal; +import org.springframework.ide.vscode.util.IDocument; +import org.springframework.ide.vscode.yaml.schema.YType; +import org.springframework.ide.vscode.yaml.schema.YTypeUtil; +import org.springframework.ide.vscode.yaml.schema.YTypedProperty; + +import io.typefox.lsapi.CompletionItemKind; + +public class DefaultCompletionFactory implements CompletionFactory { + + /** + * A sorter suitable for sorting proposals created by this factory + */ + public static final Comparator COMPARATOR = new Comparator() { + public int compare(ICompletionProposal p1, ICompletionProposal p2) { + if (p1 instanceof ScoreableProposal && p2 instanceof ScoreableProposal) { + double s1 = ((ScoreableProposal)p1).getScore(); + double s2 = ((ScoreableProposal)p2).getScore(); + if (s1==s2) { + String name1 = ((ScoreableProposal)p1).getLabel(); + String name2 = ((ScoreableProposal)p2).getLabel(); + return name1.compareTo(name2); + } else { + return Double.compare(s2, s1); + } + } + return 0; + } + }; + + + public static abstract class ScoreableProposal implements ICompletionProposal { + private static final double DEEMP_VALUE = 100000; // should be large enough to move deemphasized stuff to bottom of list. + + private double deemphasizedBy = 0.0; + public abstract double getBaseScore(); + public final double getScore() { + return getBaseScore() - deemphasizedBy; + } + public ScoreableProposal deemphasize() { + deemphasizedBy+= DEEMP_VALUE; + return this; + } + public boolean isDeemphasized() { + return deemphasizedBy > 0; + } + +// @Override +// public boolean isAutoInsertable() { +// return !isDeemphasized(); +// } + +// public StyledString getStyledDisplayString() { +// StyledString result = new StyledString(); +// highlightPattern(getHighlightPattern(), getBaseDisplayString(), result); +// return result; +// } + +// private void highlightPattern(String pattern, String data, StyledString result) { +// Styler highlightStyle = CompletionFactory.HIGHLIGHT; +// Styler plainStyle = isDeemphasized()?CompletionFactory.DEEMPHASIZE:CompletionFactory.NULL_STYLER; +// if (isDeprecated()) { +// highlightStyle = CompletionFactory.compose(highlightStyle, CompletionFactory.DEPRECATE); +// plainStyle = CompletionFactory.compose(plainStyle, CompletionFactory.DEPRECATE); +// } +// if (StringUtils.hasText(pattern)) { +// int dataPos = 0; int dataLen = data.length(); +// int patternPos = 0; int patternLen = pattern.length(); +// +// while (dataPos + * This context is typically a kind of 'dummy' context that is not + * used to generate completions (it is not possible to type anything + * in this context because... Wherever you type, you are always implicitly + * typing in one of the 'subdocuments' of the YamlFile. + *

+ * All this context needs to be able to do therefore, is to support traversal so that + * it selects the appropriate context for a subdocument. + * + * @author Kris De Volder + */ +public abstract class TopLevelAssistContext implements YamlAssistContext { + + @Override + public YamlAssistContext traverse(YamlPathSegment s) throws Exception { + Integer documentSelector = s.toIndex(); + if (documentSelector!=null) { + return getDocumentContext(documentSelector); + } + return null; + } + + @Override + public Collection getCompletions(YamlDocument doc, SNode node, int offset) throws Exception { + //This context really should never be used directly to create completions. But we provide + // a dummy implementation anyway. + return Collections.emptyList(); + } + +// @Override +// public HoverInfo getHoverInfo() { +// return null; +// } +// +// public HoverInfo getHoverInfo(YamlPathSegment lastSegment) { +// return null; +// } +// +// @Override +// public HoverInfo getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion) { +// return null; +// } + + protected abstract YamlAssistContext getDocumentContext(int documentSelector); +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YTypeAssistContext.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YTypeAssistContext.java new file mode 100644 index 000000000..1e2f7a060 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YTypeAssistContext.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) 2016 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.yaml.completion; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.completion.ICompletionProposal; +import org.springframework.ide.vscode.util.CollectionUtil; +import org.springframework.ide.vscode.util.FuzzyMatcher; +import org.springframework.ide.vscode.yaml.path.YamlPath; +import org.springframework.ide.vscode.yaml.path.YamlPathSegment; +import org.springframework.ide.vscode.yaml.path.YamlPathSegment.YamlPathSegmentType; +import org.springframework.ide.vscode.yaml.schema.YType; +import org.springframework.ide.vscode.yaml.schema.YTypeUtil; +import org.springframework.ide.vscode.yaml.schema.YTypedProperty; +import org.springframework.ide.vscode.yaml.schema.YValueHint; +import org.springframework.ide.vscode.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SChildBearingNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SKeyNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode; + +public class YTypeAssistContext extends AbstractYamlAssistContext { + + final static Logger logger = LoggerFactory.getLogger(YTypeAssistContext.class); + + final private YTypeUtil typeUtil; + final private YType type; + final private YamlAssistContext parent; + + public YTypeAssistContext(YTypeAssistContext parent, YamlPath contextPath, YType YType, YTypeUtil typeUtil) { + super(parent.documentSelector, contextPath); + this.parent = parent; + this.type = YType; + this.typeUtil = typeUtil; + } + + public YTypeAssistContext(TopLevelAssistContext parent, int documentSelector, YType type, YTypeUtil typeUtil) { + super(documentSelector, YamlPath.EMPTY); + this.type = type; + this.typeUtil = typeUtil; + this.parent = parent; + } + + @Override + public Collection getCompletions(YamlDocument doc, SNode node, int offset) throws Exception { + String query = getPrefix(doc, node, offset); + List valueCompletions = getValueCompletions(doc, offset, query); + if (!valueCompletions.isEmpty()) { + return valueCompletions; + } + return getKeyCompletions(doc, offset, query); + } + + public List getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception { + int queryOffset = offset - query.length(); + List properties = typeUtil.getProperties(type); + if (CollectionUtil.hasElements(properties)) { + ArrayList proposals = new ArrayList<>(properties.size()); + SNode contextNode = getContextNode(doc); + Set definedProps = getDefinedProperties(contextNode); + for (YTypedProperty p : properties) { + String name = p.getName(); + double score = FuzzyMatcher.matchScore(query, name); + if (score!=0) { + YamlPath relativePath = YamlPath.fromSimpleProperty(name); + YamlPathEdits edits = new YamlPathEdits(doc); + if (!definedProps.contains(name)) { + //property not yet defined + YType YType = p.getType(); + edits.delete(queryOffset, query); + edits.createPathInPlace(contextNode, relativePath, queryOffset, appendTextFor(YType)); + proposals.add(completionFactory().beanProperty(doc.getDocument(), + contextPath.toPropString(), getType(), + query, p, score, edits, typeUtil) + ); + } else { + //property already defined + // instead of filtering, navigate to the place where its defined. + deleteQueryAndLine(doc, query, queryOffset, edits); + //Cast to SChildBearingNode cannot fail because otherwise definedProps would be the empty set. + edits.createPath((SChildBearingNode) contextNode, relativePath, ""); + proposals.add( + completionFactory().beanProperty(doc.getDocument(), + contextPath.toPropString(), getType(), + query, p, score, edits, typeUtil) + .deemphasize() //deemphasize because it already exists + ); + } + } + } + return proposals; + } + return Collections.emptyList(); + } + + /** + * Computes the text that should be appended at the end of a completion + * proposal depending on what type of value is expected. + */ + protected String appendTextFor(YType type) { + //Note that proper indentation after each \n" is added automatically + //so the strings created here do not need to contain indentation spaces. + if (type==null) { + //Assume its some kind of pojo bean + return "\n"; + } else if (typeUtil.isMap(type)) { + //ready to enter nested map key on next line + return "\n"; + } if (typeUtil.isSequencable(type)) { + //ready to enter sequence element on next line + return "\n- "; + } else if (typeUtil.isAtomic(type)) { + //ready to enter whatever on the same line + return " "; + } else { + //Assume its some kind of pojo bean + return "\n"; + } + } + + private Set getDefinedProperties(SNode contextNode) { + try { + if (contextNode instanceof SChildBearingNode) { + List children = ((SChildBearingNode)contextNode).getChildren(); + if (CollectionUtil.hasElements(children)) { + Set keys = new HashSet<>(children.size()); + for (SNode c : children) { + if (c instanceof SKeyNode) { + keys.add(((SKeyNode) c).getKey()); + } + } + return keys; + } + } + } catch (Exception e) { + logger.error("Error getting defined props", e); + } + return Collections.emptySet(); + } + + private List getValueCompletions(YamlDocument doc, int offset, String query) { + YValueHint[] values = typeUtil.getHintValues(type); + if (values!=null) { + ArrayList completions = new ArrayList<>(); + for (YValueHint value : values) { + double score = FuzzyMatcher.matchScore(query, value.getValue()); + if (score!=0 && !value.equals(query)) { + DocumentEdits edits = new DocumentEdits(doc.getDocument()); + edits.delete(offset-query.length(), offset); + edits.insert(offset, value.getValue()); + completions.add(completionFactory().valueProposal(value.getValue(), query, value.getLabel(), type, score, edits)); + } + } + return completions; + } + return Collections.emptyList(); + } + + @Override + public YamlAssistContext traverse(YamlPathSegment s) { + if (s.getType()==YamlPathSegmentType.VAL_AT_KEY) { + if (typeUtil.isSequencable(type) || typeUtil.isMap(type)) { + return contextWith(s, typeUtil.getDomainType(type)); + } + String key = s.toPropString(); + Map subproperties = typeUtil.getPropertiesMap(type); + if (subproperties!=null) { + return contextWith(s, getType(subproperties.get(key))); + } + } else if (s.getType()==YamlPathSegmentType.VAL_AT_INDEX) { + if (typeUtil.isSequencable(type)) { + return contextWith(s, typeUtil.getDomainType(type)); + } + } + return null; + } + + private YType getType(YTypedProperty prop) { + if (prop!=null) { + return prop.getType(); + } + return null; + } + + private YamlAssistContext contextWith(YamlPathSegment s, YType nextType) { + if (nextType!=null) { + return new YTypeAssistContext(this, contextPath.append(s), nextType, typeUtil); + } + return null; + } + + + @Override + public String toString() { + return "TypeContext("+contextPath.toPropString()+"::"+type+")"; + } + + +// @Override +// public HoverInfo getHoverInfo() { +// if (parent!=null) { +// return parent.getHoverInfo(contextPath.getLastSegment()); +// } +// return null; +// } +// + public YType getType() { + return type; + } +// +// @Override +// public HoverInfo getHoverInfo(YamlPathSegment lastSegment) { +// //Hoverinfo is only attached to YTypedProperties so... +// switch (lastSegment.getType()) { +// case VAL_AT_KEY: +// case KEY_AT_KEY: +// YTypedProperty prop = getProperty(lastSegment.toPropString()); +// if (prop!=null) { +// return new YPropertyHoverInfo(contextPath.toPropString(), getType(), prop); +// } +// break; +// default: +// } +// return null; +// } + +// private YTypedProperty getProperty(String name) { +// return typeUtil.getPropertiesMap(getType()).get(name); +// } +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlAssistContext.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlAssistContext.java new file mode 100644 index 000000000..f23550e75 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlAssistContext.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2016 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.yaml.completion; + +import java.util.Collection; + +import org.springframework.ide.vscode.commons.completion.ICompletionProposal; +import org.springframework.ide.vscode.yaml.path.YamlNavigable; +import org.springframework.ide.vscode.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode; + +/** + * @author Kris De Volder + */ +public interface YamlAssistContext extends YamlNavigable { + Collection getCompletions(YamlDocument doc, SNode node, int offset) throws Exception; + + //TODO: conceptually... the right thing would be to only implement the second of these + // two methods and get rid of the first one. +// HoverInfo getHoverInfo(); +// HoverInfo getHoverInfo(YamlPathSegment lastSegment); +// +// HoverInfo getValueHoverInfo(YamlDocument doc, DocumentRegion documentRegion); +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlAssistContextProvider.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlAssistContextProvider.java new file mode 100644 index 000000000..de06c694d --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlAssistContextProvider.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2016 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.yaml.completion; + +import org.springframework.ide.vscode.yaml.structure.YamlDocument; + +/** + * Defines a way to obtain a 'toplevel' {@link YamlAssistContext} for a given {@link YamlDocument} + * + * @author Kris De Volder + */ +public interface YamlAssistContextProvider { + + YamlAssistContext getGlobalAssistContext(YamlDocument doc); + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlCompletionEngine.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlCompletionEngine.java new file mode 100644 index 000000000..88484ef68 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlCompletionEngine.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 2016 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.yaml.completion; + +import java.util.Collection; +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.completion.ICompletionEngine; +import org.springframework.ide.vscode.commons.completion.ICompletionProposal; +import org.springframework.ide.vscode.util.Assert; +import org.springframework.ide.vscode.util.IDocument; +import org.springframework.ide.vscode.yaml.path.YamlPath; +import org.springframework.ide.vscode.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SKeyNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNodeType; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SRootNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SSeqNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureProvider; +import org.springframework.ide.vscode.yaml.util.YamlIndentUtil; + +/** + * Implements {@link ICompletionEngine} for .yml file, based on a YamlAssistContextProvider + * which has to to be injected into engine via its contructor. + * + * @author Kris De Volder + */ +public class YamlCompletionEngine implements ICompletionEngine { + + final static Logger logger = LoggerFactory.getLogger(YamlCompletionEngine.class); + + + private final YamlAssistContextProvider contextProvider; + protected final YamlStructureProvider structureProvider; + + public YamlCompletionEngine(YamlStructureProvider structureProvider, YamlAssistContextProvider contextProvider) { + Assert.isNotNull(structureProvider); + Assert.isNotNull(contextProvider); + this.structureProvider= structureProvider; + this.contextProvider = contextProvider; + } + + protected final YamlAssistContext getGlobalContext(YamlDocument doc) { + return contextProvider.getGlobalAssistContext(doc); + } + + protected CompletionFactory proposalFactory() { + return CompletionFactory.DEFAULT; + } + + @Override + public Collection getCompletions(IDocument _doc, int offset) throws Exception { + YamlDocument doc = new YamlDocument(_doc, structureProvider); + if (!doc.isCommented(offset)) { + SRootNode root = doc.getStructure(); + SNode current = root.find(offset); + YamlPath contextPath = getContextPath(doc, current, offset); + YamlAssistContext context = getContext(doc, offset, current, contextPath); + if (context==null && isDubiousKey(current, offset)) { + current = current.getParent(); + contextPath = contextPath.dropLast(); + context = getContext(doc, offset, current, contextPath); + } + if (context!=null) { + return context.getCompletions(doc, current, offset); + } + } + return Collections.emptyList(); + } + + /** + * A 'dubious' key is when the cursor is positioned right after a key's ':' character. + * This key is 'dubious' in that if the user would type a non-whitespace character next... + * then that key is no longer a key but parses as a 'value' instead. + */ + private boolean isDubiousKey(SNode node, int offset) { + if (node.getNodeType()==SNodeType.KEY) { + SKeyNode key = (SKeyNode)node; + return key.getColonOffset()+1==offset; + } + return false; + } + + protected YamlAssistContext getContext(YamlDocument doc, int offset, SNode node, YamlPath contextPath) { + try { + return contextPath.traverse(getGlobalContext(doc)); + } catch (Exception e) { + logger.error("Error obtaining YamlAssistContext", e); + return null; + } + } + + protected YamlPath getContextPath(YamlDocument doc, SNode node, int offset) throws Exception { + if (node==null) { + return YamlPath.EMPTY; + } else if (node.getNodeType()==SNodeType.KEY) { + //slight complication. The area in the key and value of a key node represent different + // contexts for content assistance + SKeyNode keyNode = (SKeyNode)node; + if (keyNode.isInValue(offset)) { + return keyNode.getPath(); + } else { + return keyNode.getParent().getPath(); + } + } else if (node.getNodeType()==SNodeType.RAW) { + //Treat raw node as a 'key node'. This is basically assuming that is misclasified + // by structure parser because the ':' was not yet typed into the document. + + //Complication: if line with cursor is empty or the cursor is inside the indentation + // area then the structure may not reflect correctly the context. This is because + // the correct context depends on text the user has not typed yet.(which will change the + // indentation level of the current line. So we must use the cursorIndentation + // 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); + while (node.getIndent()==-1 || (node.getIndent()>=currentIndent && node.getNodeType()!=SNodeType.DOC)) { + node = node.getParent(); + } + return node.getPath(); + } else if (node.getNodeType()==SNodeType.SEQ) { + SSeqNode seqNode = (SSeqNode)node; + if (seqNode.isInValue(offset)) { + return seqNode.getPath(); + } else { + return seqNode.getParent().getPath(); + } + } else if (node.getNodeType()==SNodeType.DOC) { + return node.getPath(); + } else { + throw new IllegalStateException("Missing case"); + } + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlPathEdits.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlPathEdits.java new file mode 100644 index 000000000..bf18093f1 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/completion/YamlPathEdits.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * Copyright (c) 2015 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.yaml.completion; + +import org.springframework.ide.vscode.commons.completion.DocumentEdits; +import org.springframework.ide.vscode.util.Assert; +import org.springframework.ide.vscode.util.IRegion; +import org.springframework.ide.vscode.yaml.path.YamlPath; +import org.springframework.ide.vscode.yaml.path.YamlPathSegment; +import org.springframework.ide.vscode.yaml.path.YamlPathSegment.YamlPathSegmentType; +import org.springframework.ide.vscode.yaml.structure.YamlDocument; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SChildBearingNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SKeyNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode; +import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNodeType; +import org.springframework.ide.vscode.yaml.util.YamlIndentUtil; +import org.springframework.ide.vscode.yaml.util.YamlUtil; + +/** + * Helper class that provides methods for creating the edits in a YamlDocument that + * insert new 'property paths' into the document. + * + * @author Kris De Volder + */ +public class YamlPathEdits extends DocumentEdits { + + private YamlDocument doc; + private YamlIndentUtil indentUtil; + + public YamlPathEdits(YamlDocument doc) { + super(doc.getDocument()); + this.doc = doc; + this.indentUtil = new YamlIndentUtil(doc); + } + + /** + * Create the necessary edits to ensure that a given property + * path exists, placing cursor in the right place also to start + * start typing the property value. + *

+ * This also handles cases where all or some of the path already + * exists. In the former case no edits are performed only cursor + * movement. In the latter case, the right place to start inserting + * the 'missing' portion of the path is found and the edits + * are created there. + */ + public void createPath(SChildBearingNode node, YamlPath path, String appendText) throws Exception { + //This code doesn't handle selection of subddocuments + // or creation of new subdocuments so must not call it on + //ROOT node but start at an appropriate SDocNode (or below) + Assert.isLegal(node.getNodeType()!=SNodeType.ROOT); + if (!path.isEmpty()) { + YamlPathSegment s = path.getSegment(0); + if (s.getType()==YamlPathSegmentType.VAL_AT_KEY) { + String key = s.toPropString(); + SKeyNode existing = node.getChildWithKey(key); + if (existing==null) { + createNewPath(node, path, appendText); + } else { + createPath(existing, path.tail(), appendText); + } + } + } else { + //whole path already exists. Just try to move cursor somewhere + // sensible in the existing tail-end-node of the path. + SNode child = node.getFirstRealChild(); + if (child!=null) { + moveCursorTo(child.getStart()); + } else if (node.getNodeType()==SNodeType.KEY) { + SKeyNode keyNode = (SKeyNode) node; + int colonOffset = keyNode.getColonOffset(); + char c = doc.getChar(colonOffset+1); + if (c==' ') { + moveCursorTo(colonOffset+2); //cursor after the ": " + } else { + moveCursorTo(colonOffset+1); //cursor after the ":" + } + } + } + } + + private void createNewPath(SChildBearingNode parent, YamlPath path, String appendText) throws Exception { + int indent = getChildIndent(parent); + int insertionPoint = getNewPathInsertionOffset(parent); + boolean startOnNewLine = true; + insert(insertionPoint, createPathInsertionText(path, indent, startOnNewLine, appendText)); + } + + protected String createPathInsertionText(YamlPath path, int indent, boolean startOnNewLine, String appendText) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < path.size(); i++) { + if (startOnNewLine||i>0) { + indentUtil.addNewlineWithIndent(indent, buf); + } + String key = path.getSegment(i).toPropString(); + buf.append(YamlUtil.stringEscape(key)); + buf.append(":"); + indent += YamlIndentUtil.INDENT_BY; + } + buf.append(indentUtil.applyIndentation(appendText, indent)); + return buf.toString(); + } + + private int getChildIndent(SNode parent) { + if (parent.getNodeType()==SNodeType.DOC) { + return parent.getIndent(); + } else { + return parent.getIndent()+YamlIndentUtil.INDENT_BY; + } + } + + private int getNewPathInsertionOffset(SChildBearingNode parent) throws Exception { + int insertAfterLine = doc.getLineOfOffset(parent.getTreeEnd()); + while (insertAfterLine>=0 && doc.getLineIndentation(insertAfterLine)==-1) { + insertAfterLine--; + } + if (insertAfterLine<0) { + //This code is probably 'dead' because: + // - it can only occur if all lines in the 'parent' are empty + // - if parent is any other node than SRootNode then it must have at least one + // non-emtpy line + // => parent must be SRootNode and only contain comment or empty lines + // But in that case we will never need to compute a 'new path insertion offset' + // since we will always be in the case where completions are to be inserted + // in place (i.e. at the current cursor). + return 0; //insert at beginning of document + } else { + IRegion r = doc.getLineInformation(insertAfterLine); + return r.getOffset() + r.getLength(); + } + } + + public void createPathInPlace(SNode contextNode, YamlPath relativePath, int insertionPoint, String appendText) throws Exception { + int indent = getChildIndent(contextNode); + insert(insertionPoint, createPathInsertionText(relativePath, indent, needNewline(contextNode, insertionPoint), appendText)); + } + + private boolean needNewline(SNode contextNode, int insertionPoint) throws Exception { + if (contextNode.getNodeType()==SNodeType.SEQ) { + // after a '- ' its okay to put key on same line + return false; + } else { + return lineHasTextBefore(insertionPoint); + } + } + + private boolean lineHasTextBefore(int insertionPoint) throws Exception { + String textBefore = doc.getLineTextBefore(insertionPoint); + return !textBefore.trim().isEmpty(); + } + + /** + * Deletes this node, and all of its children. + */ + public void deleteNode(SNode node) throws Exception { + delete(node.getStart(), node.getTreeEnd()); + deleteLineBackwardAtOffset(node.getStart()); + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java index 9ac9147ed..ff86a4a52 100644 --- a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlIndentUtil.java @@ -10,11 +10,11 @@ *******************************************************************************/ package org.springframework.ide.vscode.yaml.util; -import org.springframework.ide.vscode.util.IDocument; +import org.springframework.ide.vscode.util.Assert; import org.springframework.ide.vscode.yaml.structure.YamlDocument; /** - * Helper methods to mainpulate indentation levels. + * Helper methods to manipulate indentation levels in yaml content. * * @author Kris De Volder */ @@ -34,11 +34,11 @@ public class YamlIndentUtil { public YamlIndentUtil(String newline) { this.NEWLINE = newline; + Assert.isNotNull(NEWLINE); } public YamlIndentUtil(YamlDocument doc) { - IDocument d = doc.getDocument(); - this.NEWLINE = d.getDefaultLineDelimiter(); + this(doc.getDocument().getDefaultLineDelimiter()); } /** diff --git a/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlUtil.java b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlUtil.java new file mode 100644 index 000000000..0c291cdd7 --- /dev/null +++ b/vscode-extensions/commons/yaml-commons/src/main/java/org/springframework/ide/vscode/yaml/util/YamlUtil.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2016 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.yaml.util; + +/** + * @author Kris De Volder + */ +public class YamlUtil { + + /** + * If any one of these is found at the start of a string then the string requires + * to be escaped. + */ + private static final String[] SPECIAL_START = { + //This list is probably not complete. + "!", + "#", + "&", + "*", + ">", + "|", + "?", + "{", + "}", + "[", + "]", + ",", + "\"", + "'", + "`", + "@", + "- ", + "\t", + " ", +// "\n", //also included in 'special content' so no need to check at start specifically. +// "\r" + }; + + /** + * If any of these is found at the end of a string then the string requires to be + * escaped. + */ + private static final String[] SPECIAL_END = { + "\t", + " " +// "\n", //also included in 'special content' so no need to check at start specifically. +// "\r" + }; + + /** + * If any of these is found inside a string then the string requires to be escaped. + */ + private static final String[] SPECIAL_CONTENT = { + ": ", " #", "\n", "\r" + }; + + /** + * Given a string value convert it into a format that can be inserted into a yml file. + */ + public static String stringEscape(String value) { + if (canInsertAsIs(value)) { + return value; + } + //TODO: this does not properly handle values that contain line-breaks (linebreaks are no alowed in single-line stirngs, such as may + // be used for 'keys' in yaml. And although they are allowed in mult-line strings, they will be subject to new end-of-line folding + // this processing of newlines by yaml parser will mean that the value is not the same when parsed. + //For string like that we probably need to resort to double-quoted strings using escape sequences using '\'. + //These cases are rare and not yet implemented here. + return "'"+value.replace("'", "''")+"'"; + } + + private static boolean canInsertAsIs(String value) { + //See http://www.activestate.com/blog/2014/07/yaml-pro + // section: "Don't Over-quote your Strings" + for (String special : SPECIAL_START) { + if (value.startsWith(special)) { + return false; + } + } + for (String special : SPECIAL_END) { + if (value.endsWith(special)) { + return false; + } + } + for (String special : SPECIAL_CONTENT) { + if (value.contains(special)) { + return false; + } + } + //Nothing special. Safe to include verbatim. + return true; + } + +} diff --git a/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java b/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java index 9e45a2460..12a1789eb 100644 --- a/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java +++ b/vscode-extensions/commons/yaml-commons/src/test/java/org/springframework/ide/vscode/yaml/structure/YamlStructureParserTest.java @@ -21,18 +21,14 @@ import java.util.ArrayList; import java.util.regex.Pattern; import org.junit.Test; -import org.springframework.ide.vscode.util.IDocument; import org.springframework.ide.vscode.util.TextDocument; import org.springframework.ide.vscode.yaml.path.YamlPath; import org.springframework.ide.vscode.yaml.path.YamlPathSegment; -import org.springframework.ide.vscode.yaml.structure.YamlDocument; -import org.springframework.ide.vscode.yaml.structure.YamlStructureParser; import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SChildBearingNode; import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SDocNode; import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SKeyNode; import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode; import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SRootNode; -import org.springframework.ide.vscode.yaml.structure.YamlStructureProvider; public class YamlStructureParserTest { diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/Main.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/Main.java index f5e24ef5c..898c6a942 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/Main.java +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/Main.java @@ -6,7 +6,6 @@ import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.net.Socket; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/ManifestYamlLanguageServer.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/ManifestYamlLanguageServer.java index 7ac6b3ab2..73ec8bfd8 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/ManifestYamlLanguageServer.java +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/ManifestYamlLanguageServer.java @@ -7,6 +7,7 @@ import java.util.concurrent.CompletableFuture; import javax.inject.Provider; +import org.springframework.ide.vscode.commons.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.reconcile.ReconcileProblem; import org.springframework.ide.vscode.util.Futures; @@ -14,9 +15,13 @@ import org.springframework.ide.vscode.util.SimpleLanguageServer; import org.springframework.ide.vscode.util.SimpleTextDocumentService; import org.springframework.ide.vscode.util.TextDocument; import org.springframework.ide.vscode.yaml.ast.YamlASTProvider; +import org.springframework.ide.vscode.yaml.completion.SchemaBasedYamlAssistContextProvider; +import org.springframework.ide.vscode.yaml.completion.YamlAssistContextProvider; +import org.springframework.ide.vscode.yaml.completion.YamlCompletionEngine; import org.springframework.ide.vscode.yaml.reconcile.YamlSchemaBasedReconcileEngine; import org.springframework.ide.vscode.yaml.schema.YValueHint; import org.springframework.ide.vscode.yaml.schema.YamlSchema; +import org.springframework.ide.vscode.yaml.structure.YamlStructureProvider; import org.yaml.snakeyaml.Yaml; import com.google.common.collect.ImmutableList; @@ -37,9 +42,15 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer { private static final Provider> NO_BUILDPACKS = () -> ImmutableList.of(); private Yaml yaml = new Yaml(); + private YamlSchema schema = new ManifestYmlSchema(NO_BUILDPACKS); public ManifestYamlLanguageServer() { SimpleTextDocumentService documents = getTextDocumentService(); + + YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT; + YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema); + YamlCompletionEngine yamlCompletionEngine = new YamlCompletionEngine(structureProvider, contextProvider); + VscodeCompletionEngine completionEngine = new VscodeCompletionEngineAdapter(this, yamlCompletionEngine); // SimpleWorkspaceService workspace = getWorkspaceService(); documents.onDidChangeContent(params -> { @@ -58,54 +69,8 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer { // } // }); - documents.onCompletion(params -> { - CompletableFuture promise = new CompletableFuture<>(); - CompletionListImpl completions = new CompletionListImpl(); - completions.setIncomplete(false); - List items = new ArrayList<>(); - { -// { -// label: 'TypeScript', -// kind: CompletionItemKind.Text, -// data: 1 -// }, - CompletionItemImpl item = new CompletionItemImpl(); - item.setLabel("TypeScript"); - item.setKind(CompletionItemKind.Text); - item.setData(1); - items.add(item); - } - - { -// { -// label: 'JavaScript', -// kind: CompletionItemKind.Text, -// data: 2 -// } - CompletionItemImpl item = new CompletionItemImpl(); - item.setLabel("JavaScript"); - item.setKind(CompletionItemKind.Text); - item.setData(2); - items.add(item); - } - completions.setItems(items); - - promise.complete(completions); - return promise; - }); - - documents.onCompletionResolve((_item) -> { - CompletionItemImpl item = (CompletionItemImpl) _item; - Object data = item.getData(); - if (Integer.valueOf(1).equals(data)) { - item.setDetail("TypeScript details"); - item.setDocumentation("TypeScript docs"); - } else { - item.setDetail("JavaScript details"); - item.setDocumentation("JavaScript docs"); - } - return Futures.of((CompletionItem)item); - }); + documents.onCompletion(completionEngine::getCompletions); + documents.onCompletionResolve(completionEngine::resolveCompletion); } private void validateDocument(SimpleTextDocumentService documents, TextDocument doc) { @@ -133,7 +98,6 @@ public class ManifestYamlLanguageServer extends SimpleLanguageServer { } }; - YamlSchema schema = new ManifestYmlSchema(NO_BUILDPACKS); YamlASTProvider parser = new YamlParser(yaml); YamlSchemaBasedReconcileEngine engine = new YamlSchemaBasedReconcileEngine(parser, schema); engine.reconcile(doc, problems); diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/SortKeys.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/SortKeys.java new file mode 100644 index 000000000..2ec260197 --- /dev/null +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/SortKeys.java @@ -0,0 +1,28 @@ +package org.springframework.ide.vscode.cloudfoundry.manifest.editor; + +import java.util.Iterator; + +/** + * Utility to generate 'sort keys' in ascending order. + *

+ * VSCode uses a String in each completion to determine the order of completions. + * We use a 'score' based on how well a key matches what was typed. + *

+ * To go from a 'score' to a sort-key we presort our proposals and then assign + * a sort key for vscode. + */ +public class SortKeys implements Iterator { + + private int counter; + + @Override + public boolean hasNext() { + return true; + } + + @Override + public String next() { + return String.format("%05d", counter++); + } + +} diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/VscodeCompletionEngine.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/VscodeCompletionEngine.java new file mode 100644 index 000000000..a66e99701 --- /dev/null +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/VscodeCompletionEngine.java @@ -0,0 +1,16 @@ +package org.springframework.ide.vscode.cloudfoundry.manifest.editor; + +import java.util.concurrent.CompletableFuture; + +import io.typefox.lsapi.CompletionItem; +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.TextDocumentPositionParams; + +/** + * Interface that needs to be implemented by a 'completion engine' which can be easily + * wired-up to provide completions for a Vscode language server. + */ +public interface VscodeCompletionEngine { + CompletableFuture getCompletions(TextDocumentPositionParams params); + CompletableFuture resolveCompletion(CompletionItem unresolved); +} diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/VscodeCompletionEngineAdapter.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/VscodeCompletionEngineAdapter.java new file mode 100644 index 000000000..c4cd3c162 --- /dev/null +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/cloudfoundry/manifest/editor/VscodeCompletionEngineAdapter.java @@ -0,0 +1,113 @@ +package org.springframework.ide.vscode.cloudfoundry.manifest.editor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.completion.ICompletionEngine; +import org.springframework.ide.vscode.commons.completion.ICompletionProposal; +import org.springframework.ide.vscode.util.Futures; +import org.springframework.ide.vscode.util.IRegion; +import org.springframework.ide.vscode.util.SimpleLanguageServer; +import org.springframework.ide.vscode.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.util.TextDocument; +import org.springframework.ide.vscode.yaml.completion.DefaultCompletionFactory; + +import io.typefox.lsapi.CompletionItem; +import io.typefox.lsapi.CompletionList; +import io.typefox.lsapi.TextDocumentPositionParams; +import io.typefox.lsapi.impl.CompletionItemImpl; +import io.typefox.lsapi.impl.CompletionListImpl; +import io.typefox.lsapi.impl.TextEditImpl; + +/** + * Adapts a {@link ICompletionEngine}, wrapping it, to implement {@link VscodeCompletionEngine} + */ +public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine { + + final static Logger logger = LoggerFactory.getLogger(VscodeCompletionEngineAdapter.class); + + public static final String VS_CODE_CURSOR_MARKER = "{{}}"; + + private SimpleLanguageServer server; + private ICompletionEngine engine; + + public VscodeCompletionEngineAdapter(SimpleLanguageServer server, ICompletionEngine engine) { + this.server = server; + this.engine = engine; + } + + @Override + public CompletableFuture getCompletions(TextDocumentPositionParams params) { + //TODO: This returns a CompletableFuture which suggests we should try to do expensive work asyncly. + // We are currently just doing all this in a blocking way and wrapping the already computed list into + // a trivial pre-resolved future. + try { + SimpleTextDocumentService documents = server.getTextDocumentService(); + TextDocument doc = documents.get(params); + if (doc!=null) { + int offset = doc.toOffset(params.getPosition()); + List completions = new ArrayList<>(engine.getCompletions(doc, offset)); + Collections.sort(completions, DefaultCompletionFactory.COMPARATOR); + CompletionListImpl list = new CompletionListImpl(); + list.setIncomplete(false); + List items = new ArrayList<>(completions.size()); + SortKeys sortkeys = new SortKeys(); + for (ICompletionProposal c : completions) { + try { + items.add(adaptItem(doc, c, sortkeys)); + } catch (Exception e) { + logger.error("error computing completion", e); + } + } + list.setItems(items); + return Futures.of(list); + } + } catch (Exception e) { + logger.error("error computing completions", e); + } + return SimpleTextDocumentService.NO_COMPLETIONS; + } + + private CompletionItemImpl adaptItem(TextDocument doc, ICompletionProposal completion, SortKeys sortkeys) throws Exception { + CompletionItemImpl item = new CompletionItemImpl(); + item.setLabel(completion.getLabel()); + item.setKind(completion.getKind()); + item.setSortText(sortkeys.next()); + adaptEdits(item, doc, completion.getTextEdit()); + return item; + } + + private void adaptEdits(CompletionItemImpl item, TextDocument doc, DocumentEdits edits) throws Exception { + TextDocument newDoc = doc.copy(); + edits.apply(newDoc); + + IRegion newSelection = edits.getSelection(doc); + if (newSelection==null) { + //Every 'real' edit moves the cursor. So if the newSelection is unknown it can only + //mean we are dealing with a 'null' edit. + item.setInsertText(""); + } else { + TextEditImpl fullEdit = new TextEditImpl(); + fullEdit.setRange(doc.toRange(0, doc.getLength())); + int newCursor = newSelection.getOffset(); + String newText = newDoc.getText(); + fullEdit.setNewText(newText.substring(0,newCursor)+VS_CODE_CURSOR_MARKER+newText.substring(newCursor)); + item.setTextEdit(fullEdit); + } + } + + @Override + public CompletableFuture resolveCompletion(CompletionItem unresolved) { + //TODO: item is pre-resoved so we don't do anything, but we really should somehow defer some work, such as + // for example computing docs and edits to resolve time. + //The tricky part is that we have to probably remember infos about the unresolved elements somehow so we can resolve later. + return Futures.of(unresolved); + } + +} diff --git a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/yaml/ManifestYamlLanguageServerTest.java b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/yaml/ManifestYamlLanguageServerTest.java index 86ae25c27..de65c3a94 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/yaml/ManifestYamlLanguageServerTest.java +++ b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/yaml/ManifestYamlLanguageServerTest.java @@ -15,7 +15,6 @@ import org.springframework.ide.vscode.testharness.TextDocumentInfo; import io.typefox.lsapi.CompletionItem; import io.typefox.lsapi.CompletionList; import io.typefox.lsapi.InitializeResult; -import io.typefox.lsapi.ServerCapabilities; import io.typefox.lsapi.TextDocumentSyncKind; public class ManifestYamlLanguageServerTest { @@ -38,34 +37,33 @@ public class ManifestYamlLanguageServerTest { assertExpectedInitResult(harness.intialize(workspaceRoot)); } - - @Test public void completions() throws Exception { - LanguageServerHarness harness = new LanguageServerHarness(ManifestYamlLanguageServer::new); - - File workspaceRoot = getTestResource("/workspace/"); - assertExpectedInitResult(harness.intialize(workspaceRoot)); - - TextDocumentInfo doc = harness.openDocument(getTestResource("/workspace/testfile.yml")); - - CompletionList completions = harness.getCompletions(doc, doc.positionOf("foo")); - assertThat(completions.isIncomplete()).isFalse(); - assertThat(completions.getItems()) - .extracting(CompletionItem::getLabel) - .containsExactly("TypeScript", "JavaScript"); - - List resolved = harness.resolveCompletions(completions); - assertThat(resolved) - .extracting(CompletionItem::getLabel) - .containsExactly("TypeScript", "JavaScript"); - - assertThat(resolved) - .extracting(CompletionItem::getDetail) - .containsExactly("TypeScript details", "JavaScript details"); - - assertThat(resolved) - .extracting(CompletionItem::getDocumentation) - .containsExactly("TypeScript docs", "JavaScript docs"); - } +// @Test public void completions() throws Exception { +// LanguageServerHarness harness = new LanguageServerHarness(ManifestYamlLanguageServer::new); +// +// File workspaceRoot = getTestResource("/workspace/"); +// assertExpectedInitResult(harness.intialize(workspaceRoot)); +// +// TextDocumentInfo doc = harness.openDocument(getTestResource("/workspace/testfile.yml")); +// +// CompletionList completions = harness.getCompletions(doc, doc.positionOf("foo")); +// assertThat(completions.isIncomplete()).isFalse(); +// assertThat(completions.getItems()) +// .extracting(CompletionItem::getLabel) +// .containsExactly("TypeScript", "JavaScript"); +// +// List resolved = harness.resolveCompletions(completions); +// assertThat(resolved) +// .extracting(CompletionItem::getLabel) +// .containsExactly("TypeScript", "JavaScript"); +// +// assertThat(resolved) +// .extracting(CompletionItem::getDetail) +// .containsExactly("TypeScript details", "JavaScript details"); +// +// assertThat(resolved) +// .extracting(CompletionItem::getDocumentation) +// .containsExactly("TypeScript docs", "JavaScript docs"); +// } private void assertExpectedInitResult(InitializeResult initResult) { assertThat(initResult.getCapabilities().getCompletionProvider().getResolveProvider()).isTrue();