Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

[DO NOT MERGE] Global LS Command Registry to register/unregister and execute commands #215

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions lib/adapters/command-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {LanguageClientConnection} from '../languageclient';
import {ServerCapabilities} from 'vscode-languageserver-protocol';
import {DisposableLike} from 'atom';
import * as UUID from 'uuid/v4';

const GLOBAL: any = global;

export class CommandAdapter implements DisposableLike {

private registrations: Map<string, string[]>;

constructor(private connection: LanguageClientConnection) {
this.registrations = new Map();
connection.onRegisterCommand(registration => {
if (registration.registerOptions && Array.isArray(registration.registerOptions.commands)) {
this.registerCommands(registration.id, registration.registerOptions.commands);
}
});
connection.onUnregisterCommand(unregisteration => this.unregisterCommands(unregisteration.id));
}

initialize(capabilities: ServerCapabilities) {
if (capabilities.executeCommandProvider && Array.isArray(capabilities.executeCommandProvider.commands)) {
this.registerCommands(UUID(), capabilities.executeCommandProvider.commands)
}
}

registerCommands(id: string, commands: string[]): void{
const cmdRegistry = this.getLspCommandRegistry();
const registeredCommands = commands.filter(cmd => {
const handler = (params: any[]) => this.connection.executeCommand({
command: cmd,
arguments: params
});
if (cmdRegistry.register(cmd, handler)) {
return true;
} else {
console.error(`Trying to register duplicate command: "${cmd}"`)
}
});
if (this.registrations.has(id)) {
throw new Error(`Duplicate registration id: ${id}`);
}
this.registrations.set(id, registeredCommands);
}

executeCommand(id: string, params: any[]): Promise<any> {
return this.getLspCommandRegistry().execute(id, params);
}

unregisterCommands(id: string) {
if (this.registrations.has(id)) {
const commands = this.registrations.get(id);
const cmdRegistry = this.getLspCommandRegistry();
if (commands && Array.isArray(commands)) {
commands.forEach(command => cmdRegistry.unregister(command));
}
this.registrations.delete(id);
}
}

dispose() {
const cmdRegistry = this.getLspCommandRegistry();
this.registrations.forEach(commands => commands.forEach(command => cmdRegistry.unregister(command)));
this.registrations.clear();
}

private getLspCommandRegistry(): LspCommandRegistry {
if (!GLOBAL.lspCommandRegistry) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be per-server? Is there an advantage to making it available globally to all instances of atom-languageclient?

Copy link
Contributor Author

@BoykoAlex BoykoAlex Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an advantage to it. If this registry is global then it is possible to make language servers communicate between each other. The communication to JDT LS is great for the case of Spring Tools because it's mostly an add-on over the Java tooling. Information spring tools may want from JDT LS is classpath, search, javadoc etc.

Example of communication between Spring Tools LS and JDT LS
I have extended LSP with spring/javadoc request message with java artifact binding key and project uri as parameters for the message.
The message is received on my Atom language client extension.
Access global command registry and look for command with id jdt/javadoc for example which would be the command registered by JDT LS (ide-java package)
Execute the command and pass the binding key and project URI as parameters to the jdt/javadoc command
The command execution goes to the JDT LS and comes back with the result via LSP messages
The javadoc content is extracted from the result of the command execution and sent as a reply to the initial spring/javadoc command
Thus, I have javadoc in my spring tools LS from JDT LS at the end of this

There is a related PR for the ide-java package that uses the global registry: atom/ide-java#79

Nevertheless, I understand your concern with the global registry... It also introduces new API to work with the registry... I wish there was something in Atom core that could be utilized for this purpose.

GLOBAL.lspCommandRegistry = new LspCommandRegistryImpl();
}
return <LspCommandRegistry> GLOBAL.lspCommandRegistry;
}
}

export interface LspCommandRegistry {
register(command: string, handler: (params: any[]) => Promise<any>): boolean;
execute(command: string, params: any[]): Promise<any>;
unregister(command: string): boolean;
}

class LspCommandRegistryImpl implements LspCommandRegistry {

private commandIdToHandler: Map<string, (params: any[]) => Promise<any>>;

constructor() {
this.commandIdToHandler = new Map();
}

register(command: string, handler: (params: any[]) => Promise<any>): boolean {
if (this.commandIdToHandler.has(command)) {
return false;
} else {
this.commandIdToHandler.set(command, handler);
return true;
}
}

execute(command: string, params: any[]): Promise<any> {
if (this.commandIdToHandler.has(command)) {
const handler = this.commandIdToHandler.get(command);
if (handler) {
return handler(params);
} else {
throw new Error(`Command "${command}" has no handler`);
}
} else {
throw new Error(`Command "${command}" is not registered`);
}
}

unregister(command: string): boolean {
return this.commandIdToHandler.delete(command);
}

}
7 changes: 6 additions & 1 deletion lib/auto-languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
Range,
TextEditor,
} from 'atom';
import {CommandAdapter} from './adapters/command-adapter';

export { ActiveServer, LanguageClientConnection, LanguageServerProcess };
export type ConnectionType = 'stdio' | 'socket' | 'ipc';
Expand Down Expand Up @@ -128,7 +129,7 @@ export default class AutoLanguageClient {
dynamicRegistration: false,
},
executeCommand: {
dynamicRegistration: false,
dynamicRegistration: true,
},
},
textDocument: {
Expand Down Expand Up @@ -424,6 +425,10 @@ export default class AutoLanguageClient {
}
server.disposable.add(server.signatureHelpAdapter);
}

server.commands = new CommandAdapter(server.connection);
server.commands.initialize(server.capabilities);
server.disposable.add(server.commands);
}

public shouldSyncForEditor(editor: TextEditor, projectPath: string): boolean {
Expand Down
31 changes: 31 additions & 0 deletions lib/languageclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
NullLogger,
Logger,
} from './logger';
import {Registration, RegistrationParams, Unregistration, UnregistrationParams} from "vscode-languageserver-protocol";

export * from 'vscode-languageserver-protocol';

Expand Down Expand Up @@ -211,6 +212,36 @@ export class LanguageClientConnection extends EventEmitter {
this._onNotification({method: 'textDocument/publishDiagnostics'}, callback);
}

private _onRegisterCapability(method: string, callback: (registration: Registration) => void): void {
this._onRequest({method: 'client/registerCapability'}, (params: RegistrationParams) => {
params.registrations.forEach(registration => {
if (registration.method === method) {
callback(registration);
}
});
return Promise.resolve();
});
}

private _onUnregisterCapability(method: string, callback: (unregistration: Unregistration) => void): void {
this._onRequest({method: 'client/unregisterCapability'}, (params: UnregistrationParams) => {
params.unregisterations.forEach(unregistration => {
if (unregistration.method === method) {
callback(unregistration);
}
});
return Promise.resolve();
});
}

public onRegisterCommand(callback: (registration: Registration) => void): void {
this._onRegisterCapability('workspace/executeCommand', callback);
}

public onUnregisterCommand(callback: (unregisteration: Unregistration) => void): void {
this._onUnregisterCapability('workspace/executeCommand', callback);
}

// Public: Send a `textDocument/completion` request.
//
// * `params` The {TextDocumentPositionParams} or {CompletionParams} for which
Expand Down
1 change: 1 addition & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import DownloadFile from './download-file';
import LinterPushV2Adapter from './adapters/linter-push-v2-adapter';

export * from './auto-languageclient';
export { LspCommandRegistry } from './adapters/command-adapter'
export {
AutoLanguageClient,
Convert,
Expand Down
2 changes: 2 additions & 0 deletions lib/server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ProjectFileEvent,
TextEditor,
} from 'atom';
import {CommandAdapter} from './adapters/command-adapter';

// Public: Defines the minimum surface area for an object that resembles a
// ChildProcess. This is used so that language packages with alternative
Expand All @@ -37,6 +38,7 @@ export interface ActiveServer {
process: LanguageServerProcess;
connection: ls.LanguageClientConnection;
capabilities: ls.ServerCapabilities;
commands?: CommandAdapter;
linterPushV2?: LinterPushV2Adapter;
loggingConsole?: LoggingConsoleAdapter;
docSyncAdapter?: DocumentSyncAdapter;
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"dependencies": {
"@types/atom": "^1.24.1",
"@types/node": "^8.0.41",
"@types/uuid": "^3.4.3",
"fuzzaldrin-plus": "^0.6.0",
"vscode-jsonrpc": "^3.5.0",
"vscode-languageserver-protocol": "3.6.0-next.5",
"vscode-languageserver-types": "^3.6.0-next.1"
"vscode-languageserver-types": "^3.6.0-next.1",
"uuid": "^3.2.1"
},
"atomTestRunner": "./build/test/runner",
"devDependencies": {
Expand Down