Skip to content

Commit

Permalink
Add support for CodeActions triggering commands on language servers
Browse files Browse the repository at this point in the history
Signed-off-by: Nicholas Gates <ngates@palantir.com>
  • Loading branch information
gatesn committed Aug 11, 2017
1 parent 69d27ea commit 47cc839
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 9 deletions.
4 changes: 2 additions & 2 deletions example/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
});

Expand All @@ -40,7 +40,7 @@ listen({
}
});

const services = createMonacoServices();
const services = createMonacoServices(editor);
function createLanguageClient(connection: MessageConnection): BaseLanguageClient {
return new BaseLanguageClient({
name: "Sample Language Client",
Expand Down
16 changes: 16 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -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)
});
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
8 changes: 5 additions & 3 deletions src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
51 changes: 47 additions & 4 deletions src/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -15,8 +17,7 @@ export class MonacoWorkspace implements Workspace {
protected readonly onDidCloseTextDocumentEmitter = new Emitter<TextDocument>();
protected readonly onDidChangeTextDocumentEmitter = new Emitter<TextDocumentDidChangeEvent>();

constructor(
protected readonly m2p: MonacoToProtocolConverter) {
constructor(protected readonly p2m: ProtocolToMonacoConverter, protected readonly m2p: MonacoToProtocolConverter) {
for (const model of monaco.editor.getModels()) {
this.addModel(model);
}
Expand Down Expand Up @@ -83,4 +84,46 @@ export class MonacoWorkspace implements Workspace {
return this.onDidChangeTextDocumentEmitter.event;
}

public applyEdit(workspaceEdit: WorkspaceEdit): Promise<boolean> {
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);
}

}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
51 changes: 51 additions & 0 deletions typings/monaco/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/// <reference path='../../node_modules/monaco-editor-core/monaco.d.ts'/>

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<ICommandEvent>;
executeCommand<T>(commandId: string, ...args: any[]): monaco.Promise<T>;
executeCommand(commandId: string, ...args: any[]): monaco.Promise<any>;
}

export interface ICommandHandler {
(accessor: monaco.instantiation.ServicesAccessor, ...args: any[]): void;
}

export interface ICommand {
handler: ICommandHandler;
}
}

declare module monaco.instantiation {
export interface ServiceIdentifier<T> {
(...args: any[]): void;
type: T;
}
export interface ServicesAccessor {
get<T>(id: ServiceIdentifier<T>, isOptional?: typeof optional): T;
}
export interface IInstantiationService {
}
export function optional<T>(serviceIdentifier: ServiceIdentifier<T>): (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<monaco.commands.ICommandEvent>;
executeCommand<T>(commandId: string, ...args: any[]): monaco.Promise<T>;
executeCommand(commandId: string, ...args: any[]): monaco.Promise<any>;
}
}

0 comments on commit 47cc839

Please sign in to comment.