Moved commons and concourse editor to 'headless-services'
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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("<*>");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user