diff --git a/example/src/client.ts b/example/src/client.ts index a65370710..96f5be95f 100644 --- a/example/src/client.ts +++ b/example/src/client.ts @@ -22,7 +22,7 @@ const value = `{ "$schema": "http://json.schemastore.org/coffeelint", "line_endings": "unix" }`; -monaco.editor.create(document.getElementById("container")!, { +const editor = monaco.editor.create(document.getElementById("container")!, { model: monaco.editor.createModel(value, 'json', monaco.Uri.parse('inmemory://model.json')) }); @@ -40,7 +40,7 @@ listen({ } }); -const services = createMonacoServices(); +const services = createMonacoServices(editor); function createLanguageClient(connection: MessageConnection): BaseLanguageClient { return new BaseLanguageClient({ name: "Sample Language Client", 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/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..1736fcb01 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), - window: new ConsoleWindow() + 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 0f6497060..55be96f8c 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 { @@ -15,8 +17,7 @@ export class MonacoWorkspace implements Workspace { protected readonly onDidCloseTextDocumentEmitter = new Emitter(); protected readonly onDidChangeTextDocumentEmitter = new Emitter(); - constructor( - protected readonly m2p: MonacoToProtocolConverter) { + constructor(protected readonly p2m: ProtocolToMonacoConverter, protected readonly m2p: MonacoToProtocolConverter) { for (const model of monaco.editor.getModels()) { this.addModel(model); } @@ -83,4 +84,46 @@ 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 = 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 = 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