Skip to content

Commit 47cc839

Browse files
committed
Add support for CodeActions triggering commands on language servers
Signed-off-by: Nicholas Gates <ngates@palantir.com>
1 parent 69d27ea commit 47cc839

7 files changed

Lines changed: 123 additions & 9 deletions

File tree

example/src/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const value = `{
2222
"$schema": "http://json.schemastore.org/coffeelint",
2323
"line_endings": "unix"
2424
}`;
25-
monaco.editor.create(document.getElementById("container")!, {
25+
const editor = monaco.editor.create(document.getElementById("container")!, {
2626
model: monaco.editor.createModel(value, 'json', monaco.Uri.parse('inmemory://model.json'))
2727
});
2828

@@ -40,7 +40,7 @@ listen({
4040
}
4141
});
4242

43-
const services = createMonacoServices();
43+
const services = createMonacoServices(editor);
4444
function createLanguageClient(connection: MessageConnection): BaseLanguageClient {
4545
return new BaseLanguageClient({
4646
name: "Sample Language Client",

src/commands.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) 2017 TypeFox GmbH (http://www.typefox.io). All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
import { Commands, Disposable } from 'vscode-base-languageclient/lib/services';
6+
7+
export class MonacoCommands implements Commands {
8+
9+
public constructor(protected readonly editor: monaco.editor.IStandaloneCodeEditor) { }
10+
11+
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable {
12+
return this.editor._commandService.addCommand(command, {
13+
handler: (_accessor, ...args: any[]) => callback(...args)
14+
});
15+
}
16+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
* ------------------------------------------------------------------------------------------ */
55
export * from './disposable';
6+
export * from './commands';
67
export * from './console-window';
78
export * from './languages';
89
export * from './workspace';

src/services.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
* ------------------------------------------------------------------------------------------ */
55
import { BaseLanguageClient } from "vscode-base-languageclient/lib/base";
66
import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from "./converter";
7+
import { MonacoCommands } from './commands';
78
import { MonacoLanguages } from "./languages";
89
import { MonacoWorkspace } from "./workspace";
910
import { ConsoleWindow } from "./console-window";
1011

11-
export function createMonacoServices(): BaseLanguageClient.IServices {
12+
export function createMonacoServices(editor: monaco.editor.IStandaloneCodeEditor): BaseLanguageClient.IServices {
1213
const m2p = new MonacoToProtocolConverter();
1314
const p2m = new ProtocolToMonacoConverter();
1415
return {
16+
commands: new MonacoCommands(editor),
1517
languages: new MonacoLanguages(p2m, m2p),
16-
workspace: new MonacoWorkspace(m2p),
17-
window: new ConsoleWindow()
18+
workspace: new MonacoWorkspace(p2m, m2p),
19+
window: new ConsoleWindow(),
1820
}
1921
}

src/workspace.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
* Copyright (c) 2017 TypeFox GmbH (http://www.typefox.io). All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
* ------------------------------------------------------------------------------------------ */
5-
import { MonacoToProtocolConverter } from './converter';
6-
import { Workspace, TextDocumentDidChangeEvent, TextDocument, Event, Emitter } from "vscode-base-languageclient/lib/services";
5+
import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from './converter';
6+
import { Workspace, TextDocumentDidChangeEvent, TextDocument, Event, Emitter } from 'vscode-base-languageclient/lib/services';
7+
import { WorkspaceEdit } from 'vscode-base-languageclient/lib/base';
78
import IModel = monaco.editor.IModel;
9+
import IResourceEdit = monaco.languages.IResourceEdit;
810

911
export class MonacoWorkspace implements Workspace {
1012

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

18-
constructor(
19-
protected readonly m2p: MonacoToProtocolConverter) {
20+
constructor(protected readonly p2m: ProtocolToMonacoConverter, protected readonly m2p: MonacoToProtocolConverter) {
2021
for (const model of monaco.editor.getModels()) {
2122
this.addModel(model);
2223
}
@@ -83,4 +84,46 @@ export class MonacoWorkspace implements Workspace {
8384
return this.onDidChangeTextDocumentEmitter.event;
8485
}
8586

87+
public applyEdit(workspaceEdit: WorkspaceEdit): Promise<boolean> {
88+
const edit: monaco.languages.WorkspaceEdit = this.p2m.asWorkspaceEdit(workspaceEdit);
89+
90+
// Collect all referenced models
91+
const models = edit.edits.reduce((acc: {[uri: string]: monaco.editor.IModel}, currentEdit) => {
92+
acc[currentEdit.resource.toString()] = monaco.editor.getModel(currentEdit.resource);
93+
return acc;
94+
}, {});
95+
96+
// If any of the models do not exist, refuse to apply the edit.
97+
if (!Object.keys(models).map(uri => models[uri]).every(model => !!model)) {
98+
return Promise.resolve(false);
99+
}
100+
101+
// Group edits by resource so we can batch them when applying
102+
const editsByResource = edit.edits.reduce((acc: {[uri: string]: IResourceEdit[]}, currentEdit) => {
103+
const uri = currentEdit.resource.toString();
104+
if (!(uri in acc)) {
105+
acc[uri] = [];
106+
}
107+
acc[uri].push(currentEdit);
108+
return acc;
109+
}, {});
110+
111+
// Apply edits for each resource
112+
Object.keys(editsByResource).forEach(uri => {
113+
models[uri].pushEditOperations(
114+
[], // Do not try and preserve editor selections.
115+
editsByResource[uri].map(resourceEdit => {
116+
return {
117+
identifier: {major: 1, minor: 0},
118+
range: monaco.Range.lift(resourceEdit.range),
119+
text: resourceEdit.newText,
120+
forceMoveMarkers: true,
121+
};
122+
}),
123+
() => [], // Do not try and preserve editor selections.
124+
);
125+
});
126+
return Promise.resolve(true);
127+
}
128+
86129
}

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
],
2323
"files": [
2424
"node_modules/monaco-editor-core/monaco.d.ts",
25+
"typings/monaco/index.d.ts",
2526
"typings/glob-to-regexp/index.d.ts"
2627
]
2728
}

typings/monaco/index.d.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/// <reference path='../../node_modules/monaco-editor-core/monaco.d.ts'/>
2+
3+
declare module monaco.editor {
4+
export interface IStandaloneCodeEditor {
5+
readonly _commandService: monaco.services.StandaloneCommandService;
6+
}
7+
}
8+
9+
declare module monaco.commands {
10+
11+
export interface ICommandEvent {
12+
commandId: string;
13+
}
14+
15+
export interface ICommandService {
16+
onWillExecuteCommand: monaco.IEvent<ICommandEvent>;
17+
executeCommand<T>(commandId: string, ...args: any[]): monaco.Promise<T>;
18+
executeCommand(commandId: string, ...args: any[]): monaco.Promise<any>;
19+
}
20+
21+
export interface ICommandHandler {
22+
(accessor: monaco.instantiation.ServicesAccessor, ...args: any[]): void;
23+
}
24+
25+
export interface ICommand {
26+
handler: ICommandHandler;
27+
}
28+
}
29+
30+
declare module monaco.instantiation {
31+
export interface ServiceIdentifier<T> {
32+
(...args: any[]): void;
33+
type: T;
34+
}
35+
export interface ServicesAccessor {
36+
get<T>(id: ServiceIdentifier<T>, isOptional?: typeof optional): T;
37+
}
38+
export interface IInstantiationService {
39+
}
40+
export function optional<T>(serviceIdentifier: ServiceIdentifier<T>): (target: Function, key: string, index: number) => void;
41+
}
42+
43+
declare module monaco.services {
44+
export class StandaloneCommandService implements monaco.commands.ICommandService {
45+
constructor(instantiationService: monaco.instantiation.IInstantiationService);
46+
addCommand(id: string, command: monaco.commands.ICommand): IDisposable;
47+
onWillExecuteCommand: monaco.IEvent<monaco.commands.ICommandEvent>;
48+
executeCommand<T>(commandId: string, ...args: any[]): monaco.Promise<T>;
49+
executeCommand(commandId: string, ...args: any[]): monaco.Promise<any>;
50+
}
51+
}

0 commit comments

Comments
 (0)