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 extends CompletionItem> 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();