diff --git a/example/src/client.ts b/example/src/client.ts index a65370710..872c63ca8 100644 --- a/example/src/client.ts +++ b/example/src/client.ts @@ -22,8 +22,9 @@ const value = `{ "$schema": "http://json.schemastore.org/coffeelint", "line_endings": "unix" }`; -monaco.editor.create(document.getElementById("container")!, { - model: monaco.editor.createModel(value, 'json', monaco.Uri.parse('inmemory://model.json')) +const editor = monaco.editor.create(document.getElementById("container")!, { + model: monaco.editor.createModel(value, 'json', monaco.Uri.parse('inmemory://model.json')), + glyphMargin: true }); // create the web socket @@ -40,7 +41,7 @@ listen({ } }); -const services = createMonacoServices(); +const services = createMonacoServices(editor); function createLanguageClient(connection: MessageConnection): BaseLanguageClient { return new BaseLanguageClient({ name: "Sample Language Client", diff --git a/example/src/json-server-launcher.ts b/example/src/json-server-launcher.ts index 1b7ce80f8..7f50394df 100644 --- a/example/src/json-server-launcher.ts +++ b/example/src/json-server-launcher.ts @@ -11,7 +11,7 @@ import { start } from "./json-server"; export function launch(socket: rpc.IWebSocket) { const reader = new rpc.WebSocketMessageReader(socket); const writer = new rpc.WebSocketMessageWriter(socket); - const asExternalProccess = process.argv.findIndex(value => value === '--external'); + const asExternalProccess = process.argv.findIndex(value => value === '--external') !== -1; if (asExternalProccess)  { // start the language server as an external process const extJsonServerPath = path.resolve(__dirname, 'ext-json-server.js'); diff --git a/example/src/json-server.ts b/example/src/json-server.ts index c3fa89993..fcc632b2e 100644 --- a/example/src/json-server.ts +++ b/example/src/json-server.ts @@ -8,10 +8,10 @@ import Uri from 'vscode-uri'; import { MessageReader, MessageWriter } from "vscode-jsonrpc"; import { IConnection, TextDocuments, createConnection } from 'vscode-languageserver'; import { - TextDocument, Diagnostic, CompletionList, CompletionItem, Hover, + TextDocument, Diagnostic, Command, CompletionList, CompletionItem, Hover, SymbolInformation, DocumentSymbolParams, TextEdit } from "vscode-languageserver-types"; -import { TextDocumentPositionParams, DocumentRangeFormattingParams } from 'vscode-base-languageclient/lib/protocol'; +import { TextDocumentPositionParams, DocumentRangeFormattingParams, ExecuteCommandParams, CodeActionParams } from 'vscode-base-languageclient/lib/protocol'; import { getLanguageService, LanguageService, JSONDocument } from "vscode-json-languageservice"; export function start(reader: MessageReader, writer: MessageWriter): JsonServer { @@ -55,22 +55,32 @@ export class JsonServer { return { capabilities: { textDocumentSync: this.documents.syncKind, + codeActionProvider: true, completionProvider: { resolveProvider: true, triggerCharacters: ['"', ':'] }, hoverProvider: true, documentSymbolProvider: true, - documentRangeFormattingProvider: true + documentRangeFormattingProvider: true, + executeCommandProvider: { + commands: ['json.documentUpper'] + } } } }); + this.connection.onCodeAction(params => + this.codeAction(params) + ); this.connection.onCompletion(params => this.completion(params) ); this.connection.onCompletionResolve(item => this.resolveCompletion(item) ); + this.connection.onExecuteCommand(params => + this.executeCommand(params) + ); this.connection.onHover(params => this.hover(params) ) @@ -86,6 +96,18 @@ export class JsonServer { this.connection.listen(); } + protected codeAction(params: CodeActionParams): Command[] { + return [{ + title: "Upper Case Document", + command: "json.documentUpper", + // Send a VersionedTextDocumentIdentifier + arguments: [{ + ...params.textDocument, + version: this.documents.get(params.textDocument.uri).version + }] + }]; + } + protected format(params: DocumentRangeFormattingParams): TextEdit[] { const document = this.documents.get(params.textDocument.uri); return this.jsonService.format(document, params.range, params.options) @@ -97,6 +119,24 @@ export class JsonServer { return this.jsonService.findDocumentSymbols(document, jsonDocument); } + protected executeCommand(params: ExecuteCommandParams): any { + if (params.command === "json.documentUpper" && params.arguments) { + const versionedTextDocumentIdentifier = params.arguments[0]; + this.connection.workspace.applyEdit({ + documentChanges: [{ + textDocument: versionedTextDocumentIdentifier, + edits: [{ + range: { + start: {line: 0, character: 0}, + end: {line: Number.MAX_SAFE_INTEGER, character: Number.MAX_SAFE_INTEGER} + }, + newText: this.documents.get(versionedTextDocumentIdentifier.uri).getText().toUpperCase() + }] + }] + }); + } + } + protected hover(params: TextDocumentPositionParams): Thenable { const document = this.documents.get(params.textDocument.uri); const jsonDocument = this.getJSONDocument(document); diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 000000000..42820257c --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,16 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2017 TypeFox GmbH (http://www.typefox.io). All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import { Commands, Disposable } from 'vscode-base-languageclient/lib/services'; + +export class MonacoCommands implements Commands { + + public constructor(protected readonly editor: monaco.editor.IStandaloneCodeEditor) { } + + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return this.editor._commandService.addCommand(command, { + handler: (_accessor, ...args: any[]) => callback(...args) + }); + } +} diff --git a/src/converter.ts b/src/converter.ts index 8672201cb..8c5d6547e 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -337,9 +337,7 @@ export class ProtocolToMonacoConverter { asCodeActions(commands: Command[]): monaco.languages.CodeAction[] { - return this.asCommands(commands).map((command, score) => { - return { command, score } - }); + return this.asCommands(commands).map((command, score) => ({ command, score })); } asCommand(command: Command): monaco.languages.Command { diff --git a/src/index.ts b/src/index.ts index f20a6da3e..69ad75983 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ export * from './disposable'; +export * from './commands'; export * from './console-window'; export * from './languages'; export * from './workspace'; diff --git a/src/services.ts b/src/services.ts index 6037c0063..af5670464 100644 --- a/src/services.ts +++ b/src/services.ts @@ -4,16 +4,18 @@ * ------------------------------------------------------------------------------------------ */ import { BaseLanguageClient } from "vscode-base-languageclient/lib/base"; import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from "./converter"; +import { MonacoCommands } from './commands'; import { MonacoLanguages } from "./languages"; import { MonacoWorkspace } from "./workspace"; import { ConsoleWindow } from "./console-window"; -export function createMonacoServices(): BaseLanguageClient.IServices { +export function createMonacoServices(editor: monaco.editor.IStandaloneCodeEditor): BaseLanguageClient.IServices { const m2p = new MonacoToProtocolConverter(); const p2m = new ProtocolToMonacoConverter(); return { + commands: new MonacoCommands(editor), languages: new MonacoLanguages(p2m, m2p), - workspace: new MonacoWorkspace(m2p), + workspace: new MonacoWorkspace(p2m, m2p), window: new ConsoleWindow() } } \ No newline at end of file diff --git a/src/workspace.ts b/src/workspace.ts index 5a8406b34..f22daa079 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -2,9 +2,11 @@ * Copyright (c) 2017 TypeFox GmbH (http://www.typefox.io). All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { MonacoToProtocolConverter } from './converter'; -import { Workspace, TextDocumentDidChangeEvent, TextDocument, Event, Emitter } from "vscode-base-languageclient/lib/services"; +import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from './converter'; +import { Workspace, TextDocumentDidChangeEvent, TextDocument, Event, Emitter } from 'vscode-base-languageclient/lib/services'; +import { WorkspaceEdit } from 'vscode-base-languageclient/lib/base'; import IModel = monaco.editor.IModel; +import IResourceEdit = monaco.languages.IResourceEdit; export class MonacoWorkspace implements Workspace { @@ -14,6 +16,7 @@ export class MonacoWorkspace implements Workspace { protected readonly onDidChangeTextDocumentEmitter = new Emitter(); constructor( + protected readonly p2m: ProtocolToMonacoConverter, protected readonly m2p: MonacoToProtocolConverter, protected _rootUri: string | null = null) { for (const model of monaco.editor.getModels()) { @@ -82,4 +85,50 @@ export class MonacoWorkspace implements Workspace { return this.onDidChangeTextDocumentEmitter.event; } + public applyEdit(workspaceEdit: WorkspaceEdit): Promise { + const edit: monaco.languages.WorkspaceEdit = this.p2m.asWorkspaceEdit(workspaceEdit); + + // Collect all referenced models + const models: {[uri: string]: monaco.editor.IModel} = edit.edits.reduce( + (acc: {[uri: string]: monaco.editor.IModel}, currentEdit) => { + acc[currentEdit.resource.toString()] = monaco.editor.getModel(currentEdit.resource); + return acc; + }, {} + ); + + // If any of the models do not exist, refuse to apply the edit. + if (!Object.keys(models).map(uri => models[uri]).every(model => !!model)) { + return Promise.resolve(false); + } + + // Group edits by resource so we can batch them when applying + const editsByResource: {[uri: string]: IResourceEdit[]} = edit.edits.reduce( + (acc: {[uri: string]: IResourceEdit[]}, currentEdit) => { + const uri = currentEdit.resource.toString(); + if (!(uri in acc)) { + acc[uri] = []; + } + acc[uri].push(currentEdit); + return acc; + }, {} + ); + + // Apply edits for each resource + Object.keys(editsByResource).forEach(uri => { + models[uri].pushEditOperations( + [], // Do not try and preserve editor selections. + editsByResource[uri].map(resourceEdit => { + return { + identifier: {major: 1, minor: 0}, + range: monaco.Range.lift(resourceEdit.range), + text: resourceEdit.newText, + forceMoveMarkers: true, + }; + }), + () => [], // Do not try and preserve editor selections. + ); + }); + return Promise.resolve(true); + } + } diff --git a/tsconfig.json b/tsconfig.json index 9ca0eb943..3be9956ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ ], "files": [ "node_modules/monaco-editor-core/monaco.d.ts", + "typings/monaco/index.d.ts", "typings/glob-to-regexp/index.d.ts" ] } \ No newline at end of file diff --git a/typings/monaco/index.d.ts b/typings/monaco/index.d.ts new file mode 100644 index 000000000..ef470ec53 --- /dev/null +++ b/typings/monaco/index.d.ts @@ -0,0 +1,51 @@ +/// + +declare module monaco.editor { + export interface IStandaloneCodeEditor { + readonly _commandService: monaco.services.StandaloneCommandService; + } +} + +declare module monaco.commands { + + export interface ICommandEvent { + commandId: string; + } + + export interface ICommandService { + onWillExecuteCommand: monaco.IEvent; + executeCommand(commandId: string, ...args: any[]): monaco.Promise; + executeCommand(commandId: string, ...args: any[]): monaco.Promise; + } + + export interface ICommandHandler { + (accessor: monaco.instantiation.ServicesAccessor, ...args: any[]): void; + } + + export interface ICommand { + handler: ICommandHandler; + } +} + +declare module monaco.instantiation { + export interface ServiceIdentifier { + (...args: any[]): void; + type: T; + } + export interface ServicesAccessor { + get(id: ServiceIdentifier, isOptional?: typeof optional): T; + } + export interface IInstantiationService { + } + export function optional(serviceIdentifier: ServiceIdentifier): (target: Function, key: string, index: number) => void; +} + +declare module monaco.services { + export class StandaloneCommandService implements monaco.commands.ICommandService { + constructor(instantiationService: monaco.instantiation.IInstantiationService); + addCommand(id: string, command: monaco.commands.ICommand): IDisposable; + onWillExecuteCommand: monaco.IEvent; + executeCommand(commandId: string, ...args: any[]): monaco.Promise; + executeCommand(commandId: string, ...args: any[]): monaco.Promise; + } +} \ No newline at end of file