Path: blob/main/src/vs/editor/browser/editorExtensions.ts
5220 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';27import { TriggerInlineEditCommandsRegistry } from './triggerInlineEditCommandsRegistry.js';2829export type ServicesAccessor = InstantiationServicesAccessor;30export type EditorContributionCtor = IConstructorSignature<IEditorContribution, [ICodeEditor]>;31export type DiffEditorContributionCtor = IConstructorSignature<IDiffEditorContribution, [IDiffEditor]>;3233export const enum EditorContributionInstantiation {34/**35* The contribution is created eagerly when the {@linkcode ICodeEditor} is instantiated.36* Only Eager contributions can participate in saving or restoring of view state.37*/38Eager,3940/**41* The contribution is created at the latest 50ms after the first render after attaching a text model.42* If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner.43* If there is idle time available, it will be instantiated sooner.44*/45AfterFirstRender,4647/**48* The contribution is created before the editor emits events produced by user interaction (mouse events, keyboard events).49* If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner.50* If there is idle time available, it will be instantiated sooner.51*/52BeforeFirstInteraction,5354/**55* The contribution is created when there is idle time available, at the latest 5000ms after the editor creation.56* If the contribution is explicitly requested via `getContribution`, it will be instantiated sooner.57*/58Eventually,5960/**61* The contribution is created only when explicitly requested via `getContribution`.62*/63Lazy,64}6566export interface IEditorContributionDescription {67readonly id: string;68readonly ctor: EditorContributionCtor;69readonly instantiation: EditorContributionInstantiation;70}7172export interface IDiffEditorContributionDescription {73id: string;74ctor: DiffEditorContributionCtor;75}7677//#region Command7879export interface ICommandKeybindingsOptions extends IKeybindings {80kbExpr?: ContextKeyExpression | null;81weight: number;82/**83* the default keybinding arguments84*/85args?: unknown;86}87export interface ICommandMenuOptions {88menuId: MenuId;89group: string;90order: number;91when?: ContextKeyExpression;92title: string;93icon?: ThemeIcon;94}95export interface ICommandOptions {96id: string;97precondition: ContextKeyExpression | undefined;98kbOpts?: ICommandKeybindingsOptions | ICommandKeybindingsOptions[];99metadata?: ICommandMetadata;100menuOpts?: ICommandMenuOptions | ICommandMenuOptions[];101canTriggerInlineEdits?: boolean;102}103export abstract class Command {104public readonly id: string;105public readonly precondition: ContextKeyExpression | undefined;106private readonly _kbOpts: ICommandKeybindingsOptions | ICommandKeybindingsOptions[] | undefined;107private readonly _menuOpts: ICommandMenuOptions | ICommandMenuOptions[] | undefined;108public readonly metadata: ICommandMetadata | undefined;109public readonly canTriggerInlineEdits: boolean | undefined;110111constructor(opts: ICommandOptions) {112this.id = opts.id;113this.precondition = opts.precondition;114this._kbOpts = opts.kbOpts;115this._menuOpts = opts.menuOpts;116this.metadata = opts.metadata;117this.canTriggerInlineEdits = opts.canTriggerInlineEdits;118}119120public register(): void {121122if (Array.isArray(this._menuOpts)) {123this._menuOpts.forEach(this._registerMenuItem, this);124} else if (this._menuOpts) {125this._registerMenuItem(this._menuOpts);126}127128if (this._kbOpts) {129const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts];130for (const kbOpts of kbOptsArr) {131let kbWhen = kbOpts.kbExpr;132if (this.precondition) {133if (kbWhen) {134kbWhen = ContextKeyExpr.and(kbWhen, this.precondition);135} else {136kbWhen = this.precondition;137}138}139140const desc = {141id: this.id,142weight: kbOpts.weight,143args: kbOpts.args,144when: kbWhen,145primary: kbOpts.primary,146secondary: kbOpts.secondary,147win: kbOpts.win,148linux: kbOpts.linux,149mac: kbOpts.mac,150};151152KeybindingsRegistry.registerKeybindingRule(desc);153}154}155156CommandsRegistry.registerCommand({157id: this.id,158handler: (accessor, args) => this.runCommand(accessor, args),159metadata: this.metadata160});161162if (this.canTriggerInlineEdits) {163TriggerInlineEditCommandsRegistry.registerCommand(this.id);164}165}166167private _registerMenuItem(item: ICommandMenuOptions): void {168MenuRegistry.appendMenuItem(item.menuId, {169group: item.group,170command: {171id: this.id,172title: item.title,173icon: item.icon,174precondition: this.precondition175},176when: item.when,177order: item.order178});179}180181public abstract runCommand(accessor: ServicesAccessor, args: unknown): void | Promise<void>;182}183184//#endregion Command185186//#region MultiplexingCommand187188/**189* Potential override for a command.190*191* @return `true` or a Promise if the command was successfully run. This stops other overrides from being executed.192*/193export type CommandImplementation = (accessor: ServicesAccessor, args: unknown) => boolean | Promise<void>;194195interface ICommandImplementationRegistration {196priority: number;197name: string;198implementation: CommandImplementation;199when?: ContextKeyExpression;200}201202export class MultiCommand extends Command {203204private readonly _implementations: ICommandImplementationRegistration[] = [];205206/**207* A higher priority gets to be looked at first208*/209public addImplementation(priority: number, name: string, implementation: CommandImplementation, when?: ContextKeyExpression): IDisposable {210this._implementations.push({ priority, name, implementation, when });211this._implementations.sort((a, b) => b.priority - a.priority);212return {213dispose: () => {214for (let i = 0; i < this._implementations.length; i++) {215if (this._implementations[i].implementation === implementation) {216this._implementations.splice(i, 1);217return;218}219}220}221};222}223224public runCommand(accessor: ServicesAccessor, args: unknown): void | Promise<void> {225const logService = accessor.get(ILogService);226const contextKeyService = accessor.get(IContextKeyService);227logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`);228for (const impl of this._implementations) {229if (impl.when) {230const context = contextKeyService.getContext(getActiveElement());231const value = impl.when.evaluate(context);232if (!value) {233continue;234}235}236const result = impl.implementation(accessor, args);237if (result) {238logService.trace(`Command '${this.id}' was handled by '${impl.name}'.`);239if (typeof result === 'boolean') {240return;241}242return result;243}244}245logService.trace(`The Command '${this.id}' was not handled by any implementation.`);246}247}248249//#endregion250251/**252* A command that delegates to another command's implementation.253*254* This lets different commands be registered but share the same implementation255*/256export class ProxyCommand extends Command {257constructor(258private readonly command: Command,259opts: ICommandOptions260) {261super(opts);262}263264public runCommand(accessor: ServicesAccessor, args: unknown): void | Promise<void> {265return this.command.runCommand(accessor, args);266}267}268269//#region EditorCommand270271export interface IContributionCommandOptions<T> extends ICommandOptions {272handler: (controller: T, args: unknown) => void;273}274export interface EditorControllerCommand<T extends IEditorContribution> {275new(opts: IContributionCommandOptions<T>): EditorCommand;276}277export abstract class EditorCommand extends Command {278279/**280* Create a command class that is bound to a certain editor contribution.281*/282public static bindToContribution<T extends IEditorContribution>(controllerGetter: (editor: ICodeEditor) => T | null): EditorControllerCommand<T> {283return class EditorControllerCommandImpl extends EditorCommand {284private readonly _callback: (controller: T, args: unknown) => void;285286constructor(opts: IContributionCommandOptions<T>) {287super(opts);288289this._callback = opts.handler;290}291292public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void {293const controller = controllerGetter(editor);294if (controller) {295this._callback(controller, args);296}297}298};299}300301public static runEditorCommand<T = unknown>(302accessor: ServicesAccessor,303args: T,304precondition: ContextKeyExpression | undefined,305runner: (accessor: ServicesAccessor, editor: ICodeEditor, args: T) => void | Promise<void>306): void | Promise<void> {307const codeEditorService = accessor.get(ICodeEditorService);308309// Find the editor with text focus or active310const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();311if (!editor) {312// well, at least we tried...313return;314}315316return editor.invokeWithinContext((editorAccessor) => {317const kbService = editorAccessor.get(IContextKeyService);318if (!kbService.contextMatchesRules(precondition ?? undefined)) {319// precondition does not hold320return;321}322323return runner(editorAccessor, editor, args);324});325}326327public runCommand(accessor: ServicesAccessor, args: unknown): void | Promise<void> {328return EditorCommand.runEditorCommand(accessor, args, this.precondition, (accessor, editor, args) => this.runEditorCommand(accessor, editor, args));329}330331public abstract runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void | Promise<void>;332}333334//#endregion EditorCommand335336//#region EditorAction337338export interface IEditorActionContextMenuOptions {339group: string;340order: number;341when?: ContextKeyExpression;342menuId?: MenuId;343}344export type IActionOptions = ICommandOptions & {345contextMenuOpts?: IEditorActionContextMenuOptions | IEditorActionContextMenuOptions[];346} & ({347label: nls.ILocalizedString;348alias?: string;349} | {350label: string;351alias: string;352});353354export abstract class EditorAction extends EditorCommand {355356private static convertOptions(opts: IActionOptions): ICommandOptions {357358let menuOpts: ICommandMenuOptions[];359if (Array.isArray(opts.menuOpts)) {360menuOpts = opts.menuOpts;361} else if (opts.menuOpts) {362menuOpts = [opts.menuOpts];363} else {364menuOpts = [];365}366367function withDefaults(item: Partial<ICommandMenuOptions>): ICommandMenuOptions {368if (!item.menuId) {369item.menuId = MenuId.EditorContext;370}371if (!item.title) {372item.title = typeof opts.label === 'string' ? opts.label : opts.label.value;373}374item.when = ContextKeyExpr.and(opts.precondition, item.when);375return <ICommandMenuOptions>item;376}377378if (Array.isArray(opts.contextMenuOpts)) {379menuOpts.push(...opts.contextMenuOpts.map(withDefaults));380} else if (opts.contextMenuOpts) {381menuOpts.push(withDefaults(opts.contextMenuOpts));382}383384opts.menuOpts = menuOpts;385return <ICommandOptions>opts;386}387388public readonly label: string;389public readonly alias: string;390391constructor(opts: IActionOptions) {392super(EditorAction.convertOptions(opts));393if (typeof opts.label === 'string') {394this.label = opts.label;395this.alias = opts.alias ?? opts.label;396} else {397this.label = opts.label.value;398this.alias = opts.alias ?? opts.label.original;399}400}401402public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void | Promise<void> {403this.reportTelemetry(accessor, editor);404return this.run(accessor, editor, args || {});405}406407protected reportTelemetry(accessor: ServicesAccessor, editor: ICodeEditor) {408type EditorActionInvokedClassification = {409owner: 'alexdima';410comment: 'An editor action has been invoked.';411name: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was invoked.' };412id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was invoked.' };413};414type EditorActionInvokedEvent = {415name: string;416id: string;417};418accessor.get(ITelemetryService).publicLog2<EditorActionInvokedEvent, EditorActionInvokedClassification>('editorActionInvoked', { name: this.label, id: this.id });419}420421public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void | Promise<void>;422}423424export type EditorActionImplementation = (accessor: ServicesAccessor, editor: ICodeEditor, args: unknown) => boolean | Promise<void>;425426export class MultiEditorAction extends EditorAction {427428private readonly _implementations: [number, EditorActionImplementation][] = [];429430/**431* A higher priority gets to be looked at first432*/433public addImplementation(priority: number, implementation: EditorActionImplementation): IDisposable {434this._implementations.push([priority, implementation]);435this._implementations.sort((a, b) => b[0] - a[0]);436return {437dispose: () => {438for (let i = 0; i < this._implementations.length; i++) {439if (this._implementations[i][1] === implementation) {440this._implementations.splice(i, 1);441return;442}443}444}445};446}447448public run(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void | Promise<void> {449for (const impl of this._implementations) {450const result = impl[1](accessor, editor, args);451if (result) {452if (typeof result === 'boolean') {453return;454}455return result;456}457}458}459460}461462//#endregion EditorAction463464//#region EditorAction2465466export abstract class EditorAction2 extends Action2 {467468run(accessor: ServicesAccessor, ...args: unknown[]) {469// Find the editor with text focus or active470const codeEditorService = accessor.get(ICodeEditorService);471const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();472if (!editor) {473// well, at least we tried...474return;475}476// precondition does hold477return editor.invokeWithinContext((editorAccessor) => {478const kbService = editorAccessor.get(IContextKeyService);479const logService = editorAccessor.get(ILogService);480const enabled = kbService.contextMatchesRules(this.desc.precondition ?? undefined);481if (!enabled) {482logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize());483return;484}485return this.runEditorCommand(editorAccessor, editor, ...args);486});487}488489abstract runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]): unknown;490}491492//#endregion493494// --- Registration of commands and actions495496497export function registerModelAndPositionCommand(id: string, handler: (accessor: ServicesAccessor, model: ITextModel, position: Position, ...args: unknown[]) => unknown) {498CommandsRegistry.registerCommand(id, function (accessor, ...args) {499500const instaService = accessor.get(IInstantiationService);501502const [resource, position] = args;503assertType(URI.isUri(resource));504assertType(Position.isIPosition(position));505506const model = accessor.get(IModelService).getModel(resource);507if (model) {508const editorPosition = Position.lift(position);509return instaService.invokeFunction(handler, model, editorPosition, ...args.slice(2));510}511512return accessor.get(ITextModelService).createModelReference(resource).then(reference => {513return new Promise((resolve, reject) => {514try {515const result = instaService.invokeFunction(handler, reference.object.textEditorModel, Position.lift(position), args.slice(2));516resolve(result);517} catch (err) {518reject(err);519}520}).finally(() => {521reference.dispose();522});523});524});525}526527export function registerEditorCommand<T extends EditorCommand>(editorCommand: T): T {528EditorContributionRegistry.INSTANCE.registerEditorCommand(editorCommand);529return editorCommand;530}531532export function registerEditorAction<T extends EditorAction>(ctor: { new(): T }): T {533const action = new ctor();534EditorContributionRegistry.INSTANCE.registerEditorAction(action);535return action;536}537538export function registerMultiEditorAction<T extends MultiEditorAction>(action: T): T {539EditorContributionRegistry.INSTANCE.registerEditorAction(action);540return action;541}542543export function registerInstantiatedEditorAction(editorAction: EditorAction): void {544EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction);545}546547/**548* Registers an editor contribution. Editor contributions have a lifecycle which is bound549* to a specific code editor instance.550*/551export function registerEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void {552EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor, instantiation);553}554555/**556* Registers a diff editor contribution. Diff editor contributions have a lifecycle which557* is bound to a specific diff editor instance.558*/559export function registerDiffEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void {560EditorContributionRegistry.INSTANCE.registerDiffEditorContribution(id, ctor);561}562563export namespace EditorExtensionsRegistry {564565export function getEditorCommand(commandId: string): EditorCommand {566return EditorContributionRegistry.INSTANCE.getEditorCommand(commandId);567}568569export function getEditorActions(): Iterable<EditorAction> {570return EditorContributionRegistry.INSTANCE.getEditorActions();571}572573export function getEditorContributions(): IEditorContributionDescription[] {574return EditorContributionRegistry.INSTANCE.getEditorContributions();575}576577export function getSomeEditorContributions(ids: string[]): IEditorContributionDescription[] {578return EditorContributionRegistry.INSTANCE.getEditorContributions().filter(c => ids.indexOf(c.id) >= 0);579}580581export function getDiffEditorContributions(): IDiffEditorContributionDescription[] {582return EditorContributionRegistry.INSTANCE.getDiffEditorContributions();583}584}585586// Editor extension points587const Extensions = {588EditorCommonContributions: 'editor.contributions'589};590591class EditorContributionRegistry {592593public static readonly INSTANCE = new EditorContributionRegistry();594595private readonly editorContributions: IEditorContributionDescription[] = [];596private readonly diffEditorContributions: IDiffEditorContributionDescription[] = [];597private readonly editorActions: EditorAction[] = [];598private readonly editorCommands: { [commandId: string]: EditorCommand } = Object.create(null);599600constructor() {601}602603public registerEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void {604this.editorContributions.push({ id, ctor: ctor as EditorContributionCtor, instantiation });605}606607public getEditorContributions(): IEditorContributionDescription[] {608return this.editorContributions.slice(0);609}610611public registerDiffEditorContribution<Services extends BrandedService[]>(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void {612this.diffEditorContributions.push({ id, ctor: ctor as DiffEditorContributionCtor });613}614615public getDiffEditorContributions(): IDiffEditorContributionDescription[] {616return this.diffEditorContributions.slice(0);617}618619public registerEditorAction(action: EditorAction) {620action.register();621this.editorActions.push(action);622}623624public getEditorActions(): Iterable<EditorAction> {625return this.editorActions;626}627628public registerEditorCommand(editorCommand: EditorCommand) {629editorCommand.register();630this.editorCommands[editorCommand.id] = editorCommand;631}632633public getEditorCommand(commandId: string): EditorCommand {634return (this.editorCommands[commandId] || null);635}636637}638Registry.add(Extensions.EditorCommonContributions, EditorContributionRegistry.INSTANCE);639640function registerCommand<T extends Command>(command: T): T {641command.register();642return command;643}644645export const UndoCommand = registerCommand(new MultiCommand({646id: 'undo',647precondition: undefined,648kbOpts: {649weight: KeybindingWeight.EditorCore,650primary: KeyMod.CtrlCmd | KeyCode.KeyZ651},652menuOpts: [{653menuId: MenuId.MenubarEditMenu,654group: '1_do',655title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"),656order: 1657}, {658menuId: MenuId.CommandPalette,659group: '',660title: nls.localize('undo', "Undo"),661order: 1662}, {663menuId: MenuId.SimpleEditorContext,664group: '1_do',665title: nls.localize('undo', "Undo"),666order: 1667}]668}));669670registerCommand(new ProxyCommand(UndoCommand, { id: 'default:undo', precondition: undefined }));671672export const RedoCommand = registerCommand(new MultiCommand({673id: 'redo',674precondition: undefined,675kbOpts: {676weight: KeybindingWeight.EditorCore,677primary: KeyMod.CtrlCmd | KeyCode.KeyY,678secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ],679mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ }680},681menuOpts: [{682menuId: MenuId.MenubarEditMenu,683group: '1_do',684title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"),685order: 2686}, {687menuId: MenuId.CommandPalette,688group: '',689title: nls.localize('redo', "Redo"),690order: 1691}, {692menuId: MenuId.SimpleEditorContext,693group: '1_do',694title: nls.localize('redo', "Redo"),695order: 2696}]697}));698699registerCommand(new ProxyCommand(RedoCommand, { id: 'default:redo', precondition: undefined }));700701export const SelectAllCommand = registerCommand(new MultiCommand({702id: 'editor.action.selectAll',703precondition: undefined,704kbOpts: {705weight: KeybindingWeight.EditorCore,706kbExpr: null,707primary: KeyMod.CtrlCmd | KeyCode.KeyA708},709menuOpts: [{710menuId: MenuId.MenubarSelectionMenu,711group: '1_basic',712title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"),713order: 1714}, {715menuId: MenuId.CommandPalette,716group: '',717title: nls.localize('selectAll', "Select All"),718order: 1719}, {720menuId: MenuId.SimpleEditorContext,721group: '9_select',722title: nls.localize('selectAll', "Select All"),723order: 1724}]725}));726727728