/*--------------------------------------------------------------------------------------------- * 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 { createDecorator, ServiceIdentifier, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IEditorOptions, ITextEditorOptions, Position, IResourceInput } from 'vs/platform/editor/common/editor'; import URI from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; import { basename } from 'vs/base/common/paths'; import { EditorInput, EditorOptions, TextEditorOptions, Extensions as EditorExtensions, IResourceDiffInput, IResourceSideBySideInput, IUntitledResourceInput, SideBySideEditorInput, IFileEditorInput, IFileInputFactory, IEditorInputFactoryRegistry, IEditorOpeningEvent, IEditorCloseEvent, IEditorIdentifier, CloseDirection, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import * as nls from 'vs/nls'; import { getPathLabel } from 'vs/base/common/labels'; import { ResourceMap } from 'vs/base/common/map'; import { Event, Emitter, once } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput'; import { Schemas } from 'vs/base/common/network'; import { IEditorGroupService, IEditorTabOptions, GroupArrangement } from 'vs/workbench/services/group/common/groupService'; export const IWorkbenchEditorService = createDecorator('editorService'); export type IResourceInputType = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput; export type ICloseEditorsFilter = { except?: IEditorInput, direction?: CloseDirection, savedOnly?: boolean }; /** * The editor service allows to open editors and work on the active * editor input and models. */ export interface IWorkbenchEditorService { _serviceBrand: ServiceIdentifier; /** * Returns the currently active editor or null if none. */ getActiveEditor(): IEditor; /** * Returns the currently active editor input or null if none. */ getActiveEditorInput(): IEditorInput; /** * Returns an array of visible editors. */ getVisibleEditors(): IEditor[]; /** * Opens an Editor on the given input with the provided options at the given position. If sideBySide parameter * is provided, causes the editor service to decide in what position to open the input. */ openEditor(input: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise; openEditor(input: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise; /** * Specific overload to open an instance of IResourceInput, IResourceDiffInput or IResourceSideBySideInput. */ openEditor(input: IResourceInputType, position?: Position): TPromise; openEditor(input: IResourceInputType, sideBySide?: boolean): TPromise; /** * Similar to #openEditor() but allows to open multiple editors for different positions at the same time. If there are * more than one editor per position, only the first one will be active and the others stacked behind inactive. */ openEditors(editors: { input: IResourceInputType, position?: Position }[]): TPromise; openEditors(editors: { input: IEditorInput, position?: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise; openEditors(editors: { input: IResourceInputType }[], sideBySide?: boolean): TPromise; openEditors(editors: { input: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], sideBySide?: boolean): TPromise; /** * Given a list of editors to replace, will look across all groups where this editor is open (active or hidden) * and replace it with the new editor and the provied options. */ replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise; replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise; /** * Closes the editor at the provided position. */ closeEditor(position: Position, input: IEditorInput): TPromise; /** * Closes all editors of the provided groups, or all editors across all groups * if no position is provided. */ closeEditors(positions?: Position[]): TPromise; /** * Closes editors of a specific group at the provided position. If the optional editor is provided to exclude, it * will not be closed. The direction can be used in that case to control if all other editors should get closed, * or towards a specific direction. */ closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise; /** * Closes the provided editors of a specific group at the provided position. */ closeEditors(position: Position, editors: IEditorInput[]): TPromise; /** * Closes specific editors across all groups at once. */ closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise; /** * Closes specific editors across all groups at once. */ closeEditors(editors: { positionOne?: IEditorInput[], positionTwo?: IEditorInput[], positionThree?: IEditorInput[] }): TPromise; /** * Allows to resolve an untyped input to a workbench typed instanceof editor input */ createInput(input: IResourceInputType): IEditorInput; } export interface IEditorPart { openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise; openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise; openEditors(editors: { input: IEditorInput, position?: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise; openEditors(editors: { input: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], sideBySide?: boolean): TPromise; replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise; closeEditors(positions?: Position[]): TPromise; closeEditor(position: Position, input: IEditorInput): TPromise; closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise; closeEditors(position: Position, editors: IEditorInput[]): TPromise; closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise; closeEditors(editors: { positionOne?: IEditorInput[], positionTwo?: IEditorInput[], positionThree?: IEditorInput[] }): TPromise; getActiveEditor(): IEditor; getVisibleEditors(): IEditor[]; getActiveEditorInput(): IEditorInput; } export class NoOpEditorPart implements IEditorPart, IEditorGroupService { _serviceBrand: ServiceIdentifier; stacks: any; constructor(private instantiationService: IInstantiationService) { this.stacks = new NoOpEditorStacksModel(); } onEditorsChanged: Event = new Emitter().event; onEditorOpening: Event = new Emitter().event; onEditorOpenFail: Event = new Emitter().event; onEditorGroupMoved: Event = new Emitter().event; onGroupOrientationChanged: Event = new Emitter().event; onTabOptionsChanged: Event = new Emitter().event; focusGroup(...args: any[]) { } activateGroup(...args: any[]) { } moveGroup(...args: any[]) { } arrangeGroups(arrangement: GroupArrangement): void { } setGroupOrientation(orientation: 'vertical' | 'horizontal'): void { } getGroupOrientation(): 'vertical' | 'horizontal' { return 'vertical'; } resizeGroup(position: Position, groupSizeChange: number): void { } pinEditor(...args: any[]) { } moveEditor(...args: any[]) { } getStacksModel(): any { return this.stacks; } getTabOptions(): IEditorTabOptions { return Object.create(null); } invokeWithinEditorContext(fn: (accessor: ServicesAccessor) => T): T { return this.instantiationService.invokeFunction(fn); } openEditor(...args: any[]) { return TPromise.as(void 0); } openEditors(...args: any[]) { return TPromise.as([]); } replaceEditors(editors: { toReplace: IEditorInput; replaceWith: IEditorInput; options?: IEditorOptions | ITextEditorOptions; }[], position?: Position): TPromise { return TPromise.as(void 0); } closeEditors(...args: any[]) { return TPromise.as(void 0); } closeEditor(position: Position, input: IEditorInput): TPromise { return TPromise.as(void 0); } getActiveEditor(): IEditor { return null; } getVisibleEditors(): IEditor[] { return []; } getActiveEditorInput(): IEditorInput { return null; } hideTabs(...args: any[]): void { } hasEditorsToRestore(): boolean { return false; } restoreEditors(): any { return TPromise.as([]); } } export class NoOpEditorStacksModel { onModelChanged: Event = new Emitter().event; onWillCloseEditor: Event = new Emitter().event; onEditorClosed: Event = new Emitter().event; groups: any[] = []; activeGroup: any; isActive(group: any): boolean { return false; } getGroup(id: number): any { return null; } positionOfGroup(group: any): Position { return Position.ONE; } groupAt(position: Position): any { return null; } next(jumpGroups: boolean, cycleAtEnd?: boolean): IEditorIdentifier { return null; } previous(jumpGroups: boolean, cycleAtStart?: boolean): IEditorIdentifier { return null; } last(): IEditorIdentifier { return null; } isOpen(resource: URI): boolean { return false; } toString(): string { return ''; } } type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; export class WorkbenchEditorService implements IWorkbenchEditorService { public _serviceBrand: any; private static CACHE: ResourceMap = new ResourceMap(); private editorPart: IEditorPart | IWorkbenchEditorService; private fileInputFactory: IFileInputFactory; constructor( editorPart: IEditorPart | IWorkbenchEditorService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, @IInstantiationService private instantiationService: IInstantiationService, @IEnvironmentService private environmentService: IEnvironmentService, @IFileService private fileService: IFileService ) { this.editorPart = editorPart; this.fileInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileInputFactory(); } public getActiveEditor(): IEditor { return this.editorPart.getActiveEditor(); } public getActiveEditorInput(): IEditorInput { return this.editorPart.getActiveEditorInput(); } public getVisibleEditors(): IEditor[] { return this.editorPart.getVisibleEditors(); } public openEditor(input: IEditorInput, options?: IEditorOptions, sideBySide?: boolean): TPromise; public openEditor(input: IEditorInput, options?: IEditorOptions, position?: Position): TPromise; public openEditor(input: IResourceInputType, position?: Position): TPromise; public openEditor(input: IResourceInputType, sideBySide?: boolean): TPromise; public openEditor(input: any, arg2?: any, arg3?: any): TPromise { if (!input) { return TPromise.wrap(null); } // Workbench Input Support if (input instanceof EditorInput) { return this.doOpenEditor(input, this.toOptions(arg2), arg3); } // Support opening foreign resources (such as a http link that points outside of the workbench) const resourceInput = input; if (resourceInput.resource instanceof URI) { const schema = resourceInput.resource.scheme; if (schema === Schemas.http || schema === Schemas.https) { window.open(resourceInput.resource.toString(true)); return TPromise.wrap(null); } } // Untyped Text Editor Support (required for code that uses this service below workbench level) const textInput = input; const typedInput = this.createInput(textInput); if (typedInput) { return this.doOpenEditor(typedInput, TextEditorOptions.from(textInput), arg2); } return TPromise.wrap(null); } private toOptions(options?: IEditorOptions | EditorOptions): EditorOptions { if (!options || options instanceof EditorOptions) { return options as EditorOptions; } const textOptions: ITextEditorOptions = options; if (!!textOptions.selection) { return TextEditorOptions.create(options); } return EditorOptions.create(options); } /** * Allow subclasses to implement their own behavior for opening editor (see below). */ protected doOpenEditor(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise; protected doOpenEditor(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise; protected doOpenEditor(input: IEditorInput, options?: EditorOptions, arg3?: any): TPromise { return this.editorPart.openEditor(input, options, arg3); } public openEditors(editors: { input: IResourceInputType, position: Position }[]): TPromise; public openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions }[]): TPromise; public openEditors(editors: { input: IResourceInputType }[], sideBySide?: boolean): TPromise; public openEditors(editors: { input: IEditorInput, options?: IEditorOptions }[], sideBySide?: boolean): TPromise; public openEditors(editors: any[], sideBySide?: boolean): TPromise { const inputs = editors.map(editor => this.createInput(editor.input)); const typedInputs: { input: IEditorInput, position: Position, options?: EditorOptions }[] = inputs.map((input, index) => { const options = editors[index].input instanceof EditorInput ? this.toOptions(editors[index].options) : TextEditorOptions.from(editors[index].input); return { input, options, position: editors[index].position }; }); return this.editorPart.openEditors(typedInputs, sideBySide); } public replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise; public replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions }[], position?: Position): TPromise; public replaceEditors(editors: any[], position?: Position): TPromise { const toReplaceInputs = editors.map(editor => this.createInput(editor.toReplace)); const replaceWithInputs = editors.map(editor => this.createInput(editor.replaceWith)); const typedReplacements: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: EditorOptions }[] = editors.map((editor, index) => { const options = editor.toReplace instanceof EditorInput ? this.toOptions(editor.options) : TextEditorOptions.from(editor.replaceWith); return { toReplace: toReplaceInputs[index], replaceWith: replaceWithInputs[index], options }; }); return this.editorPart.replaceEditors(typedReplacements, position); } public closeEditor(position: Position, input: IEditorInput): TPromise { return this.doCloseEditor(position, input); } protected doCloseEditor(position: Position, input: IEditorInput): TPromise { return this.editorPart.closeEditor(position, input); } public closeEditors(positions?: Position[]): TPromise; public closeEditors(position: Position, filter?: ICloseEditorsFilter): TPromise; public closeEditors(position: Position, editors: IEditorInput[]): TPromise; public closeEditors(editors: { positionOne?: ICloseEditorsFilter, positionTwo?: ICloseEditorsFilter, positionThree?: ICloseEditorsFilter }): TPromise; public closeEditors(editors: { positionOne?: IEditorInput[], positionTwo?: IEditorInput[], positionThree?: IEditorInput[] }): TPromise; public closeEditors(positionsOrEditors: any, filterOrEditors?: any): TPromise { return this.editorPart.closeEditors(positionsOrEditors, filterOrEditors); } public createInput(input: IEditorInput): EditorInput; public createInput(input: IResourceInputType): EditorInput; public createInput(input: any): IEditorInput { // Workbench Input Support if (input instanceof EditorInput) { return input; } // Side by Side Support const resourceSideBySideInput = input; if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) { const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource }); const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource }); return new SideBySideEditorInput(resourceSideBySideInput.label || masterInput.getName(), typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), detailInput, masterInput); } // Diff Editor Support const resourceDiffInput = input; if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) { const leftInput = this.createInput({ resource: resourceDiffInput.leftResource }); const rightInput = this.createInput({ resource: resourceDiffInput.rightResource }); const label = resourceDiffInput.label || nls.localize('compareLabels', "{0} ↔ {1}", this.toDiffLabel(leftInput, this.workspaceContextService, this.environmentService), this.toDiffLabel(rightInput, this.workspaceContextService, this.environmentService)); return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput); } // Untitled file support const untitledInput = input; if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === Schemas.untitled)) { return this.untitledEditorService.createOrGet(untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource, untitledInput.language, untitledInput.contents, untitledInput.encoding); } // Resource Editor Support const resourceInput = input; if (resourceInput.resource instanceof URI) { let label = resourceInput.label; if (!label && resourceInput.resource.scheme !== Schemas.data) { label = basename(resourceInput.resource.fsPath); // derive the label from the path (but not for data URIs) } return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding); } return null; } private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string): ICachedEditorInput { if (WorkbenchEditorService.CACHE.has(resource)) { const input = WorkbenchEditorService.CACHE.get(resource); if (input instanceof ResourceEditorInput) { input.setName(label); input.setDescription(description); } else if (!(input instanceof DataUriEditorInput)) { input.setPreferredEncoding(encoding); } return input; } let input: ICachedEditorInput; // File if (this.fileService.canHandleResource(resource)) { input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService); } // Data URI else if (resource.scheme === Schemas.data) { input = instantiationService.createInstance(DataUriEditorInput, label, description, resource); } // Resource else { input = instantiationService.createInstance(ResourceEditorInput, label, description, resource); } WorkbenchEditorService.CACHE.set(resource, input); once(input.onDispose)(() => { WorkbenchEditorService.CACHE.delete(resource); }); return input; } private toDiffLabel(input: EditorInput, context: IWorkspaceContextService, environment: IEnvironmentService): string { const res = input.getResource(); // Do not try to extract any paths from simple untitled editors if (res.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(res)) { return input.getName(); } // Otherwise: for diff labels prefer to see the path as part of the label return getPathLabel(res.fsPath, context, environment); } } export interface IEditorOpenHandler { (input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise; (input: IEditorInput, options?: EditorOptions, position?: Position): TPromise; } export interface IEditorCloseHandler { (position: Position, input: IEditorInput): TPromise; } /** * Subclass of workbench editor service that delegates all calls to the provided editor service. Subclasses can choose to override the behavior * of openEditor() and closeEditor() by providing a handler. * * This gives clients a chance to override the behavior of openEditor() and closeEditor(). */ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { private editorOpenHandler: IEditorOpenHandler; private editorCloseHandler: IEditorCloseHandler; constructor( @IUntitledEditorService untitledEditorService: IUntitledEditorService, @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService ) { super( editorService, untitledEditorService, workspaceContextService, instantiationService, environmentService, fileService ); } public setEditorOpenHandler(handler: IEditorOpenHandler): void { this.editorOpenHandler = handler; } public setEditorCloseHandler(handler: IEditorCloseHandler): void { this.editorCloseHandler = handler; } protected doOpenEditor(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise; protected doOpenEditor(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise; protected doOpenEditor(input: IEditorInput, options?: EditorOptions, arg3?: any): TPromise { const handleOpen = this.editorOpenHandler ? this.editorOpenHandler(input, options, arg3) : TPromise.as(void 0); return handleOpen.then(editor => { if (editor) { return TPromise.as(editor); } return super.doOpenEditor(input, options, arg3); }); } protected doCloseEditor(position: Position, input: IEditorInput): TPromise { const handleClose = this.editorCloseHandler ? this.editorCloseHandler(position, input) : TPromise.as(void 0); return handleClose.then(() => { return super.doCloseEditor(position, input); }); } }