VS Code 依赖注入原理:https://zhaomenghuan.js.org/blog/vscode-workbench-source-code-interpretation.html#vs-code-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E5%8E%9F%E7%90%86
Language Server Extension:你可以实现自动补全 autocomplete, 错误检查 error-checking (diagnostics), 跳转到定义 jump-to-definition:https://code.visualstudio.com/api/language-extensions/language-server-extension-guide
registerSingleton(
ISymbolNavigationService, // identifier
SymbolNavigationService, // ctor of an implementation
InstantiationType.Delayed // delay instantiation of this service until is actually needed
);
const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
export const enum InstantiationType {
/**
* Instantiate this service as soon as a consumer depdends on it. _Note_ that this
* is more costly as some upfront work is done that is likely not needed
*/
Eager = 0,
/**
* Instantiate this service as soon as a consumer uses it. This is the _better_
* way of registering a service.
*/
Delayed = 1
}
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean | InstantiationType): void {
if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: any[]) => T, [], Boolean(supportsDelayedInstantiation));
}
_registry.push([id, ctorOrDescriptor]);
}
// src/vs/platform/instantiation/common/descriptors.ts
export class SyncDescriptor<T> {
readonly ctor: any;
readonly staticArguments: any[];
readonly supportsDelayedInstantiation: boolean;
constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
this.ctor = ctor;
this.staticArguments = staticArguments;
this.supportsDelayedInstantiation = supportsDelayedInstantiation;
}
}
const services = new ServiceCollection();
...
services.set(ILogService, logService);
services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource));
services.set(ILifecycleService, new SyncDescriptor(LifecycleService));
...
new InstantiationService(services, true);
/**
* The *only* valid way to create a {{ServiceIdentifier}}.
*/
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId)!;
}
const id = <any>function (target: Function, key: string, index: number): any {
if (arguments.length !== 3) {
throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
}
storeServiceDependency(id, target, index);
};
id.toString = () => serviceId;
_util.serviceIds.set(serviceId, id);
return id;
}
class XXX {
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
}
}
export abstract class Disposable implements IDisposable {
/**
* A disposable that does nothing when it is disposed of.
*
* TODO: This should not be a static property.
*/
static readonly None = Object.freeze<IDisposable>({ dispose() { } });
protected readonly _store = new DisposableStore();
constructor() {
trackDisposable(this);
setParentOfDisposable(this._store, this);
}
public dispose(): void {
markAsDisposed(this);
this._store.dispose();
}
/**
* Adds `o` to the collection of disposables managed by this object.
*/
protected _register<T extends IDisposable>(o: T): T {
if ((o as unknown as Disposable) === this) {
throw new Error('Cannot register a disposable on itself!');
}
return this._store.add(o);
}
}
export class DisposableStore implements IDisposable {
static DISABLE_DISPOSED_WARNING = false;
private readonly _toDispose = new Set<IDisposable>();
private _isDisposed = false;
constructor() {
trackDisposable(this);
}
/**
* Dispose of all registered disposables and mark this object as disposed.
*
* Any future disposables added to this object will be disposed of on `add`.
*/
public dispose(): void {
if (this._isDisposed) {
return;
}
markAsDisposed(this);
this._isDisposed = true;
this.clear();
}
/**
* @return `true` if this object has been disposed of.
*/
public get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Dispose of all registered disposables but do not mark this object as disposed.
*/
public clear(): void {
if (this._toDispose.size === 0) {
return;
}
try {
dispose(this._toDispose);
} finally {
this._toDispose.clear();
}
}
/**
* Add a new {@link IDisposable disposable} to the collection.
*/
public add<T extends IDisposable>(o: T): T {
if (!o) {
return o;
}
if ((o as unknown as DisposableStore) === this) {
throw new Error('Cannot register a disposable on itself!');
}
setParentOfDisposable(o, this);
if (this._isDisposed) {
if (!DisposableStore.DISABLE_DISPOSED_WARNING) {
console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack);
}
} else {
this._toDispose.add(o);
}
return o;
}
}
export function dispose<T extends IDisposable>(arg: T | Iterable<T> | undefined): any {
if (Iterable.is(arg)) {
const errors: any[] = [];
for (const d of arg) {
if (d) {
try {
d.dispose();
} catch (e) {
errors.push(e);
}
}
}
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new AggregateError(errors, 'Encountered errors while disposing of store');
}
return Array.isArray(arg) ? [] : arg;
} else if (arg) {
arg.dispose();
return arg;
}
}
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
class DefaultFoldingRangeProvider extends Disposable implements IWorkbenchContribution {
static readonly configName = 'editor.defaultFoldingRangeProvider';
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();
this._store.add(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this));
this._store.add(FoldingController.setFoldingRangeProviderSelector(this._selectFoldingRangeProvider.bind(this)));
this._updateConfigValues();
}
// 具体的业务逻辑先不看了
}
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
...editorConfigurationBaseNode,
properties: {
[DefaultFoldingRangeProvider.configName]: {
description: nls.localize('formatter.default', "Defines a default folding range provider that takes precedence over all other folding range providers. Must be the identifier of an extension contributing a folding range provider."),
type: ['string', 'null'],
default: null,
enum: DefaultFoldingRangeProvider.extensionIds,
enumItemLabels: DefaultFoldingRangeProvider.extensionItemLabels,
markdownEnumDescriptions: DefaultFoldingRangeProvider.extensionDescriptions
}
}
});
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(
DefaultFoldingRangeProvider,
LifecyclePhase.Restored
);
export interface IRegistry {
/**
* Adds the extension functions and properties defined by data to the
* platform. The provided id must be unique.
* @param id a unique identifier
* @param data a contribution
*/
add(id: string, data: any): void;
/**
* Returns true iff there is an extension with the provided id.
* @param id an extension identifier
*/
knows(id: string): boolean;
/**
* Returns the extension functions and properties defined by the specified key or null.
* @param id an extension identifier
*/
as<T>(id: string): T;
}
class RegistryImpl implements IRegistry {
private readonly data = new Map<string, any>();
public add(id: string, data: any): void {
Assert.ok(Types.isString(id));
Assert.ok(Types.isObject(data));
Assert.ok(!this.data.has(id), 'There is already an extension with this id');
this.data.set(id, data);
}
public knows(id: string): boolean {
return this.data.has(id);
}
public as(id: string): any {
return this.data.get(id) || null;
}
}
export const Registry: IRegistry = new RegistryImpl();
export interface IConfigurationRegistry {
/**
* Register a configuration to the registry.
*/
registerConfiguration(configuration: IConfigurationNode): void;
// ...
}
class ConfigurationRegistry implements IConfigurationRegistry {
public registerConfiguration(configuration: IConfigurationNode, validate: boolean = true): void {
this.registerConfigurations([configuration], validate);
}
// ....
}
export const Extensions = {
Configuration: 'base.contributions.configuration'
};
const configurationRegistry = new ConfigurationRegistry();
Registry.add(Extensions.Configuration, configurationRegistry);
abstract class FoldingAction<T> extends EditorAction {
abstract invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: T, languageConfigurationService: ILanguageConfigurationService): void;
public override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: T): void | Promise<void> {
const languageConfigurationService = accessor.get(ILanguageConfigurationService);
const foldingController = FoldingController.get(editor);
if (!foldingController) {
return;
}
const foldingModelPromise = foldingController.getFoldingModel();
if (foldingModelPromise) {
this.reportTelemetry(accessor, editor);
return foldingModelPromise.then(foldingModel => {
if (foldingModel) {
this.invoke(foldingController, foldingModel, editor, args, languageConfigurationService);
const selection = editor.getSelection();
if (selection) {
foldingController.reveal(selection.getStartPosition());
}
}
});
}
}
protected getSelectedLines(editor: ICodeEditor) {
const selections = editor.getSelections();
return selections ? selections.map(s => s.startLineNumber) : [];
}
protected getLineNumbers(args: FoldingArguments, editor: ICodeEditor) {
if (args && args.selectionLines) {
return args.selectionLines.map(l => l + 1); // to 0-bases line numbers
}
return this.getSelectedLines(editor);
}
public run(_accessor: ServicesAccessor, _editor: ICodeEditor): void {
}
}
class FoldAction extends FoldingAction<FoldingArguments> {
constructor() {
super({
id: 'editor.fold',
label: nls.localize('foldAction.label', "Fold"),
alias: 'Fold',
precondition: CONTEXT_FOLDING_ENABLED,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketLeft,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketLeft
},
weight: KeybindingWeight.EditorContrib
},
description: {
description: 'Fold the content in the editor',
args: [
{
name: 'Fold editor argument',
description: `Property-value pairs that can be passed through this argument:
* 'levels': Number of levels to fold.
* 'direction': If 'up', folds given number of levels up otherwise folds down.
* 'selectionLines': Array of the start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used.
If no levels or direction is set, folds the region at the locations or if already collapsed, the first uncollapsed parent instead.
`,
constraint: foldingArgumentsConstraint,
schema: {
'type': 'object',
'properties': {
'levels': {
'type': 'number',
},
'direction': {
'type': 'string',
'enum': ['up', 'down'],
},
'selectionLines': {
'type': 'array',
'items': {
'type': 'number'
}
}
}
}
}
]
}
});
}
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: FoldingArguments): void {
const lineNumbers = this.getLineNumbers(args, editor);
const levels = args && args.levels;
const direction = args && args.direction;
if (typeof levels !== 'number' && typeof direction !== 'string') {
// fold the region at the location or if already collapsed, the first uncollapsed parent instead.
setCollapseStateUp(foldingModel, true, lineNumbers);
} else {
if (direction === 'up') {
setCollapseStateLevelsUp(foldingModel, true, levels || 1, lineNumbers);
} else {
setCollapseStateLevelsDown(foldingModel, true, levels || 1, lineNumbers);
}
}
}
}
registerEditorContribution(FoldingController.ID, FoldingController, EditorContributionInstantiation.Eager); // eager because it uses `saveViewState`/`restoreViewState`
registerEditorAction(UnfoldAction);
registerEditorAction(UnFoldRecursivelyAction);
registerEditorAction(FoldAction);
registerEditorAction(FoldRecursivelyAction);
registerEditorAction(FoldAllAction);
registerEditorAction(UnfoldAllAction);
registerEditorAction(FoldAllBlockCommentsAction);
registerEditorAction(FoldAllRegionsAction);
registerEditorAction(UnfoldAllRegionsAction);
registerEditorAction(FoldAllRegionsExceptAction);
registerEditorAction(UnfoldAllRegionsExceptAction);
registerEditorAction(ToggleFoldAction);
registerEditorAction(GotoParentFoldAction);
registerEditorAction(GotoPreviousFoldAction);
registerEditorAction(GotoNextFoldAction);
registerEditorAction(FoldRangeFromSelectionAction);
registerEditorAction(RemoveFoldRangeFromSelectionAction);
for (let i = 1; i <= 7; i++) {
registerInstantiatedEditorAction(
new FoldLevelAction({
id: FoldLevelAction.ID(i),
label: nls.localize('foldLevelAction.label', "Fold Level {0}", i),
alias: `Fold Level ${i}`,
precondition: CONTEXT_FOLDING_ENABLED,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | (KeyCode.Digit0 + i)),
weight: KeybindingWeight.EditorContrib
}
})
);
}
CommandsRegistry.registerCommand('_executeFoldingRangeProvider', async function (accessor, ...args) {
// ...
});
/*---------------------------------------------------------------------------------------------
* 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<IEditorContribution, [ICodeEditor]>;
export type DiffEditorContributionCtor = IConstructorSignature<IDiffEditorContribution, [IDiffEditor]>;
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<void>;
}
//#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<void>;
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<void> {
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<void> {
return this.command.runCommand(accessor, args);
}
}
//#region EditorCommand
export interface IContributionCommandOptions<T> extends ICommandOptions {
handler: (controller: T, args: any) => void;
}
export interface EditorControllerCommand<T extends IEditorContribution> {
new(opts: IContributionCommandOptions<T>): EditorCommand;
}
export abstract class EditorCommand extends Command {
/**
* Create a command class that is bound to a certain editor contribution.
*/
public static bindToContribution<T extends IEditorContribution>(controllerGetter: (editor: ICodeEditor) => T | null): EditorControllerCommand<T> {
return class EditorControllerCommandImpl extends EditorCommand {
private readonly _callback: (controller: T, args: any) => void;
constructor(opts: IContributionCommandOptions<T>) {
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>
): void | Promise<void> {
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<void> {
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<void>;
}
//#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>): ICommandMenuOptions {
if (!item.menuId) {
item.menuId = MenuId.EditorContext;
}
if (!item.title) {
item.title = opts.label;
}
item.when = ContextKeyExpr.and(opts.precondition, item.when);
return <ICommandMenuOptions>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 <ICommandOptions>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<void> {
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<EditorActionInvokedEvent, EditorActionInvokedClassification>('editorActionInvoked', { name: this.label, id: this.id });
}
public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void>;
}
export type EditorActionImplementation = (accessor: ServicesAccessor, editor: ICodeEditor, args: any) => boolean | Promise<void>;
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<void> {
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);
if (kbService.contextMatchesRules(withNullAsUndefined(this.desc.precondition))) {
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<T extends EditorCommand>(editorCommand: T): T {
EditorContributionRegistry.INSTANCE.registerEditorCommand(editorCommand);
return editorCommand;
}
export function registerEditorAction<T extends EditorAction>(ctor: { new(): T }): T {
const action = new ctor();
EditorContributionRegistry.INSTANCE.registerEditorAction(action);
return action;
}
export function registerMultiEditorAction<T extends MultiEditorAction>(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<Services extends BrandedService[]>(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<Services extends BrandedService[]>(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<EditorAction> {
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<Services extends BrandedService[]>(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<Services extends BrandedService[]>(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<EditorAction> {
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<T extends Command>(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
}]
}));
/**
* A helper to delay (debounce) execution of a task that is being requested often.
*
* Following the throttler, now imagine the mail man wants to optimize the number of
* trips proactively. The trip itself can be long, so he decides not to make the trip
* as soon as a letter is submitted. Instead he waits a while, in case more
* letters are submitted. After said waiting period, if no letters were submitted, he
* decides to make the trip. Imagine that N more letters were submitted after the first
* one, all within a short period of time between each other. Even though N+1
* submissions occurred, only 1 delivery was made.
*
* The delayer offers this behavior via the trigger() method, into which both the task
* to be executed and the waiting period (delay) must be passed in as arguments. Following
* the example:
*
* const delayer = new Delayer(WAITING_PERIOD);
* const letters = [];
*
* function letterReceived(l) {
* letters.push(l);
* delayer.trigger(() => { return makeTheTrip(); });
* }
*/
export class Delayer<T> implements IDisposable {
private deferred: IScheduledLater | null;
private completionPromise: Promise<any> | null;
private doResolve: ((value?: any | Promise<any>) => void) | null;
private doReject: ((err: any) => void) | null;
private task: ITask<T | Promise<T>> | null;
constructor(public defaultDelay: number | typeof MicrotaskDelay) {
this.deferred = null;
this.completionPromise = null;
this.doResolve = null;
this.doReject = null;
this.task = null;
}
trigger(task: ITask<T | Promise<T>>, delay = this.defaultDelay): Promise<T> {
this.task = task;
this.cancelTimeout();
if (!this.completionPromise) {
this.completionPromise = new Promise((resolve, reject) => {
this.doResolve = resolve;
this.doReject = reject;
}).then(() => {
this.completionPromise = null;
this.doResolve = null;
if (this.task) {
const task = this.task;
this.task = null;
return task();
}
return undefined;
});
}
const fn = () => {
this.deferred = null;
this.doResolve?.(null);
};
this.deferred = delay === MicrotaskDelay ? microtaskDeferred(fn) : timeoutDeferred(delay, fn);
return this.completionPromise;
}
isTriggered(): boolean {
return !!this.deferred?.isTriggered();
}
cancel(): void {
this.cancelTimeout();
if (this.completionPromise) {
this.doReject?.(new CancellationError());
this.completionPromise = null;
}
}
private cancelTimeout(): void {
this.deferred?.dispose();
this.deferred = null;
}
dispose(): void {
this.cancel();
}
}
/**
* Schedule a callback to be run at the next animation frame.
* This allows multiple parties to register callbacks that should run at the next animation frame.
* If currently in an animation frame, `runner` will be executed immediately.
* @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately).
*/
export let runAtThisOrScheduleAtNextAnimationFrame: (runner: () => void, priority?: number) => IDisposable;
/**
* Schedule a callback to be run at the next animation frame.
* This allows multiple parties to register callbacks that should run at the next animation frame.
* If currently in an animation frame, `runner` will be executed at the next animation frame.
* @return token that can be used to cancel the scheduled runner.
*/
export let scheduleAtNextAnimationFrame: (runner: () => void, priority?: number) => IDisposable;
class AnimationFrameQueueItem implements IDisposable {
private _runner: () => void;
public priority: number;
private _canceled: boolean;
constructor(runner: () => void, priority: number = 0) {
this._runner = runner;
this.priority = priority;
this._canceled = false;
}
public dispose(): void {
this._canceled = true;
}
public execute(): void {
if (this._canceled) {
return;
}
try {
this._runner();
} catch (e) {
onUnexpectedError(e);
}
}
// Sort by priority (largest to lowest)
public static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
return b.priority - a.priority;
}
}
(function () {
/**
* The runners scheduled at the next animation frame
*/
let NEXT_QUEUE: AnimationFrameQueueItem[] = [];
/**
* The runners scheduled at the current animation frame
*/
let CURRENT_QUEUE: AnimationFrameQueueItem[] | null = null;
/**
* A flag to keep track if the native requestAnimationFrame was already called
*/
let animFrameRequested = false;
/**
* A flag to indicate if currently handling a native requestAnimationFrame callback
*/
let inAnimationFrameRunner = false;
const animationFrameRunner = () => {
animFrameRequested = false;
CURRENT_QUEUE = NEXT_QUEUE;
NEXT_QUEUE = [];
inAnimationFrameRunner = true;
while (CURRENT_QUEUE.length > 0) {
CURRENT_QUEUE.sort(AnimationFrameQueueItem.sort);
const top = CURRENT_QUEUE.shift()!;
top.execute();
}
inAnimationFrameRunner = false;
};
scheduleAtNextAnimationFrame = (runner: () => void, priority: number = 0) => {
const item = new AnimationFrameQueueItem(runner, priority);
NEXT_QUEUE.push(item);
if (!animFrameRequested) {
animFrameRequested = true;
requestAnimationFrame(animationFrameRunner);
}
return item;
};
runAtThisOrScheduleAtNextAnimationFrame = (runner: () => void, priority?: number) => {
if (inAnimationFrameRunner) {
const item = new AnimationFrameQueueItem(runner, priority);
CURRENT_QUEUE!.push(item);
return item;
} else {
return scheduleAtNextAnimationFrame(runner, priority);
}
};
})();
export function measure(callback: () => void): IDisposable {
return scheduleAtNextAnimationFrame(callback, 10000 /* must be early */);
}
export function modify(callback: () => void): IDisposable {
return scheduleAtNextAnimationFrame(callback, -10000 /* must be late */);
}