/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import {TPromise} from 'vs/base/common/winjs.base'; import {EventEmitter} from 'vs/base/common/eventEmitter'; import Event, {Emitter} from 'vs/base/common/event'; import types = require('vs/base/common/types'); import URI from 'vs/base/common/uri'; import {IEditor, ICommonCodeEditor, IEditorViewState, IEditorOptions as ICodeEditorOptions} from 'vs/editor/common/editorCommon'; import {IEditorInput, IEditorModel, IEditorOptions, ITextEditorOptions, IResourceInput, Position} from 'vs/platform/editor/common/editor'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {Event as BaseEvent} from 'vs/base/common/events'; import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService'; import {SyncDescriptor, AsyncDescriptor} from 'vs/platform/instantiation/common/descriptors'; import {IInstantiationService, IConstructorSignature0} from 'vs/platform/instantiation/common/instantiation'; import {IModel} from 'vs/editor/common/editorCommon'; export enum ConfirmResult { SAVE, DONT_SAVE, CANCEL } export interface IEditorDescriptor { getId(): string; getName(): string; describes(obj: any): boolean; } export const Extensions = { Editors: 'workbench.contributions.editors' }; export interface IEditorRegistry { /** * Registers an editor to the platform for the given input type. The second parameter also supports an * array of input classes to be passed in. If the more than one editor is registered for the same editor * input, the input itself will be asked which editor it prefers if this method is provided. Otherwise * the first editor in the list will be returned. * * @param editorInputDescriptor a constructor function that returns an instance of EditorInput for which the * registered editor should be used for. */ registerEditor(descriptor: IEditorDescriptor, editorInputDescriptor: SyncDescriptor): void; registerEditor(descriptor: IEditorDescriptor, editorInputDescriptor: SyncDescriptor[]): void; /** * Returns the editor descriptor for the given input or null if none. */ getEditor(input: EditorInput): IEditorDescriptor; /** * Returns the editor descriptor for the given identifier or null if none. */ getEditorById(editorId: string): IEditorDescriptor; /** * Returns an array of registered editors known to the platform. */ getEditors(): IEditorDescriptor[]; /** * Registers the default input to be used for files in the workbench. * * @param editorInputDescriptor a descriptor that resolves to an instance of EditorInput that * should be used to handle file inputs. */ registerDefaultFileInput(editorInputDescriptor: AsyncDescriptor): void; /** * Returns a descriptor of the default input to be used for files in the workbench. * * @return a descriptor that resolves to an instance of EditorInput that should be used to handle * file inputs. */ getDefaultFileInput(): AsyncDescriptor; /** * 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: IConstructorSignature0): void; /** * Returns the editor input factory for the given editor input. * * @param editorInputId the identifier of the editor input */ getEditorInputFactory(editorInputId: string): IEditorInputFactory; setInstantiationService(service: IInstantiationService): void; } export interface IEditorInputFactory { /** * 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; /** * 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; } /** * 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 EventEmitter implements IEditorInput { protected _onDidChangeDirty: Emitter; private disposed: boolean; constructor() { super(); this._onDidChangeDirty = new Emitter(); this.disposed = false; } /** * Fired when the dirty state of this input changes. */ public get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } /** * Returns the name of this input that can be shown to the user. Examples include showing the name of the input * above the editor area when the input is shown. */ public getName(): string { return null; } /** * Returns the description of this input that can be shown to the user. Examples include showing the description of * the input above the editor area to the side of the name of the input. * * @param verbose controls if the description should be short or can contain additional details. */ public getDescription(verbose?: boolean): string { return null; } /** * Returns the unique type identifier of this input. */ public abstract getTypeId(): string; /** * 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. */ public getPreferredEditorId(candidates: string[]): string { if (candidates && candidates.length > 0) { return candidates[0]; } return null; } /** * Returns a type of EditorModel that represents the resolved input. Subclasses should * override to provide a meaningful model. The optional second argument allows to specify * if the EditorModel should be refreshed before returning it. Depending on the implementation * this could mean to refresh the editor model contents with the version from disk. */ public abstract resolve(refresh?: boolean): TPromise; /** * An editor that is dirty will be asked to be saved once it closes. */ public isDirty(): boolean { return false; } /** * Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result. */ public confirmSave(): ConfirmResult { return ConfirmResult.DONT_SAVE; } /** * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ public save(): TPromise { return TPromise.as(true); } /** * Reverts the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ public revert(): TPromise { return TPromise.as(true); } /** * Called when this input is no longer opened in any editor. Subclasses can free resources as needed. */ public close(): void { this.dispose(); } /** * Subclasses can set this to false if it does not make sense to split the editor input. */ public supportsSplitEditor(): boolean { return true; } /** * Returns true if this input is identical to the otherInput. */ public matches(otherInput: any): boolean { return this === otherInput; } /** * Called when an editor input is no longer needed. Allows to free up any resources taken by * resolving the editor input. */ public dispose(): void { this._onDidChangeDirty.dispose(); this.disposed = true; this.emit('dispose'); super.dispose(); } /** * Returns whether this input was disposed or not. */ public isDisposed(): boolean { return this.disposed; } } export class EditorInputEvent extends BaseEvent { private _editorInput: IEditorInput; private prevented: boolean; constructor(editorInput: IEditorInput) { super(null); this._editorInput = editorInput; } public get editorInput(): IEditorInput { return this._editorInput; } public prevent(): void { this.prevented = true; } public isPrevented(): boolean { return this.prevented; } } export 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 input if known. */ getEncoding(): string; /** * Sets the encoding for the input for saving. */ setEncoding(encoding: string, mode: EncodingMode): 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 { /** * Gets the mime type of the file this input is about. */ getMime(): string; /** * Sets the mime type of the file this input is about. */ setMime(mime: string): void; /** * Gets the absolute file resource URI this input is about. */ getResource(): URI; /** * Sets the absolute file resource URI this input is about. */ setResource(resource: URI): void; /** * Sets the preferred encodingt to use for this input. */ setPreferredEncoding(encoding: string): void; } /** * The base class of untitled editor inputs in the workbench. */ export abstract class UntitledEditorInput extends EditorInput implements IEncodingSupport { abstract getResource(): URI; abstract isDirty(): boolean; abstract suggestFileName(): string; abstract getMime(): string; abstract getEncoding(): string; abstract setEncoding(encoding: string, mode: EncodingMode): void; } /** * The base class of editor inputs that have an original and modified side. */ export abstract class BaseDiffEditorInput extends EditorInput { private _originalInput: EditorInput; private _modifiedInput: EditorInput; constructor(originalInput: EditorInput, modifiedInput: EditorInput) { super(); this._originalInput = originalInput; this._modifiedInput = modifiedInput; } public get originalInput(): EditorInput { return this._originalInput; } public get modifiedInput(): EditorInput { return this._modifiedInput; } public isDirty(): boolean { return this._modifiedInput.isDirty(); } public confirmSave(): ConfirmResult { return this._modifiedInput.confirmSave(); } public save(): TPromise { return this._modifiedInput.save(); } public revert(): TPromise { return this._modifiedInput.revert(); } } export interface ITextEditorModel extends IEditorModel { textEditorModel: IModel; } /** * 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 EventEmitter implements IEditorModel { /** * Causes this model to load returning a promise when loading is completed. */ public load(): TPromise { return TPromise.as(this); } /** * Returns whether this model was loaded or not. */ public isResolved(): boolean { return true; } /** * Subclasses should implement to free resources that have been claimed through loading. */ public dispose(): void { this.emit('dispose'); super.dispose(); } } /** * 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. */ public static create(settings: IEditorOptions): EditorOptions { let options = new EditorOptions(); options.preserveFocus = settings.preserveFocus; options.forceOpen = settings.forceOpen; options.revealIfVisible = settings.revealIfVisible; options.pinned = settings.pinned; options.index = settings.index; options.inactive = settings.inactive; return options; } /** * Inherit all options from other EditorOptions instance. */ public mixin(other: EditorOptions): void { this.preserveFocus = other.preserveFocus; this.forceOpen = other.forceOpen; this.revealIfVisible = other.revealIfVisible; this.pinned = other.pinned; this.index = other.index; } /** * Tells the editor to not receive keyboard focus when the editor is being opened. By default, * the editor will receive keyboard focus on open. */ public preserveFocus: boolean; /** * Tells the editor to replace the editor input in the editor even if it is identical to the one * already showing. By default, the editor will not replace the input if it is identical to the * one showing. */ public forceOpen: boolean; /** * Will reveal the editor if it is already opened and visible in any of the opened editor groups. */ public revealIfVisible: boolean; /** * 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. */ public pinned: boolean; /** * The index in the document stack where to insert the editor into when opening. */ public index: number; /** * An active editor that is opened will show its contents directly. Set to true to open an editor * in the background. */ public inactive: boolean; } /** * Base Text Editor Options. */ export class TextEditorOptions extends EditorOptions { protected startLineNumber: number; protected startColumn: number; protected endLineNumber: number; protected endColumn: number; private editorViewState: IEditorViewState; private editorOptions: ICodeEditorOptions; public static from(input: IResourceInput): TextEditorOptions { let options: TextEditorOptions = null; if (input && input.options) { if (input.options.selection || input.options.forceOpen || input.options.revealIfVisible || input.options.preserveFocus || input.options.pinned || input.options.inactive || typeof input.options.index === 'number') { options = new TextEditorOptions(); } if (input.options.selection) { let selection = input.options.selection; options.selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn); } if (input.options.forceOpen) { options.forceOpen = true; } if (input.options.revealIfVisible) { options.revealIfVisible = true; } if (input.options.preserveFocus) { options.preserveFocus = true; } if (input.options.pinned) { options.pinned = true; } if (input.options.inactive) { options.inactive = true; } if (typeof input.options.index === 'number') { options.index = input.options.index; } } return options; } /** * Helper to create TextEditorOptions inline. */ public static create(settings: ITextEditorOptions): TextEditorOptions { let options = new TextEditorOptions(); options.preserveFocus = settings.preserveFocus; options.forceOpen = settings.forceOpen; options.revealIfVisible = settings.revealIfVisible; options.pinned = settings.pinned; options.index = settings.index; if (settings.selection) { options.startLineNumber = settings.selection.startLineNumber; options.startColumn = settings.selection.startColumn; options.endLineNumber = settings.selection.endLineNumber || settings.selection.startLineNumber; options.endColumn = settings.selection.endColumn || settings.selection.startColumn; } return options; } /** * Returns if this options object has objects defined for the editor. */ public hasOptionsDefined(): boolean { return !!this.editorViewState || (!types.isUndefinedOrNull(this.startLineNumber) && !types.isUndefinedOrNull(this.startColumn)); } /** * Tells the editor to set show the given selection when the editor is being opened. */ public selection(startLineNumber: number, startColumn: number, endLineNumber: number = startLineNumber, endColumn: number = startColumn): EditorOptions { this.startLineNumber = startLineNumber; this.startColumn = startColumn; this.endLineNumber = endLineNumber; this.endColumn = endColumn; return this; } /** * Sets the view state to be used when the editor is opening. */ public fromEditor(editor: IEditor): void { // View state this.editorViewState = editor.saveViewState(); // Selected editor options const codeEditor = editor; if (typeof codeEditor.getConfiguration === 'function') { const config = codeEditor.getConfiguration(); if (config && config.viewInfo && config.wrappingInfo) { this.editorOptions = Object.create(null); this.editorOptions.renderWhitespace = config.viewInfo.renderWhitespace; this.editorOptions.renderControlCharacters = config.viewInfo.renderControlCharacters; this.editorOptions.wrappingColumn = config.wrappingInfo.isViewportWrapping ? 0 : -1; } } } /** * Apply the view state or selection to the given editor. * * @return if something was applied */ public apply(editor: IEditor): boolean { // Editor options if (this.editorOptions) { editor.updateOptions(this.editorOptions); } // View state return this.applyViewState(editor); } private applyViewState(editor: IEditor): boolean { let gotApplied = false; // First try viewstate if (this.editorViewState) { editor.restoreViewState(this.editorViewState); gotApplied = true; } // Otherwise check for selection else if (!types.isUndefinedOrNull(this.startLineNumber) && !types.isUndefinedOrNull(this.startColumn)) { // Select if (!types.isUndefinedOrNull(this.endLineNumber) && !types.isUndefinedOrNull(this.endColumn)) { let range = { startLineNumber: this.startLineNumber, startColumn: this.startColumn, endLineNumber: this.endLineNumber, endColumn: this.endColumn }; editor.setSelection(range); editor.revealRangeInCenter(range); } // Reveal else { let pos = { lineNumber: this.startLineNumber, column: this.startColumn }; editor.setPosition(pos); editor.revealPositionInCenter(pos); } gotApplied = true; } return gotApplied; } } export interface ITextDiffEditorOptions extends ITextEditorOptions { /** * Whether to auto reveal the first change when the text editor is opened or not. By default * the first change will not be revealed. */ autoRevealFirstChange: boolean; } /** * Base Text Diff Editor Options. */ export class TextDiffEditorOptions extends TextEditorOptions { /** * Helper to create TextDiffEditorOptions inline. */ public static create(settings: ITextDiffEditorOptions): TextDiffEditorOptions { let options = new TextDiffEditorOptions(); options.autoRevealFirstChange = settings.autoRevealFirstChange; options.preserveFocus = settings.preserveFocus; options.forceOpen = settings.forceOpen; options.revealIfVisible = settings.revealIfVisible; options.pinned = settings.pinned; options.index = settings.index; if (settings.selection) { options.startLineNumber = settings.selection.startLineNumber; options.startColumn = settings.selection.startColumn; options.endLineNumber = settings.selection.endLineNumber || settings.selection.startLineNumber; options.endColumn = settings.selection.endColumn || settings.selection.startColumn; } return options; } /** * Whether to auto reveal the first change when the text editor is opened or not. By default * the first change will not be revealed. */ public autoRevealFirstChange: boolean; } /** * Given an input, tries to get the associated URI for it (either file or untitled scheme). */ export function getUntitledOrFileResource(input: IEditorInput, supportDiff?: boolean): URI { if (!input) { return null; } // Untitled if (input instanceof UntitledEditorInput) { return input.getResource(); } // File let fileInput = asFileEditorInput(input, supportDiff); return fileInput && fileInput && fileInput.getResource(); } export function getResource(input: IEditorInput): URI { if (input && typeof (input).getResource === 'function') { let candidate = (input).getResource(); if (candidate instanceof URI) { return candidate; } } return getUntitledOrFileResource(input, true); } /** * Helper to return all opened editors with resources not belonging to the currently opened workspace. */ export function getOutOfWorkspaceEditorResources(editorGroupService: IEditorGroupService, contextService: IWorkspaceContextService): URI[] { const resources: URI[] = []; editorGroupService.getStacksModel().groups.forEach(group => { const editors = group.getEditors(); editors.forEach(editor => { const fileInput = asFileEditorInput(editor, true); if (fileInput && !contextService.isInsideWorkspace(fileInput.getResource())) { resources.push(fileInput.getResource()); } }); }); return resources; } /** * Returns the object as IFileEditorInput only if it matches the signature. */ export function asFileEditorInput(obj: any, supportDiff?: boolean): IFileEditorInput { if (!obj) { return null; } // Check for diff if we are asked to if (supportDiff && obj instanceof BaseDiffEditorInput) { obj = (obj).modifiedInput; } let i = obj; return i instanceof EditorInput && types.areFunctions(i.setResource, i.setMime, i.setEncoding, i.getEncoding, i.getResource, i.getMime) ? i : null; } export interface IStacksModelChangeEvent { group: IEditorGroup; editor?: IEditorInput; structural?: boolean; } export interface IEditorStacksModel { onModelChanged: Event; onEditorClosed: Event; groups: IEditorGroup[]; activeGroup: IEditorGroup; isActive(IEditorGroup): boolean; getGroup(id: GroupIdentifier): IEditorGroup; positionOfGroup(group: IEditorGroup): Position; groupAt(position: Position): IEditorGroup; next(): IEditorIdentifier; previous(): IEditorIdentifier; isOpen(editor: IEditorInput): boolean; isOpen(resource: URI): boolean; toString(): string; } export interface IEditorGroup { id: GroupIdentifier; label: string; count: number; activeEditor: IEditorInput; previewEditor: IEditorInput; getEditor(index: number): IEditorInput; indexOf(editor: IEditorInput): number; contains(editor: IEditorInput): boolean; contains(resource: URI): boolean; getEditors(mru?: boolean): IEditorInput[]; isActive(editor: IEditorInput): boolean; isPreview(editor: IEditorInput): boolean; isPinned(editor: IEditorInput): boolean; } export interface IEditorIdentifier { group: IEditorGroup; editor: IEditorInput; } export interface IEditorContext extends IEditorIdentifier { event: any; } export interface IGroupEvent { editor: IEditorInput; pinned: boolean; index: number; } export type GroupIdentifier = number; export const EditorOpenPositioning = { LEFT: 'left', RIGHT: 'right', FIRST: 'first', LAST: 'last' }; export interface IWorkbenchEditorConfiguration { workbench: { editor: { showTabs: boolean; enablePreview: boolean; enablePreviewFromQuickOpen: boolean; openPositioning: string; } }; } export const ActiveEditorMovePositioning = { FIRST: 'first', LAST: 'last', LEFT: 'left', RIGHT: 'right', CENTER: 'center', POSITION: 'position', }; export const ActiveEditorMovePositioningBy = { TAB: 'tab', GROUP: 'group' }; export interface ActiveEditorMoveArguments { to?: string; by?: string; value?: number; } export var EditorCommands = { MoveActiveEditor: 'moveActiveEditor' };