Passes all our tests but...
Unfortunately vscode doesn't like our completions because they span multiple lines.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -9,4 +9,7 @@ public class BadLocationException extends Exception {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public BadLocationException() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,4 +20,10 @@ public class Assert {
|
||||
}
|
||||
}
|
||||
|
||||
public static void isNotNull(Object it) {
|
||||
if (it==null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user