diff --git a/lib/adapters/command-adapter.ts b/lib/adapters/command-adapter.ts new file mode 100644 index 0000000..0b9a3a4 --- /dev/null +++ b/lib/adapters/command-adapter.ts @@ -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; + + 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 { + 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) { + GLOBAL.lspCommandRegistry = new LspCommandRegistryImpl(); + } + return GLOBAL.lspCommandRegistry; + } +} + +export interface LspCommandRegistry { + register(command: string, handler: (params: any[]) => Promise): boolean; + execute(command: string, params: any[]): Promise; + unregister(command: string): boolean; +} + +class LspCommandRegistryImpl implements LspCommandRegistry { + + private commandIdToHandler: Map Promise>; + + constructor() { + this.commandIdToHandler = new Map(); + } + + register(command: string, handler: (params: any[]) => Promise): boolean { + if (this.commandIdToHandler.has(command)) { + return false; + } else { + this.commandIdToHandler.set(command, handler); + return true; + } + } + + execute(command: string, params: any[]): Promise { + 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); + } + +} diff --git a/lib/auto-languageclient.ts b/lib/auto-languageclient.ts index b238dcd..29e8fdd 100644 --- a/lib/auto-languageclient.ts +++ b/lib/auto-languageclient.ts @@ -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'; @@ -128,7 +129,7 @@ export default class AutoLanguageClient { dynamicRegistration: false, }, executeCommand: { - dynamicRegistration: false, + dynamicRegistration: true, }, }, textDocument: { @@ -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 { diff --git a/lib/languageclient.ts b/lib/languageclient.ts index cb3b5ba..d7c1c5d 100644 --- a/lib/languageclient.ts +++ b/lib/languageclient.ts @@ -5,6 +5,7 @@ import { NullLogger, Logger, } from './logger'; +import {Registration, RegistrationParams, Unregistration, UnregistrationParams} from "vscode-languageserver-protocol"; export * from 'vscode-languageserver-protocol'; @@ -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 diff --git a/lib/main.ts b/lib/main.ts index 3c5ac3f..ce0df12 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -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, diff --git a/lib/server-manager.ts b/lib/server-manager.ts index 87bd6e4..d2669f8 100644 --- a/lib/server-manager.ts +++ b/lib/server-manager.ts @@ -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 @@ -37,6 +38,7 @@ export interface ActiveServer { process: LanguageServerProcess; connection: ls.LanguageClientConnection; capabilities: ls.ServerCapabilities; + commands?: CommandAdapter; linterPushV2?: LinterPushV2Adapter; loggingConsole?: LoggingConsoleAdapter; docSyncAdapter?: DocumentSyncAdapter; diff --git a/package.json b/package.json index 17b442a..7b031a3 100644 --- a/package.json +++ b/package.json @@ -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": {