/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; import { IEditorContribution, IDiffEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { MenuId, MenuRegistry, Action2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor as InstantiationServicesAccessor, BrandedService, IInstantiationService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindings, KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withNullAsUndefined, assertType } from 'vs/base/common/types'; import { ThemeIcon } from 'vs/base/common/themables'; import { IDisposable } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ILogService } from 'vs/platform/log/common/log'; export type ServicesAccessor = InstantiationServicesAccessor; export type EditorContributionCtor = IConstructorSignature; export type DiffEditorContributionCtor = IConstructorSignature; export const enum EditorContributionInstantiation { /** * The contribution is created eagerly when the {@linkcode ICodeEditor} is instantiated. * Only Eager contributions can participate in saving or restoring of view state. */ Eager, /** * The contribution is created at the latest 50ms after the first render after attaching a text model. * If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner. * If there is idle time available, it will be instantiated sooner. */ AfterFirstRender, /** * The contribution is created before the editor emits events produced by user interaction (mouse events, keyboard events). * If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner. * If there is idle time available, it will be instantiated sooner. */ BeforeFirstInteraction, /** * The contribution is created when there is idle time available, at the latest 5000ms after the editor creation. * If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner. */ Eventually, /** * The contribution is created only when explicitly requested via `getContribution`. */ Lazy, } export interface IEditorContributionDescription { readonly id: string; readonly ctor: EditorContributionCtor; readonly instantiation: EditorContributionInstantiation; } export interface IDiffEditorContributionDescription { id: string; ctor: DiffEditorContributionCtor; } //#region Command export interface ICommandKeybindingsOptions extends IKeybindings { kbExpr?: ContextKeyExpression | null; weight: number; /** * the default keybinding arguments */ args?: any; } export interface ICommandMenuOptions { menuId: MenuId; group: string; order: number; when?: ContextKeyExpression; title: string; icon?: ThemeIcon; } export interface ICommandOptions { id: string; precondition: ContextKeyExpression | undefined; kbOpts?: ICommandKeybindingsOptions | ICommandKeybindingsOptions[]; description?: ICommandHandlerDescription; menuOpts?: ICommandMenuOptions | ICommandMenuOptions[]; } export abstract class Command { public readonly id: string; public readonly precondition: ContextKeyExpression | undefined; private readonly _kbOpts: ICommandKeybindingsOptions | ICommandKeybindingsOptions[] | undefined; private readonly _menuOpts: ICommandMenuOptions | ICommandMenuOptions[] | undefined; private readonly _description: ICommandHandlerDescription | undefined; constructor(opts: ICommandOptions) { this.id = opts.id; this.precondition = opts.precondition; this._kbOpts = opts.kbOpts; this._menuOpts = opts.menuOpts; this._description = opts.description; } public register(): void { if (Array.isArray(this._menuOpts)) { this._menuOpts.forEach(this._registerMenuItem, this); } else if (this._menuOpts) { this._registerMenuItem(this._menuOpts); } if (this._kbOpts) { const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts]; for (const kbOpts of kbOptsArr) { let kbWhen = kbOpts.kbExpr; if (this.precondition) { if (kbWhen) { kbWhen = ContextKeyExpr.and(kbWhen, this.precondition); } else { kbWhen = this.precondition; } } const desc = { id: this.id, weight: kbOpts.weight, args: kbOpts.args, when: kbWhen, primary: kbOpts.primary, secondary: kbOpts.secondary, win: kbOpts.win, linux: kbOpts.linux, mac: kbOpts.mac, }; KeybindingsRegistry.registerKeybindingRule(desc); } } CommandsRegistry.registerCommand({ id: this.id, handler: (accessor, args) => this.runCommand(accessor, args), description: this._description }); } private _registerMenuItem(item: ICommandMenuOptions): void { MenuRegistry.appendMenuItem(item.menuId, { group: item.group, command: { id: this.id, title: item.title, icon: item.icon, precondition: this.precondition }, when: item.when, order: item.order }); } public abstract runCommand(accessor: ServicesAccessor, args: any): void | Promise; } //#endregion Command //#region MultiplexingCommand /** * Potential override for a command. * * @return `true` if the command was successfully run. This stops other overrides from being executed. */ export type CommandImplementation = (accessor: ServicesAccessor, args: unknown) => boolean | Promise; interface ICommandImplementationRegistration { priority: number; name: string; implementation: CommandImplementation; } export class MultiCommand extends Command { private readonly _implementations: ICommandImplementationRegistration[] = []; /** * A higher priority gets to be looked at first */ public addImplementation(priority: number, name: string, implementation: CommandImplementation): IDisposable { this._implementations.push({ priority, name, implementation }); this._implementations.sort((a, b) => b.priority - a.priority); return { dispose: () => { for (let i = 0; i < this._implementations.length; i++) { if (this._implementations[i].implementation === implementation) { this._implementations.splice(i, 1); return; } } } }; } public runCommand(accessor: ServicesAccessor, args: any): void | Promise { const logService = accessor.get(ILogService); logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`); for (const impl of this._implementations) { const result = impl.implementation(accessor, args); if (result) { logService.trace(`Command '${this.id}' was handled by '${impl.name}'.`); if (typeof result === 'boolean') { return; } return result; } } logService.trace(`The Command '${this.id}' was not handled by any implementation.`); } } //#endregion /** * A command that delegates to another command's implementation. * * This lets different commands be registered but share the same implementation */ export class ProxyCommand extends Command { constructor( private readonly command: Command, opts: ICommandOptions ) { super(opts); } public runCommand(accessor: ServicesAccessor, args: any): void | Promise { return this.command.runCommand(accessor, args); } } //#region EditorCommand export interface IContributionCommandOptions extends ICommandOptions { handler: (controller: T, args: any) => void; } export interface EditorControllerCommand { new(opts: IContributionCommandOptions): EditorCommand; } export abstract class EditorCommand extends Command { /** * Create a command class that is bound to a certain editor contribution. */ public static bindToContribution(controllerGetter: (editor: ICodeEditor) => T | null): EditorControllerCommand { return class EditorControllerCommandImpl extends EditorCommand { private readonly _callback: (controller: T, args: any) => void; constructor(opts: IContributionCommandOptions) { super(opts); this._callback = opts.handler; } public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { const controller = controllerGetter(editor); if (controller) { this._callback(controller, args); } } }; } public static runEditorCommand( accessor: ServicesAccessor, args: any, precondition: ContextKeyExpression | undefined, runner: (accessor: ServicesAccessor | null, editor: ICodeEditor, args: any) => void | Promise ): void | Promise { const codeEditorService = accessor.get(ICodeEditorService); // Find the editor with text focus or active const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); if (!editor) { // well, at least we tried... return; } return editor.invokeWithinContext((editorAccessor) => { const kbService = editorAccessor.get(IContextKeyService); if (!kbService.contextMatchesRules(withNullAsUndefined(precondition))) { // precondition does not hold return; } return runner(editorAccessor, editor, args); }); } public runCommand(accessor: ServicesAccessor, args: any): void | Promise { return EditorCommand.runEditorCommand(accessor, args, this.precondition, (accessor, editor, args) => this.runEditorCommand(accessor, editor, args)); } public abstract runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void | Promise; } //#endregion EditorCommand //#region EditorAction export interface IEditorActionContextMenuOptions { group: string; order: number; when?: ContextKeyExpression; menuId?: MenuId; } export interface IActionOptions extends ICommandOptions { label: string; alias: string; contextMenuOpts?: IEditorActionContextMenuOptions | IEditorActionContextMenuOptions[]; } export abstract class EditorAction extends EditorCommand { private static convertOptions(opts: IActionOptions): ICommandOptions { let menuOpts: ICommandMenuOptions[]; if (Array.isArray(opts.menuOpts)) { menuOpts = opts.menuOpts; } else if (opts.menuOpts) { menuOpts = [opts.menuOpts]; } else { menuOpts = []; } function withDefaults(item: Partial): ICommandMenuOptions { if (!item.menuId) { item.menuId = MenuId.EditorContext; } if (!item.title) { item.title = opts.label; } item.when = ContextKeyExpr.and(opts.precondition, item.when); return item; } if (Array.isArray(opts.contextMenuOpts)) { menuOpts.push(...opts.contextMenuOpts.map(withDefaults)); } else if (opts.contextMenuOpts) { menuOpts.push(withDefaults(opts.contextMenuOpts)); } opts.menuOpts = menuOpts; return opts; } public readonly label: string; public readonly alias: string; constructor(opts: IActionOptions) { super(EditorAction.convertOptions(opts)); this.label = opts.label; this.alias = opts.alias; } public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { this.reportTelemetry(accessor, editor); return this.run(accessor, editor, args || {}); } protected reportTelemetry(accessor: ServicesAccessor, editor: ICodeEditor) { type EditorActionInvokedClassification = { owner: 'alexdima'; comment: 'An editor action has been invoked.'; name: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was invoked.' }; id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was invoked.' }; }; type EditorActionInvokedEvent = { name: string; id: string; }; accessor.get(ITelemetryService).publicLog2('editorActionInvoked', { name: this.label, id: this.id }); } public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise; } export type EditorActionImplementation = (accessor: ServicesAccessor, editor: ICodeEditor, args: any) => boolean | Promise; export class MultiEditorAction extends EditorAction { private readonly _implementations: [number, EditorActionImplementation][] = []; /** * A higher priority gets to be looked at first */ public addImplementation(priority: number, implementation: EditorActionImplementation): IDisposable { this._implementations.push([priority, implementation]); this._implementations.sort((a, b) => b[0] - a[0]); return { dispose: () => { for (let i = 0; i < this._implementations.length; i++) { if (this._implementations[i][1] === implementation) { this._implementations.splice(i, 1); return; } } } }; } public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { for (const impl of this._implementations) { const result = impl[1](accessor, editor, args); if (result) { if (typeof result === 'boolean') { return; } return result; } } } } //#endregion EditorAction //#region EditorAction2 export abstract class EditorAction2 extends Action2 { run(accessor: ServicesAccessor, ...args: any[]) { // Find the editor with text focus or active const codeEditorService = accessor.get(ICodeEditorService); const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); if (!editor) { // well, at least we tried... return; } // precondition does hold return editor.invokeWithinContext((editorAccessor) => { const kbService = editorAccessor.get(IContextKeyService); const logService = editorAccessor.get(ILogService); const enabled = kbService.contextMatchesRules(withNullAsUndefined(this.desc.precondition)); if (!enabled) { logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize()); return; } return this.runEditorCommand(editorAccessor, editor!, ...args); }); } abstract runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): any; } //#endregion // --- Registration of commands and actions export function registerModelAndPositionCommand(id: string, handler: (accessor: ServicesAccessor, model: ITextModel, position: Position, ...args: any[]) => any) { CommandsRegistry.registerCommand(id, function (accessor, ...args) { const instaService = accessor.get(IInstantiationService); const [resource, position] = args; assertType(URI.isUri(resource)); assertType(Position.isIPosition(position)); const model = accessor.get(IModelService).getModel(resource); if (model) { const editorPosition = Position.lift(position); return instaService.invokeFunction(handler, model, editorPosition, ...args.slice(2)); } return accessor.get(ITextModelService).createModelReference(resource).then(reference => { return new Promise((resolve, reject) => { try { const result = instaService.invokeFunction(handler, reference.object.textEditorModel, Position.lift(position), args.slice(2)); resolve(result); } catch (err) { reject(err); } }).finally(() => { reference.dispose(); }); }); }); } export function registerEditorCommand(editorCommand: T): T { EditorContributionRegistry.INSTANCE.registerEditorCommand(editorCommand); return editorCommand; } export function registerEditorAction(ctor: { new(): T }): T { const action = new ctor(); EditorContributionRegistry.INSTANCE.registerEditorAction(action); return action; } export function registerMultiEditorAction(action: T): T { EditorContributionRegistry.INSTANCE.registerEditorAction(action); return action; } export function registerInstantiatedEditorAction(editorAction: EditorAction): void { EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction); } /** * Registers an editor contribution. Editor contributions have a lifecycle which is bound * to a specific code editor instance. */ export function registerEditorContribution(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void { EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor, instantiation); } /** * Registers a diff editor contribution. Diff editor contributions have a lifecycle which * is bound to a specific diff editor instance. */ export function registerDiffEditorContribution(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void { EditorContributionRegistry.INSTANCE.registerDiffEditorContribution(id, ctor); } export namespace EditorExtensionsRegistry { export function getEditorCommand(commandId: string): EditorCommand { return EditorContributionRegistry.INSTANCE.getEditorCommand(commandId); } export function getEditorActions(): Iterable { return EditorContributionRegistry.INSTANCE.getEditorActions(); } export function getEditorContributions(): IEditorContributionDescription[] { return EditorContributionRegistry.INSTANCE.getEditorContributions(); } export function getSomeEditorContributions(ids: string[]): IEditorContributionDescription[] { return EditorContributionRegistry.INSTANCE.getEditorContributions().filter(c => ids.indexOf(c.id) >= 0); } export function getDiffEditorContributions(): IDiffEditorContributionDescription[] { return EditorContributionRegistry.INSTANCE.getDiffEditorContributions(); } } // Editor extension points const Extensions = { EditorCommonContributions: 'editor.contributions' }; class EditorContributionRegistry { public static readonly INSTANCE = new EditorContributionRegistry(); private readonly editorContributions: IEditorContributionDescription[] = []; private readonly diffEditorContributions: IDiffEditorContributionDescription[] = []; private readonly editorActions: EditorAction[] = []; private readonly editorCommands: { [commandId: string]: EditorCommand } = Object.create(null); constructor() { } public registerEditorContribution(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void { this.editorContributions.push({ id, ctor: ctor as EditorContributionCtor, instantiation }); } public getEditorContributions(): IEditorContributionDescription[] { return this.editorContributions.slice(0); } public registerDiffEditorContribution(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void { this.diffEditorContributions.push({ id, ctor: ctor as DiffEditorContributionCtor }); } public getDiffEditorContributions(): IDiffEditorContributionDescription[] { return this.diffEditorContributions.slice(0); } public registerEditorAction(action: EditorAction) { action.register(); this.editorActions.push(action); } public getEditorActions(): Iterable { return this.editorActions; } public registerEditorCommand(editorCommand: EditorCommand) { editorCommand.register(); this.editorCommands[editorCommand.id] = editorCommand; } public getEditorCommand(commandId: string): EditorCommand { return (this.editorCommands[commandId] || null); } } Registry.add(Extensions.EditorCommonContributions, EditorContributionRegistry.INSTANCE); function registerCommand(command: T): T { command.register(); return command; } export const UndoCommand = registerCommand(new MultiCommand({ id: 'undo', precondition: undefined, kbOpts: { weight: KeybindingWeight.EditorCore, primary: KeyMod.CtrlCmd | KeyCode.KeyZ }, menuOpts: [{ menuId: MenuId.MenubarEditMenu, group: '1_do', title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), order: 1 }, { menuId: MenuId.CommandPalette, group: '', title: nls.localize('undo', "Undo"), order: 1 }] })); registerCommand(new ProxyCommand(UndoCommand, { id: 'default:undo', precondition: undefined })); export const RedoCommand = registerCommand(new MultiCommand({ id: 'redo', precondition: undefined, kbOpts: { weight: KeybindingWeight.EditorCore, primary: KeyMod.CtrlCmd | KeyCode.KeyY, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ], mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ } }, menuOpts: [{ menuId: MenuId.MenubarEditMenu, group: '1_do', title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), order: 2 }, { menuId: MenuId.CommandPalette, group: '', title: nls.localize('redo', "Redo"), order: 1 }] })); registerCommand(new ProxyCommand(RedoCommand, { id: 'default:redo', precondition: undefined })); export const SelectAllCommand = registerCommand(new MultiCommand({ id: 'editor.action.selectAll', precondition: undefined, kbOpts: { weight: KeybindingWeight.EditorCore, kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KeyA }, menuOpts: [{ menuId: MenuId.MenubarSelectionMenu, group: '1_basic', title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), order: 1 }, { menuId: MenuId.CommandPalette, group: '', title: nls.localize('selectAll', "Select All"), order: 1 }] }));