Passes all our tests but...

Unfortunately vscode doesn't like our completions because they span multiple lines.
This commit is contained in:
Kris De Volder
2016-10-04 15:56:55 -07:00
parent 741d110bad
commit 5cf7740a7c
34 changed files with 1981 additions and 107 deletions

View File

@@ -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.
* <p>
* 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).
* <p>
* 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'.
* <p>
* 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 (tOffset<tStart) {
return tOffset;
} else {
return tOffset + text.length();
}
}
};
}
selection = tStart+text.length();
}
public void delete(final int start, final int end) throws BadLocationException {
final int tStart = org2new.trasform(start);
if (end>start) { // 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<Edit> edits = new ArrayList<Edit>();
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.
* <p>
* 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<doc.getNumberOfLines()-1) {
IRegion nextLine = doc.getLineInformation(lineNumber+1);
startOfDeletion = line.getOffset();
endOfDeletion = nextLine.getOffset();
} else {
startOfDeletion = line.getOffset();
endOfDeletion = endOf(line);
}
delete(startOfDeletion, endOfDeletion);
}
private int endOf(IRegion line) {
return line.getOffset()+line.getLength();
}
public void replace(int start, int end, String newText) {
delete(start, end);
insert(start, newText);
}
}

View File

@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2015, 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.commons.completion;
import java.util.Collection;
import org.springframework.ide.vscode.util.IDocument;
/**
* @author Kris De Volder
*/
public interface ICompletionEngine {
Collection<ICompletionProposal> getCompletions(IDocument document, int offset) throws Exception;
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -9,4 +9,7 @@ public class BadLocationException extends Exception {
super(e);
}
public BadLocationException() {
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<CompletionList> NO_COMPLETIONS = Futures.of(
new CompletionListImpl(false, Collections.emptyList()));
@Override
public CompletableFuture<CompletionList> 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());
}
}

View File

@@ -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<text.length()) {
return text.charAt(offset);
} catch (Exception e) {
throw new BadLocationException(e);
}
throw new BadLocationException();
}
@Override
@@ -209,7 +221,22 @@ public class TextDocument implements IDocument {
@Override
public int getLineOffset(int line) {
// TODO Auto-generated method stub
return 0;
return lineStarts()[line];
}
public int toOffset(Position position) {
int line = position.getLine();
int lineStart = lineStarts()[line];
return lineStart + position.getCharacter();
}
@Override
public void replace(int start, int len, String ins) {
text = text.substring(0, start) + ins + text.substring(start+len);
}
public synchronized TextDocument copy() {
return new TextDocument(this);
}
}

View File

@@ -44,7 +44,8 @@ public class Editor {
}
}
private static final String CURSOR = "<*>";
private static final String CURSOR = "<*>"; // used by our test harness
private static final String VS_CODE_CURSOR_MARKER = "{{}}"; //vscode uses this in edits to mark cursor position
private static final Comparator<Diagnostic> PROBLEM_COMPARATOR = new Comparator<Diagnostic>() {
@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<CompletionItem> items = new ArrayList<>(cl.getItems());
Collections.sort(items, new Comparator<CompletionItem>() {
@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() {

View File

@@ -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;

View File

@@ -45,6 +45,11 @@
<artifactId>slf4j-api</artifactId>
<version>${slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j-version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View File

@@ -20,4 +20,10 @@ public class Assert {
}
}
public static void isNotNull(Object it) {
if (it==null) {
throw new NullPointerException();
}
}
}

View File

@@ -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.
* <p>
* 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<plen) {
if (dpos>=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<dlen) {
//data left over
//gaps++; don't count end skipped chars as a real 'gap'. Otherwise we
//tend to favor matches at the end of the string over matches in the middle.
skips+=dlen-dpos; //but do count the extra chars at end => 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
}
}
}

View File

@@ -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();
// }
}

View File

@@ -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);
}

View File

@@ -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<ICompletionProposal> COMPARATOR = new Comparator<ICompletionProposal>() {
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<dataLen && patternPos<patternLen) {
// int pChar = pattern.charAt(patternPos++);
// int highlightPos = data.indexOf(pChar, dataPos);
// if (dataPos<highlightPos) {
// result.append(data.substring(dataPos, highlightPos), plainStyle);
// }
// result.append(data.charAt(highlightPos), highlightStyle);
// dataPos = highlightPos+1;
// }
// if (dataPos<dataLen) {
// result.append(data.substring(dataPos), plainStyle);
// }
// } else { //no pattern to highlight
// result.append(data, plainStyle);
// }
// }
// protected abstract boolean isDeprecated();
// protected abstract String getHighlightPattern();
// protected abstract String getBaseDisplayString();
// @Override
// public String getAdditionalProposalInfo() {
// HoverInfo hoverInfo = getAdditionalProposalInfo(new NullProgressMonitor());
// if (hoverInfo!=null) {
// return hoverInfo.getHtml();
// }
// return null;
// }
// @Override
// public abstract HoverInfo getAdditionalProposalInfo(IProgressMonitor monitor);
// @Override
// public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
// return null;
// }
//
// @Override
// public int getPrefixCompletionStart(IDocument document, int completionOffset) {
// return completionOffset;
// }
}
public static class BeanPropertyProposal extends ScoreableProposal {
private IDocument doc;
private String contextProperty;
private YType contextType;
private String query;
private YTypedProperty p;
private double baseScore;
private DocumentEdits edits;
private YTypeUtil typeUtil;
public BeanPropertyProposal(IDocument doc, String contextProperty, YType contextType, String query, YTypedProperty p, double score, DocumentEdits edits, YTypeUtil typeUtil) {
super();
this.doc = doc;
this.contextProperty = contextProperty;
this.contextType = contextType;
this.query = query;
this.p = p;
this.baseScore = score;
this.edits = edits;
this.typeUtil = typeUtil;
}
@Override
public double getBaseScore() {
return baseScore;
}
@Override
public String getLabel() {
return p.getName();
}
@Override
public CompletionItemKind getKind() {
return CompletionItemKind.Field;
}
@Override
public DocumentEdits getTextEdit() {
return edits;
}
}
public class ValueProposal extends ScoreableProposal {
private String value;
private String query;
private String label;
private YType type;
private double baseScore;
private DocumentEdits edits;
public ValueProposal(String value, String query, String label, YType type, double score, DocumentEdits edits) {
this.value = value;
this.query = query;
this.label = label;
this.type = type;
this.baseScore = score;
this.edits = edits;
}
@Override
public double getBaseScore() {
return baseScore;
}
@Override
public String getLabel() {
return label;
}
@Override
public CompletionItemKind getKind() {
return CompletionItemKind.Keyword;
}
@Override
public DocumentEdits getTextEdit() {
return edits;
}
@Override
public String toString() {
return "ValueProposal("+value+")";
}
}
@Override
public ICompletionProposal beanProperty(IDocument doc, String contextProperty, YType contextType, String query, YTypedProperty p, double score, DocumentEdits edits, YTypeUtil typeUtil) {
return new BeanPropertyProposal(doc, contextProperty, contextType, query, p, score, edits, typeUtil);
}
@Override
public ICompletionProposal valueProposal(String value, String query, String label, YType type, double score, DocumentEdits edits) {
return new ValueProposal(value, query, label, type, score, edits);
}
}

View File

@@ -0,0 +1,38 @@
/*******************************************************************************
* 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.schema.YamlSchema;
import org.springframework.ide.vscode.yaml.structure.YamlDocument;
/**
* A {@link YamlAssistContextProvider} that creates {@link YamlAssistContext}s from {@link YamlSchema}
*
* @author Kris De Volder
*/
public class SchemaBasedYamlAssistContextProvider implements YamlAssistContextProvider {
private YamlSchema schema;
public SchemaBasedYamlAssistContextProvider(YamlSchema schema) {
this.schema = schema;
}
@Override
public YamlAssistContext getGlobalAssistContext(YamlDocument doc) {
return new TopLevelAssistContext() {
@Override
protected YamlAssistContext getDocumentContext(int documentSelector) {
return new YTypeAssistContext(this, documentSelector, schema.getTopLevelType(), schema.getTypeUtil());
}
};
}
}

View File

@@ -0,0 +1,67 @@
/*******************************************************************************
* Copyright (c) 2015, 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.springframework.ide.vscode.commons.completion.ICompletionProposal;
import org.springframework.ide.vscode.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.yaml.structure.YamlDocument;
import org.springframework.ide.vscode.yaml.structure.YamlStructureParser.SNode;
/**
* Abstract YamlAssistContext for the toplevel of a YamlDocument.
* <p>
* 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.
* <p>
* 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<ICompletionProposal> 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);
}

View File

@@ -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<ICompletionProposal> getCompletions(YamlDocument doc, SNode node, int offset) throws Exception {
String query = getPrefix(doc, node, offset);
List<ICompletionProposal> valueCompletions = getValueCompletions(doc, offset, query);
if (!valueCompletions.isEmpty()) {
return valueCompletions;
}
return getKeyCompletions(doc, offset, query);
}
public List<ICompletionProposal> getKeyCompletions(YamlDocument doc, int offset, String query) throws Exception {
int queryOffset = offset - query.length();
List<YTypedProperty> properties = typeUtil.getProperties(type);
if (CollectionUtil.hasElements(properties)) {
ArrayList<ICompletionProposal> proposals = new ArrayList<>(properties.size());
SNode contextNode = getContextNode(doc);
Set<String> 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<String> getDefinedProperties(SNode contextNode) {
try {
if (contextNode instanceof SChildBearingNode) {
List<SNode> children = ((SChildBearingNode)contextNode).getChildren();
if (CollectionUtil.hasElements(children)) {
Set<String> 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<ICompletionProposal> getValueCompletions(YamlDocument doc, int offset, String query) {
YValueHint[] values = typeUtil.getHintValues(type);
if (values!=null) {
ArrayList<ICompletionProposal> 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<String, YTypedProperty> 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);
// }
}

View File

@@ -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<YamlAssistContext> {
Collection<ICompletionProposal> 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);
}

View File

@@ -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);
}

View File

@@ -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<ICompletionProposal> 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");
}
}
}

View File

@@ -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.
* <p>
* 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());
}
}

View File

@@ -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());
}
/**

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<Collection<YValueHint>> 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<CompletionList> promise = new CompletableFuture<>();
CompletionListImpl completions = new CompletionListImpl();
completions.setIncomplete(false);
List<CompletionItemImpl> 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);

View File

@@ -0,0 +1,28 @@
package org.springframework.ide.vscode.cloudfoundry.manifest.editor;
import java.util.Iterator;
/**
* Utility to generate 'sort keys' in ascending order.
* <p>
* 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.
* <p>
* 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<String> {
private int counter;
@Override
public boolean hasNext() {
return true;
}
@Override
public String next() {
return String.format("%05d", counter++);
}
}

View File

@@ -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<CompletionList> getCompletions(TextDocumentPositionParams params);
CompletableFuture<CompletionItem> resolveCompletion(CompletionItem unresolved);
}

View File

@@ -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<CompletionList> 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<ICompletionProposal> completions = new ArrayList<>(engine.getCompletions(doc, offset));
Collections.sort(completions, DefaultCompletionFactory.COMPARATOR);
CompletionListImpl list = new CompletionListImpl();
list.setIncomplete(false);
List<CompletionItemImpl> 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<CompletionItem> 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);
}
}

View File

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