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,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>commons-language-server</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,3 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8

View File

@@ -0,0 +1,5 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.8

View File

@@ -0,0 +1,59 @@
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_missing_override_annotations_interface_methods=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_functional_interfaces=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=false
sp_cleanup.format_source_code=false
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.insert_inferred_type_arguments=false
sp_cleanup.make_local_variable_final=true
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=false
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=true
sp_cleanup.organize_imports=true
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_redundant_type_arguments=false
sp_cleanup.remove_trailing_whitespaces=true
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=true
sp_cleanup.remove_unnecessary_nls_tags=false
sp_cleanup.remove_unused_imports=false
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_anonymous_class_creation=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_lambda=true
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true

View File

@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@@ -0,0 +1,55 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>commons-language-server</artifactId>
<name>commons-language-server</name>
<description>Shared utilities for building vscode language servers in Java</description>
<parent>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>commons-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>commons-util</artifactId>
<version>${project.version}</version>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson-2-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson-2-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-2-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson-2-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson-2-version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor-version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,25 @@
/*******************************************************************************
* Copyright (c) 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.commons.languageserver;
/**
* Central place to define constants for the known language-ids that we care
* about.
*
* @author Kris De Volder
*/
public class LanguageIds {
public static final String PLAINTEXT = "plaintext";
public static final String CONCOURSE_TASK = "concourse-task-yaml";
public static final String CONCOURSE_PIPELINE = "concourse-pipeline-yaml";
}

View File

@@ -0,0 +1,248 @@
/*******************************************************************************
* 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.commons.languageserver;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.Channels;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Provider;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.LanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.LoggingFormat;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
/**
* Abstract class meant to minimize the amount of code needed to
* write to create suitable 'main' method to launch a language server.
* <p>
* The easiest way to use this is to create your own static main method.
* Then call this class's start method with a Provider<SimpleLanguageServer> as
* a argument.
* <p>
* Alternatively, you can also subclass it and implement the abstract
* `createServer` method.
*
* @author Kris De Volder
* @author Martin Lippert
*/
public abstract class LaunguageServerApp {
private static final String STANDALONE_STARTUP = "standalone-startup";
private static final int SERVER_STANDALONE_PORT = 5007;
public static void start(Provider<SimpleLanguageServer> languageServerFactory) throws IOException, InterruptedException {
LaunguageServerApp app = new LaunguageServerApp() {
@Override
protected SimpleLanguageServer createServer() {
return languageServerFactory.get();
}
};
if (System.getProperty(STANDALONE_STARTUP, "false").equals("true")) {
app.startAsServer();
}
else {
app.start();
}
}
public static void startAsServer(Provider<SimpleLanguageServer> languageServerFactory) throws IOException, InterruptedException {
LaunguageServerApp app = new LaunguageServerApp() {
@Override
protected SimpleLanguageServer createServer() {
return languageServerFactory.get();
}
};
app.startAsServer();
}
protected static class Connection {
final InputStream in;
final OutputStream out;
final Socket socket;
private Connection(InputStream in, OutputStream out, Socket socket) {
this.in = in;
this.out = out;
this.socket = socket;
}
void dispose() {
if (in != null) {
try {
in.close();
} catch (IOException e) {
LOG.log(Level.SEVERE, e.getMessage(), e);
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
LOG.log(Level.SEVERE, e.getMessage(), e);
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
LOG.log(Level.SEVERE, e.getMessage(), e);
}
}
}
}
public void start() throws IOException {
LOG.info("Starting LS");
Connection connection = null;
try {
LoggingFormat.startLogging();
connection = connectToNode();
run(connection);
} catch (Throwable t) {
LOG.log(Level.SEVERE, t.getMessage(), t);
System.exit(1);
} finally {
if (connection != null) {
connection.dispose();
}
}
}
/**
* starts up the language server and let it listen for connections from the outside
* instead of connecting itself to an existing port or channel.
*
* This is meant for development only, to reduce turnaround times while working
* on the language server from within an IDE, so that you can start the language
* server right away in debug mode and let the vscode extension connect to that
* instance instead of vice versa.
*
* Source of inspiration:
* https://github.com/itemis/xtext-languageserver-example/blob/master/org.xtext.example.mydsl.ide/src/org/xtext/example/mydsl/ide/RunServer.java
*/
public void startAsServer() throws IOException, InterruptedException {
LOG.info("Starting LS as standlone server");
Function<MessageConsumer, MessageConsumer> wrapper = consumer -> {
MessageConsumer result = consumer;
return result;
};
SimpleLanguageServer languageServer = createServer();
Launcher<STS4LanguageClient> launcher = createSocketLauncher(languageServer, STS4LanguageClient.class,
new InetSocketAddress("localhost", SERVER_STANDALONE_PORT), Executors.newCachedThreadPool(), wrapper);
languageServer.connect(launcher.getRemoteProxy());
Future<?> future = launcher.startListening();
while (!future.isDone()) {
Thread.sleep(10_000l);
}
}
private <T> Launcher<T> createSocketLauncher(Object localService, Class<T> remoteInterface, SocketAddress socketAddress, ExecutorService executorService, Function<MessageConsumer, MessageConsumer> wrapper) throws IOException {
AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open().bind(socketAddress);
AsynchronousSocketChannel socketChannel;
try {
socketChannel = serverSocket.accept().get();
return Launcher.createIoLauncher(localService, remoteInterface, Channels.newInputStream(socketChannel), Channels.newOutputStream(socketChannel), executorService, wrapper);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
}
private static Connection connectToNode() throws IOException {
String port = System.getProperty("server.port");
if (port != null) {
Socket socket = new Socket("localhost", Integer.parseInt(port));
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
LOG.info("Connected to parent using socket on port " + port);
return new Connection(in, out, socket);
}
else {
InputStream in = System.in;
PrintStream out = System.out;
LOG.info("Connected to parent using stdio");
return new Connection(in, out, null);
}
}
public static final Logger LOG = Logger.getLogger("main");
/**
* Listen for requests from the parent node process.
* Send replies asynchronously.
* When the request stream is closed, wait for 5s for all outstanding responses to compute, then return.
* @throws ExecutionException
* @throws InterruptedException
*/
protected void run(Connection connection) throws InterruptedException, ExecutionException {
LanguageServer server = createServer();
ExecutorService executor = Executors.newCachedThreadPool();
Function<MessageConsumer, MessageConsumer> wrapper = (MessageConsumer consumer) -> {
return (msg) -> {
try {
consumer.consume(msg);
} catch (UnsupportedOperationException e) {
//log a warning and ignore. We are getting some messages from vsCode the server doesn't know about
LOG.log(Level.WARNING, "Unsupported message was ignored!", e);
}
};
};
Launcher<STS4LanguageClient> launcher = Launcher.createLauncher(server,
STS4LanguageClient.class,
connection.in,
connection.out,
executor,
wrapper
);
if (server instanceof LanguageClientAware) {
LanguageClient client = launcher.getRemoteProxy();
((LanguageClientAware) server).connect(client);
}
launcher.startListening().get();
}
protected abstract SimpleLanguageServer createServer();
}

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* 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.commons.languageserver;
public class ProgressParams {
/**
* An id representing the enitity for which progress messages are to be shown.
*/
private String id;
/**
* Updates the current statusMsg associated with a given the id. If null, then the message
* is cleared.
*/
private String statusMsg;
public ProgressParams() {
}
public ProgressParams(String id, String statusMsg) {
super();
this.id = id;
this.statusMsg = statusMsg;
}
@Override
public String toString() {
return "ProgressParams [id=" + id + ", statusMsg=" + statusMsg + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((statusMsg == null) ? 0 : statusMsg.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ProgressParams other = (ProgressParams) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (statusMsg == null) {
if (other.statusMsg != null)
return false;
} else if (!statusMsg.equals(other.statusMsg))
return false;
return true;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getStatusMsg() {
return statusMsg;
}
public void setStatusMsg(String statusMsg) {
this.statusMsg = statusMsg;
}
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* 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.commons.languageserver;
@FunctionalInterface
public interface ProgressService {
/**
* Sends a progress event to the LSP client. A taskId is an arbirary id
* that can be chosen by the caller. The purpose of the id is to be a 'unique'
* id for some kind of 'long running job'. Only a single 'statusMsg' is associated
* with a given taskId at any one time. Each event updates the message shown
* to the user replacing the old one.
* <p>
* Updating the message to 'null' erases the previous message without showing
* a new one.
* <p>
* More than one message may be shown simultaneously to the user, if they
* have different taskId.
*
* @param taskId
* @param statusMsg
*/
void progressEvent(String taskId, String statusMsg);
}

View File

@@ -0,0 +1,27 @@
/*******************************************************************************
* 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.commons.languageserver;
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
import org.eclipse.lsp4j.services.LanguageClient;
/**
* Some 'custom' extensions to standard LSP {@link LanguageClient}.
*
* @author
*/
public interface STS4LanguageClient extends LanguageClient {
@JsonNotification("sts/progress")
void progress(ProgressParams progressEvent);
}

View File

@@ -0,0 +1,418 @@
/*******************************************************************************
* 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 java.util.ArrayList;
import org.eclipse.lsp4j.TextEdit;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.BadLocationException;
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.Region;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* Helper to make it easier to create composite modifications to IDocument.
* <p>
* It allows building up a sequence of edits which are all expressed in terms of
* offsets in the unmodified document. (So, when computing edits based on a
* some kind of AST its is not necessary to recompute the AST or update its
* position information after each small modification).
* <p>
* Similar functionality to Eclipse's {@link TextEdit} but unlike {@link TextEdit}
* it is not as finicky with respect to overlapping edits. We consider the
* order in which edits are created meaningful and give a logical semantics
* to edits that 'overlap'.
* <p>
* Also, each edit affects the cursor position placing it at the end of
* that edit. This will mostly do what you would want it to, provided that
* you save the edit where you want the cursor to end-up at for last.
*
* @author Kris De Volder
*/
public class DocumentEdits implements ProposalApplier {
// Note: for small number of edits this implementation is okay.
// for large number of edits it is potentially slow because of the
// way it transforms edit coordinates (a growing chain of
// OffsetTransformer is created so every extra edit added
// will take O(n) to preform the transform on its coordinates.
// So applying 'n' edits is O(n^2).
//
// A smarter way of doing this is possible. Here's a possible idea:
//
// For simplicity sake assume that all edits are 'independent' (i.e.
// changing their executing order doesn't matter.
//
// It is advantageous to sort the edits by position and execute them
// high to low because... we can then guarantee that the transform
// function that will apply to each edit does nothing on the coordinates
// that it cares about (since all prior edits only affect higher offets)
//
// Unfortunately the simplifying assumption does not allways hold.
// There are two problems:
//
// 1) updating the selection is order dependent.
// => this can be solved by observing that only the last
// edit operation need update the selection since it cancels
// all prior selections.
// => Mark the last operation with a 'flag' 'setSelection=true'
// and do not update the selection in any other operations.
//
// 2) some edits may not be independent
// => When the edits are sorted in descending order based on their 'end'
// coordinate them 'conflicting' edits should be adjacent and we can
// 'group them' together into 'cluster' where we can preserve their
// relative execution order.
// => While executing a 'cluster' we shall keep track of the offset
// transform function just like the current implementation does.
// => When the cluster of 'conflicting' operations has been dealt with
// the offset transform function no longer matters for the
// remaining edits who's offsets are all strictly 'smaller'.
// Thus the trasnform function can be discarded.
//
// Assuming most edits are independent and only a few of them conflict, then
// this algorithm can provide equivalent functinonality to the current one
// but for an 'average' performance which is O(n*log(n))
// Of course worst-case is still O(n^2) but we wouldn't expect to hit that case
// assuming we mostly have lots of small edits to disjoint sections of the document.
//
// So... edit operations could be sorted based on their position
// and executed in decreasing order of their 'start'.
//
// The tricky part would be to preserve the order-dependent semantics.
public static class TextReplace {
public final int start;
public final int end;
public final String newText;
public TextReplace(int start, int end, String newText) {
super();
this.start = start;
this.end = end;
this.newText = newText;
}
public IRegion getRegion() {
return new Region(start, end-start);
}
}
/**
* When an insert occurs, a single position in the file transforms ambiguously into two different
* positions after the insertion, depending on whether we 'float' marker for the position after the
* inserted block or leave it at the beginning.
*/
public enum Direction {
/**
* Transform positions around inserts to stick to the front of the inserted block.
*/
BEFORE,
/**
*
* Transform positions around inserts to stick to the end of the inserted block.
*/
AFTER
}
private class Insertion extends Edit {
private int offset;
private String text;
public Insertion(int offset, String insert) {
this.offset = offset;
this.text = insert;
}
@Override
void apply(DocumentState doc) throws BadLocationException {
doc.insert(offset, text);
}
@Override
public String toString() {
return "ins("+text+"@"+offset+")";
}
@Override
public int getStart() {
return offset;
}
@Override
public int getEnd() {
return offset;
}
}
private abstract class Edit {
public abstract int getStart();
public abstract int getEnd();
abstract void apply(DocumentState doc) throws BadLocationException;
@Override
public abstract String toString();
}
private class Deletion extends Edit {
private int start;
private int end;
public Deletion(int start, int end) {
this.start = start;
this.end = end;
}
@Override
void apply(DocumentState doc) throws BadLocationException {
doc.delete(start, end);
}
@Override
public String toString() {
return "del("+start+"->"+end+")";
}
@Override
public int getStart() {
return start;
}
@Override
public int getEnd() {
return end;
}
}
private interface OffsetTransformer {
int transform(int offset, Direction dir);
}
private static final OffsetTransformer NULL_TRANSFORM = new OffsetTransformer() {
@Override
public int transform(int offset, Direction dir) {
return offset;
}
};
/**
* DocumentState provides methods to modify a document, its methods accept
* offsets expressed relative to the original document contents and keeps track
* of a OffsetTransformer that maps them to offsets in the current document.
*/
private static class DocumentState {
private IDocument doc; //may be null, in which case no actual modifications are performed
private OffsetTransformer org2new = NULL_TRANSFORM;
private int selection = -1; //-1 Means no edits where applied that change selection so
// the current selection is unknown
public DocumentState(IDocument doc) {
this.doc = doc;
}
public void insert(int start, final String text) throws BadLocationException {
final int tStart = org2new.transform(start, Direction.AFTER);
if (!text.isEmpty()) {
if (doc!=null) {
doc.replace(tStart, 0, text);
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
@Override
public int transform(int org, Direction dir) {
int tOffset = parent.transform(org, dir);
if (tOffset<tStart) {
return tOffset;
} else if (tOffset>tStart) {
return tOffset + text.length();
} else /* tOffset==tStart*/ {
if (dir==Direction.BEFORE) {
return tOffset;
} else {
return tOffset + text.length();
}
}
}
};
}
selection = tStart+text.length();
}
public void delete(final int start, final int end) throws BadLocationException {
final int tStart = org2new.transform(start, Direction.AFTER);
if (end>start) { // skip work for 'delete nothing' op
final int tEnd = org2new.transform(end, Direction.AFTER);
if (tEnd>tStart) { // skip work for 'delete nothing' op
if (doc!=null) {
doc.replace(tStart, tEnd-tStart, "");
}
final OffsetTransformer parent = org2new;
org2new = new OffsetTransformer() {
@Override
public int transform(int org, Direction dir) {
int tOffset = parent.transform(org, dir);
if (tOffset<=tStart) {
return tOffset;
} else if (tOffset>=tEnd) {
return tOffset - tEnd + tStart;
} else {
return start;
}
}
};
}
}
selection = tStart;
}
@Override
public String toString() {
if (doc==null) {
return super.toString();
}
StringBuilder buf = new StringBuilder();
buf.append("DocumentState(\n");
buf.append(doc.get()+"\n");
buf.append(")\n");
return buf.toString();
}
}
private ArrayList<Edit> edits = new ArrayList<Edit>();
private IDocument doc;
public DocumentEdits(IDocument doc) {
this.doc = doc;
}
public void delete(int start, int end) {
Assert.isLegal(start<=end);
edits.add(new Deletion(start, end));
}
public void delete(int offset, String text) {
delete(offset, offset+text.length());
}
public void insert(int offset, String insert) {
edits.add(new Insertion(offset, insert));
}
@Override
public IRegion getSelection() throws Exception {
DocumentState selectionState = new DocumentState(null);
for (Edit edit : edits) {
edit.apply(selectionState);
}
if (selectionState.selection>=0) {
return new Region(selectionState.selection, 0);
}
return null;
}
public TextReplace asReplacement(TextDocument doc) throws BadLocationException {
if (!edits.isEmpty()) {
int start = edits.stream().mapToInt(Edit::getStart).min().getAsInt();
int end = edits.stream().mapToInt(Edit::getEnd).max().getAsInt();
DocumentState state = new DocumentState(doc.copy());
for (Edit edit : edits) {
edit.apply(state);
}
int newStart = state.org2new.transform(start, Direction.BEFORE);
int newEnd = state.org2new.transform(end, Direction.AFTER);
return new TextReplace(start, end, state.doc.textBetween(newStart, newEnd));
}
return null;
}
@Override
public void apply(IDocument _doc) throws Exception {
DocumentState doc = new DocumentState(_doc);
for (Edit edit : edits) {
edit.apply(doc);
}
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("DocumentModifier(\n");
for (Edit edit : edits) {
buf.append(" "+edit);
}
buf.append(")\n");
return buf.toString();
}
public void moveCursorTo(int newCursor) {
insert(newCursor, "");
}
public void deleteLineBackwardAtOffset(int offset) throws Exception {
int line = doc.getLineOfOffset(offset);
deleteLineBackward(line);
}
/**
* Deletes the line of text with given line number, including either the following or
* preceding newline. If there is a choice between the preceding or following newline,
* the preceding newline is deleted. This will leave the cursor at the end of
* the preceding line.
* <p>
* Note: a similar operation 'deleteLineForward' could be implemented prefering to
* delete the following newline. This would be equivalent except that it will leave the
* cursor at the start of the following line.
*/
public void deleteLineBackward(int lineNumber) throws BadLocationException {
IRegion line = doc.getLineInformation(lineNumber);
int startOfDeletion;
int endOfDeletion;
if (lineNumber>0) {
IRegion previousLine = doc.getLineInformation(lineNumber-1);
startOfDeletion = endOf(previousLine);
endOfDeletion = endOf(line);
} else if (lineNumber<doc.getNumberOfLines()-1) {
IRegion nextLine = doc.getLineInformation(lineNumber+1);
startOfDeletion = line.getOffset();
endOfDeletion = nextLine.getOffset();
} else {
startOfDeletion = line.getOffset();
endOfDeletion = endOf(line);
}
delete(startOfDeletion, endOfDeletion);
}
private int endOf(IRegion line) {
return line.getOffset()+line.getLength();
}
public void replace(int start, int end, String newText) {
delete(start, end);
insert(start, newText);
}
/**
* Adds extra indentation at the position of the first edit in this {@link DocumentEdits}
*/
public void indentFirstEdit(String indentString) {
if (edits.size()>0) {
Edit firstEdit = edits.get(0);
int offset = firstEdit.getStart();
edits.add(0, new Insertion(offset, indentString));
}
}
}

View File

@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.completion;
import java.util.Collection;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* @author Kris De Volder
*/
public interface ICompletionEngine {
Collection<ICompletionProposal> getCompletions(IDocument document, int offset) throws Exception;
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* 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.commons.languageserver.completion;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.util.Renderable;
/**
* Replaces STS/Eclipse's ICompletionProposal
*/
public interface ICompletionProposal {
/**
* Transforms a proposal to make it standout less somehow.
*/
ICompletionProposal deemphasize();
String getLabel();
CompletionItemKind getKind();
DocumentEdits getTextEdit();
String getDetail();
Renderable getDocumentation();
default String getFilterText() { return getLabel(); }
}

View File

@@ -0,0 +1,29 @@
/*******************************************************************************
* 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.commons.languageserver.completion;
import java.util.concurrent.Callable;
/**
* Temprary placeholder which sort of replaces the LazyProposalApplier from old STS.
* It really does nothing right now. Somehow this should be tied into LS protocol so
* that the edits are not computed until a completion is being resolved.
* <p>
* Right now this is not lazy at all and the completion edits are just computed immediatly.
*/
public class LazyProposalApplier {
public static DocumentEdits from(Callable<DocumentEdits> createEdits) throws Exception {
return createEdits.call();
}
}

View File

@@ -0,0 +1,36 @@
/*******************************************************************************
* Copyright (c) 2015 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.completion;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.IRegion;
/**
* Interface that represents the methods that one needs to implement in order
* to define how content assist proposal is applied to a IDocument
*
* @author Kris De Volder
*/
public interface ProposalApplier {
/**
* {@link ProposalApplier} that does nothing whatsoever.
*/
static ProposalApplier NULL = new ProposalApplier() {
@Override public IRegion getSelection() { return null; }
@Override public void apply(IDocument doc) {}
@Override public String toString() { return "NULL";};
};
IRegion getSelection() throws Exception;
void apply(IDocument doc) throws Exception;
}

View File

@@ -0,0 +1,123 @@
/*******************************************************************************
* 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.commons.languageserver.completion;
import java.util.Comparator;
public abstract class ScoreableProposal implements ICompletionProposal {
private static final double DEEMP_VALUE = 100000; // should be large enough to move deemphasized stuff to bottom of list.
private double deemphasizedBy = 0.0;
/**
* A sorter suitable for sorting ScoreableProposals based on their score.
*/
public static final Comparator<ICompletionProposal> COMPARATOR = new Comparator<ICompletionProposal>() {
@Override
public int compare(ICompletionProposal p1, ICompletionProposal p2) {
if (p1 instanceof ScoreableProposal && p2 instanceof ScoreableProposal) {
double s1 = ((ScoreableProposal)p1).getScore();
double s2 = ((ScoreableProposal)p2).getScore();
if (s1==s2) {
String name1 = ((ScoreableProposal)p1).getLabel();
String name2 = ((ScoreableProposal)p2).getLabel();
return name1.compareTo(name2);
} else {
return Double.compare(s2, s1);
}
}
return 0;
}
};
public abstract double getBaseScore();
public final double getScore() {
return getBaseScore() - deemphasizedBy;
}
@Override
public ScoreableProposal deemphasize() {
deemphasizedBy+= DEEMP_VALUE;
return this;
}
public boolean isDeemphasized() {
return deemphasizedBy > 0;
}
// @Override
// public boolean isAutoInsertable() {
// return !isDeemphasized();
// }
// public StyledString getStyledDisplayString() {
// StyledString result = new StyledString();
// highlightPattern(getHighlightPattern(), getBaseDisplayString(), result);
// return result;
// }
// private void highlightPattern(String pattern, String data, StyledString result) {
// Styler highlightStyle = CompletionFactory.HIGHLIGHT;
// Styler plainStyle = isDeemphasized()?CompletionFactory.DEEMPHASIZE:CompletionFactory.NULL_STYLER;
// if (isDeprecated()) {
// highlightStyle = CompletionFactory.compose(highlightStyle, CompletionFactory.DEPRECATE);
// plainStyle = CompletionFactory.compose(plainStyle, CompletionFactory.DEPRECATE);
// }
// if (StringUtils.hasText(pattern)) {
// int dataPos = 0; int dataLen = data.length();
// int patternPos = 0; int patternLen = pattern.length();
//
// while (dataPos<dataLen && patternPos<patternLen) {
// int pChar = pattern.charAt(patternPos++);
// int highlightPos = data.indexOf(pChar, dataPos);
// if (dataPos<highlightPos) {
// result.append(data.substring(dataPos, highlightPos), plainStyle);
// }
// result.append(data.charAt(highlightPos), highlightStyle);
// dataPos = highlightPos+1;
// }
// if (dataPos<dataLen) {
// result.append(data.substring(dataPos), plainStyle);
// }
// } else { //no pattern to highlight
// result.append(data, plainStyle);
// }
// }
// protected abstract boolean isDeprecated();
// protected abstract String getHighlightPattern();
// protected abstract String getBaseDisplayString();
// @Override
// public String getAdditionalProposalInfo() {
// HoverInfo hoverInfo = getAdditionalProposalInfo(new NullProgressMonitor());
// if (hoverInfo!=null) {
// return hoverInfo.getHtml();
// }
// return null;
// }
// @Override
// public abstract HoverInfo getAdditionalProposalInfo(IProgressMonitor monitor);
// @Override
// public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
// return null;
// }
//
// @Override
// public int getPrefixCompletionStart(IDocument document, int completionOffset) {
// return completionOffset;
// }
@Override
public String toString() {
return getLabel();
}
}

View File

@@ -0,0 +1,27 @@
/*******************************************************************************
* 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.commons.languageserver.completion;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.TextDocumentPositionParams;
/**
* Interface that needs to be implemented by a 'completion engine' which can be easily
* wired-up to provide completions for a Vscode language server.
*/
public interface VscodeCompletionEngine {
CompletableFuture<CompletionList> getCompletions(TextDocumentPositionParams params);
CompletableFuture<CompletionItem> resolveCompletion(CompletionItem unresolved);
}

View File

@@ -0,0 +1,157 @@
/*******************************************************************************
* 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.commons.languageserver.completion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.TextReplace;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.SortKeys;
import org.springframework.ide.vscode.commons.util.Futures;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* Adapts a {@link ICompletionEngine}, wrapping it, to implement {@link VscodeCompletionEngine}
*/
public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
private final static int MAX_COMPLETIONS = 20;
private int maxCompletions = MAX_COMPLETIONS;
final static Logger logger = LoggerFactory.getLogger(VscodeCompletionEngineAdapter.class);
public static final String VS_CODE_CURSOR_MARKER = "{{}}";
private SimpleLanguageServer server;
private ICompletionEngine engine;
public VscodeCompletionEngineAdapter(SimpleLanguageServer server, ICompletionEngine engine) {
this.server = server;
this.engine = engine;
}
public void setMaxCompletionsNumber(int maxCompletions) {
this.maxCompletions = maxCompletions;
}
@Override
public CompletableFuture<CompletionList> getCompletions(TextDocumentPositionParams params) {
return getCompletionsMono(params).toFuture();
}
private Mono<CompletionList> getCompletionsMono(TextDocumentPositionParams params) {
SimpleTextDocumentService documents = server.getTextDocumentService();
TextDocument doc = documents.get(params).copy();
if (doc!=null) {
return Mono.fromCallable(() -> {
//TODO: This callable is a 'big lump of work' so can't be canceled in pieces.
// Should we push using of reactive streams down further and compose this all
// using reactive style? If not then this is overkill could just as well use
// only standard Java API such as Executor and CompletableFuture directly.
int offset = doc.toOffset(params.getPosition());
List<ICompletionProposal> completions = new ArrayList<>(engine.getCompletions(doc, offset));
Collections.sort(completions, ScoreableProposal.COMPARATOR);
CompletionList list = new CompletionList();
list.setIsIncomplete(false);
List<CompletionItem> items = new ArrayList<>(completions.size());
SortKeys sortkeys = new SortKeys();
int count = 0;
for (ICompletionProposal c : completions) {
count++;
if (maxCompletions > 0 && count>maxCompletions) {
list.setIsIncomplete(true);
break;
}
try {
items.add(adaptItem(doc, c, sortkeys));
} catch (Exception e) {
logger.error("error computing completion", e);
}
}
list.setItems(items);
return list;
})
.subscribeOn(Schedulers.elastic()); //!!! without this the mono will just be computed on the same thread that calls it.
}
return Mono.just(SimpleTextDocumentService.NO_COMPLETIONS);
}
private CompletionItem adaptItem(TextDocument doc, ICompletionProposal completion, SortKeys sortkeys) throws Exception {
CompletionItem item = new CompletionItem();
item.setLabel(completion.getLabel());
item.setKind(completion.getKind());
item.setSortText(sortkeys.next());
item.setFilterText(completion.getFilterText());
item.setDetail(completion.getDetail());
item.setDocumentation(toMarkdown(completion.getDocumentation()));
adaptEdits(item, doc, completion.getTextEdit());
return item;
}
private String toMarkdown(Renderable r) {
if (r!=null) {
return r.toMarkdown();
}
return null;
}
private void adaptEdits(CompletionItem item, TextDocument doc, DocumentEdits edits) throws Exception {
TextReplace replaceEdit = edits.asReplacement(doc);
if (replaceEdit==null) {
//The original edit does nothing.
item.setInsertText("");
} else {
TextDocument newDoc = doc.copy();
edits.apply(newDoc);
TextEdit vscodeEdit = new TextEdit();
vscodeEdit.setRange(doc.toRange(replaceEdit.start, replaceEdit.end-replaceEdit.start));
vscodeEdit.setNewText(vscodeIndentFix(doc, vscodeEdit.getRange().getStart(), replaceEdit.newText));
//TODO: cursor offset within newText? for now we assume its always at the end.
item.setTextEdit(vscodeEdit);
}
}
private String vscodeIndentFix(TextDocument doc, Position start, String newText) {
//Vscode applies some magic indent to a multi-line edit text. We do everything ourself so we have adjust for the magic
// and do some kind of 'inverse magic' here.
//See here: https://github.com/Microsoft/language-server-protocol/issues/83
int referenceLine = start.getLine();
int referenceLineIndent = doc.getLineIndentation(referenceLine);
int vscodeMagicIndent = Math.min(start.getCharacter(), referenceLineIndent);
return vscodeMagicIndent>0
? StringUtil.stripIndentation(vscodeMagicIndent, newText)
: newText;
}
@Override
public CompletableFuture<CompletionItem> resolveCompletion(CompletionItem unresolved) {
//TODO: item is pre-resoved so we don't do anything, but we really should somehow defer some work, such as
// for example computing docs and edits to resolve time.
//The tricky part is that we have to probably remember infos about the unresolved elements somehow so we can resolve later.
return Futures.of(unresolved);
}
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* Copyright (c) 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.commons.languageserver.definition;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import reactor.core.publisher.Flux;
/**
* {@link SimpleDefinitionFinder} provides a 'dummy' implementation of
* @author Kris De Volder
*/
public class SimpleDefinitionFinder<T extends SimpleLanguageServer> implements DefinitionHandler {
protected final T server;
public SimpleDefinitionFinder(T server) {
this.server = server;
}
@Override
public CompletableFuture<List<Location>> handle(TextDocumentPositionParams position) {
return findDefinitions(position)
.collect(Collectors.toList())
.toFuture();
}
/**
* This is meant to be overridden by subclass. This method provides a simple implementation
* of 'goto definition' (which is not one you probably want to use in practice, but it
* might be usful just to test whether things are wired up correctly to make the
* 'goto definition' action in vscode work.
* <p>
* The implementation provided here simply looks for the first occurrence of the word
* currently pointed at in the current document using String.indexOf.
*/
protected Flux<Location> findDefinitions(TextDocumentPositionParams params) {
try {
TextDocument doc = server.getTextDocumentService().get(params);
int offset = doc.toOffset(params.getPosition());
int start = offset;
while (Character.isLetter(doc.getSafeChar(start))) {
start--;
}
start = start+1;
int end = offset;
while (Character.isLetter(doc.getSafeChar(end))) {
end++;
}
String word = doc.textBetween(start, end);
Log.log("Looking for definition of '"+word+"'");
String text = doc.get();
int def = text.indexOf(word);
if (def>=0) {
return Flux.just(
new Location(params.getTextDocument().getUri(),
doc.toRange(def, word.length())
)
)
.doOnNext((Location loc) -> {
Log.log("definition: "+loc);
});
}
} catch (Exception e) {
Log.log(e);
}
return Flux.empty();
}
}

View File

@@ -0,0 +1,23 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.hover;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.IRegion;
import reactor.util.function.Tuple2;
public interface HoverInfoProvider {
Tuple2<Renderable, IRegion> getHoverInfo(IDocument document, int offset) throws Exception;
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.hover;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.TextDocumentPositionParams;
public interface VscodeHoverEngine {
CompletableFuture<Hover> getHover(TextDocumentPositionParams params);
}

View File

@@ -0,0 +1,100 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.hover;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.IRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableList;
import reactor.util.function.Tuple2;
public class VscodeHoverEngineAdapter implements VscodeHoverEngine {
public enum HoverType {
MARKDOWN,
HTML
}
private HoverInfoProvider hoverInfoProvider;
private SimpleLanguageServer server;
private HoverType type;
final static Logger logger = LoggerFactory.getLogger(VscodeHoverEngineAdapter.class);
public VscodeHoverEngineAdapter(SimpleLanguageServer server, HoverInfoProvider hoverInfoProvider) {
this(server, hoverInfoProvider, HoverType.MARKDOWN);
}
public VscodeHoverEngineAdapter(SimpleLanguageServer server, HoverInfoProvider hoverInfoProvider, HoverType type) {
this.hoverInfoProvider = hoverInfoProvider;
this.server = server;
this.type = type;
}
public void setHoverType(HoverType type) {
this.type = type;
}
@Override
public CompletableFuture<Hover> getHover(TextDocumentPositionParams params) {
//TODO: This returns a CompletableFuture which suggests we should try to do expensive work asyncly.
// We are currently just doing all this in a blocking way and wrapping the already computed list into
// a trivial pre-resolved future.
try {
SimpleTextDocumentService documents = server.getTextDocumentService();
TextDocument doc = documents.get(params);
if (doc!=null) {
int offset = doc.toOffset(params.getPosition());
Tuple2<Renderable, IRegion> hoverTuple = hoverInfoProvider.getHoverInfo(doc, offset);
if (hoverTuple != null) {
Renderable hoverInfo = hoverTuple.getT1();
IRegion region = hoverTuple.getT2();
Range range = doc.toRange(region.getOffset(), region.getLength());
String rendered = render(hoverInfo, type);
if (StringUtil.hasText(rendered)) {
Hover hover = new Hover(ImmutableList.of(Either.forLeft(rendered)), range);
return CompletableFuture.completedFuture(hover);
}
}
}
} catch (Exception e) {
logger.error("error computing hover", e);
}
return SimpleTextDocumentService.NO_HOVER;
}
private static String render(Renderable renderable, HoverType type) {
switch (type) {
case HTML:
return renderable.toHtml();
case MARKDOWN:
return renderable.toMarkdown();
default:
return renderable.toMarkdown();
}
}
}

View File

@@ -0,0 +1,37 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.quickfix;
import java.util.List;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
/**
* Represents a strategy for computing potential quickfixes for a given problem.
*
* @author Kris De Volder
*/
@FunctionalInterface
public interface ProblemFixer {
/**
* Implementor can inspect the problem and quickfix context provided as parameters.
* <p>
* If the problem is deemed fixable, the strategy can contribute one or more fixes by
* adding them to the list of proposals (provided as third parameter).
*/
void contributeFixes(
QuickfixContext context, ReconcileProblem problem,
List<ICompletionProposal> proposals
);
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.quickfix;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* Provides access to additional context info and objects that quickfixes might
* need in order to be able to apply themselves.
*
* @author Kris De Volder
*/
public interface QuickfixContext {
// IProject getProject();
// IPreferenceStore getWorkspacePreferences();
// IPreferenceStore getProjectPreferences();
// IJavaProject getJavaProject();
// UserInteractions getUI();
IDocument getDocument();
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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.commons.languageserver.reconcile;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* A fake reconcule engine which is not useful except for quickly testing
* whether stuff is wired up correctly to the editor.
*/
public class BadWordReconcileEngine implements IReconcileEngine {
static enum BWProblemType implements ProblemType {
VERY_BAD_WORD,
BAD_WORD;
@Override
public ProblemSeverity getDefaultSeverity() {
if (this==VERY_BAD_WORD) {
return ProblemSeverity.ERROR;
} else {
return ProblemSeverity.WARNING;
}
}
@Override
public String getCode() {
return this.name();
}
}
private final String[] BADWORDS = {
"bar", "foo"
};
public BadWordReconcileEngine() {
}
@Override
public void reconcile(IDocument doc, IProblemCollector problemCollector) {
String text = doc.get();
System.out.println(">>>> reconciling for bad words ==========");
System.out.println(text);
System.out.println("<<<< reconciling for bad words ==========");
problemCollector.beginCollecting();
try {
for (String badword : BADWORDS) {
int pos = 0;
while (pos>=0 && pos < text.length()) {
int badPos = text.indexOf(badword, pos);
if (badPos>=0) {
if (badword.equals(BADWORDS[0])) {
problemCollector.accept(new ReconcileProblemImpl(BWProblemType.VERY_BAD_WORD, "'"+badword+"' is a VERY bad word", badPos, badword.length()));
} else {
problemCollector.accept(new ReconcileProblemImpl(BWProblemType.BAD_WORD, "'"+badword+"' is a bad word", badPos, badword.length()));
}
pos = badPos+1;
} else {
pos = badPos;
}
}
}
} finally {
problemCollector.endCollecting();
}
}
}

View File

@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2014-2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.reconcile;
public interface IProblemCollector {
void beginCollecting();
void endCollecting();
void accept(ReconcileProblem problem);
/**
* Problem collector that simply ignores/discards anything passed to it.
*/
IProblemCollector NULL = new IProblemCollector() {
public void beginCollecting() {
}
public void endCollecting() {
}
public void accept(ReconcileProblem problem) {
}
};
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* 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.reconcile;
import org.springframework.ide.vscode.commons.util.text.IDocument;
public interface IReconcileEngine {
IReconcileEngine NULL = (d, p) -> {
p.beginCollecting();
p.endCollecting();
};
public void reconcile(IDocument doc, IProblemCollector problemCollector);
}

View File

@@ -0,0 +1,23 @@
/*******************************************************************************
* 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.commons.languageserver.reconcile;
/**
* @author Kris De Volder
*/
public enum ProblemSeverity {
IGNORE,
WARNING,
ERROR;
}

View File

@@ -0,0 +1,29 @@
/*******************************************************************************
* 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.commons.languageserver.reconcile;
/**
* Besides the methods below, the only hard requirement for a 'problem type' is
* that it is a unique object that is not 'equals' to any other object.
* <p>
* It is probably nice if you implement a good toString however.
* <p>
* A good way to implement a discrete set of problemType objects is as an enum
* that implements this interace.
*
* @author Kris De Volder
*/
public interface ProblemType {
ProblemSeverity getDefaultSeverity();
String toString();
String getCode();
}

View File

@@ -0,0 +1,16 @@
/*******************************************************************************
* Copyright (c) 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.commons.languageserver.reconcile;
public interface ProblemTypeProvider {
ProblemType getProblemType();
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* Copyright (c) 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.commons.languageserver.reconcile;
import org.springframework.ide.vscode.commons.util.ValueParseException;
/**
* Exception if there is a failure when parsing a value. It does not wrap
* other exceptions such that when thrown, the parse exception is the "deepest"
* error.
*
*/
public class ReconcileException extends ValueParseException implements ProblemTypeProvider {
private static final long serialVersionUID = 1L;
private final ProblemType problemType;
public ReconcileException(String message, ProblemType problemType) {
super(message);
this.problemType = problemType;
}
@Override
public ProblemType getProblemType() {
return problemType;
}
}

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* 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.commons.languageserver.reconcile;
/**
* Minamal interface that objects representing a reconciler problem must
* implement.
*
* @author Kris De Volder
*/
public interface ReconcileProblem {
ProblemType getType();
String getMessage();
int getOffset();
int getLength();
String getCode();
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* 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.commons.languageserver.reconcile;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
/**
* An implementation of {@link ReconcileProblem} that is just a simple data object.
*
* @author Kris De Volder
*/
public class ReconcileProblemImpl implements ReconcileProblem {
final private ProblemType type;
final private String msg;
final private int offset;
final private int len;
public ReconcileProblemImpl(ProblemType type, String msg, int offset, int len) {
super();
this.type = type;
this.msg = msg;
this.offset = offset;
this.len = len;
}
@Override
public ProblemType getType() {
return type;
}
@Override
public String getMessage() {
return msg;
}
@Override
public int getOffset() {
return offset;
}
@Override
public int getLength() {
return len;
}
@Override
public String getCode() {
return getType().getCode();
}
/**
* Attempt to enlarge a empty document region to include a
* character that can be visibly underlined.
*/
protected static DocumentRegion makeVisible(DocumentRegion region) {
DocumentRegion altRegion = region.textAfter(1);
if (!altRegion.isEmpty() && canUnderline(altRegion.charAt(0))) {
return altRegion;
}
altRegion = region.textBefore(1);
if (!altRegion.isEmpty() && canUnderline(altRegion.charAt(0))) {
return altRegion;
}
return region;
}
private static boolean canUnderline(char c) {
return c!='\n'&&c!='\r';
}
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.TextDocumentPositionParams;
@FunctionalInterface
public interface CompletionHandler {
CompletableFuture<CompletionList> handle(TextDocumentPositionParams params);
}

View File

@@ -0,0 +1,21 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.CompletionItem;
@FunctionalInterface
public interface CompletionResolveHandler {
CompletableFuture<CompletionItem> handle(CompletionItem unresolved);
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 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.commons.languageserver.util;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.TextDocumentPositionParams;
@FunctionalInterface
public interface DefinitionHandler {
CompletableFuture<List<Location>> handle(TextDocumentPositionParams position);
}

View File

@@ -0,0 +1,285 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.BadLocationException;
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.Region;
/**
* A non-sucky alternative to {@link IRegion}. Represents a region of text in a document.
* <p>
* Caution: assumes the underlying document is not mutated during the lifetime of the
* region object (otherwise start/end positions may no longer be valid).
* <p>
* Implements {@link CharSequence} for convenience (e.g you can use {@link DocumentRegion} as
* input to a {@link Pattern} and other standard JRE functions which expect a {@link CharSequence}.
*
* @author Kris De Volder
*/
public class DocumentRegion implements CharSequence {
final IDocument doc;
final int start;
final int end;
public DocumentRegion(IDocument doc, IRegion r) {
this(doc,
r.getOffset(),
r.getOffset()+r.getLength()
);
}
/**
* Constructs a {@link DocumentRegion} on a given document. Tries its
* best to behave sensibly when passed 'strange' coordinates by
* adjusting them logically rather than throw an Exception.
* <p>
* A position before the start of the document is moved to be the start
* of the document.
* <p>
* A position after the end of the document is moved to the end
* of the document.
* <p>
* If 'end' position is before the start position it is moved be
* exactly at the start position (this avoids region with
* negative length).
*/
public DocumentRegion(IDocument doc, int start, int end) {
this.doc = doc;
this.start = limitRange(start, 0, doc.getLength());
this.end = limitRange(end, start, doc.getLength());
}
/**
* Create {@link DocumentRegion} covering the whole document.
*/
public DocumentRegion(IDocument doc) {
this(doc, 0, doc.getLength());
}
private int limitRange(int offset, int min, int max) {
if (offset<min) {
return min;
}
if (offset>max) {
return max;
}
return offset;
}
@Override
public String toString() {
return DocumentUtil.textBetween(doc, start, end);
}
public DocumentRegion trim() {
return trimEnd().trimStart();
}
public DocumentRegion trimStart() {
int howMany = 0;
int len = length();
while (howMany<len && Character.isWhitespace(charAt(howMany))) {
howMany++;
}
return subSequence(howMany, len);
}
public DocumentRegion trimEnd() {
int howMany = 0; //how many chars to remove from the end
int len = length();
int lastChar = len-1;
while (howMany<len && Character.isWhitespace(charAt(lastChar-howMany))) {
howMany++;
}
if (howMany>0) {
return subSequence(0, len-howMany);
}
return this;
}
/**
* Gets character from the region, offset from the start of the region
* @return the character from the document (char)0 if the offset is outside the region.
*/
@Override
public char charAt(int offset) {
if (offset<0 || offset>=length()) {
throw new IndexOutOfBoundsException(""+offset);
}
try {
return doc.getChar(start+offset);
} catch (BadLocationException e) {
throw new IndexOutOfBoundsException(""+offset);
}
}
@Override
public int length() {
return end-start;
}
@Override
public DocumentRegion subSequence(int start, int end) {
int len = length();
Assert.isLegal(start>=0);
Assert.isLegal(end<=len);
if (start==0 && end==len) {
return this;
}
return new DocumentRegion(doc, this.start+start, this.start+end);
}
public boolean isEmpty() {
return length()==0;
}
public DocumentRegion subSequence(int start) {
return subSequence(start, length());
}
public IRegion asRegion() {
return new Region(start, end-start);
}
public int indexOf(char ch, int fromIndex) {
while (fromIndex < length()) {
if (charAt(fromIndex)==ch) {
return fromIndex;
}
fromIndex++;
}
return -1;
}
public DocumentRegion[] split(char c) {
List<DocumentRegion> pieces = new ArrayList<>();
int start = 0;
int end;
while ((end=indexOf(c, start))>=0) {
pieces.add(subSequence(start, end));
start = end+1;
}
// Do not forget the last piece!
pieces.add(subSequence(start, length()));
return pieces.toArray(new DocumentRegion[pieces.size()]);
}
public DocumentRegion[] split(Pattern delimiter) {
List<DocumentRegion> pieces = new ArrayList<>();
int start = 0;
Matcher matcher = delimiter.matcher(this);
while (matcher.find(start)) {
int end = matcher.start();
pieces.add(subSequence(start, end));
start = matcher.end();
}
// Do not forget the last piece!
pieces.add(subSequence(start, length()));
return pieces.toArray(new DocumentRegion[pieces.size()]);
}
/**
* Removes a single occurrence of pat from the start of this region.
*/
public DocumentRegion trimStart(Pattern pat) {
pat = Pattern.compile("^("+pat.pattern()+")");
Matcher matcher = pat.matcher(this);
if (matcher.find()) {
return subSequence(matcher.end());
}
return this;
}
/**
* Removes a single occurrence of pat from the end of this region.
*/
public DocumentRegion trimEnd(Pattern pat) {
pat = Pattern.compile("("+pat.pattern()+")$");
Matcher matcher = pat.matcher(this);
if (matcher.find()) {
return subSequence(0, matcher.start());
}
return this;
}
/**
* Get the region after this one with a given lenght.
* <p>
* If the document is too short to provide the requested lenght
* then the region is truncated to end of the document.
*/
public DocumentRegion textAfter(int len) {
Assert.isLegal(len>=0);
return new DocumentRegion(doc, end, end+len);
}
/**
* Get the region before this one with a given lenght.
* <p>
* If the requested region extends before the start of the document,
* then the region is shortened so its start coincides with document start.
*/
public DocumentRegion textBefore(int len) {
Assert.isLegal(len>=0);
return new DocumentRegion(doc, start-len, start);
}
public IDocument getDocument() {
return doc;
}
/**
* Get the start of this region in 'absolute' terms (i.e. relative to the document).
*/
public int getStart() {
return start;
}
/**
* Get the end of this region in 'absolute' terms (i.e. relative to the document).
*/
public int getEnd() {
return end;
}
/**
* Convert the given document offset into an offset relative to this region.
*/
public int toRelative(int offset) {
return offset-start;
}
public int getLength() {
return length();
}
public boolean endsWith(CharSequence string) {
int myLen = length();
int strLen = string.length();
if (myLen>=strLen) {
for (int i = 0; i < strLen; i++) {
if (charAt(myLen-strLen+i)!=string.charAt(i)) {
return false;
}
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.IDocument;
public class DocumentUtil {
//TODO: this stuff belongs in IDocument and its implementation, not here. This class should be removed.
/**
* Fetch text between two offsets. Doesn't throw BadLocationException.
* If either one or both of the offsets points outside the
* document then they will be adjusted to point the appropriate boundary to
* retrieve the text just upto the end or beginning of the document instead.
*/
public static String textBetween(IDocument doc, int start, int end) {
Assert.isLegal(start<=end);
if (start>=doc.getLength()) {
return "";
}
if (start<0) {
start = 0;
}
if (end>doc.getLength()) {
end = doc.getLength();
}
if (end<start) {
end = start;
}
try {
return doc.get(start, end-start);
} catch (BadLocationException e) {
//unless the code above is wrong... this is supposed to be impossible!
throw new IllegalStateException("Bug!", e);
}
}
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.TextDocumentPositionParams;
@FunctionalInterface
public interface HoverHandler {
CompletableFuture<Hover> handle(TextDocumentPositionParams params);
}

View File

@@ -0,0 +1,62 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
public class JSON {
public static final ObjectMapper JSON = new ObjectMapper().registerModule(new Jdk8Module())
.registerModule(new JSR310Module())
.registerModule(pathAsJson())
.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
private static SimpleModule pathAsJson() {
SimpleModule m = new SimpleModule();
m.addSerializer(Path.class, new JsonSerializer<Path>() {
@Override
public void serialize(Path path,
JsonGenerator gen,
SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
gen.writeString(path.toString());
}
});
m.addDeserializer(Path.class, new JsonDeserializer<Path>() {
@Override
public Path deserialize(JsonParser parse,
DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return Paths.get(parse.getText());
}
});
return m;
}
}

View File

@@ -0,0 +1,32 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class ListenerList<T> {
private List<Consumer<T>> listeners = new ArrayList<>();
public synchronized void fire(T evt) {
for (Consumer<T> l : listeners) {
l.accept(evt);
}
}
public void add(Consumer<T> l) {
listeners.add(l);
}
}

View File

@@ -0,0 +1,82 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.StringJoiner;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
public class LoggingFormat extends Formatter {
public static ThreadLocal<Integer> request = new ThreadLocal<>();
@Override
public String format(LogRecord record) {
StringJoiner joiner = new StringJoiner("\t");
joiner.add(record.getLevel().getName());
joiner.add(requestAsString());
joiner.add(Thread.currentThread().getName());
joiner.add(Instant.ofEpochMilli(record.getMillis()).toString());
// joiner.add(record.getLoggerName());
joiner.add(record.getSourceClassName() + "#" + record.getSourceMethodName());
joiner.add(record.getMessage());
String result = joiner.toString() + "\n";
Throwable thrown = record.getThrown();
if (thrown != null) {
StringWriter stackTrace = new StringWriter();
PrintWriter print = new PrintWriter(stackTrace);
thrown.printStackTrace(print);
print.flush();
result = result + stackTrace + "\n";
}
return result;
}
private CharSequence requestAsString() {
Integer i = request.get();
if (i == null)
return "?";
else
return Integer.toString(i);
}
private static boolean started = false;
public static void startLogging() throws IOException {
if (!started) {
started = true;
Logger root = Logger.getLogger("");
// for (Handler h : ImmutableList.copyOf(root.getHandlers())) {
// root.removeHandler(h);
// }
// FileHandler file = new FileHandler("sts4-extension.%g.log", 100_000, 1, false);
// root.addHandler(file);
for (Handler h : root.getHandlers())
h.setFormatter(new LoggingFormat());
}
}
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* 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.util;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.IDocument;
public abstract class PrefixFinder {
public String getPrefix(IDocument doc, int offset, int lowerBound) {
try {
if (doc == null || offset > doc.getLength())
return null;
int prefixStart = offset;
while (prefixStart > lowerBound && isPrefixChar(doc.getChar(prefixStart-1))) {
prefixStart--;
}
return doc.get(prefixStart, offset-prefixStart);
} catch (BadLocationException e) {
return null;
}
}
public String getPrefix(IDocument doc, int offset) {
return getPrefix(doc, offset, 0);
}
protected abstract boolean isPrefixChar(char c);
}

View File

@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 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.commons.languageserver.util;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.ReferenceParams;
@FunctionalInterface
public interface ReferencesHandler {
CompletableFuture<List<? extends Location>> handle(ReferenceParams params);
}

View File

@@ -0,0 +1,37 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
/**
* Handler for $ messages. They should/could be ignored since they depend on the
* implementation capabilities on the server.
*
* @author Alex Boyko
*
*/
@JsonSegment("$")
public interface ServiceNotificationsClient {
@JsonNotification
default void setTraceNotification(Object param) {
// Ignore Message
}
@JsonNotification
default void logTraceNotification(Object param) {
// Ignore Message
}
}

View File

@@ -0,0 +1,54 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.Map;
/**
* Convenience wrapper around a 'settings' object. Provides some useful accessor methods to
* retrieve properties from the settings object.
*/
public class Settings {
private Object settings;
public Settings(Object settings) {
this.settings = settings;
}
public Integer getInt(String... names) {
Object val = getProperty(names);
if (val instanceof Number) {
return ((Number) val).intValue();
}
return null;
}
public Object getProperty(String... names) {
return getProperty(settings, names, 0);
}
@SuppressWarnings("rawtypes")
private static Object getProperty(Object settings, String[] names, int i) {
if (i >= names.length) {
return settings;
} else if (settings instanceof Map) {
Object sub = ((Map)settings).get(names[i]);
return getProperty(sub, names, i+1);
} else {
return null;
}
}
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
public class ShowMessageException extends RuntimeException {
private static final long serialVersionUID = 1L;
public final MessageParams message;
public ShowMessageException(MessageParams message, Exception cause) {
super(message.getMessage(), cause);
this.message = message;
}
public static ShowMessageException error(String message, Exception cause) {
return create(MessageType.Error, message, cause);
}
public static ShowMessageException warning(String message, Exception cause) {
return create(MessageType.Warning, message, cause);
}
private static ShowMessageException create(MessageType warning, String message, Exception cause) {
MessageParams m = new MessageParams();
m.setMessage(message);
m.setType(warning);
return new ShowMessageException(m, cause);
}
}

View File

@@ -0,0 +1,238 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.InitializeParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.MessageParams;
import org.eclipse.lsp4j.MessageType;
import org.eclipse.lsp4j.ServerCapabilities;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.LanguageClientAware;
import org.eclipse.lsp4j.services.LanguageServer;
import org.springframework.ide.vscode.commons.languageserver.ProgressParams;
import org.springframework.ide.vscode.commons.languageserver.ProgressService;
import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
/**
* Abstract base class to implement LanguageServer. Bits and pieces copied from
* the 'JavaLanguageServer' example which seem generally useful / reusable end up in
* here so we can try to keep the subclass itself more 'clutter free' and focus on
* what its really doing and not the 'wiring and plumbing'.
*/
public abstract class SimpleLanguageServer implements LanguageServer, LanguageClientAware, ServiceNotificationsClient {
private static final Logger LOG = Logger.getLogger(SimpleLanguageServer.class.getName());
private static final Scheduler RECONCILER_SCHEDULER = Schedulers.newSingle("Reconciler");
private Path workspaceRoot;
private SimpleTextDocumentService tds;
private SimpleWorkspaceService workspace;
private STS4LanguageClient client;
private ProgressService progressService = (String taskId, String statusMsg) -> {
STS4LanguageClient client = SimpleLanguageServer.this.client;
if (client!=null) {
client.progress(new ProgressParams(taskId, statusMsg));
}
};
private CompletableFuture<Void> busyReconcile = CompletableFuture.completedFuture(null);
@Override
public void connect(LanguageClient _client) {
this.client = (STS4LanguageClient) _client;
}
@Override
public CompletableFuture<InitializeResult> initialize(InitializeParams params) {
// LOG.info("Initializing");
String rootPath = params.getRootPath();
if (rootPath==null) {
// LOG.warning("workspaceRoot NOT SET");
} else {
this.workspaceRoot= Paths.get(rootPath).toAbsolutePath().normalize();
// LOG.info("workspaceRoot = "+workspaceRoot);
}
InitializeResult result = new InitializeResult();
ServerCapabilities cap = getServerCapabilities();
result.setCapabilities(cap);
return CompletableFuture.completedFuture(result);
}
public void onError(String message, Throwable error) {
LanguageClient cl = this.client;
if (cl != null) {
if (error instanceof ShowMessageException)
client.showMessage(((ShowMessageException) error).message);
else {
LOG.log(Level.SEVERE, message, error);
MessageParams m = new MessageParams();
m.setMessage(message);
m.setType(MessageType.Error);
client.showMessage(m);
}
}
}
protected abstract ServerCapabilities getServerCapabilities();
@Override
public CompletableFuture<Object> shutdown() {
return CompletableFuture.completedFuture(new Object());
}
@Override
public void exit() {
System.exit(0);
}
public Path getWorkspaceRoot() {
return workspaceRoot;
}
@Override
public synchronized SimpleTextDocumentService getTextDocumentService() {
if (tds==null) {
tds = createTextDocumentService();
}
return tds;
}
protected SimpleTextDocumentService createTextDocumentService() {
return new SimpleTextDocumentService(this);
}
public SimpleWorkspaceService createWorkspaceService() {
return new SimpleWorkspaceService();
}
@Override
public synchronized SimpleWorkspaceService getWorkspaceService() {
if (workspace==null) {
workspace = createWorkspaceService();
}
return workspace;
}
/**
* Convenience method. Subclasses can call this to use a {@link IReconcileEngine} ported
* from old STS codebase to validate a given {@link TextDocument} and publish Diagnostics.
*/
protected void validateWith(TextDocument _doc, IReconcileEngine engine) {
TextDocument doc = _doc.copy();
CompletableFuture<Void> reconcileSession = this.busyReconcile = new CompletableFuture<Void>();
SimpleTextDocumentService documents = getTextDocumentService();
IProblemCollector problems = new IProblemCollector() {
private List<Diagnostic> diagnostics = new ArrayList<>();
@Override
public void endCollecting() {
documents.publishDiagnostics(doc, diagnostics);
}
@Override
public void beginCollecting() {
diagnostics.clear();
}
@Override
public void accept(ReconcileProblem problem) {
try {
DiagnosticSeverity severity = getDiagnosticSeverity(problem);
if (severity!=null) {
Diagnostic d = new Diagnostic();
d.setCode(problem.getCode());
d.setMessage(problem.getMessage());
d.setRange(doc.toRange(problem.getOffset(), problem.getLength()));
d.setSeverity(severity);
diagnostics.add(d);
}
} catch (BadLocationException e) {
LOG.log(Level.WARNING, "Invalid reconcile problem ignored", e);
}
}
};
// Avoid running in the same thread as lsp4j as it can result
// in long "hangs" for slow reconcile providers
Mono.fromRunnable(() -> {
engine.reconcile(doc, problems);
})
.doOnTerminate((ignore1, ignore2) -> {
reconcileSession.complete(null);
})
.subscribeOn(RECONCILER_SCHEDULER)
.subscribe();
}
protected DiagnosticSeverity getDiagnosticSeverity(ReconcileProblem problem) {
ProblemSeverity severity = problem.getType().getDefaultSeverity();
switch (severity) {
case ERROR:
return DiagnosticSeverity.Error;
case WARNING:
return DiagnosticSeverity.Warning;
case IGNORE:
return null;
default:
throw new IllegalStateException("Bug! Missing switch case?");
}
}
public void waitForReconcile() throws Exception {
while (!this.busyReconcile.isDone()) {
this.busyReconcile.get();
}
}
public LanguageClient getClient() {
return client;
}
public ProgressService getProgressService() {
return progressService;
}
}

View File

@@ -0,0 +1,331 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.CodeLensParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentHighlight;
import org.eclipse.lsp4j.DocumentOnTypeFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.DocumentSymbolParams;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.services.TextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.LanguageIds;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableList;
public class SimpleTextDocumentService implements TextDocumentService {
private static final Logger LOG = Logger.getLogger(SimpleTextDocumentService.class.getName());
final private SimpleLanguageServer server;
private Map<String, TextDocument> documents = new HashMap<>();
private ListenerList<TextDocumentContentChange> documentChangeListeners = new ListenerList<>();
private CompletionHandler completionHandler = null;
private CompletionResolveHandler completionResolveHandler = null;
private HoverHandler hoverHandler = null;
private DefinitionHandler definitionHandler;
private ReferencesHandler referencesHandler;
public SimpleTextDocumentService(SimpleLanguageServer server) {
this.server = server;
}
public synchronized void onHover(HoverHandler h) {
Assert.isNull("A hover handler is already set, multiple handlers not supported yet", hoverHandler);
this.hoverHandler = h;
}
public synchronized void onCompletion(CompletionHandler h) {
Assert.isNull("A completion handler is already set, multiple handlers not supported yet", completionHandler);
this.completionHandler = h;
}
public synchronized void onCompletionResolve(CompletionResolveHandler h) {
Assert.isNull("A completionResolveHandler handler is already set, multiple handlers not supported yet", completionResolveHandler);
this.completionResolveHandler = h;
}
public synchronized void onDefinition(DefinitionHandler h) {
Assert.isNull("A defintion handler is already set, multiple handlers not supported yet", definitionHandler);
this.definitionHandler = h;
}
public synchronized void onRefeences(ReferencesHandler h) {
Assert.isNull("A references handler is already set, multiple handlers not supported yet", referencesHandler);
this.referencesHandler = h;
}
/**
* Gets all documents this service is tracking, generally these are the documents that have been opened / changed,
* and not yet closed.
*/
public synchronized Collection<TextDocument> getAll() {
return new ArrayList<>(documents.values());
}
@Override
public final void didChange(DidChangeTextDocumentParams params) {
try {
VersionedTextDocumentIdentifier docId = params.getTextDocument();
String url = docId.getUri();
//LOG.info("didChange: "+url);
if (url!=null) {
TextDocument doc = getDocument(url);
for (TextDocumentContentChangeEvent change : params.getContentChanges()) {
doc.apply(change);
didChangeContent(doc, change);
}
}
} catch (BadLocationException e) {
LOG.log(Level.SEVERE, ExceptionUtil.getMessage(e), e);
}
}
@Override
public void didOpen(DidOpenTextDocumentParams params) {
//LOG.info("didOpen: "+params.getUri());
//Example message:
//{
// "jsonrpc":"2.0",
// "method":"textDocument/didOpen",
// "params":{
// "textDocument":{
// "uri":"file:///home/kdvolder/tmp/hello-java/hello.txt",
// "languageId":"plaintext",
// "version":1,
// "text":"This is some text ya-all o\nsss typescript\n"
// }
// }
//}
String url = params.getTextDocument().getUri();
String languageId = params.getTextDocument().getLanguageId();
if (url!=null) {
String text = params.getTextDocument().getText();
TextDocument doc = createDocument(url, languageId);
doc.setText(text);
TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent() {
@Override
public Range getRange() {
return null;
}
@Override
public Integer getRangeLength() {
return null;
}
@Override
public String getText() {
return text;
}
};
TextDocumentContentChange evt = new TextDocumentContentChange(doc, change);
documentChangeListeners.fire(evt);
}
}
@Override
public void didClose(DidCloseTextDocumentParams params) {
//LOG.info("didClose: "+params.getTextDocument().getUri());
String url = params.getTextDocument().getUri();
if (url!=null) {
documents.remove(url);
}
}
void didChangeContent(TextDocument doc, TextDocumentContentChangeEvent change) {
documentChangeListeners.fire(new TextDocumentContentChange(doc, change));
}
public void onDidChangeContent(Consumer<TextDocumentContentChange> l) {
documentChangeListeners.add(l);
}
private synchronized TextDocument getDocument(String url) {
TextDocument doc = documents.get(url);
if (doc==null) {
LOG.warning("Trying to get document ["+url+"] but it did not exists. Creating it with language-id 'plaintext'");
doc = createDocument(url, LanguageIds.PLAINTEXT);
}
return doc;
}
private synchronized TextDocument createDocument(String url, String languageId) {
if (documents.get(url)!=null) {
LOG.warning("Creating document ["+url+"] but it already exists. Existing document discarded!");
}
TextDocument doc = new TextDocument(url, languageId);
documents.put(url, doc);
return doc;
}
public final static CompletionList NO_COMPLETIONS = new CompletionList(false, Collections.emptyList());
public final static CompletableFuture<Hover> NO_HOVER = CompletableFuture.completedFuture(new Hover(ImmutableList.of(), null));
public final static CompletableFuture<List<? extends Location>> NO_REFERENCES = CompletableFuture.completedFuture(ImmutableList.of());
@Override
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(TextDocumentPositionParams position) {
CompletionHandler h = completionHandler;
if (h!=null) {
return completionHandler.handle(position)
.thenApply(Either::forRight);
}
return CompletableFuture.completedFuture(Either.forRight(NO_COMPLETIONS));
}
@Override
public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) {
CompletionResolveHandler h = completionResolveHandler;
if (h!=null) {
return h.handle(unresolved);
}
return null;
}
@Override
public CompletableFuture<Hover> hover(TextDocumentPositionParams position) {
HoverHandler h = hoverHandler;
if (h!=null) {
return hoverHandler.handle(position);
}
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams position) {
return CompletableFuture.completedFuture(null);
}
@SuppressWarnings({ "unchecked"})
@Override
public CompletableFuture<List<? extends Location>> definition(TextDocumentPositionParams position) {
DefinitionHandler h = this.definitionHandler;
if (h!=null) {
Object r = h.handle(position); //YUCK!
return (CompletableFuture<List<? extends Location>>) r;
}
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<List<? extends Location>> references(ReferenceParams params) {
ReferencesHandler h = this.referencesHandler;
if (h != null) {
return h.handle(params);
}
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<List<? extends SymbolInformation>> documentSymbol(DocumentSymbolParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<List<? extends Command>> codeAction(CodeActionParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved) {
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
@Override
public CompletableFuture<WorkspaceEdit> rename(RenameParams params) {
return CompletableFuture.completedFuture(null);
}
@Override
public void didSave(DidSaveTextDocumentParams params) {
}
public void publishDiagnostics(TextDocument doc, List<Diagnostic> diagnostics) {
LanguageClient client = server.getClient();
if (client!=null && diagnostics!=null) {
PublishDiagnosticsParams params = new PublishDiagnosticsParams();
params.setUri(doc.getUri());
params.setDiagnostics(diagnostics);
client.publishDiagnostics(params);
}
}
public synchronized TextDocument get(TextDocumentPositionParams params) {
return documents.get(params.getTextDocument().getUri());
}
@Override
public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams position) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
}

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.services.WorkspaceService;
public class SimpleWorkspaceService implements WorkspaceService {
private ListenerList<Settings> configurationListeners = new ListenerList<>();
@Override
public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) {
// TODO Auto-generated method stub
return null;
}
@Override
public void didChangeConfiguration(DidChangeConfigurationParams params) {
configurationListeners.fire(new Settings(params.getSettings()));
}
@Override
public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
// TODO Auto-generated method stub
}
public void onDidChangeConfiguraton(Consumer<Settings> l) {
configurationListeners.add(l);
}
}

View File

@@ -0,0 +1,39 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import java.util.Iterator;
/**
* Utility to generate 'sort keys' in ascending order.
* <p>
* VSCode uses a String in each completion to determine the order of completions.
* We use a 'score' based on how well a key matches what was typed.
* <p>
* To go from a 'score' to a sort-key we presort our proposals and then assign
* a sort key for vscode.
*/
public class SortKeys implements Iterator<String> {
private int counter;
@Override
public boolean hasNext() {
return true;
}
@Override
public String next() {
return String.format("%05d", counter++);
}
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* 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.commons.languageserver.util;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public class TextDocumentContentChange {
private final TextDocument document;
private final TextDocumentContentChangeEvent change;
public TextDocumentContentChange(TextDocument doc, TextDocumentContentChangeEvent change) {
this.document = doc;
this.change = change;
}
public TextDocument getDocument() {
return document;
}
public TextDocumentContentChangeEvent getChange() {
return change;
}
}