/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceEditorInput, IResourceEditorInput, EditorActivation, EditorOpenContext, ITextEditorSelection, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl, IComposite } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; import { isEqual, dirname } from 'vs/base/common/resources'; import { IRange } from 'vs/editor/common/core/range'; import { createMemoizer } from 'vs/base/common/decorators'; import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false); export const ActiveEditorContext = new RawContextKey('activeEditor', null); export const ActiveEditorIsReadonlyContext = new RawContextKey('activeEditorIsReadonly', false); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); export const EditorPinnedContext = new RawContextKey('editorPinned', false); export const EditorGroupActiveEditorDirtyContext = new RawContextKey('groupActiveEditorDirty', false); export const EditorGroupEditorsCountContext = new RawContextKey('groupEditorsCount', 0); export const NoEditorsVisibleContext = EditorsVisibleContext.toNegated(); export const TextCompareEditorVisibleContext = new RawContextKey('textCompareEditorVisible', false); export const TextCompareEditorActiveContext = new RawContextKey('textCompareEditorActive', false); export const ActiveEditorGroupEmptyContext = new RawContextKey('activeEditorGroupEmpty', false); export const ActiveEditorGroupIndexContext = new RawContextKey('activeEditorGroupIndex', 0); export const ActiveEditorGroupLastContext = new RawContextKey('activeEditorGroupLast', false); export const MultipleEditorGroupsContext = new RawContextKey('multipleEditorGroups', false); export const SingleEditorGroupsContext = MultipleEditorGroupsContext.toNegated(); export const InEditorZenModeContext = new RawContextKey('inZenMode', false); export const IsCenteredLayoutContext = new RawContextKey('isCenteredLayout', false); export const SplitEditorsVertically = new RawContextKey('splitEditorsVertically', false); export const EditorAreaVisibleContext = new RawContextKey('editorAreaVisible', true); /** * Text diff editor id. */ export const TEXT_DIFF_EDITOR_ID = 'workbench.editors.textDiffEditor'; /** * Binary diff editor id. */ export const BINARY_DIFF_EDITOR_ID = 'workbench.editors.binaryResourceDiffEditor'; /** * The editor pane is the container for workbench editors. */ export interface IEditorPane extends IComposite { /** * The assigned input of this editor. */ readonly input: IEditorInput | undefined; /** * The assigned group this editor is showing in. */ readonly group: IEditorGroup | undefined; /** * The minimum width of this editor. */ readonly minimumWidth: number; /** * The maximum width of this editor. */ readonly maximumWidth: number; /** * The minimum height of this editor. */ readonly minimumHeight: number; /** * The maximum height of this editor. */ readonly maximumHeight: number; /** * An event to notify whenever minimum/maximum width/height changes. */ readonly onDidSizeConstraintsChange: Event<{ width: number; height: number; } | undefined>; /** * Returns the underlying control of this editor. Callers need to cast * the control to a specific instance as needed, e.g. by using the * `isCodeEditor` helper method to access the text code editor. */ getControl(): IEditorControl | undefined; /** * Finds out if this editor is visible or not. */ isVisible(): boolean; } /** * Overrides `IEditorPane` where `input` and `group` are known to be set. */ export interface IVisibleEditorPane extends IEditorPane { readonly input: IEditorInput; readonly group: IEditorGroup; } /** * The text editor pane is the container for workbench text editors. */ export interface ITextEditorPane extends IEditorPane { /** * Returns the underlying text editor widget of this editor. */ getControl(): IEditor | undefined; /** * Returns the current view state of the text editor if any. */ getViewState(): IEditorViewState | undefined; } export function isTextEditorPane(thing: IEditorPane | undefined): thing is ITextEditorPane { const candidate = thing as ITextEditorPane | undefined; return typeof candidate?.getViewState === 'function'; } /** * The text editor pane is the container for workbench text diff editors. */ export interface ITextDiffEditorPane extends IEditorPane { /** * Returns the underlying text editor widget of this editor. */ getControl(): IDiffEditor | undefined; } /** * Marker interface for the control inside an editor pane. Callers * have to cast the control to work with it, e.g. via methods * such as `isCodeEditor(control)`. */ export interface IEditorControl extends ICompositeControl { } export interface IFileEditorInputFactory { createFileEditorInput(resource: URI, encoding: string | undefined, mode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; isFileEditorInput(obj: unknown): obj is IFileEditorInput; } interface ICustomEditorInputFactory { createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise; } export interface IEditorInputFactoryRegistry { /** * Registers the file editor input factory to use for file inputs. */ registerFileEditorInputFactory(factory: IFileEditorInputFactory): void; /** * Returns the file editor input factory to use for file inputs. */ getFileEditorInputFactory(): IFileEditorInputFactory; /** * Registers the custom editor input factory to use for custom inputs. */ registerCustomEditorInputFactory(factory: ICustomEditorInputFactory): void; /** * Returns the custom editor input factory to use for custom inputs. */ getCustomEditorInputFactory(): ICustomEditorInputFactory; /** * Registers a editor input factory for the given editor input to the registry. An editor input factory * is capable of serializing and deserializing editor inputs from string data. * * @param editorInputId the identifier of the editor input * @param factory the editor input factory for serialization/deserialization */ registerEditorInputFactory(editorInputId: string, ctor: { new(...Services: Services): IEditorInputFactory }): IDisposable; /** * Returns the editor input factory for the given editor input. * * @param editorInputId the identifier of the editor input */ getEditorInputFactory(editorInputId: string): IEditorInputFactory | undefined; /** * Starts the registry by providing the required services. */ start(accessor: ServicesAccessor): void; } export interface IEditorInputFactory { /** * Determines whether the given editor input can be serialized by the factory. */ canSerialize(editorInput: IEditorInput): boolean; /** * Returns a string representation of the provided editor input that contains enough information * to deserialize back to the original editor input from the deserialize() method. */ serialize(editorInput: EditorInput): string | undefined; /** * Returns an editor input from the provided serialized form of the editor input. This form matches * the value returned from the serialize() method. */ deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined; } export interface IUntitledTextResourceEditorInput extends IBaseResourceEditorInput { /** * Optional resource. If the resource is not provided a new untitled file is created (e.g. Untitled-1). * If the used scheme for the resource is not `untitled://`, `forceUntitled: true` must be configured to * force use the provided resource as associated path. As such, the resource will be used when saving * the untitled editor. */ readonly resource?: URI; /** * Optional language of the untitled resource. */ readonly mode?: string; /** * Optional contents of the untitled resource. */ readonly contents?: string; /** * Optional encoding of the untitled resource. */ readonly encoding?: string; } export interface IResourceDiffEditorInput extends IBaseResourceEditorInput { /** * The left hand side URI to open inside a diff editor. */ readonly leftResource: URI; /** * The right hand side URI to open inside a diff editor. */ readonly rightResource: URI; } export const enum Verbosity { SHORT, MEDIUM, LONG } export const enum SaveReason { /** * Explicit user gesture. */ EXPLICIT = 1, /** * Auto save after a timeout. */ AUTO = 2, /** * Auto save after editor focus change. */ FOCUS_CHANGE = 3, /** * Auto save after window change. */ WINDOW_CHANGE = 4 } export interface ISaveOptions { /** * An indicator how the save operation was triggered. */ reason?: SaveReason; /** * Forces to save the contents of the working copy * again even if the working copy is not dirty. */ readonly force?: boolean; /** * Instructs the save operation to skip any save participants. */ readonly skipSaveParticipants?: boolean; /** * A hint as to which file systems should be available for saving. */ readonly availableFileSystems?: string[]; } export interface IRevertOptions { /** * Forces to load the contents of the working copy * again even if the working copy is not dirty. */ readonly force?: boolean; /** * A soft revert will clear dirty state of a working copy * but will not attempt to load it from its persisted state. * * This option may be used in scenarios where an editor is * closed and where we do not require to load the contents. */ readonly soft?: boolean; } export interface IMoveResult { editor: EditorInput | IResourceEditorInputType; options?: IEditorOptions; } export interface IEditorInput extends IDisposable { /** * Triggered when this input is disposed. */ readonly onDispose: Event; /** * Triggered when this input changes its dirty state. */ readonly onDidChangeDirty: Event; /** * Triggered when this input changes its label */ readonly onDidChangeLabel: Event; /** * Returns the optional associated resource of this input. * * This resource should be unique for all editors of the same * kind and is often used to identify the editor input among * others. */ readonly resource: URI | undefined; /** * Unique type identifier for this inpput. */ getTypeId(): string; /** * Returns the display name of this input. */ getName(): string; /** * Returns the display description of this input. */ getDescription(verbosity?: Verbosity): string | undefined; /** * Returns the display title of this input. */ getTitle(verbosity?: Verbosity): string | undefined; /** * Resolves the input. */ resolve(): Promise; /** * Returns if this input is readonly or not. */ isReadonly(): boolean; /** * Returns if the input is an untitled editor or not. */ isUntitled(): boolean; /** * Returns if this input is dirty or not. */ isDirty(): boolean; /** * Returns if this input is currently being saved or soon to be * saved. Based on this assumption the editor may for example * decide to not signal the dirty state to the user assuming that * the save is scheduled to happen anyway. */ isSaving(): boolean; /** * Saves the editor. The provided groupId helps implementors * to e.g. preserve view state of the editor and re-open it * in the correct group after saving. * * @returns the resulting editor input (typically the same) of * this operation or `undefined` to indicate that the operation * failed or was canceled. */ save(group: GroupIdentifier, options?: ISaveOptions): Promise; /** * Saves the editor to a different location. The provided `group` * helps implementors to e.g. preserve view state of the editor * and re-open it in the correct group after saving. * * @returns the resulting editor input (typically a different one) * of this operation or `undefined` to indicate that the operation * failed or was canceled. */ saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise; /** * Reverts this input from the provided group. */ revert(group: GroupIdentifier, options?: IRevertOptions): Promise; /** * Called to determine how to handle a resource that is moved that matches * the editors resource (or is a child of). * * Implementors are free to not implement this method to signal no intent * to participate. If an editor is returned though, it will replace the * current one with that editor and optional options. */ move(group: GroupIdentifier, target: URI): IMoveResult | undefined; /** * Returns if the other object matches this input. */ matches(other: unknown): boolean; /** * Returns if this editor is disposed. */ isDisposed(): boolean; } /** * Editor inputs are lightweight objects that can be passed to the workbench API to open inside the editor part. * Each editor input is mapped to an editor that is capable of opening it through the Platform facade. */ export abstract class EditorInput extends Disposable implements IEditorInput { protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; protected readonly _onDidChangeLabel = this._register(new Emitter()); readonly onDidChangeLabel = this._onDidChangeLabel.event; private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; private disposed: boolean = false; abstract get resource(): URI | undefined; abstract getTypeId(): string; getName(): string { return `Editor ${this.getTypeId()}`; } getDescription(verbosity?: Verbosity): string | undefined { return undefined; } getTitle(verbosity?: Verbosity): string { return this.getName(); } /** * Returns the preferred editor for this input. A list of candidate editors is passed in that whee registered * for the input. This allows subclasses to decide late which editor to use for the input on a case by case basis. */ getPreferredEditorId(candidates: string[]): string | undefined { return firstOrDefault(candidates); } /** * Returns a descriptor suitable for telemetry events. * * Subclasses should extend if they can contribute. */ getTelemetryDescriptor(): { [key: string]: unknown } { /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { "typeId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ return { typeId: this.getTypeId() }; } /** * Returns a type of EditorModel that represents the resolved input. Subclasses should * override to provide a meaningful model. */ abstract resolve(): Promise; isReadonly(): boolean { return true; } isUntitled(): boolean { return false; } isDirty(): boolean { return false; } isSaving(): boolean { return false; } async save(group: GroupIdentifier, options?: ISaveOptions): Promise { return this; } async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { return this; } async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { } move(group: GroupIdentifier, target: URI): IMoveResult | undefined { return undefined; } /** * Subclasses can set this to false if it does not make sense to split the editor input. */ supportsSplitEditor(): boolean { return true; } matches(otherInput: unknown): boolean { return this === otherInput; } isDisposed(): boolean { return this.disposed; } dispose(): void { if (!this.disposed) { this.disposed = true; this._onDispose.fire(); } super.dispose(); } } export abstract class TextResourceEditorInput extends EditorInput { private static readonly MEMOIZER = createMemoizer(); constructor( public readonly resource: URI, @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @ITextFileService protected readonly textFileService: ITextFileService, @ILabelService protected readonly labelService: ILabelService, @IFileService protected readonly fileService: IFileService, @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService ) { super(); this.registerListeners(); } protected registerListeners(): void { // Clear label memoizer on certain events that have impact this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); } private onLabelEvent(scheme: string): void { if (scheme === this.resource.scheme) { // Clear any cached labels from before TextResourceEditorInput.MEMOIZER.clear(); // Trigger recompute of label this._onDidChangeLabel.fire(); } } getName(): string { return this.basename; } @TextResourceEditorInput.MEMOIZER private get basename(): string { return this.labelService.getUriBasenameLabel(this.resource); } getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { switch (verbosity) { case Verbosity.SHORT: return this.shortDescription; case Verbosity.LONG: return this.longDescription; case Verbosity.MEDIUM: default: return this.mediumDescription; } } @TextResourceEditorInput.MEMOIZER private get shortDescription(): string { return this.labelService.getUriBasenameLabel(dirname(this.resource)); } @TextResourceEditorInput.MEMOIZER private get mediumDescription(): string { return this.labelService.getUriLabel(dirname(this.resource), { relative: true }); } @TextResourceEditorInput.MEMOIZER private get longDescription(): string { return this.labelService.getUriLabel(dirname(this.resource)); } @TextResourceEditorInput.MEMOIZER private get shortTitle(): string { return this.getName(); } @TextResourceEditorInput.MEMOIZER private get mediumTitle(): string { return this.labelService.getUriLabel(this.resource, { relative: true }); } @TextResourceEditorInput.MEMOIZER private get longTitle(): string { return this.labelService.getUriLabel(this.resource); } getTitle(verbosity: Verbosity): string { switch (verbosity) { case Verbosity.SHORT: return this.shortTitle; case Verbosity.LONG: return this.longTitle; default: case Verbosity.MEDIUM: return this.mediumTitle; } } isUntitled(): boolean { return this.resource.scheme === Schemas.untitled; } isReadonly(): boolean { if (this.isUntitled()) { return false; // untitled is never readonly } return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } isSaving(): boolean { if (this.isUntitled()) { return false; // untitled is never saving automatically } if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { return true; // a short auto save is configured, treat this as being saved } return false; } async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { return this.doSave(group, options, false); } saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { return this.doSave(group, options, true); } private async doSave(group: GroupIdentifier, options: ISaveOptions | undefined, saveAs: boolean): Promise { // Save / Save As let target: URI | undefined; if (saveAs) { target = await this.textFileService.saveAs(this.resource, undefined, options); } else { target = await this.textFileService.save(this.resource, options); } if (!target) { return undefined; // save cancelled } if (!isEqual(target, this.resource)) { return this.editorService.createEditorInput({ resource: target }); } return this; } async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { await this.textFileService.revert(this.resource, options); } } export const enum EncodingMode { /** * Instructs the encoding support to encode the current input with the provided encoding */ Encode, /** * Instructs the encoding support to decode the current input with the provided encoding */ Decode } export interface IEncodingSupport { /** * Gets the encoding of the type if known. */ getEncoding(): string | undefined; /** * Sets the encoding for the type for saving. */ setEncoding(encoding: string, mode: EncodingMode): void; } export interface IModeSupport { /** * Sets the language mode of the type. */ setMode(mode: string): void; } /** * This is a tagging interface to declare an editor input being capable of dealing with files. It is only used in the editor registry * to register this kind of input to the platform. */ export interface IFileEditorInput extends IEditorInput, IEncodingSupport, IModeSupport { /** * Gets the resource this editor is about. */ readonly resource: URI; /** * Sets the preferred encoding to use for this input. */ setPreferredEncoding(encoding: string): void; /** * Sets the preferred language mode to use for this input. */ setPreferredMode(mode: string): void; /** * Forces this file input to open as binary instead of text. */ setForceOpenAsBinary(): void; /** * Figure out if the input has been resolved or not. */ isResolved(): boolean; } /** * Side by side editor inputs that have a master and details side. */ export class SideBySideEditorInput extends EditorInput { static readonly ID: string = 'workbench.editorinputs.sidebysideEditorInput'; constructor( protected readonly name: string | undefined, private readonly description: string | undefined, private readonly _details: EditorInput, private readonly _master: EditorInput ) { super(); this.registerListeners(); } get resource(): URI | undefined { return undefined; } get master(): EditorInput { return this._master; } get details(): EditorInput { return this._details; } getTypeId(): string { return SideBySideEditorInput.ID; } getName(): string { if (!this.name) { return localize('sideBySideLabels', "{0} - {1}", this._details.getName(), this._master.getName()); } return this.name; } getDescription(): string | undefined { return this.description; } isReadonly(): boolean { return this.master.isReadonly(); } isUntitled(): boolean { return this.master.isUntitled(); } isDirty(): boolean { return this.master.isDirty(); } isSaving(): boolean { return this.master.isSaving(); } save(group: GroupIdentifier, options?: ISaveOptions): Promise { return this.master.save(group, options); } saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { return this.master.saveAs(group, options); } revert(group: GroupIdentifier, options?: IRevertOptions): Promise { return this.master.revert(group, options); } getTelemetryDescriptor(): { [key: string]: unknown } { const descriptor = this.master.getTelemetryDescriptor(); return Object.assign(descriptor, super.getTelemetryDescriptor()); } private registerListeners(): void { // When the details or master input gets disposed, dispose this diff editor input const onceDetailsDisposed = Event.once(this.details.onDispose); this._register(onceDetailsDisposed(() => { if (!this.isDisposed()) { this.dispose(); } })); const onceMasterDisposed = Event.once(this.master.onDispose); this._register(onceMasterDisposed(() => { if (!this.isDisposed()) { this.dispose(); } })); // Reemit some events from the master side to the outside this._register(this.master.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(this.master.onDidChangeLabel(() => this._onDidChangeLabel.fire())); } async resolve(): Promise { return null; } matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; } if (otherInput) { if (!(otherInput instanceof SideBySideEditorInput)) { return false; } return this.details.matches(otherInput.details) && this.master.matches(otherInput.master); } return false; } } export interface ITextEditorModel extends IEditorModel { textEditorModel: ITextModel; } /** * The editor model is the heavyweight counterpart of editor input. Depending on the editor input, it * connects to the disk to retrieve content and may allow for saving it back or reverting it. Editor models * are typically cached for some while because they are expensive to construct. */ export class EditorModel extends Disposable implements IEditorModel { private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; /** * Causes this model to load returning a promise when loading is completed. */ async load(): Promise { return this; } /** * Returns whether this model was loaded or not. */ isResolved(): boolean { return true; } /** * Subclasses should implement to free resources that have been claimed through loading. */ dispose(): void { this._onDispose.fire(); super.dispose(); } } export interface IEditorInputWithOptions { editor: IEditorInput; options?: IEditorOptions | ITextEditorOptions; } export function isEditorInputWithOptions(obj: unknown): obj is IEditorInputWithOptions { const editorInputWithOptions = obj as IEditorInputWithOptions; return !!editorInputWithOptions && !!editorInputWithOptions.editor; } /** * The editor options is the base class of options that can be passed in when opening an editor. */ export class EditorOptions implements IEditorOptions { /** * Helper to create EditorOptions inline. */ static create(settings: IEditorOptions): EditorOptions { const options = new EditorOptions(); options.overwrite(settings); return options; } /** * Tells the editor to not receive keyboard focus when the editor is being opened. * * Will also not activate the group the editor opens in unless the group is already * the active one. This behaviour can be overridden via the `activation` option. */ preserveFocus: boolean | undefined; /** * This option is only relevant if an editor is opened into a group that is not active * already and allows to control if the inactive group should become active, restored * or preserved. * * By default, the editor group will become active unless `preserveFocus` or `inactive` * is specified. */ activation: EditorActivation | undefined; /** * Tells the editor to reload the editor input in the editor even if it is identical to the one * already showing. By default, the editor will not reload the input if it is identical to the * one showing. */ forceReload: boolean | undefined; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. */ revealIfVisible: boolean | undefined; /** * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. */ revealIfOpened: boolean | undefined; /** * An editor that is pinned remains in the editor stack even when another editor is being opened. * An editor that is not pinned will always get replaced by another editor that is not pinned. */ pinned: boolean | undefined; /** * The index in the document stack where to insert the editor into when opening. */ index: number | undefined; /** * An active editor that is opened will show its contents directly. Set to true to open an editor * in the background without loading its contents. * * Will also not activate the group the editor opens in unless the group is already * the active one. This behaviour can be overridden via the `activation` option. */ inactive: boolean | undefined; /** * Will not show an error in case opening the editor fails and thus allows to show a custom error * message as needed. By default, an error will be presented as notification if opening was not possible. */ ignoreError: boolean | undefined; /** * Does not use editor overrides while opening the editor. */ ignoreOverrides: boolean | undefined; /** * A optional hint to signal in which context the editor opens. * * If configured to be `EditorOpenContext.USER`, this hint can be * used in various places to control the experience. For example, * if the editor to open fails with an error, a notification could * inform about this in a modal dialog. If the editor opened through * some background task, the notification would show in the background, * not as a modal dialog. */ context: EditorOpenContext | undefined; /** * Overwrites option values from the provided bag. */ overwrite(options: IEditorOptions): EditorOptions { if (typeof options.forceReload === 'boolean') { this.forceReload = options.forceReload; } if (typeof options.revealIfVisible === 'boolean') { this.revealIfVisible = options.revealIfVisible; } if (typeof options.revealIfOpened === 'boolean') { this.revealIfOpened = options.revealIfOpened; } if (typeof options.preserveFocus === 'boolean') { this.preserveFocus = options.preserveFocus; } if (typeof options.activation === 'number') { this.activation = options.activation; } if (typeof options.pinned === 'boolean') { this.pinned = options.pinned; } if (typeof options.inactive === 'boolean') { this.inactive = options.inactive; } if (typeof options.ignoreError === 'boolean') { this.ignoreError = options.ignoreError; } if (typeof options.index === 'number') { this.index = options.index; } if (typeof options.ignoreOverrides === 'boolean') { this.ignoreOverrides = options.ignoreOverrides; } if (typeof options.context === 'number') { this.context = options.context; } return this; } } /** * Base Text Editor Options. */ export class TextEditorOptions extends EditorOptions implements ITextEditorOptions { /** * Text editor selection. */ selection: ITextEditorSelection | undefined; /** * Text editor view state. */ editorViewState: IEditorViewState | undefined; /** * Option to control the text editor selection reveal type. */ selectionRevealType: TextEditorSelectionRevealType | undefined; static from(input?: IBaseResourceEditorInput): TextEditorOptions | undefined { if (!input || !input.options) { return undefined; } return TextEditorOptions.create(input.options); } /** * Helper to convert options bag to real class */ static create(options: ITextEditorOptions = Object.create(null)): TextEditorOptions { const textEditorOptions = new TextEditorOptions(); textEditorOptions.overwrite(options); return textEditorOptions; } /** * Overwrites option values from the provided bag. */ overwrite(options: ITextEditorOptions): TextEditorOptions { super.overwrite(options); if (options.selection) { this.selection = { startLineNumber: options.selection.startLineNumber, startColumn: options.selection.startColumn, endLineNumber: options.selection.endLineNumber ?? options.selection.startLineNumber, endColumn: options.selection.endColumn ?? options.selection.startColumn }; } if (options.viewState) { this.editorViewState = options.viewState as IEditorViewState; } if (typeof options.selectionRevealType !== 'undefined') { this.selectionRevealType = options.selectionRevealType; } return this; } /** * Returns if this options object has objects defined for the editor. */ hasOptionsDefined(): boolean { return !!this.editorViewState || !!this.selectionRevealType || !!this.selection; } /** * Create a TextEditorOptions inline to be used when the editor is opening. */ static fromEditor(editor: IEditor, settings?: IEditorOptions): TextEditorOptions { const options = TextEditorOptions.create(settings); // View state options.editorViewState = withNullAsUndefined(editor.saveViewState()); return options; } /** * Apply the view state or selection to the given editor. * * @return if something was applied */ apply(editor: IEditor, scrollType: ScrollType): boolean { let gotApplied = false; // First try viewstate if (this.editorViewState) { editor.restoreViewState(this.editorViewState); gotApplied = true; } // Otherwise check for selection else if (this.selection) { const range: IRange = { startLineNumber: this.selection.startLineNumber, startColumn: this.selection.startColumn, endLineNumber: this.selection.endLineNumber ?? this.selection.startLineNumber, endColumn: this.selection.endColumn ?? this.selection.startColumn }; editor.setSelection(range); if (this.selectionRevealType === TextEditorSelectionRevealType.NearTop) { editor.revealRangeNearTop(range, scrollType); } else if (this.selectionRevealType === TextEditorSelectionRevealType.NearTopIfOutsideViewport) { editor.revealRangeNearTopIfOutsideViewport(range, scrollType); } else if (this.selectionRevealType === TextEditorSelectionRevealType.CenterIfOutsideViewport) { editor.revealRangeInCenterIfOutsideViewport(range, scrollType); } else { editor.revealRangeInCenter(range, scrollType); } gotApplied = true; } return gotApplied; } } export interface IEditorIdentifier { groupId: GroupIdentifier; editor: IEditorInput; } /** * The editor commands context is used for editor commands (e.g. in the editor title) * and we must ensure that the context is serializable because it potentially travels * to the extension host! */ export interface IEditorCommandsContext { groupId: GroupIdentifier; editorIndex?: number; } export class EditorCommandsContextActionRunner extends ActionRunner { constructor( private context: IEditorCommandsContext ) { super(); } run(action: IAction): Promise { return super.run(action, this.context); } } export interface IEditorCloseEvent extends IEditorIdentifier { replaced: boolean; index: number; } export type GroupIdentifier = number; export interface IWorkbenchEditorConfiguration { workbench: { editor: IEditorPartConfiguration, iconTheme: string; }; } interface IEditorPartConfiguration { showTabs?: boolean; highlightModifiedTabs?: boolean; tabCloseButton?: 'left' | 'right' | 'off'; tabSizing?: 'fit' | 'shrink'; titleScrollbarSizing?: 'default' | 'large'; focusRecentEditorAfterClose?: boolean; showIcons?: boolean; enablePreview?: boolean; enablePreviewFromQuickOpen?: boolean; closeOnFileDelete?: boolean; openPositioning?: 'left' | 'right' | 'first' | 'last'; openSideBySideDirection?: 'right' | 'down'; closeEmptyGroups?: boolean; revealIfOpen?: boolean; mouseBackForwardToNavigate?: boolean; labelFormat?: 'default' | 'short' | 'medium' | 'long'; restoreViewState?: boolean; splitSizing?: 'split' | 'distribute'; limit?: { enabled?: boolean; value?: number; perEditorGroup?: boolean; }; } export interface IEditorPartOptions extends IEditorPartConfiguration { iconTheme?: string; } export interface IEditorPartOptionsChangeEvent { oldPartOptions: IEditorPartOptions; newPartOptions: IEditorPartOptions; } export enum SideBySideEditor { MASTER = 1, DETAILS = 2, BOTH = 3 } export interface IResourceOptions { supportSideBySide?: SideBySideEditor; filterByScheme?: string | string[]; } export function toResource(editor: IEditorInput | undefined): URI | undefined; export function toResource(editor: IEditorInput | undefined, options: IResourceOptions & { supportSideBySide?: SideBySideEditor.MASTER | SideBySideEditor.DETAILS }): URI | undefined; export function toResource(editor: IEditorInput | undefined, options: IResourceOptions & { supportSideBySide: SideBySideEditor.BOTH }): URI | { master?: URI, detail?: URI } | undefined; export function toResource(editor: IEditorInput | undefined, options?: IResourceOptions): URI | { master?: URI, detail?: URI } | undefined { if (!editor) { return undefined; } if (options?.supportSideBySide && editor instanceof SideBySideEditorInput) { if (options?.supportSideBySide === SideBySideEditor.BOTH) { return { master: toResource(editor.master, { filterByScheme: options.filterByScheme }), detail: toResource(editor.details, { filterByScheme: options.filterByScheme }) }; } editor = options.supportSideBySide === SideBySideEditor.MASTER ? editor.master : editor.details; } const resource = editor.resource; if (!resource || !options || !options.filterByScheme) { return resource; } if (Array.isArray(options.filterByScheme)) { if (options.filterByScheme.some(scheme => resource.scheme === scheme)) { return resource; } } else { if (options.filterByScheme === resource.scheme) { return resource; } } return undefined; } export const enum CloseDirection { LEFT, RIGHT } export interface IEditorMemento { saveEditorState(group: IEditorGroup, resource: URI, state: T): void; saveEditorState(group: IEditorGroup, editor: EditorInput, state: T): void; loadEditorState(group: IEditorGroup, resource: URI): T | undefined; loadEditorState(group: IEditorGroup, editor: EditorInput): T | undefined; clearEditorState(resource: URI, group?: IEditorGroup): void; clearEditorState(editor: EditorInput, group?: IEditorGroup): void; moveEditorState(source: URI, target: URI): void; } class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private instantiationService: IInstantiationService | undefined; private fileEditorInputFactory: IFileEditorInputFactory | undefined; private customEditorInputFactory: ICustomEditorInputFactory | undefined; private readonly editorInputFactoryConstructors: Map> = new Map(); private readonly editorInputFactoryInstances: Map = new Map(); start(accessor: ServicesAccessor): void { const instantiationService = this.instantiationService = accessor.get(IInstantiationService); this.editorInputFactoryConstructors.forEach((ctor, key) => { this.createEditorInputFactory(key, ctor, instantiationService); }); this.editorInputFactoryConstructors.clear(); } private createEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0, instantiationService: IInstantiationService): void { const instance = instantiationService.createInstance(ctor); this.editorInputFactoryInstances.set(editorInputId, instance); } registerFileEditorInputFactory(factory: IFileEditorInputFactory): void { this.fileEditorInputFactory = factory; } getFileEditorInputFactory(): IFileEditorInputFactory { return assertIsDefined(this.fileEditorInputFactory); } registerCustomEditorInputFactory(factory: ICustomEditorInputFactory): void { this.customEditorInputFactory = factory; } getCustomEditorInputFactory(): ICustomEditorInputFactory { return assertIsDefined(this.customEditorInputFactory); } registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): IDisposable { if (!this.instantiationService) { this.editorInputFactoryConstructors.set(editorInputId, ctor); } else { this.createEditorInputFactory(editorInputId, ctor, this.instantiationService); } return toDisposable(() => { this.editorInputFactoryConstructors.delete(editorInputId); this.editorInputFactoryInstances.delete(editorInputId); }); } getEditorInputFactory(editorInputId: string): IEditorInputFactory | undefined { return this.editorInputFactoryInstances.get(editorInputId); } } export const Extensions = { EditorInputFactories: 'workbench.contributions.editor.inputFactories' }; Registry.add(Extensions.EditorInputFactories, new EditorInputFactoryRegistry()); export async function pathsToEditors(paths: IPathData[] | undefined, fileService: IFileService): Promise<(IResourceEditorInput | IUntitledTextResourceEditorInput)[]> { if (!paths || !paths.length) { return []; } const editors = await Promise.all(paths.map(async path => { const resource = URI.revive(path.fileUri); if (!resource || !fileService.canHandleResource(resource)) { return; } const exists = (typeof path.exists === 'boolean') ? path.exists : await fileService.exists(resource); const options: ITextEditorOptions = (exists && typeof path.lineNumber === 'number') ? { selection: { startLineNumber: path.lineNumber, startColumn: path.columnNumber || 1 }, pinned: true } : { pinned: true }; let input: IResourceEditorInput | IUntitledTextResourceEditorInput; if (!exists) { input = { resource, options, forceUntitled: true }; } else { input = { resource, options, forceFile: true }; } return input; })); return coalesce(editors); } export const enum EditorsOrder { /** * Editors sorted by most recent activity (most recent active first) */ MOST_RECENTLY_ACTIVE, /** * Editors sorted by sequential order */ SEQUENTIAL }