Path: blob/main/src/vs/editor/browser/editorExtensions.ts
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../nls.js';6import { URI } from '../../base/common/uri.js';7import { ICodeEditor, IDiffEditor } from './editorBrowser.js';8import { ICodeEditorService } from './services/codeEditorService.js';9import { Position } from '../common/core/position.js';10import { IEditorContribution, IDiffEditorContribution } from '../common/editorCommon.js';11import { ITextModel } from '../common/model.js';12import { IModelService } from '../common/services/model.js';13import { ITextModelService } from '../common/services/resolverService.js';14import { MenuId, MenuRegistry, Action2 } from '../../platform/actions/common/actions.js';15import { CommandsRegistry, ICommandMetadata } from '../../platform/commands/common/commands.js';16import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from '../../platform/contextkey/common/contextkey.js';17import { ServicesAccessor as InstantiationServicesAccessor, BrandedService, IInstantiationService, IConstructorSignature } from '../../platform/instantiation/common/instantiation.js';18import { IKeybindings, KeybindingsRegistry, KeybindingWeight } from '../../platform/keybinding/common/keybindingsRegistry.js';19import { Registry } from '../../platform/registry/common/platform.js';20import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js';21import { assertType } from '../../base/common/types.js';22import { ThemeIcon } from '../../base/common/themables.js';23import { IDisposable } from '../../base/common/lifecycle.js';24import { KeyMod, KeyCode } from '../../base/common/keyCodes.js';25import { ILogService } from '../../platform/log/common/log.js';26import { getActiveElement } from '../../base/browser/dom.js';2728export type ServicesAccessor = InstantiationServicesAccessor;29export type EditorContributionCtor = IConstructorSignature<IEditorContribution, [ICodeEditor]>;30export type DiffEditorContributionCtor = IConstructorSignature<IDiffEditorContribution, [IDiffEditor]>;3132export const enum EditorContributionInstantiation {33/**34* The contribution is created eagerly when the {@linkcode ICodeEditor} is instantiated.35* Only Eager contributions can participate in saving or restoring of view state.36*/37Eager,3839/**40* The contribution is created at the latest 50ms after the first render after attaching a text model.41* If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner.42* If there is idle time available, it will be instantiated sooner.43*/44AfterFirstRender,4546/**47* The contribution is created before the editor emits events produced by user interaction (mouse events, keyboard events).48* If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner.49* If there is idle time available, it will be instantiated sooner.50*/51BeforeFirstInteraction,5253/**54* The contribution is created when there is idle time available, at the latest 5000ms after the editor creation.55* If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner.56*/57Eventually,5859/**60* The contribution is created only when explicitly requested via `getContribution`.61*/62Lazy,63}6465export interface IEditorContributionDescription {66readonly id: string;67readonly ctor: EditorContributionCtor;68readonly instantiation: EditorContributionInstantiation;69}7071export interface IDiffEditorContributionDescription {72id: string;73ctor: DiffEditorContributionCtor;74}7576//#region Command7778export interface ICommandKeybindingsOptions extends IKeybindings {79kbExpr?: ContextKeyExpression | null;80weight: number;81/**82* the default keybinding arguments83*/84args?: any;85}86export interface ICommandMenuOptions {87menuId: MenuId;88group: string;89order: number;90when?: ContextKeyExpression;91title: string;92icon?: ThemeIcon;93}94export interface ICommandOptions {95id: string;96precondition: ContextKeyExpression | undefined;97kbOpts?: ICommandKeybindingsOptions | ICommandKeybindingsOptions[];98metadata?: ICommandMetadata;99menuOpts?: ICommandMenuOptions | ICommandMenuOptions[];100}101export abstract class Command {102public readonly id: string;103public readonly precondition: ContextKeyExpression | undefined;104private readonly _kbOpts: ICommandKeybindingsOptions | ICommandKeybindingsOptions[] | undefined;105private readonly _menuOpts: ICommandMenuOptions | ICommandMenuOptions[] | undefined;106public readonly metadata: ICommandMetadata | undefined;107108constructor(opts: ICommandOptions) {109this.id = opts.id;110this.precondition = opts.precondition;111this._kbOpts = opts.kbOpts;112this._menuOpts = opts.menuOpts;113this.metadata = opts.metadata;114}115116public register(): void {117118if (Array.isArray(this._menuOpts)) {119this._menuOpts.forEach(this._registerMenuItem, this);120} else if (this._menuOpts) {121this._registerMenuItem(this._menuOpts);122}123124if (this._kbOpts) {125const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts];126for (const kbOpts of kbOptsArr) {127let kbWhen = kbOpts.kbExpr;128if (this.precondition) {129if (kbWhen) {130kbWhen = ContextKeyExpr.and(kbWhen, this.precondition);131} else {132kbWhen = this.precondition;133}134}135136const desc = {137id: this.id,138weight: kbOpts.weight,139args: kbOpts.args,140when: kbWhen,141primary: kbOpts.primary,142secondary: kbOpts.secondary,143win: kbOpts.win,144linux: kbOpts.linux,145mac: kbOpts.mac,146};147148KeybindingsRegistry.registerKeybindingRule(desc);149}150}151152CommandsRegistry.registerCommand({153id: this.id,154handler: (accessor, args) => this.runCommand(accessor, args),155metadata: this.metadata156});157}158159private _registerMenuItem(item: ICommandMenuOptions): void {160MenuRegistry.appendMenuItem(item.menuId, {161group: item.group,162command: {163id: this.id,164title: item.title,165icon: item.icon,166precondition: this.precondition167},168when: item.when,169order: item.order170});171}172173public abstract runCommand(accessor: ServicesAccessor, args: any): void | Promise<void>;174}175176//#endregion Command177178//#region MultiplexingCommand179180/**181* Potential override for a command.182*183* @return `true` or a Promise if the command was successfully run. This stops other overrides from being executed.184*/185export type CommandImplementation = (accessor: ServicesAccessor, args: unknown) => boolean | Promise<void>;186187interface ICommandImplementationRegistration {188priority: number;189name: string;190implementation: CommandImplementation;191when?: ContextKeyExpression;192}193194export class MultiCommand extends Command {195196private readonly _implementations: ICommandImplementationRegistration[] = [];197198/**199* A higher priority gets to be looked at first200*/201public addImplementation(priority: number, name: string, implementation: CommandImplementation, when?: ContextKeyExpression): IDisposable {202this._implementations.push({ priority, name, implementation, when });203this._implementations.sort((a, b) => b.priority - a.priority);204return {205dispose: () => {206for (let i = 0; i < this._implementations.length; i++) {207if (this._implementations[i].implementation === implementation) {208this._implementations.splice(i, 1);209return;210}211}212}213};214}215216public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {217const logService = accessor.get(ILogService);218const contextKeyService = accessor.get(IContextKeyService);219logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`);220for (const impl of this._implementations) {221if (impl.when) {222const context = contextKeyService.getContext(getActiveElement());223const value = impl.when.evaluate(context);224if (!value) {225continue;226}227}228const result = impl.implementation(accessor, args);229if (result) {230logService.trace(`Command '${this.id}' was handled by '${impl.name}'.`);231if (typeof result === 'boolean') {232return;233}234return result;235}236}237logService.trace(`The Command '${this.id}' was not handled by any implementation.`);238}239}240241//#endregion242243/**244* A command that delegates to another command's implementation.245*246* This lets different commands be registered but share the same implementation247*/248export class ProxyCommand extends Command {249constructor(250private readonly command: Command,251opts: ICommandOptions252) {253super(opts);254}255256public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {257return this.command.runCommand(accessor, args);258}259}260261//#region EditorCommand262263export interface IContributionCommandOptions<T> extends ICommandOptions {264handler: (controller: T, args: any) => void;265}266export interface EditorControllerCommand<T extends IEditorContribution> {267new(opts: IContributionCommandOptions<T>): EditorCommand;268}269export abstract class EditorCommand extends Command {270271/**272* Create a command class that is bound to a certain editor contribution.273*/274public static bindToContribution<T extends IEditorContribution>(controllerGetter: (editor: ICodeEditor) => T | null): EditorControllerCommand<T> {275return class EditorControllerCommandImpl extends EditorCommand {276private readonly _callback: (controller: T, args: any) => void;277278constructor(opts: IContributionCommandOptions<T>) {279super(opts);280281this._callback = opts.handler;282}283284public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {285const controller = controllerGetter(editor);286if (controller) {287this._callback(controller, args);288}289}290};291}292293public static runEditorCommand(294accessor: ServicesAccessor,295args: any,296precondition: ContextKeyExpression | undefined,297runner: (accessor: ServicesAccessor, editor: ICodeEditor, args: any) => void | Promise<void>298): void | Promise<void> {299const codeEditorService = accessor.get(ICodeEditorService);300301// Find the editor with text focus or active302const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();303if (!editor) {304// well, at least we tried...305return;306}307308return editor.invokeWithinContext((editorAccessor) => {309const kbService = editorAccessor.get(IContextKeyService);310if (!kbService.contextMatchesRules(precondition ?? undefined)) {311// precondition does not hold312return;313}314315return runner(editorAccessor, editor, args);316});317}318319public runCommand(accessor: ServicesAccessor, args: any): void | Promise<void> {320return EditorCommand.runEditorCommand(accessor, args, this.precondition, (accessor, editor, args) => this.runEditorCommand(accessor, editor, args));321}322323public abstract runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void>;324}325326//#endregion EditorCommand327328//#region EditorAction329330export interface IEditorActionContextMenuOptions {331group: string;332order: number;333when?: ContextKeyExpression;334menuId?: MenuId;335}336export type IActionOptions = ICommandOptions & {337contextMenuOpts?: IEditorActionContextMenuOptions | IEditorActionContextMenuOptions[];338} & ({339label: nls.ILocalizedString;340alias?: string;341} | {342label: string;343alias: string;344});345346export abstract class EditorAction extends EditorCommand {347348private static convertOptions(opts: IActionOptions): ICommandOptions {349350let menuOpts: ICommandMenuOptions[];351if (Array.isArray(opts.menuOpts)) {352menuOpts = opts.menuOpts;353} else if (opts.menuOpts) {354menuOpts = [opts.menuOpts];355} else {356menuOpts = [];357}358359function withDefaults(item: Partial<ICommandMenuOptions>): ICommandMenuOptions {360if (!item.menuId) {361item.menuId = MenuId.EditorContext;362}363if (!item.title) {364item.title = typeof opts.label === 'string' ? opts.label : opts.label.value;365}366item.when = ContextKeyExpr.and(opts.precondition, item.when);367return <ICommandMenuOptions>item;368}369370if (Array.isArray(opts.contextMenuOpts)) {371menuOpts.push(...opts.contextMenuOpts.map(withDefaults));372} else if (opts.contextMenuOpts) {373menuOpts.push(withDefaults(opts.contextMenuOpts));374}375376opts.menuOpts = menuOpts;377return <ICommandOptions>opts;378}379380public readonly label: string;381public readonly alias: string;382383constructor(opts: IActionOptions) {384super(EditorAction.convertOptions(opts));385if (typeof opts.label === 'string') {386this.label = opts.label;387this.alias = opts.alias ?? opts.label;388} else {389this.label = opts.label.value;390this.alias = opts.alias ?? opts.label.original;391}392}393394public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void> {395this.reportTelemetry(accessor, editor);396return this.run(accessor, editor, args || {});397}398399protected reportTelemetry(accessor: ServicesAccessor, editor: ICodeEditor) {400type EditorActionInvokedClassification = {401owner: 'alexdima';402comment: 'An editor action has been invoked.';403name: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was invoked.' };404id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was invoked.' };405};406type EditorActionInvokedEvent = {407name: string;408id: string;409};410accessor.get(ITelemetryService).publicLog2<EditorActionInvokedEvent, EditorActionInvokedClassification>('editorActionInvoked', { name: this.label, id: this.id });411}412413public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void>;414}415416export type EditorActionImplementation = (accessor: ServicesAccessor, editor: ICodeEditor, args: any) => boolean | Promise<void>;417418export class MultiEditorAction extends EditorAction {419420private readonly _implementations: [number, EditorActionImplementation][] = [];421422/**423* A higher priority gets to be looked at first424*/425public addImplementation(priority: number, implementation: EditorActionImplementation): IDisposable {426this._implementations.push([priority, implementation]);427this._implementations.sort((a, b) => b[0] - a[0]);428return {429dispose: () => {430for (let i = 0; i < this._implementations.length; i++) {431if (this._implementations[i][1] === implementation) {432this._implementations.splice(i, 1);433return;434}435}436}437};438}439440public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void> {441for (const impl of this._implementations) {442const result = impl[1](accessor, editor, args);443if (result) {444if (typeof result === 'boolean') {445return;446}447return result;448}449}450}451452}453454//#endregion EditorAction455456//#region EditorAction2457458export abstract class EditorAction2 extends Action2 {459460run(accessor: ServicesAccessor, ...args: any[]) {461// Find the editor with text focus or active462const codeEditorService = accessor.get(ICodeEditorService);463const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();464if (!editor) {465// well, at least we tried...466return;467}468// precondition does hold469return editor.invokeWithinContext((editorAccessor) => {470const kbService = editorAccessor.get(IContextKeyService);471const logService = editorAccessor.get(ILogService);472const enabled = kbService.contextMatchesRules(this.desc.precondition ?? undefined);473if (!enabled) {474logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize());475return;476}477return this.runEditorCommand(editorAccessor, editor, ...args);478});479}480481abstract runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: any[]): any;482}483484//#endregion485486// --- Registration of commands and actions487488489export function registerModelAndPositionCommand(id: string, handler: (accessor: ServicesAccessor, model: ITextModel, position: Position, ...args: any[]) => any) {490CommandsRegistry.registerCommand(id, function (accessor, ...args) {491492const instaService = accessor.get(IInstantiationService);493494const [resource, position] = args;495assertType(URI.isUri(resource));496assertType(Position.isIPosition(position));497498const model = accessor.get(IModelService).getModel(resource);499if (model) {500const editorPosition = Position.lift(position);501return instaService.invokeFunction(handler, model, editorPosition, ...args.slice(2));502}503504return accessor.get(ITextModelService).createModelReference(resource).then(reference => {505return new Promise((resolve, reject) => {506try {507const result = instaService.invokeFunction(handler, reference.object.textEditorModel, Position.lift(position), args.slice(2));508resolve(result);509} catch (err) {510reject(err);511}512}).finally(() => {513reference.dispose();514});515});516});517}518519export function registerEditorCommand<T extends EditorCommand>(editorCommand: T): T {520EditorContributionRegistry.INSTANCE.registerEditorCommand(editorCommand);521return editorCommand;522}523524export function registerEditorAction<T extends EditorAction>(ctor: { new(): T }): T {525const action = new ctor();526EditorContributionRegistry.INSTANCE.registerEditorAction(action);527return action;528}529530export function registerMultiEditorAction<T extends MultiEditorAction>(action: T): T {531EditorContributionRegistry.INSTANCE.registerEditorAction(action);532return action;533}534535export function registerInstantiatedEditorAction(editorAction: EditorAction): void {536EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction);537}538539/**540* Registers an editor contribution. Editor contributions have a lifecycle which is bound541* to a specific code editor instance.542*/543export function registerEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void {544EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor, instantiation);545}546547/**548* Registers a diff editor contribution. Diff editor contributions have a lifecycle which549* is bound to a specific diff editor instance.550*/551export function registerDiffEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void {552EditorContributionRegistry.INSTANCE.registerDiffEditorContribution(id, ctor);553}554555export namespace EditorExtensionsRegistry {556557export function getEditorCommand(commandId: string): EditorCommand {558return EditorContributionRegistry.INSTANCE.getEditorCommand(commandId);559}560561export function getEditorActions(): Iterable<EditorAction> {562return EditorContributionRegistry.INSTANCE.getEditorActions();563}564565export function getEditorContributions(): IEditorContributionDescription[] {566return EditorContributionRegistry.INSTANCE.getEditorContributions();567}568569export function getSomeEditorContributions(ids: string[]): IEditorContributionDescription[] {570return EditorContributionRegistry.INSTANCE.getEditorContributions().filter(c => ids.indexOf(c.id) >= 0);571}572573export function getDiffEditorContributions(): IDiffEditorContributionDescription[] {574return EditorContributionRegistry.INSTANCE.getDiffEditorContributions();575}576}577578// Editor extension points579const Extensions = {580EditorCommonContributions: 'editor.contributions'581};582583class EditorContributionRegistry {584585public static readonly INSTANCE = new EditorContributionRegistry();586587private readonly editorContributions: IEditorContributionDescription[] = [];588private readonly diffEditorContributions: IDiffEditorContributionDescription[] = [];589private readonly editorActions: EditorAction[] = [];590private readonly editorCommands: { [commandId: string]: EditorCommand } = Object.create(null);591592constructor() {593}594595public registerEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void {596this.editorContributions.push({ id, ctor: ctor as EditorContributionCtor, instantiation });597}598599public getEditorContributions(): IEditorContributionDescription[] {600return this.editorContributions.slice(0);601}602603public registerDiffEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void {604this.diffEditorContributions.push({ id, ctor: ctor as DiffEditorContributionCtor });605}606607public getDiffEditorContributions(): IDiffEditorContributionDescription[] {608return this.diffEditorContributions.slice(0);609}610611public registerEditorAction(action: EditorAction) {612action.register();613this.editorActions.push(action);614}615616public getEditorActions(): Iterable<EditorAction> {617return this.editorActions;618}619620public registerEditorCommand(editorCommand: EditorCommand) {621editorCommand.register();622this.editorCommands[editorCommand.id] = editorCommand;623}624625public getEditorCommand(commandId: string): EditorCommand {626return (this.editorCommands[commandId] || null);627}628629}630Registry.add(Extensions.EditorCommonContributions, EditorContributionRegistry.INSTANCE);631632function registerCommand<T extends Command>(command: T): T {633command.register();634return command;635}636637export const UndoCommand = registerCommand(new MultiCommand({638id: 'undo',639precondition: undefined,640kbOpts: {641weight: KeybindingWeight.EditorCore,642primary: KeyMod.CtrlCmd | KeyCode.KeyZ643},644menuOpts: [{645menuId: MenuId.MenubarEditMenu,646group: '1_do',647title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"),648order: 1649}, {650menuId: MenuId.CommandPalette,651group: '',652title: nls.localize('undo', "Undo"),653order: 1654}, {655menuId: MenuId.SimpleEditorContext,656group: '1_do',657title: nls.localize('undo', "Undo"),658order: 1659}]660}));661662registerCommand(new ProxyCommand(UndoCommand, { id: 'default:undo', precondition: undefined }));663664export const RedoCommand = registerCommand(new MultiCommand({665id: 'redo',666precondition: undefined,667kbOpts: {668weight: KeybindingWeight.EditorCore,669primary: KeyMod.CtrlCmd | KeyCode.KeyY,670secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ],671mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ }672},673menuOpts: [{674menuId: MenuId.MenubarEditMenu,675group: '1_do',676title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"),677order: 2678}, {679menuId: MenuId.CommandPalette,680group: '',681title: nls.localize('redo', "Redo"),682order: 1683}, {684menuId: MenuId.SimpleEditorContext,685group: '1_do',686title: nls.localize('redo', "Redo"),687order: 2688}]689}));690691registerCommand(new ProxyCommand(RedoCommand, { id: 'default:redo', precondition: undefined }));692693export const SelectAllCommand = registerCommand(new MultiCommand({694id: 'editor.action.selectAll',695precondition: undefined,696kbOpts: {697weight: KeybindingWeight.EditorCore,698kbExpr: null,699primary: KeyMod.CtrlCmd | KeyCode.KeyA700},701menuOpts: [{702menuId: MenuId.MenubarSelectionMenu,703group: '1_basic',704title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"),705order: 1706}, {707menuId: MenuId.CommandPalette,708group: '',709title: nls.localize('selectAll', "Select All"),710order: 1711}, {712menuId: MenuId.SimpleEditorContext,713group: '9_select',714title: nls.localize('selectAll', "Select All"),715order: 1716}]717}));718719720