Skip to content

Commit

Permalink
Add support for CodeActions triggering commands provided by the langu…
Browse files Browse the repository at this point in the history
…age server

Signed-off-by: Nicholas Gates <ngates@palantir.com>
  • Loading branch information
gatesn committed Aug 28, 2017
1 parent 86b28d4 commit fad66e0
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 10 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
46 changes: 43 additions & 3 deletions example/src/json-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
)
Expand All @@ -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)
Expand All @@ -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<Hover> {
const document = this.documents.get(params.textDocument.uri);
const jsonDocument = this.getJSONDocument(document);
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(),
}
}
53 changes: 51 additions & 2 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 @@ -14,6 +16,7 @@ export class MonacoWorkspace implements Workspace {
protected readonly onDidChangeTextDocumentEmitter = new Emitter<TextDocumentDidChangeEvent>();

constructor(
protected readonly p2m: ProtocolToMonacoConverter,
protected readonly m2p: MonacoToProtocolConverter,
protected _rootUri: string | null = null) {
for (const model of monaco.editor.getModels()) {
Expand Down Expand Up @@ -82,4 +85,50 @@ 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: {[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);
}

}
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 fad66e0

Please sign in to comment.