Moved commons and concourse editor to 'headless-services'

This commit is contained in:
Kris De Volder
2017-04-06 16:27:12 -07:00
parent 3abf189512
commit bf8f8cffa9
608 changed files with 600 additions and 51 deletions

View File

@@ -0,0 +1,630 @@
/*******************************************************************************
* Copyright (c) 2016-2017 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.languageserver.testharness;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.text.BadLocationException;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.MarkedString;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.junit.Assert;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import reactor.core.publisher.Flux;
public class Editor {
static class EditorState {
String documentContents;
int selectionStart;
int selectionEnd;
public EditorState(String text) {
selectionStart = text.indexOf(CURSOR);
if (selectionStart>=0) {
text = text.substring(0,selectionStart) + text.substring(selectionStart+CURSOR.length());
selectionEnd = text.indexOf(CURSOR, selectionStart);
if (selectionEnd>=0) {
text = text.substring(0, selectionEnd) + text.substring(selectionEnd+CURSOR.length());
} else {
selectionEnd = selectionStart;
}
} else {
//No CURSOR markers found
selectionStart = text.length();
selectionEnd = text.length();
}
this.documentContents = text;
}
}
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
public int compare(Diagnostic o1, Diagnostic o2) {
int diff = compare(o1.getRange().getStart(), o2.getRange().getStart());
if (diff!=0) return diff;
return compare(o1.getRange().getEnd(), o2.getRange().getEnd());
}
private int compare(Position p1, Position p2) {
int d = p1.getLine() - p2.getLine();
if (d!=0) return d;
return p1.getCharacter() - p2.getCharacter();
}
};
private LanguageServerHarness harness;
private TextDocumentInfo document;
private int selectionEnd;
private int selectionStart;
private Set<String> ignoredTypes;
private String languageId;
public Editor(LanguageServerHarness harness, String contents, String languageId) throws Exception {
this.harness = harness;
this.languageId = new String(languageId); // So we can catch bugs that use == for langauge id comparison.
EditorState state = new EditorState(contents);
this.document = harness.openDocument(harness.createWorkingCopy(state.documentContents, this.languageId));
this.selectionStart = state.selectionStart;
this.selectionEnd = state.selectionEnd;
this.ignoredTypes = new HashSet<>();
}
/**
* Check that a 'expectedProblems' are found by the reconciler. Expected problems are
* specified by string of the form "${badSnippet}|${messageSnippet}" or
* "${badSnippet}^${followSnippet}|${messageSnippet}"
* <p>
* The badSnippet is the text expected to be covered by the marker's region and the message snippet must
* be found in the error marker's message.
* <p>
* In addition, if followSnippet is specified, the text that comes right after the error marker must match it.
* <p>
* The expected problems are matched one-to-one in the order given (so markers in the
* editor must appear in the expected order for the assert to pass).
*
* @param editor
* @param expectedProblems
* @throws BadLocationException
*/
public List<Diagnostic> assertProblems(String... expectedProblems) throws Exception {
Editor editor = this;
List<Diagnostic> actualProblems = new ArrayList<>(editor.reconcile().stream().filter(d -> {
return !ignoredTypes.contains(d.getCode());
}).collect(Collectors.toList()));
Collections.sort(actualProblems, PROBLEM_COMPARATOR);
String bad = null;
if (actualProblems.size()!=expectedProblems.length) {
bad = "Wrong number of problems (expecting "+expectedProblems.length+" but found "+actualProblems.size()+")";
} else {
for (int i = 0; i < expectedProblems.length; i++) {
if (!matchProblem(actualProblems.get(i), expectedProblems[i])) {
bad = "First mismatch at index "+i+": "+expectedProblems[i]+"\n";
break;
}
}
}
if (bad!=null) {
fail(bad+problemSumary(editor, actualProblems));
}
return ImmutableList.copyOf(actualProblems);
}
private String problemSumary(Editor editor, List<Diagnostic> actualProblems) throws Exception {
StringBuilder buf = new StringBuilder();
for (Diagnostic p : actualProblems) {
buf.append("\n----------------------\n");
String snippet = editor.getText(p.getRange());
buf.append("("+p.getRange().getStart().getLine()+", "+p.getRange().getStart().getCharacter()+")["+snippet+"]:\n");
buf.append(" "+p.getMessage());
}
return buf.toString();
}
/**
* Get the editor text, with cursor markers inserted (for easy textual comparison
* after applying a proposal)
*/
public String getText() {
String text = document.getText();
text = text.substring(0, selectionEnd) + CURSOR + text.substring(selectionEnd);
if (selectionStart<selectionEnd) {
text = text.substring(0,selectionStart) + CURSOR + text.substring(selectionStart);
}
return deWindowsify(text);
}
public void setText(String content) throws Exception {
EditorState state = new EditorState(content);
document = harness.changeDocument(document.getUri(), state.documentContents);
this.selectionStart = state.selectionStart;
this.selectionEnd = state.selectionEnd;
}
/**
* @return The 'raw' text in the editor, i.e. without the cursor markers.
*/
public String getRawText() throws Exception {
return document.getText();
}
private void replaceText(int start, int end, String newText) {
document = harness.changeDocument(document.getUri(), start, end, newText);
}
public void setRawText(String newContent) throws Exception {
document = harness.changeDocument(document.getUri(), newContent);
}
public String getText(Range range) {
return document.getText(range);
}
private String deWindowsify(String text) {
return text.replaceAll("\\r\\n", "\n");
}
private boolean matchProblem(Diagnostic problem, String expect) {
String[] parts = expect.split("\\|");
assertEquals(2, parts.length);
String badSnippet = parts[0];
String snippetFollow = null;
int carretOffset = badSnippet.indexOf('^');
if (carretOffset>=0) {
snippetFollow = badSnippet.substring(carretOffset+1);
badSnippet = badSnippet.substring(0, carretOffset);
}
String messageSnippet = parts[1];
boolean spaceSensitive = badSnippet.trim().length()<badSnippet.length();
boolean emptyRange = problem.getRange().getStart().equals(problem.getRange().getEnd());
String actualBadSnippet = emptyRange
? getCharAt(problem.getRange().getStart())
: getText(problem.getRange());
if (!spaceSensitive) {
actualBadSnippet = actualBadSnippet.trim();
}
return actualBadSnippet.equals(badSnippet)
&& ( snippetFollow==null ||
snippetFollow.equals(getText(problem.getRange().getEnd(), snippetFollow.length())))
&& problem.getMessage().contains(messageSnippet);
}
private String getText(Position start, int length) {
int offset = document.toOffset(start);
String text = document.getText();
return text.substring(offset, offset+length);
}
private String getCharAt(Position start) {
String text = document.getText();
int offset = document.toOffset(start);
return offset<text.length()
? text.substring(offset, offset+1)
: "";
}
public List<Diagnostic> reconcile() throws Exception {
PublishDiagnosticsParams diagnostics = harness.getDiagnostics(document);
if (diagnostics!=null) {
return diagnostics.getDiagnostics();
}
return Collections.emptyList();
}
public void assertCompletions(String... expectTextAfter) throws Exception {
StringBuilder expect = new StringBuilder();
StringBuilder actual = new StringBuilder();
for (String after : expectTextAfter) {
expect.append(after);
expect.append("\n-------------------\n");
}
for (CompletionItem completion : getCompletions()) {
Editor editor = this.clone();
editor.apply(completion);
actual.append(editor.getText());
actual.append("\n-------------------\n");
}
assertEquals(expect.toString(), actual.toString());
}
public void assertCompletionLabels(String... expectedLabels) throws Exception {
StringBuilder expect = new StringBuilder();
StringBuilder actual = new StringBuilder();
for (String label : expectedLabels) {
expect.append(label);
expect.append("\n");
}
for (CompletionItem completion : getCompletions()) {
actual.append(completion.getLabel());
actual.append("\n");
}
assertEquals(expect.toString(), actual.toString());
}
public void assertContainsCompletions(String... expectTextAfter) throws Exception {
StringBuilder actual = new StringBuilder();
for (CompletionItem completion : getCompletions()) {
Editor editor = this.clone();
editor.apply(completion);
actual.append(editor.getText());
actual.append("\n-------------------\n");
}
String actualText = actual.toString();
for (String after : expectTextAfter) {
assertContains(after, actualText);
}
}
public void assertDoesNotContainCompletions(String... notToBeFound) throws Exception {
StringBuilder actual = new StringBuilder();
for (CompletionItem completion : getCompletions()) {
Editor editor = this.clone();
editor.apply(completion);
actual.append(editor.getText());
actual.append("\n-------------------\n");
}
String actualText = actual.toString();
for (String after : notToBeFound) {
assertDoesNotContain(after, actualText);
}
}
public void apply(CompletionItem completion) throws Exception {
TextEdit edit = completion.getTextEdit();
String docText = document.getText();
if (edit!=null) {
String replaceWith = edit.getNewText();
//Apply indentfix, this is magic vscode seems to apply to edits returned by language server. So our harness has to
// mimick that behavior. See https://github.com/Microsoft/language-server-protocol/issues/83
int referenceLine = edit.getRange().getStart().getLine();
int cursorOffset = edit.getRange().getStart().getCharacter();
String referenceIndent = document.getLineIndentString(referenceLine);
if (cursorOffset<referenceIndent.length()) {
referenceIndent = referenceIndent.substring(0, cursorOffset);
}
replaceWith = replaceWith.replaceAll("\\n", "\n"+referenceIndent);
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());
replaceText(start, end, replaceWith);
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) {
String s = completion.getInsertText();
if (s==null) {
//If no insertText is provided the label is used
s = completion.getLabel();
}
return s;
}
@Override
public Editor clone() {
try {
return new Editor(harness, getText(), getLanguageId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public List<CompletionItem> getCompletions() throws Exception {
CompletionList cl = harness.getCompletions(this.document, this.getCursor());
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;
}
public CompletionItem getFirstCompletion() throws Exception {
return getCompletions().get(0);
}
private Position getCursor() {
return document.toPosition(selectionStart);
}
public void assertIsHoverRegion(String string) throws Exception {
int hoverPosition = getHoverPosition(string, 1);
Hover hover = harness.getHover(document, document.toPosition(hoverPosition));
assertEquals(string, getText(hover.getRange()));
}
public void assertHoverContains(String hoverOver, int occurrence, String snippet) throws Exception {
int hoverPosition = getHoverPosition(hoverOver, occurrence);
Hover hover = harness.getHover(document, document.toPosition(hoverPosition));
assertContains(snippet, hoverString(hover));
}
protected String hoverString(Hover hover) {
StringBuilder buf = new StringBuilder();
boolean first = true;
for (Either<String, MarkedString> block : hover.getContents()) {
if (!first) {
buf.append("\n\n");
}
if (block.isLeft()) {
String s = block.getLeft();
buf.append(s);
} else if (block.isRight()) {
MarkedString ms = block.getRight();
buf.append("```"+ms.getLanguage()+"\n");
buf.append(ms.getValue());
buf.append("\n```");
}
first = false;
}
return buf.toString();
}
private int getHoverPosition(String hoverOver, int occurrence) throws Exception {
assertTrue(occurrence>0);
return occurrences(getRawText(), hoverOver)
.elementAt(occurrence-1)
.map(offset -> offset + hoverOver.length()/2)
.block();
}
private Flux<Integer> occurrences(String text, String substring) {
return Flux.fromIterable(() -> new Iterator<Integer>() {
int searchFrom = 0;
@Override
public boolean hasNext() {
return searchFrom>=0 && searchFrom < text.length() && text.indexOf(substring, searchFrom) >= 0;
}
@Override
public Integer next() {
int found = text.indexOf(substring, searchFrom);
assertTrue(found>=0);
searchFrom = found+1;
return found;
}
});
}
public void assertHoverContains(String hoverOver, String snippet) throws Exception {
int hoverPosition = getHoverPosition(hoverOver,1);
Hover hover = harness.getHover(document, document.toPosition(hoverPosition));
assertContains(snippet, hoverString(hover));
}
public void assertNoHover(String hoverOver) throws Exception {
int hoverPosition = getRawText().indexOf(hoverOver) + hoverOver.length() / 2;
Hover hover = harness.getHover(document, document.toPosition(hoverPosition));
assertTrue(hover.getContents().isEmpty());
}
/**
* Verifies an expected textSnippet is contained in the hover text that is
* computed when hovering mouse at position at the end of first occurrence of
* a given string in the editor.
*/
public void assertHoverText(String afterString, String expectSnippet) throws Exception {
int pos = getRawText().indexOf(afterString);
if (pos>=0) {
pos += afterString.length();
}
Hover hover = harness.getHover(document, document.toPosition(pos));
assertContains(expectSnippet, hoverString(hover));
}
/**
* Verifies an expected text is the hover text that is computed when
* hovering mouse at position at the end of first occurrence of a given
* string in the editor.
*/
public void assertHoverExactText(String afterString, String expectedHover) throws Exception {
int pos = getRawText().indexOf(afterString);
if (pos>=0) {
pos += afterString.length();
}
Hover hover = harness.getHover(document, document.toPosition(pos));
assertEquals(expectedHover, hoverString(hover));
}
public CompletionItem assertCompletionDetails(String expectLabel, String expectDetail, String expectDocSnippet) throws Exception {
CompletionItem it = harness.resolveCompletionItem(assertCompletionWithLabel(expectLabel));
if (expectDetail!=null) {
assertEquals(expectDetail, it.getDetail());
}
if (expectDocSnippet!=null) {
assertContains(expectDocSnippet, it.getDocumentation());
}
return it;
}
protected CompletionItem assertCompletionWithLabel(String expectLabel) throws Exception {
return getCompletions().stream()
.filter((item) -> item.getLabel().equals(expectLabel))
.findFirst()
.get();
}
public void assertCompletionWithLabel(String expectLabel, String expectedResult) throws Exception {
CompletionItem completion = assertCompletionWithLabel(expectLabel);
String saveText = getText();
apply(completion);
assertEquals(expectedResult, getText());
setText(saveText);
}
public void setSelection(int start, int end) {
Assert.assertTrue(start>=0);
Assert.assertTrue(end>=start);
Assert.assertTrue(end<=document.getText().length());
this.selectionStart = start;
this.selectionEnd = end;
}
@Override
public String toString() {
return "Editor(\n"+getText()+"\n)";
}
public void assertLinkTargets(String hoverOver, String... expecteds) {
throw new UnsupportedOperationException("Not implemented yet!");
// Editor editor = this;
// int pos = editor.middleOf(hoverOver);
// assertTrue("Not found in editor: '"+hoverOver+"'", pos>=0);
//
// List<IJavaElement> targets = getLinkTargets(editor, pos);
// assertEquals(expecteds.length, targets.size());
// for (int i = 0; i < expecteds.length; i++) {
// assertEquals(expecteds[i], JavaElementLabels.getElementLabel(targets.get(i), JavaElementLabels.DEFAULT_QUALIFIED | JavaElementLabels.M_PARAMETER_TYPES));
// }
}
/**
* Get a problem that covers the given text in the editor. Throws exception
* if no matching problem is found.
*/
public Diagnostic assertProblem(String coveredText) throws Exception {
Editor editor = this;
List<Diagnostic> problems = editor.reconcile();
for (Diagnostic p : problems) {
String c = editor.getText(p.getRange());
if (c.equals(coveredText)) {
return p;
}
}
fail("No problem found covering the text '"+coveredText+"' in: \n"
+ problemSumary(editor, problems)
);
return null; //unreachable but compiler doesn't know
}
public CompletionItem assertFirstQuickfix(Diagnostic problem, String expectLabel) {
throw new UnsupportedOperationException("Not implemented yet!");
}
public void assertText(String expected) {
assertEquals(expected, getText());
}
public void ignoreProblem(Object type) {
ignoredTypes.add(type.toString());
}
public void assertGotoDefinition(Position pos, Range expectedTarget) throws Exception {
TextDocumentIdentifier textDocumentId = document.getId();
TextDocumentPositionParams params = new TextDocumentPositionParams(textDocumentId, textDocumentId.getUri(), pos);
List<? extends Location> defs = harness.getDefinitions(params);
assertEquals(1, defs.size());
assertEquals(new Location(textDocumentId.getUri(), expectedTarget), defs.get(0));
}
/**
* Determines the position of (the middle of) a snippet of text in the document.
*
* @param contextSnippet A larger snippet containing the actual snippet to look for.
* This larger snippet is used to narrow the section of the document
* where we look for the actual snippet. This is useful when the snippet
* occurs multiple times in the document.
* @param focusSnippet The snippet to look for
*/
public Position positionOf(String longSnippet, String focusSnippet) throws Exception {
Range r = rangeOf(longSnippet, focusSnippet);
return r==null?null:r.getStart();
}
/**
* Determines the range of a snippet of text in the document.
*
* @param contextSnippet A larger snippet containing the actual snippet to look for.
* This larger snippet is used to narrow the section of the document
* where we look for the actual snippet. This is useful when the snippet
* occurs multiple times in the document.
* @param focusSnippet The snippet to look for
*/
public Range rangeOf(String longSnippet, String focusSnippet) throws Exception {
int relativeOffset = longSnippet.indexOf(focusSnippet);
int contextStart = getRawText().indexOf(longSnippet);
Assert.assertTrue("'"+longSnippet+"' not found in editor", contextStart>=0);
int start = contextStart+relativeOffset;
return new Range(document.toPosition(start), document.toPosition(start+focusSnippet.length()));
}
public String getLanguageId() {
return languageId;
}
}

View File

@@ -0,0 +1,409 @@
/*******************************************************************************
* Copyright (c) 2016-2017 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.languageserver.testharness;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.assertj.core.api.Condition;
import org.eclipse.lsp4j.ClientCapabilities;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.MessageActionItem;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ShowMessageRequestParams;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentItem;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextDocumentSyncOptions;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.springframework.ide.vscode.commons.languageserver.LanguageIds;
import org.springframework.ide.vscode.commons.languageserver.ProgressParams;
import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
public class LanguageServerHarness {
//Warning this 'harness' is incomplete. Growing it as needed.
private Random random = new Random();
private Callable<? extends SimpleLanguageServer> factory;
private String defaultLanguageId;
private SimpleLanguageServer server;
private InitializeResult initResult;
private Map<String,TextDocumentInfo> documents = new HashMap<>();
private Map<String, PublishDiagnosticsParams> diagnostics = new HashMap<>();
public LanguageServerHarness(Callable<? extends SimpleLanguageServer> factory, String defaultLanguageId) {
this.factory = factory;
this.defaultLanguageId = defaultLanguageId;
}
public LanguageServerHarness(Callable<? extends SimpleLanguageServer> factory) throws Exception {
this(factory, LanguageIds.PLAINTEXT);
}
public synchronized TextDocumentInfo getOrReadFile(File file, String languageId) throws Exception {
String uri = file.toURI().toString();
TextDocumentInfo d = documents.get(uri);
if (d==null) {
documents.put(uri, d = readFile(file, languageId));
}
return d;
}
public TextDocumentInfo readFile(File file, String languageId) throws Exception {
byte[] encoded = Files.readAllBytes(file.toPath());
String content = new String(encoded, getEncoding());
TextDocumentItem document = new TextDocumentItem();
document.setText(content);
document.setUri(file.toURI().toString());
document.setVersion(getFirstVersion());
document.setLanguageId(languageId);
return new TextDocumentInfo(document);
}
private synchronized TextDocumentItem setDocumentContent(String uri, String newContent) {
TextDocumentInfo o = documents.get(uri);
TextDocumentItem n = new TextDocumentItem();
n.setLanguageId(o.getLanguageId());
n.setText(newContent);
n.setVersion(o.getVersion()+1);
n.setUri(o.getUri());
documents.put(uri, new TextDocumentInfo(n));
return n;
}
protected Charset getEncoding() {
return Charset.forName("utf8");
}
protected String getDefaultLanguageId() {
return defaultLanguageId;
}
protected String getFileExtension() {
return ".txt";
}
private synchronized void receiveDiagnostics(PublishDiagnosticsParams diags) {
this.diagnostics.put(diags.getUri(), diags);
}
public InitializeResult intialize(File workspaceRoot) throws Exception {
server = factory.call();
int parentPid = random.nextInt(40000)+1000;
InitializeParams initParams = new InitializeParams();
initParams.setRootPath(workspaceRoot== null?null:workspaceRoot.toString());
initParams.setProcessId(parentPid);
ClientCapabilities clientCap = new ClientCapabilities();
initParams.setCapabilities(clientCap);
initResult = server.initialize(initParams).get();
if (server instanceof LanguageClientAware) {
((LanguageClientAware) server).connect(new STS4LanguageClient() {
@Override
public void telemetryEvent(Object object) {
// TODO Auto-generated method stub
}
@Override
public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams requestParams) {
// TODO Auto-generated method stub
return CompletableFuture.completedFuture(new MessageActionItem("Some Message Request Answer"));
}
@Override
public void showMessage(MessageParams messageParams) {
// TODO Auto-generated method stub
}
@Override
public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {
receiveDiagnostics(diagnostics);
}
@Override
public void logMessage(MessageParams message) {
// TODO Auto-generated method stub
}
@Override
public void progress(ProgressParams progressEvent) {
// TODO Auto-generated method stub
}
});
}
return initResult;
}
public TextDocumentInfo openDocument(TextDocumentInfo documentInfo) throws Exception {
DidOpenTextDocumentParams didOpen = new DidOpenTextDocumentParams();
didOpen.setTextDocument(documentInfo.getDocument());
if (server!=null) {
server.getTextDocumentService().didOpen(didOpen);
}
return documentInfo;
}
public TextDocumentInfo openDocument(File file, String languageId) throws Exception {
return openDocument(getOrReadFile(file, languageId));
}
public synchronized TextDocumentInfo changeDocument(String uri, int start, int end, String replaceText) {
TextDocumentInfo oldDoc = documents.get(uri);
String oldContent = oldDoc.getText();
String newContent = oldContent.substring(0, start) + replaceText + oldContent.substring(end);
TextDocumentItem textDocument = setDocumentContent(uri, newContent);
DidChangeTextDocumentParams didChange = new DidChangeTextDocumentParams();
VersionedTextDocumentIdentifier version = new VersionedTextDocumentIdentifier();
version.setUri(uri);
version.setVersion(textDocument.getVersion());
didChange.setTextDocument(version);
switch (getDocumentSyncMode()) {
case None:
break; //nothing todo
case Incremental: {
TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent();
change.setRange(new Range(oldDoc.toPosition(start), oldDoc.toPosition(end)));
change.setRangeLength(end-start);
change.setText(replaceText);
didChange.setContentChanges(Collections.singletonList(change));
break;
}
case Full: {
TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent();
change.setText(newContent);
didChange.setContentChanges(Collections.singletonList(change));
break;
}
default:
throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode());
}
if (server!=null) {
server.getTextDocumentService().didChange(didChange);
}
return documents.get(uri);
}
public TextDocumentInfo changeDocument(String uri, String newContent) throws Exception {
TextDocumentItem textDocument = setDocumentContent(uri, newContent);
DidChangeTextDocumentParams didChange = new DidChangeTextDocumentParams();
VersionedTextDocumentIdentifier version = new VersionedTextDocumentIdentifier();
version.setUri(uri);
version.setVersion(textDocument.getVersion());
didChange.setTextDocument(version);
switch (getDocumentSyncMode()) {
case None:
break; //nothing todo
case Incremental:
case Full:
TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent();
change.setText(newContent);
didChange.setContentChanges(Collections.singletonList(change));
break;
default:
throw new IllegalStateException("Unkown SYNC mode: "+getDocumentSyncMode());
}
if (server!=null) {
server.getTextDocumentService().didChange(didChange);
}
return documents.get(uri);
}
private TextDocumentSyncKind getDocumentSyncMode() {
if (initResult!=null) {
Either<TextDocumentSyncKind, TextDocumentSyncOptions> mode = initResult.getCapabilities().getTextDocumentSync();
if (mode.isLeft()) {
return mode.getLeft();
} else {
throw new IllegalStateException("Harness doesn't support fancy Sync options yet!");
}
}
return TextDocumentSyncKind.None;
}
public PublishDiagnosticsParams getDiagnostics(TextDocumentInfo doc) throws Exception {
this.server.waitForReconcile();
return diagnostics.get(doc.getUri());
}
public static Condition<Diagnostic> isDiagnosticWithSeverity(DiagnosticSeverity severity) {
return new Condition<>(
(d) -> d.getSeverity()==severity,
"Diagnostic with severity '"+severity+"'"
);
}
public static Condition<Diagnostic> isDiagnosticCovering(TextDocumentInfo doc, String string) {
return new Condition<>(
(d) -> isDiagnosticCovering(d, doc, string),
"Diagnostic covering '"+string+"'"
);
}
public static final Condition<Diagnostic> isWarning = isDiagnosticWithSeverity(DiagnosticSeverity.Warning);
public static boolean isDiagnosticCovering(Diagnostic diag, TextDocumentInfo doc, String string) {
Range rng = diag.getRange();
String actualText = doc.getText(rng);
return string.equals(actualText);
}
public static Condition<Diagnostic> isDiagnosticOnLine(int line) {
return new Condition<>(
(d) -> d.getRange().getStart().getLine()==line,
"Diagnostic on line "+line
);
}
public CompletionList getCompletions(TextDocumentInfo doc, Position cursor) throws Exception {
TextDocumentPositionParams params = new TextDocumentPositionParams();
params.setPosition(cursor);
params.setTextDocument(doc.getId());
server.waitForReconcile();
Either<List<CompletionItem>, CompletionList> completions = server.getTextDocumentService().completion(params).get();
if (completions.isLeft()) {
List<CompletionItem> list = completions.getLeft();
return new CompletionList(false, list);
} else /* sompletions.isRight() */ {
return completions.getRight();
}
}
public Hover getHover(TextDocumentInfo document, Position cursor) throws Exception {
TextDocumentPositionParams params = new TextDocumentPositionParams();
params.setPosition(cursor);
params.setTextDocument(document.getId());
return server.getTextDocumentService().hover(params ).get();
}
public CompletionItem resolveCompletionItem(CompletionItem unresolved) {
try {
return server.getTextDocumentService().resolveCompletionItem(unresolved).get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public List<CompletionItem> resolveCompletions(CompletionList completions) {
return completions.getItems().stream()
.map(this::resolveCompletionItem)
.collect(Collectors.toList());
}
public Editor newEditor(String contents) throws Exception {
return new Editor(this, contents, getDefaultLanguageId());
}
public Editor newEditor(String languageId, String contents) throws Exception {
return new Editor(this, contents, languageId);
}
public synchronized TextDocumentInfo createWorkingCopy(String contents, String languageId) throws Exception {
TextDocumentItem doc = new TextDocumentItem();
doc.setLanguageId(languageId);
doc.setText(contents);
doc.setUri(createTempUri());
doc.setVersion(getFirstVersion());
TextDocumentInfo docinfo = new TextDocumentInfo(doc);
documents.put(docinfo.getUri(), docinfo);
return docinfo;
}
protected int getFirstVersion() {
return 1;
}
protected String createTempUri() throws Exception {
return File.createTempFile("workingcopy", getFileExtension()).toURI().toString();
}
public void assertCompletion(String textBefore, String expectTextAfter) throws Exception {
Editor editor = newEditor(textBefore);
List<CompletionItem> completions = editor.getCompletions();
assertNotNull(completions);
assertFalse(completions.isEmpty());
CompletionItem completion = editor.getFirstCompletion();
editor.apply(completion);
assertEquals(expectTextAfter, editor.getText());
}
public void assertCompletions(String textBefore, String... expectTextAfter) throws Exception {
Editor editor = newEditor(textBefore);
StringBuilder expect = new StringBuilder();
StringBuilder actual = new StringBuilder();
for (String after : expectTextAfter) {
expect.append(after);
expect.append("\n-------------------\n");
}
List<? extends CompletionItem> completions = editor.getCompletions();
for (CompletionItem ci : completions) {
editor = newEditor(textBefore);
editor.apply(ci);
actual.append(editor.getText());
actual.append("\n-------------------\n");
}
assertEquals(expect.toString(), actual.toString());
}
public void assertCompletionDisplayString(String editorContents, String expected) throws Exception {
Editor editor = newEditor(editorContents);
CompletionItem completion = editor.getFirstCompletion();
assertEquals(expected, completion.getLabel());
}
public List<? extends Location> getDefinitions(TextDocumentPositionParams params) throws Exception {
server.waitForReconcile(); //goto definitions relies on reconciler infos! Must wait or race condition breaking tests occasionally.
return server.getTextDocumentService().definition(params).get();
}
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* Copyright (c) 2016-2017 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.languageserver.testharness;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.Collection;
public class TestAsserts {
public static void assertContains(String needle, String haystack) {
if (haystack==null || !haystack.contains(needle)) {
fail("Not found: "+needle+"\n in \n"+haystack);
}
}
public static void assertDoesNotContain(String needle, String haystack) {
if (haystack!=null && haystack.contains(needle)) {
fail("Found: "+needle+"\n in \n"+haystack);
}
}
public static <T> T assertOneElement(Collection<T> collection) {
assertEquals("Wrong number of elements in "+ collection, 1, collection.size());
for (T t : collection) {
return t;
}
throw new AssertionError("No elements found");
}
}

View File

@@ -0,0 +1,165 @@
/*******************************************************************************
* Copyright (c) 2016-2017 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.languageserver.testharness;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentItem;
/**
* Deprecated, we should get rid of this class and use {@link TextDocument}.
*/
@Deprecated
public class TextDocumentInfo {
Pattern NEWLINE = Pattern.compile("\\r|\\n|\\r\\n|\\n\\r");
private final TextDocumentItem document;
private int[] _lineStarts;
public TextDocumentInfo(TextDocumentItem document) {
this.document = document;
}
public String getLanguageId() {
return getDocument().getLanguageId();
}
public int getVersion() {
return getDocument().getVersion();
}
public String getText() {
return getDocument().getText();
}
public String getUri() {
return getDocument().getUri();
}
public TextDocumentItem getDocument() {
return document;
}
public String getText(Range rng) {
String txt = getText();
int start = Math.max(0, toOffset(rng.getStart()));
int end = Math.min(txt.length(), toOffset(rng.getEnd()));
return txt.substring(start, end);
}
public int toOffset(Position p) {
int startOfLine = startOfLine(p.getLine());
return startOfLine+p.getCharacter();
}
private int startOfLine(int line) {
return lineStarts()[line];
}
private int[] lineStarts() {
if (_lineStarts==null) {
_lineStarts = parseLines();
}
return _lineStarts;
}
private int[] parseLines() {
List<Integer> lineStarts = new ArrayList<>();
lineStarts.add(0);
Matcher matcher = NEWLINE.matcher(getText());
int pos = 0;
while (matcher.find(pos)) {
lineStarts.add(pos = matcher.end());
}
int[] array = new int[lineStarts.size()];
for (int i = 0; i < array.length; i++) {
array[i] = lineStarts.get(i);
}
return array;
}
/**
* Find and return the (first) position of a given text snippet in the
* document.
*
* @return The position, or null if the snippet can't be found.
*/
public Position positionOf(String snippet) {
int offset = getText().indexOf(snippet);
if (offset>=0) {
return toPosition(offset);
}
return null;
}
public Position toPosition(int offset) {
int line = lineNumber(offset);
int startOfLine = startOfLine(line);
int column = offset - startOfLine;
Position pos = new Position();
pos.setCharacter(column);
pos.setLine(line);
return pos;
}
/**
* Determine the line-number a given offset (i.e. what line is the offset inside of?)
*/
private int lineNumber(int offset) {
int[] lineStarts = lineStarts();
// TODO Could use binary search which is faster
int lineNumber = 0;
for (int i = 0; i < lineStarts.length; i++) {
if (lineStarts[i]<=offset) {
lineNumber = i;
} else {
return lineNumber;
}
}
return lineNumber;
}
public TextDocumentIdentifier getId() {
TextDocumentIdentifier id = new TextDocumentIdentifier();
id.setUri(getUri());
return id;
}
public String getLineIndentString(int line) {
int start = startOfLine(line);
int scan = start;
char c = getSafeChar(scan);
StringBuilder indentStr = new StringBuilder();
while (c==' '|| c=='\t') {
indentStr.append(c);
c = getSafeChar(++scan);
}
return indentStr.toString();
}
private char getSafeChar(int pos) {
String text = getText();
if (pos>0 && pos<text.length()) {
return text.charAt(pos);
}
return 0;
}
}

View File

@@ -0,0 +1,197 @@
/*******************************************************************************
* 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.languageserver.completion;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.IRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness;
/**
* @author Kris De Volder
*/
public class DocumentEditsTest {
//TODO: it is rather strange to put this test in the language-server-test-harness' project.
// It really belongs in commons-language-server, but unfortunately that makes it impossible
// for the test to use language-server-test-harness (it requires making commons-language-server depend on
// language-server-test-harness which causes a cyclic dependency).
private LanguageServerHarness harness;
@Before
public void setup() throws Exception {
harness = new LanguageServerHarness(null);
}
class TestSubject {
private Editor editor;
private DocumentEdits edits;
private String orgText;
public TestSubject(String contents) throws Exception {
this.orgText = contents;
reset();
}
public void reset() throws Exception {
this.editor = harness.newEditor(orgText);
this.edits = new DocumentEdits(getFreshDocument(editor));
}
private IDocument getFreshDocument(Editor editor) throws Exception {
TextDocument doc = new TextDocument(null, editor.getLanguageId());
doc.setText(editor.getRawText());
return doc;
}
public void del(String snippet) {
int start = orgText.indexOf(snippet);
assertTrue(start>=0);
int end = start + snippet.length();
edits.delete(start, end);
}
public void expect(String expect) throws Exception {
apply(editor, edits);
assertEquals(expect, editor.getText());
}
private void apply(Editor editor, DocumentEdits edit) throws Exception {
IDocument document = getFreshDocument(editor);
edits.apply(document);
editor.setRawText(document.get());
IRegion sel = edit.getSelection();
int selectionStart = sel.getOffset();
int selectionEnd = selectionStart+sel.getLength();
editor.setSelection(selectionStart, selectionEnd);
}
public void insBefore(String before, String insert) {
int offset = orgText.indexOf(before);
assertTrue(offset>=0);
edits.insert(offset, insert);
}
public void delLineAt(String snippet) throws Exception {
int offset = orgText.indexOf(snippet);
assertTrue(offset>=0);
edits.deleteLineBackwardAtOffset(offset);
}
public void delLine(int i) throws Exception {
edits.deleteLineBackward(0);
}
}
@Test public void testDeletes() throws Exception {
TestSubject it;
it = new TestSubject("0123456789<*>");
it.del("123");
it.del("567");
it.expect("04<*>89");
it = new TestSubject("0123456789<*>");
it.del("567");
it.del("123");
it.expect("0<*>489");
it = new TestSubject("0123456789<*>");
it.del("012345");
it.del("345");
it.expect("<*>6789");
it = new TestSubject("0123456789<*>");
it.del("345");
it.del("012345");
it.expect("<*>6789");
it = new TestSubject("0123456789<*>");
it.del("2345");
it.del("34567");
it.expect("01<*>89");
it = new TestSubject("0123456789<*>");
it.del("123");
it.del("234");
it.del("7");
it.expect("056<*>89");
}
@Test public void testInserts() throws Exception {
TestSubject it;
it = new TestSubject("The fox jumps over the dog!");
it.insBefore("fox", "quick ");
it.insBefore("fox", "brown ");
it.insBefore("dog", "lazy ");
it.expect("The quick brown fox jumps over the lazy <*>dog!");
}
@Test public void testInsertAndDelete() throws Exception {
TestSubject it;
it = new TestSubject("The fox jumps over the dog!");
it.insBefore("fox", "quick "); //"The quick fox jumps..."
it.del("The fox"); //" jumps..."
it.insBefore("fox", "A rabbit");//"A rabbit jumps ..."
it.expect("A rabbit<*> jumps over the dog!");
}
@Test public void testDeleteLine() throws Exception {
TestSubject it;
it = new TestSubject(
"Line 0\n" +
"Line 1\n" +
"Line 2"
);
it.delLineAt("0");
it.expect(
"<*>Line 1\n" +
"Line 2"
);
it.reset();
it.delLineAt("1");
it.expect(
"Line 0<*>\n" +
"Line 2"
);
it.reset();
it.delLineAt("2");
it.expect(
"Line 0\n" +
"Line 1<*>"
);
it = new TestSubject("Line 0"); //special case: no newlines in document
it.delLineAt("0");
it.expect("<*>");
it = new TestSubject("");
it.delLine(0);
it.expect("<*>");
}
}