Skip to content

Commit

Permalink
Support for 'xml/executeClientCommand` access to server from extension
Browse files Browse the repository at this point in the history
Signed-off-by: BoykoAlex <[email protected]>
  • Loading branch information
BoykoAlex authored and angelozerr committed Oct 13, 2020
1 parent f160854 commit 11f6f6d
Show file tree
Hide file tree
Showing 11 changed files with 510 additions and 17 deletions.
11 changes: 6 additions & 5 deletions docs/LSP-Extensions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
The XML Language Server implements the following extensions that are not part of the standard LSP

| Method | Kind | Notes |
|--------------|-------------------|----------------------------------------------------------------------|
| xml/closeTag | Request to server | Retrieves the close Tag to insert in a given position of a document. |
The XML Language Server implements the following extensions that are not part of the standard LSP

| Method | Kind | Notes |
|--------------------------|-------------------|----------------------------------------------------------------------|
| xml/closeTag | Request to server | Retrieves the close Tag to insert in a given position of a document. |
| xml/executeClientCommand | Request to client | Executes command on the client. |
83 changes: 83 additions & 0 deletions docs/LemMinX-Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,89 @@ The [LemMinx Extensions API](https://github.com/eclipse/lemminx/tree/master/org.
- Formatter with [IFormatterParticipant](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/format/IFormatterParticipant.java)
- Symbols with [ISymbolsProviderParticipant](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/ISymbolsProviderParticipant.java)

## XML Language Server services available for extensions
XML Language Server extension may need to use standard Language Server features such as commands, documents and ability to manipulate documents. These are available to extensions indirectly via specialized service API.

#### Command Service
The command service is a wrapper around Language Server client functionality allowing to register/unregister/execute commands via Language Server Protocol. The service can be accessed like the following:

```java
public class FooPlugin implements IXMLExtension {

@Override
public void start(InitializeParams params, XMLExtensionsRegistry registry) {
// Register here custom server command
IXMLCommandService commandService = registry.getCommandService();
commandService.registerCommand("my-cmd", (params, cancelChecker) -> "executed");
}

@Override
public void stop(XMLExtensionsRegistry registry) {
// Unregister here custom server command
IXMLCommandService commandService = registry.getCommandService();
commandService.unregisterCommand("my-cmd");
}
}
```

There are two types of commands XML LS allows extender to work with via `IXMLCommandService`:
- Server command that can be executed from the client (via `workspace/executeCommand` request message from the LSP spec)
- Client command that can be executed from the server (via `xml/executeClientCommand` request message - XML extension of the LSP spec)

A server command should implement the [IDelegateCommandHandler](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/IXMLCommandService.java).

```java
public interface IDelegateCommandHandler {

/**
* Executes a command
* @param params command execution parameters
* @param cancelChecker check if cancel has been requested
* @return the result of the command
* @throws Exception the unhandled exception will be wrapped in
* <code>org.eclipse.lsp4j.jsonrpc.ResponseErrorException</code>
* and be wired back to the JSON-RPC protocol caller
*/
Object executeCommand(ExecuteCommandParams params, CancelChecker cancelChecker) throws Exception;
}
```

A server command can be registered/unregistered like follows:

```java
// Command registration
commandService.registerCommand("my-cmd", (params, cancelChecker) -> "executed");

//Unregister command
commandService.unregisterCommand("my-cmd");
```

A client command can be executed like follows:

```java
// Opens up terminal view in VSCode client
commandService.executeClientCommand(new ExecuteCommandParams("workbench.action.terminal.toggleTerminal", Collections.emptyList()))
```

Note that XML LS client (VSCode in particular) registers a client command `xml.workspace.executeCommand` to allow other extensions to execute XML LS commands. Thus XML LS extension can execute `"my-cmd"` command registered above with the following snippet:

```java
// Execute command. The result should be "executed" string
Object result = commandService.executeClientCommand(new ExecuteCommandParams("xml.workspace.executeCommand", Arrays.asList("my-cmd"))).get();
```

See definition of [IXMLCommandService](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/extensions/commands/IXMLCommandService.java)

#### Document Provider
The document provider allows for finding the document from the document URI and listing all XML documents. Note that the document provider is only aware of the XML documents it is working with (opened XML documents).

See definition of [IXMLDocumentProvider](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/IXMLDocumentProvider.java)

#### Validation Service
The validation service allows for triggering validation of all opened XML documents on server side.

See definition of [IXMLValidationService](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/services/IXMLValidationService.java)

## Adding custom settings for your extension

Your extension can have its own custom settings to control its behavior. Your new setting must start with the prefix `xml`, so for example `xml.myextension.mycustomsetting`. Reading the settings can be done from the `doSave` method in your XMLExtension. The `doSave` method is called with a `Settings` context on extension startup and also whenever the settings are updated. For an example you can look at the [Content Model Settings](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/settings/ContentModelSettings.java) and how they are updated in the [doSave method](https://github.com/eclipse/lemminx/blob/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/contentmodel/ContentModelPlugin.java#L73).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
import static org.eclipse.lsp4j.jsonrpc.CompletableFutures.computeAsync;

import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import org.eclipse.lemminx.client.ExtendedClientCapabilities;
import org.eclipse.lemminx.commons.ModelTextDocument;
Expand All @@ -36,10 +39,10 @@
import org.eclipse.lemminx.logs.LogHelper;
import org.eclipse.lemminx.services.IXMLDocumentProvider;
import org.eclipse.lemminx.services.IXMLNotificationService;
import org.eclipse.lemminx.services.IXMLValidationService;
import org.eclipse.lemminx.services.XMLLanguageService;
import org.eclipse.lemminx.settings.AllXMLSettings;
import org.eclipse.lemminx.settings.InitializationOptionsSettings;
import org.eclipse.lemminx.settings.LogsSettings;
import org.eclipse.lemminx.settings.ServerSettings;
import org.eclipse.lemminx.settings.SharedSettings;
import org.eclipse.lemminx.settings.XMLCodeLensSettings;
Expand Down Expand Up @@ -71,7 +74,8 @@
*
*/
public class XMLLanguageServer
implements ProcessLanguageServer, XMLLanguageServerAPI, IXMLDocumentProvider, IXMLNotificationService {
implements ProcessLanguageServer, XMLLanguageServerAPI, IXMLDocumentProvider,
IXMLNotificationService, IXMLValidationService {

private static final Logger LOGGER = Logger.getLogger(XMLLanguageServer.class.getName());

Expand All @@ -81,14 +85,18 @@ public class XMLLanguageServer
private XMLLanguageClientAPI languageClient;
private final ScheduledExecutorService delayer;
private Integer parentProcessId;
public XMLCapabilityManager capabilityManager;
private XMLCapabilityManager capabilityManager;

public XMLLanguageServer() {
xmlTextDocumentService = new XMLTextDocumentService(this);
xmlWorkspaceService = new XMLWorkspaceService(this);

xmlLanguageService = new XMLLanguageService();
xmlLanguageService.setDocumentProvider(this);
xmlLanguageService.setNotificationService(this);
xmlTextDocumentService = new XMLTextDocumentService(this);
xmlWorkspaceService = new XMLWorkspaceService(this);
xmlLanguageService.setCommandService(xmlWorkspaceService);
xmlLanguageService.setValidationService(this);

delayer = Executors.newScheduledThreadPool(1);
}

Expand Down Expand Up @@ -297,4 +305,22 @@ public void sendNotification(String message, MessageType messageType, Command...
public SharedSettings getSharedSettings() {
return xmlTextDocumentService.getSharedSettings();
}

@Override
public Collection<DOMDocument> getAllDocuments() {
return xmlTextDocumentService.allDocuments().stream()
.map(m -> m.getModel().getNow(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

@Override
public void validate(DOMDocument document) {
xmlTextDocumentService.validate(document);
}

public XMLCapabilityManager getCapabilityManager() {
return capabilityManager;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ private void triggerValidationFor(TextDocument document) {
});
}

private void validate(DOMDocument xmlDocument) throws CancellationException {
void validate(DOMDocument xmlDocument) throws CancellationException {
CancelChecker cancelChecker = xmlDocument.getCancelChecker();
cancelChecker.checkCanceled();
getXMLLanguageService().publishDiagnostics(xmlDocument,
Expand Down Expand Up @@ -564,6 +564,10 @@ public SharedSettings getSharedSettings() {
public ModelTextDocument<DOMDocument> getDocument(String uri) {
return documents.get(uri);
}

public Collection<ModelTextDocument<DOMDocument>> allDocuments() {
return documents.all();
}

public boolean documentIsOpen(String uri) {
ModelTextDocument<DOMDocument> document = getDocument(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,81 @@
*/
package org.eclipse.lemminx;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;

import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.DidChangeWatchedFilesParams;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.FileEvent;
import org.eclipse.lsp4j.SymbolInformation;
import org.eclipse.lsp4j.WorkspaceSymbolParams;
import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.eclipse.lsp4j.services.WorkspaceService;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/**
* XML workspace service.
*
*/
public class XMLWorkspaceService implements WorkspaceService {
public class XMLWorkspaceService implements WorkspaceService, IXMLCommandService {

private static final String WORKSPACE_EXECUTE_COMMAND = "workspace/executeCommand";

private final XMLLanguageServer xmlLanguageServer;


private final Map<String, IDelegateCommandHandler> commands;

private final Map<String, String> commandRegistrations;

public XMLWorkspaceService(XMLLanguageServer xmlLanguageServer) {
this.xmlLanguageServer = xmlLanguageServer;
this.commands = new HashMap<>();
this.commandRegistrations = new HashMap<>();
}

@Override
public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) {
return null;
}



@Override
public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) {
synchronized (commands) {
IDelegateCommandHandler handler = commands.get(params.getCommand());
if (handler == null) {
throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InternalError, "No command handler for the command: " + params.getCommand(), null));
}
return CompletableFutures.computeAsync(cancelChecker -> {
try {
return handler.executeCommand(params, cancelChecker);
} catch (Exception e) {
if (e instanceof ResponseErrorException) {
throw (ResponseErrorException) e;
} else if (e instanceof CancellationException) {
throw (CancellationException) e;
}
throw new ResponseErrorException(
new ResponseError(ResponseErrorCode.UnknownErrorCode, e.getMessage(), e));
}
});
}
}

@Override
public void didChangeConfiguration(DidChangeConfigurationParams params) {
xmlLanguageServer.updateSettings(params.getSettings());
xmlLanguageServer.capabilityManager.syncDynamicCapabilitiesWithPreferences();
xmlLanguageServer.getCapabilityManager().syncDynamicCapabilitiesWithPreferences();
}

@Override
Expand All @@ -54,4 +99,33 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {
}
}
}

@Override
public void registerCommand(String commandId, IDelegateCommandHandler handler) {
synchronized (commands) {
if (commands.containsKey(commandId)) {
throw new IllegalArgumentException("Command with id '" + commandId + "' is already registered");
}
String registrationId = UUID.randomUUID().toString();
xmlLanguageServer.getCapabilityManager().registerCapability(registrationId, WORKSPACE_EXECUTE_COMMAND, ImmutableMap.of("commands", ImmutableList.of(commandId)));
commandRegistrations.put(commandId, registrationId);
commands.put(commandId, handler);
}
}

@Override
public void unregisterCommand(String commandId) {
synchronized (commands) {
commands.remove(commandId);
String registrationId = commandRegistrations.remove(commandId);
if (registrationId != null) {
xmlLanguageServer.getCapabilityManager().unregisterCapability(registrationId, WORKSPACE_EXECUTE_COMMAND);
}
}
}

@Override
public CompletableFuture<Object> executeClientCommand(ExecuteCommandParams command) {
return xmlLanguageServer.getLanguageClient().executeClientCommand(command);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
*/
package org.eclipse.lemminx.customservice;

import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
import org.eclipse.lsp4j.jsonrpc.services.JsonSegment;
import org.eclipse.lsp4j.services.LanguageClient;

Expand All @@ -28,8 +32,21 @@ public interface XMLLanguageClientAPI extends LanguageClient {
*
* @param command
*/
@JsonNotification
@JsonNotification("actionableNotification")
default void actionableNotification(ActionableNotification command) {
throw new UnsupportedOperationException();
}

/**
* Executes the command on the client which gives an opportunity to execute a
* command registered by a different Language Server
*
* @param params command execution parameters
* @return the result of the command execution
*/
@JsonRequest("executeClientCommand")
default CompletableFuture<Object> executeClientCommand(ExecuteCommandParams params) {
throw new UnsupportedOperationException();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
*/
package org.eclipse.lemminx.services;

import java.util.Collection;
import java.util.Collections;

import org.eclipse.lemminx.dom.DOMDocument;

/**
Expand All @@ -31,4 +34,12 @@ public interface IXMLDocumentProvider {
* null otherwise.
*/
DOMDocument getDocument(String uri);

/**
* All known documents XML server is working with at the moment
* @return XML documents
*/
default Collection<DOMDocument> getAllDocuments() {
return Collections.emptyList();
}
}
Loading

0 comments on commit 11f6f6d

Please sign in to comment.