Skip to content

Commit f195c00

Browse files
authored
Add API to register global actions, commands, or keybinding rules (microsoft#163859)
1 parent d477351 commit f195c00

File tree

6 files changed

+232
-42
lines changed

6 files changed

+232
-42
lines changed

src/vs/editor/browser/editorExtensions.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,12 @@ export abstract class EditorCommand extends Command {
248248
};
249249
}
250250

251-
public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {
251+
public static runEditorCommand(
252+
accessor: ServicesAccessor,
253+
args: any,
254+
precondition: ContextKeyExpression | undefined,
255+
runner: (accessor: ServicesAccessor | null, editor: ICodeEditor, args: any) => void | Promise<void>
256+
): void | Promise<void> {
252257
const codeEditorService = accessor.get(ICodeEditorService);
253258

254259
// Find the editor with text focus or active
@@ -260,15 +265,19 @@ export abstract class EditorCommand extends Command {
260265

261266
return editor.invokeWithinContext((editorAccessor) => {
262267
const kbService = editorAccessor.get(IContextKeyService);
263-
if (!kbService.contextMatchesRules(withNullAsUndefined(this.precondition))) {
268+
if (!kbService.contextMatchesRules(withNullAsUndefined(precondition))) {
264269
// precondition does not hold
265270
return;
266271
}
267272

268-
return this.runEditorCommand(editorAccessor, editor!, args);
273+
return runner(editorAccessor, editor, args);
269274
});
270275
}
271276

277+
public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {
278+
return EditorCommand.runEditorCommand(accessor, args, this.precondition, (accessor, editor, args) => this.runEditorCommand(accessor, editor, args));
279+
}
280+
272281
public abstract runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void | Promise<void>;
273282
}
274283

src/vs/editor/standalone/browser/standaloneEditor.ts

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import 'vs/css!./standalone-tokens';
7-
import { IDisposable } from 'vs/base/common/lifecycle';
7+
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
88
import { splitLines } from 'vs/base/common/strings';
99
import { URI } from 'vs/base/common/uri';
1010
import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements';
@@ -23,12 +23,16 @@ import { IModelService } from 'vs/editor/common/services/model';
2323
import { createWebWorker as actualCreateWebWorker, IWebWorkerOptions, MonacoWebWorker } from 'vs/editor/browser/services/webWorker';
2424
import * as standaloneEnums from 'vs/editor/common/standalone/standaloneEnums';
2525
import { Colorizer, IColorizerElementOptions, IColorizerOptions } from 'vs/editor/standalone/browser/colorizer';
26-
import { createTextModel, IStandaloneCodeEditor, IStandaloneDiffEditor, IStandaloneDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, StandaloneDiffEditor, StandaloneEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor';
27-
import { IEditorOverrideServices, StandaloneServices } from 'vs/editor/standalone/browser/standaloneServices';
26+
import { createTextModel, IActionDescriptor, IStandaloneCodeEditor, IStandaloneDiffEditor, IStandaloneDiffEditorConstructionOptions, IStandaloneEditorConstructionOptions, StandaloneDiffEditor, StandaloneEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor';
27+
import { IEditorOverrideServices, StandaloneKeybindingService, StandaloneServices } from 'vs/editor/standalone/browser/standaloneServices';
2828
import { StandaloneThemeService } from 'vs/editor/standalone/browser/standaloneThemeService';
2929
import { IStandaloneThemeData, IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneTheme';
30-
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
30+
import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands';
3131
import { IMarker, IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers';
32+
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
33+
import { EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
34+
import { IMenuItem, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
35+
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
3236

3337
/**
3438
* Create a new editor under `domElement`.
@@ -99,6 +103,119 @@ export function createDiffNavigator(diffEditor: IStandaloneDiffEditor, opts?: ID
99103
return new DiffNavigator(diffEditor, opts);
100104
}
101105

106+
/**
107+
* Description of a command contribution
108+
*/
109+
export interface ICommandDescriptor {
110+
/**
111+
* An unique identifier of the contributed command.
112+
*/
113+
id: string;
114+
/**
115+
* Callback that will be executed when the command is triggered.
116+
*/
117+
run: ICommandHandler;
118+
}
119+
120+
/**
121+
* Add a command.
122+
*/
123+
export function addCommand(descriptor: ICommandDescriptor): IDisposable {
124+
if ((typeof descriptor.id !== 'string') || (typeof descriptor.run !== 'function')) {
125+
throw new Error('Invalid command descriptor, `id` and `run` are required properties!');
126+
}
127+
return CommandsRegistry.registerCommand(descriptor.id, descriptor.run);
128+
}
129+
130+
/**
131+
* Add an action to all editors.
132+
*/
133+
export function addEditorAction(descriptor: IActionDescriptor): IDisposable {
134+
if ((typeof descriptor.id !== 'string') || (typeof descriptor.label !== 'string') || (typeof descriptor.run !== 'function')) {
135+
throw new Error('Invalid action descriptor, `id`, `label` and `run` are required properties!');
136+
}
137+
138+
const precondition = ContextKeyExpr.deserialize(descriptor.precondition);
139+
const run = (accessor: ServicesAccessor, ...args: any[]): void | Promise<void> => {
140+
return EditorCommand.runEditorCommand(accessor, args, precondition, (accessor, editor, args) => Promise.resolve(descriptor.run(editor, ...args)));
141+
};
142+
143+
const toDispose = new DisposableStore();
144+
145+
// Register the command
146+
toDispose.add(CommandsRegistry.registerCommand(descriptor.id, run));
147+
148+
// Register the context menu item
149+
if (descriptor.contextMenuGroupId) {
150+
const menuItem: IMenuItem = {
151+
command: {
152+
id: descriptor.id,
153+
title: descriptor.label
154+
},
155+
when: precondition,
156+
group: descriptor.contextMenuGroupId,
157+
order: descriptor.contextMenuOrder || 0
158+
};
159+
toDispose.add(MenuRegistry.appendMenuItem(MenuId.EditorContext, menuItem));
160+
}
161+
162+
// Register the keybindings
163+
if (Array.isArray(descriptor.keybindings)) {
164+
const keybindingService = StandaloneServices.get(IKeybindingService);
165+
if (!(keybindingService instanceof StandaloneKeybindingService)) {
166+
console.warn('Cannot add keybinding because the editor is configured with an unrecognized KeybindingService');
167+
} else {
168+
const keybindingsWhen = ContextKeyExpr.and(precondition, ContextKeyExpr.deserialize(descriptor.keybindingContext));
169+
toDispose.add(keybindingService.addDynamicKeybindings(descriptor.keybindings.map((keybinding) => {
170+
return {
171+
keybinding,
172+
command: descriptor.id,
173+
when: keybindingsWhen
174+
};
175+
})));
176+
}
177+
}
178+
179+
return toDispose;
180+
}
181+
182+
/**
183+
* A keybinding rule.
184+
*/
185+
export interface IKeybindingRule {
186+
keybinding: number;
187+
command?: string | null;
188+
commandArgs?: any;
189+
when?: string | null;
190+
}
191+
192+
/**
193+
* Add a keybinding rule.
194+
*/
195+
export function addKeybindingRule(rule: IKeybindingRule): IDisposable {
196+
return addKeybindingRules([rule]);
197+
}
198+
199+
/**
200+
* Add keybinding rules.
201+
*/
202+
export function addKeybindingRules(rules: IKeybindingRule[]): IDisposable {
203+
const keybindingService = StandaloneServices.get(IKeybindingService);
204+
if (!(keybindingService instanceof StandaloneKeybindingService)) {
205+
console.warn('Cannot add keybinding because the editor is configured with an unrecognized KeybindingService');
206+
return Disposable.None;
207+
}
208+
209+
return keybindingService.addDynamicKeybindings(rules.map((rule) => {
210+
return {
211+
keybinding: rule.keybinding,
212+
command: rule.command,
213+
commandArgs: rule.commandArgs,
214+
when: ContextKeyExpr.deserialize(rule.when),
215+
};
216+
}));
217+
}
218+
102219
/**
103220
* Create a new editor model.
104221
* You can specify the language that should be set for this model or let the language be inferred from the `uri`.
@@ -325,6 +442,11 @@ export function createMonacoEditorAPI(): typeof monaco.editor {
325442
createDiffEditor: <any>createDiffEditor,
326443
createDiffNavigator: <any>createDiffNavigator,
327444

445+
addCommand: <any>addCommand,
446+
addEditorAction: <any>addEditorAction,
447+
addKeybindingRule: <any>addKeybindingRule,
448+
addKeybindingRules: <any>addKeybindingRules,
449+
328450
createModel: <any>createModel,
329451
setModelLanguage: <any>setModelLanguage,
330452
setModelMarkers: <any>setModelMarkers,

src/vs/editor/standalone/browser/standaloneServices.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom';
1414
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
1515
import { Emitter, Event } from 'vs/base/common/event';
1616
import { Keybinding, ResolvedKeybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keybindings';
17-
import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
17+
import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableStore, Disposable, combinedDisposable } from 'vs/base/common/lifecycle';
1818
import { OS, isLinux, isMacintosh } from 'vs/base/common/platform';
1919
import Severity from 'vs/base/common/severity';
2020
import { URI } from 'vs/base/common/uri';
@@ -323,9 +323,16 @@ export class StandaloneCommandService implements ICommandService {
323323
}
324324
}
325325

326+
export interface IKeybindingRule {
327+
keybinding: number;
328+
command?: string | null;
329+
commandArgs?: any;
330+
when?: ContextKeyExpression | null;
331+
}
332+
326333
export class StandaloneKeybindingService extends AbstractKeybindingService {
327334
private _cachedResolver: KeybindingResolver | null;
328-
private readonly _dynamicKeybindings: IKeybindingItem[];
335+
private _dynamicKeybindings: IKeybindingItem[];
329336
private readonly _domNodeListeners: DomNodeListeners[];
330337

331338
constructor(
@@ -403,39 +410,45 @@ export class StandaloneKeybindingService extends AbstractKeybindingService {
403410
codeEditorService.listDiffEditors().forEach(addDiffEditor);
404411
}
405412

406-
public addDynamicKeybinding(commandId: string, _keybinding: number, handler: ICommandHandler, when: ContextKeyExpression | undefined): IDisposable {
407-
const keybinding = createKeybinding(_keybinding, OS);
408-
409-
const toDispose = new DisposableStore();
413+
public addDynamicKeybinding(command: string, keybinding: number, handler: ICommandHandler, when: ContextKeyExpression | undefined): IDisposable {
414+
return combinedDisposable(
415+
CommandsRegistry.registerCommand(command, handler),
416+
this.addDynamicKeybindings([{
417+
keybinding,
418+
command,
419+
when
420+
}])
421+
);
422+
}
410423

411-
if (keybinding) {
412-
this._dynamicKeybindings.push({
413-
keybinding: keybinding.parts,
414-
command: commandId,
415-
when: when,
424+
public addDynamicKeybindings(rules: IKeybindingRule[]): IDisposable {
425+
const entries: IKeybindingItem[] = rules.map((rule) => {
426+
const keybinding = createKeybinding(rule.keybinding, OS);
427+
return {
428+
keybinding: keybinding?.parts ?? null,
429+
command: rule.command ?? null,
430+
commandArgs: rule.commandArgs,
431+
when: rule.when,
416432
weight1: 1000,
417433
weight2: 0,
418434
extensionId: null,
419435
isBuiltinExtension: false
420-
});
421-
422-
toDispose.add(toDisposable(() => {
423-
for (let i = 0; i < this._dynamicKeybindings.length; i++) {
424-
const kb = this._dynamicKeybindings[i];
425-
if (kb.command === commandId) {
426-
this._dynamicKeybindings.splice(i, 1);
427-
this.updateResolver();
428-
return;
429-
}
430-
}
431-
}));
432-
}
433-
434-
toDispose.add(CommandsRegistry.registerCommand(commandId, handler));
436+
};
437+
});
438+
this._dynamicKeybindings = this._dynamicKeybindings.concat(entries);
435439

436440
this.updateResolver();
437441

438-
return toDispose;
442+
return toDisposable(() => {
443+
// Search the first entry and remove them all since they will be contiguous
444+
for (let i = 0; i < this._dynamicKeybindings.length; i++) {
445+
if (this._dynamicKeybindings[i] === entries[0]) {
446+
this._dynamicKeybindings.splice(i, entries.length);
447+
this.updateResolver();
448+
return;
449+
}
450+
}
451+
});
439452
}
440453

441454
private updateResolver(): void {

src/vs/monaco.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,50 @@ declare namespace monaco.editor {
925925

926926
export function createDiffNavigator(diffEditor: IStandaloneDiffEditor, opts?: IDiffNavigatorOptions): IDiffNavigator;
927927

928+
/**
929+
* Description of a command contribution
930+
*/
931+
export interface ICommandDescriptor {
932+
/**
933+
* An unique identifier of the contributed command.
934+
*/
935+
id: string;
936+
/**
937+
* Callback that will be executed when the command is triggered.
938+
*/
939+
run: ICommandHandler;
940+
}
941+
942+
/**
943+
* Add a command.
944+
*/
945+
export function addCommand(descriptor: ICommandDescriptor): IDisposable;
946+
947+
/**
948+
* Add an action to all editors.
949+
*/
950+
export function addEditorAction(descriptor: IActionDescriptor): IDisposable;
951+
952+
/**
953+
* A keybinding rule.
954+
*/
955+
export interface IKeybindingRule {
956+
keybinding: number;
957+
command?: string | null;
958+
commandArgs?: any;
959+
when?: string | null;
960+
}
961+
962+
/**
963+
* Add a keybinding rule.
964+
*/
965+
export function addKeybindingRule(rule: IKeybindingRule): IDisposable;
966+
967+
/**
968+
* Add keybinding rules.
969+
*/
970+
export function addKeybindingRules(rules: IKeybindingRule[]): IDisposable;
971+
928972
/**
929973
* Create a new editor model.
930974
* You can specify the language that should be set for this model or let the language be inferred from the `uri`.

src/vs/platform/keybinding/common/keybindingsRegistry.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { combinedDisposable, DisposableStore, IDisposable, toDisposable } from '
1313
import { LinkedList } from 'vs/base/common/linkedList';
1414

1515
export interface IKeybindingItem {
16-
keybinding: (SimpleKeybinding | ScanCodeBinding)[];
17-
command: string;
16+
keybinding: (SimpleKeybinding | ScanCodeBinding)[] | null;
17+
command: string | null;
1818
commandArgs?: any;
1919
when: ContextKeyExpression | null | undefined;
2020
weight1: number;
@@ -238,11 +238,13 @@ function sorter(a: IKeybindingItem, b: IKeybindingItem): number {
238238
if (a.weight1 !== b.weight1) {
239239
return a.weight1 - b.weight1;
240240
}
241-
if (a.command < b.command) {
242-
return -1;
243-
}
244-
if (a.command > b.command) {
245-
return 1;
241+
if (a.command && b.command) {
242+
if (a.command < b.command) {
243+
return -1;
244+
}
245+
if (a.command > b.command) {
246+
return 1;
247+
}
246248
}
247249
return a.weight2 - b.weight2;
248250
}

src/vs/workbench/services/keybinding/browser/keybindingService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
458458
return result;
459459
}
460460

461-
private _assertBrowserConflicts(kb: (SimpleKeybinding | ScanCodeBinding)[], commandId: string): boolean {
461+
private _assertBrowserConflicts(kb: (SimpleKeybinding | ScanCodeBinding)[], commandId: string | null): boolean {
462462
if (BrowserFeatures.keyboard === KeyboardSupport.Always) {
463463
return false;
464464
}

0 commit comments

Comments
 (0)