From 7e81c46f6c02cffe3774541d3696c1d3d25b78e6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 2 May 2018 09:42:50 +0200 Subject: [PATCH] grid - adopt new services in title controls --- .../parts/editor2/nextNoTabsTitleControl.ts | 12 +- .../parts/editor2/nextTabsTitleControl.ts | 90 +++--- .../browser/parts/editor2/nextTitleControl.ts | 68 +++-- src/vs/workbench/common/editor.ts | 2 +- .../workbench/electron-browser/workbench.ts | 2 +- .../editor/browser/nextEditorService.ts | 282 +++++++++++++++++- .../editor/common/nextEditorService.ts | 78 +---- 7 files changed, 357 insertions(+), 177 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor2/nextNoTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor2/nextNoTabsTitleControl.ts index d2fec9de69c..f223800bf7a 100644 --- a/src/vs/workbench/browser/parts/editor2/nextNoTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor2/nextNoTabsTitleControl.ts @@ -48,8 +48,8 @@ export class NextNoTabsTitleControl extends NextTitleControl { this.createEditorActionsToolBar(actionsContainer); // Context Menu - this._register(DOM.addDisposableListener(this.titleContainer, DOM.EventType.CONTEXT_MENU, (e: Event) => this.onContextMenu({ group: this.group, editor: this.group.activeEditor }, e, this.titleContainer))); - this._register(DOM.addDisposableListener(this.titleContainer, TouchEventType.Contextmenu, (e: Event) => this.onContextMenu({ group: this.group, editor: this.group.activeEditor }, e, this.titleContainer))); + this._register(DOM.addDisposableListener(this.titleContainer, DOM.EventType.CONTEXT_MENU, (e: Event) => this.onContextMenu(this.group.activeEditor, e, this.titleContainer))); + this._register(DOM.addDisposableListener(this.titleContainer, TouchEventType.Contextmenu, (e: Event) => this.onContextMenu(this.group.activeEditor, e, this.titleContainer))); } private onTitleLabelClick(e: MouseEvent): void { @@ -60,7 +60,7 @@ export class NextNoTabsTitleControl extends NextTitleControl { private onTitleDoubleClick(e: MouseEvent): void { DOM.EventHelper.stop(e); - this.editorGroupService.pinEditor(this.group, this.group.activeEditor); + this.groupController.pinEditor(this.group.activeEditor); } private onTitleClick(e: MouseEvent | GestureEvent): void { @@ -75,7 +75,7 @@ export class NextNoTabsTitleControl extends NextTitleControl { // - mouse click: do not focus group if there are more than one as it otherwise makes group DND funky // - touch: always focus else if ((this.stacks.groups.length === 1 || !(e instanceof MouseEvent)) && !DOM.isAncestor(((e as GestureEvent).initialTarget || e.target || e.srcElement) as HTMLElement, this.editorActionsToolbar.getContainer())) { - this.editorGroupService.focusGroup(this.group); + this.groupController.focus(); } } @@ -89,7 +89,7 @@ export class NextNoTabsTitleControl extends NextTitleControl { } const isPinned = this.group.isPinned(this.group.activeEditor); - const isActive = this.nextEditorGroupsService.isGroupActive(this.group.id); + const isActive = this.groupController.isActive; // Dirty state if (editor.isDirty()) { @@ -102,7 +102,7 @@ export class NextNoTabsTitleControl extends NextTitleControl { const resource = toResource(editor, { supportSideBySide: true }); const name = editor.getName() || ''; - const labelFormat = this.editorGroupService.getTabOptions().labelFormat; + const labelFormat = 'default'; //TODO@grid support tab options (this.editorGroupService.getTabOptions().labelFormat); let description: string; if (labelFormat === 'default' && !isActive) { description = ''; // hide description when group is not active and style is 'default' diff --git a/src/vs/workbench/browser/parts/editor2/nextTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor2/nextTabsTitleControl.ts index 97bed87b54f..09668315d9e 100644 --- a/src/vs/workbench/browser/parts/editor2/nextTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor2/nextTabsTitleControl.ts @@ -13,7 +13,7 @@ import * as DOM from 'vs/base/browser/dom'; import { isMacintosh } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { Position, IEditorInput, Verbosity, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; +import { IEditorInput, Verbosity, IUntitledResourceInput } from 'vs/platform/editor/common/editor'; import { IEditorGroup, toResource } from 'vs/workbench/common/editor'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -22,7 +22,7 @@ import { ResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IWorkbenchEditorService, DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import { IEditorTabOptions } from 'vs/workbench/services/group/common/groupService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -43,6 +43,7 @@ import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { INextEditorGroupsService } from 'vs/workbench/services/editor/common/nextEditorGroupsService'; +import { INextEditorService } from 'vs/workbench/services/editor/common/nextEditorService'; interface IEditorInputLabel { name: string; @@ -70,9 +71,8 @@ export class NextTabsTitleControl extends NextTitleControl { group: IEditorGroup, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IEditorGroupService editorGroupService: IEditorGroupService, @INextEditorGroupsService nextEditorGroupsService: INextEditorGroupsService, + @INextEditorService private nextEditorService: INextEditorService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, @@ -82,7 +82,7 @@ export class NextTabsTitleControl extends NextTitleControl { @IThemeService themeService: IThemeService, @IExtensionService extensionService: IExtensionService ) { - super(parent, group, contextMenuService, instantiationService, editorService, editorGroupService, nextEditorGroupsService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService); + super(parent, group, contextMenuService, instantiationService, nextEditorGroupsService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService); this.tabDisposeables = []; this.editorLabels = []; @@ -93,7 +93,6 @@ export class NextTabsTitleControl extends NextTitleControl { } private createScopedInstantiationService(): IInstantiationService { - const stacks = this.editorGroupService.getStacksModel(); const delegatingEditorService = this.instantiationService.createInstance(DelegatingWorkbenchEditorService); // We create a scoped instantiation service to override the behaviour when closing an inactive editor @@ -102,10 +101,10 @@ export class NextTabsTitleControl extends NextTitleControl { // the inactive editors because closing the active one will always cause a tab switch that sets focus. // We also want to block the tabs container to reveal the currently active tab because that makes it very // hard to close multiple inactive tabs next to each other. - delegatingEditorService.setEditorCloseHandler((position, editor) => { - const group = stacks.groupAt(position); - if (group && stacks.isActive(group) && !group.isActive(editor)) { - this.editorGroupService.focusGroup(group); + delegatingEditorService.setEditorCloseHandler((groupId, editor) => { + const group = this.nextEditorGroupsService.getGroup(groupId); + if (group.isActive && group.activeEditor !== editor) { + group.focus(); } this.blockRevealActiveTab = true; @@ -139,7 +138,7 @@ export class NextTabsTitleControl extends NextTitleControl { if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) { DOM.EventHelper.stop(e); - this.editorService.openEditor({ options: { pinned: true, index: this.group.count /* always at the end */ } } as IUntitledResourceInput).done(null, errors.onUnexpectedError); // untitled are always pinned + this.nextEditorService.openEditor({ options: { pinned: true, index: this.group.count /* always at the end */ } } as IUntitledResourceInput); // untitled are always pinned } })); @@ -211,10 +210,7 @@ export class NextTabsTitleControl extends NextTitleControl { const target = e.target; if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) { - const targetPosition = this.stacks.positionOfGroup(this.group); - const targetIndex = this.group.count; - - this.onDrop(e, this.group, targetPosition, targetIndex); + this.onDrop(e, this.group.count); } })); @@ -262,7 +258,7 @@ export class NextTabsTitleControl extends NextTitleControl { const labels = this.getTabLabels(editorsOfGroup); // Tab label and styles - const isGroupActive = this.nextEditorGroupsService.isGroupActive(this.group.id); + const isGroupActive = this.groupController.isActive; editorsOfGroup.forEach((editor, index) => { const tabContainer = this.tabsContainer.children[index] as HTMLElement; if (!tabContainer) { @@ -285,7 +281,7 @@ export class NextTabsTitleControl extends NextTitleControl { tabContainer.style.borderRightColor = (index === editorsOfGroup.length - 1) ? (this.getColor(TAB_BORDER) || this.getColor(contrastBorder)) : null; tabContainer.style.outlineColor = this.getColor(activeContrastBorder); - const tabOptions = this.editorGroupService.getTabOptions(); + const tabOptions = {} as IEditorTabOptions; // TODO@grid support tab options (this.editorGroupService.getTabOptions()); ['off', 'left', 'right'].forEach(option => { const domAction = tabOptions.tabCloseButton === option ? DOM.addClass : DOM.removeClass; @@ -349,7 +345,7 @@ export class NextTabsTitleControl extends NextTitleControl { } private getTabLabels(editors: IEditorInput[]): IEditorInputLabel[] { - const labelFormat = this.editorGroupService.getTabOptions().labelFormat; + const labelFormat = 'default'; // TODO@grid support tab options (this.editorGroupService.getTabOptions().labelFormat); const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat); // Build labels and descriptions for each editor @@ -613,9 +609,8 @@ export class NextTabsTitleControl extends NextTitleControl { return void 0; // only for left mouse click } - const { editor, position } = this.getGroupPositionAndEditor(index); if (!this.isTabActionBar(((e as GestureEvent).initialTarget || e.target || e.srcElement) as HTMLElement)) { - setTimeout(() => this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError)); // timeout to keep focus in editor after mouse up + setTimeout(() => this.groupController.openEditor(this.group.getEditor(index))); // timeout to keep focus in editor after mouse up } return void 0; @@ -624,9 +619,7 @@ export class NextTabsTitleControl extends NextTitleControl { const showContextMenu = (e: Event) => { DOM.EventHelper.stop(e); - const { group, editor } = this.getGroupPositionAndEditor(index); - - this.onContextMenu({ group, editor }, e, tab); + this.onContextMenu(this.group.getEditor(index), e, tab); }; // Open on Click @@ -668,12 +661,10 @@ export class NextTabsTitleControl extends NextTitleControl { const event = new StandardKeyboardEvent(e); let handled = false; - const { group, position, editor } = this.getGroupPositionAndEditor(index); - // Run action on Enter/Space if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { handled = true; - this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError); + this.groupController.openEditor(this.group.getEditor(index)); } // Navigate in editors @@ -686,13 +677,13 @@ export class NextTabsTitleControl extends NextTitleControl { } else if (event.equals(KeyCode.Home)) { targetIndex = 0; } else { - targetIndex = group.count - 1; + targetIndex = this.group.count - 1; } - const target = group.getEditor(targetIndex); + const target = this.group.getEditor(targetIndex); if (target) { handled = true; - this.editorService.openEditor(target, { preserveFocus: true }, position).done(null, errors.onUnexpectedError); + this.groupController.openEditor(target, { preserveFocus: true }); (this.tabsContainer.childNodes[targetIndex]).focus(); } } @@ -711,24 +702,20 @@ export class NextTabsTitleControl extends NextTitleControl { disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DBLCLICK, (e: MouseEvent) => { DOM.EventHelper.stop(e); - const { group, editor } = this.getGroupPositionAndEditor(index); - - this.editorGroupService.pinEditor(group, editor); + this.groupController.pinEditor(this.group.getEditor(index)); })); // Context menu disposables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => { DOM.EventHelper.stop(e, true); - const { group, editor } = this.getGroupPositionAndEditor(index); - this.onContextMenu({ group, editor }, e, tab); + this.onContextMenu(this.group.getEditor(index), e, tab); }, true /* use capture to fix https://github.com/Microsoft/vscode/issues/19145 */)); // Drag start disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => { - const { group, editor } = this.getGroupPositionAndEditor(index); - - this.transfer.setData([new DraggedEditorIdentifier({ editor, group })], DraggedEditorIdentifier.prototype); + const editor = this.group.getEditor(index); + this.transfer.setData([new DraggedEditorIdentifier({ editor, group: this.group })], DraggedEditorIdentifier.prototype); e.dataTransfer.effectAllowed = 'copyMove'; @@ -758,8 +745,7 @@ export class NextTabsTitleControl extends NextTitleControl { let draggedEditorIsTab = false; const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0; if (draggedEditor) { - const { group, editor } = this.getGroupPositionAndEditor(index); - if (draggedEditor.editor === editor && draggedEditor.group === group) { + if (draggedEditor.editor === this.group.getEditor(index) && draggedEditor.group === this.group) { draggedEditorIsTab = true; } } @@ -795,9 +781,7 @@ export class NextTabsTitleControl extends NextTitleControl { DOM.removeClass(tab, 'dragged-over'); this.updateDropFeedback(tab, false, index); - const { group, position } = this.getGroupPositionAndEditor(index); - - this.onDrop(e, group, position, index); + this.onDrop(e, index); })); return combinedDisposable(disposables); @@ -807,14 +791,7 @@ export class NextTabsTitleControl extends NextTitleControl { return !!DOM.findParentWithClass(element, 'monaco-action-bar', 'tab'); } - private getGroupPositionAndEditor(index: number): { group: IEditorGroup, position: Position, editor: IEditorInput } { - const position = this.stacks.positionOfGroup(this.group); - const editor = this.group.getEditor(index); - - return { group: this.group, position, editor }; - } - - private onDrop(e: DragEvent, group: IEditorGroup, targetPosition: Position, targetIndex: number): void { + private onDrop(e: DragEvent, targetIndex: number): void { DOM.EventHelper.stop(e, true); this.updateDropFeedback(this.tabsContainer, false); @@ -825,13 +802,14 @@ export class NextTabsTitleControl extends NextTitleControl { if (draggedEditor) { // Move editor to target position and index - if (this.isMoveOperation(e, draggedEditor.group, group)) { - this.editorGroupService.moveEditor(draggedEditor.editor, draggedEditor.group, group, { index: targetIndex }); + if (this.isMoveOperation(e, draggedEditor.group)) { + const sourceGroup = this.nextEditorGroupsService.getGroup(draggedEditor.group.id); + sourceGroup.moveEditor(draggedEditor.editor, this.groupController, { index: targetIndex }); } // Copy: just open editor at target index else { - this.editorService.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }, targetPosition).done(null, errors.onUnexpectedError); + this.groupController.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }); } this.transfer.clearData(); @@ -840,14 +818,14 @@ export class NextTabsTitleControl extends NextTitleControl { // External DND else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false /* open workspace file as file if dropped */ }); - dropHandler.handleDrop(e, () => this.editorGroupService.focusGroup(targetPosition), targetPosition, targetIndex); + dropHandler.handleDrop(e, () => this.groupController.focus(), this.group.id /* TODO@grid position => group id */, targetIndex); } } - private isMoveOperation(e: DragEvent, source: IEditorGroup, target: IEditorGroup) { + private isMoveOperation(e: DragEvent, source: IEditorGroup) { const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); - return !isCopy || source.id === target.id; + return !isCopy || source.id === this.group.id; } dispose(): void { diff --git a/src/vs/workbench/browser/parts/editor2/nextTitleControl.ts b/src/vs/workbench/browser/parts/editor2/nextTitleControl.ts index c8679e07957..3ad5cd72926 100644 --- a/src/vs/workbench/browser/parts/editor2/nextTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor2/nextTitleControl.ts @@ -16,9 +16,7 @@ import * as arrays from 'vs/base/common/arrays'; import { IEditorStacksModel, IEditorIdentifier, EditorInput, toResource, IEditorCommandsContext, IEditorGroup, EditorOptions } from 'vs/workbench/common/editor'; import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -37,7 +35,8 @@ import { isDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension } from 'vs/base/browser/dom'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { INextEditorGroupsService, Direction } from 'vs/workbench/services/editor/common/nextEditorGroupsService'; +import { INextEditorGroupsService, Direction, INextEditorGroup } from 'vs/workbench/services/editor/common/nextEditorGroupsService'; +import { IEditorInput } from 'vs/platform/editor/common/editor'; export interface IToolbarActions { primary: IAction[]; @@ -46,9 +45,11 @@ export interface IToolbarActions { export interface INextTitleAreaControl extends IDisposable { - openEditor(input: EditorInput, options?: EditorOptions): void; + openEditor(editor: EditorInput, options?: EditorOptions): void; + closeEditor(editor: EditorInput): void; + moveEditor(editor: EditorInput, targetIndex: number): void; setActive(isActive: boolean): void; - pinEditor(input: EditorInput): void; + pinEditor(editor: EditorInput): void; layout(dimension: Dimension): void; } @@ -79,8 +80,6 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre protected group: IEditorGroup, @IContextMenuService protected contextMenuService: IContextMenuService, @IInstantiationService protected instantiationService: IInstantiationService, - @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, - @IEditorGroupService protected editorGroupService: IEditorGroupService, @INextEditorGroupsService protected nextEditorGroupsService: INextEditorGroupsService, @IContextKeyService protected contextKeyService: IContextKeyService, @IKeybindingService protected keybindingService: IKeybindingService, @@ -93,7 +92,6 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre ) { super(themeService); - this.stacks = editorGroupService.getStacksModel(); this.mapActionsToEditors = Object.create(null); this.titleAreaUpdateScheduler = new RunOnceScheduler(() => this.onSchedule(), 0); @@ -156,6 +154,10 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre this.titleAreaToolbarUpdateScheduler.cancel(); // a title area update will always refresh the toolbar too } + protected get groupController(): INextEditorGroup { + return this.nextEditorGroupsService.getGroup(this.group.id); + } + protected doUpdate(): void { this.doRefresh(); } @@ -198,13 +200,12 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre } protected actionItemProvider(action: Action): IActionItem { - const position = this.stacks.positionOfGroup(this.group); - const editor = this.editorService.getVisibleEditors()[position]; + const activeControl = this.groupController.activeControl; // Check Active Editor let actionItem: IActionItem; - if (editor instanceof BaseEditor) { - actionItem = editor.getActionItem(action); + if (activeControl instanceof BaseEditor) { + actionItem = activeControl.getActionItem(action); } // Check extensions @@ -220,20 +221,19 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre const secondary: IAction[] = []; const { group } = identifier; - const position = this.stacks.positionOfGroup(group); // Update the resource context this.resourceContext.set(group && toResource(group.activeEditor, { supportSideBySide: true })); // Editor actions require the editor control to be there, so we retrieve it via service - const control = this.editorService.getVisibleEditors()[position]; - if (control instanceof BaseEditor && control.input && typeof control.position === 'number') { + const activeControl = this.groupController.activeControl; + if (activeControl instanceof BaseEditor && activeControl.input && typeof activeControl.position === 'number') { // Editor Control Actions - let editorActions = this.mapActionsToEditors[control.getId()]; + let editorActions = this.mapActionsToEditors[activeControl.getId()]; if (!editorActions) { - editorActions = { primary: control.getActions(), secondary: control.getSecondaryActions() }; - this.mapActionsToEditors[control.getId()] = editorActions; + editorActions = { primary: activeControl.getActions(), secondary: activeControl.getSecondaryActions() }; + this.mapActionsToEditors[activeControl.getId()] = editorActions; } primary.push(...editorActions.primary); secondary.push(...editorActions.secondary); @@ -243,7 +243,7 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre // use the correct context key service per editor only once. Don't // take this code as sample of how to work with menus this.disposeOnEditorActions = dispose(this.disposeOnEditorActions); - const widget = control.getControl(); + const widget = activeControl.getControl(); const codeEditor = isCodeEditor(widget) && widget || isDiffEditor(widget) && widget.getModifiedEditor(); const scopedContextKeyService = codeEditor && codeEditor.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService; const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService); @@ -264,7 +264,7 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre protected updateEditorActionsToolbar(): void { const editor = this.group.activeEditor; - const isActive = this.nextEditorGroupsService.isGroupActive(this.group.id); + const isActive = this.groupController.isActive; // Update Editor Actions Toolbar let primaryEditorActions: IAction[] = []; @@ -295,8 +295,7 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre secondaryEditorActions = prepareActions(editorActions.secondary); - const tabOptions = this.editorGroupService.getTabOptions(); - tabOptions.showTabs = true; // TODO@grid support real options + const tabOptions = { showTabs: true }; // TODO@grid support real options (this.editorGroupService.getTabOptions();) const primaryEditorActionIds = primaryEditorActions.map(a => a.id); if (!tabOptions.showTabs) { @@ -328,11 +327,11 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre this.currentSecondaryEditorActionIds = []; } - protected onContextMenu(identifier: IEditorIdentifier, e: Event, node: HTMLElement): void { + protected onContextMenu(editor: IEditorInput, e: Event, node: HTMLElement): void { // Update the resource context const currentContext = this.resourceContext.get(); - this.resourceContext.set(toResource(identifier.editor, { supportSideBySide: true })); + this.resourceContext.set(toResource(editor, { supportSideBySide: true })); // Find target anchor let anchor: HTMLElement | { x: number, y: number } = node; @@ -349,18 +348,15 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => TPromise.as(actions), - getActionsContext: () => ({ groupId: identifier.group.id, editorIndex: identifier.group.indexOf(identifier.editor) } as IEditorCommandsContext), + getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.indexOf(editor) } as IEditorCommandsContext), getKeyBinding: (action) => this.getKeybinding(action), onHide: (cancel) => { // restore previous context this.resourceContext.set(currentContext); - // restore focus to active editor if any - const editor = this.editorService.getActiveEditor(); - if (editor) { - editor.focus(); - } + // restore focus to active group + this.nextEditorGroupsService.activeGroup.focus(); } }); } @@ -385,11 +381,19 @@ export abstract class NextTitleControl extends Themable implements INextTitleAre //#region INextTitleAreaControl - openEditor(input: EditorInput, options?: EditorOptions): void { + openEditor(editor: EditorInput, options?: EditorOptions): void { this.doScheduleRefresh(true); // TODO@grid optimize if possible } - pinEditor(input: EditorInput): void { + closeEditor(editor: EditorInput): void { + this.doScheduleRefresh(); // TODO@grid optimize if possible + } + + moveEditor(editor: EditorInput, targetIndex: number): void { + this.doScheduleUpdate(true); // TODO@grid optimize if possible + } + + pinEditor(editor: EditorInput): void { this.doScheduleUpdate(true); // TODO@grid optimize if possible } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 29cb171b382..2fdac503628 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -789,7 +789,7 @@ export interface IEditorGroup { } export interface IEditorIdentifier { - group: IEditorGroup; + group: IEditorGroup; // TODO@grid this should be the group identifier instead editor: IEditorInput; } diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index b24d5e6b48b..475a4742ec2 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -597,8 +597,8 @@ export class Workbench implements IPartService { // Editor service (next editor part) this.editorPart = this.instantiationService.createInstance(NextEditorPart, Identifiers.EDITOR_PART /*, !this.hasFilesToCreateOpenOrDiff*/); this.toUnbind.push(toDisposable(() => this.editorPart.shutdown())); - serviceCollection.set(INextEditorService, new SyncDescriptor(NextEditorService, this.editorPart)); serviceCollection.set(INextEditorGroupsService, this.editorPart); + serviceCollection.set(INextEditorService, new SyncDescriptor(NextEditorService)); // Legacy Editor Services this.noOpEditorPart = new NoOpEditorPart(this.instantiationService); diff --git a/src/vs/workbench/services/editor/browser/nextEditorService.ts b/src/vs/workbench/services/editor/browser/nextEditorService.ts index 41be37a8ae8..8cd50eb6852 100644 --- a/src/vs/workbench/services/editor/browser/nextEditorService.ts +++ b/src/vs/workbench/services/editor/browser/nextEditorService.ts @@ -5,26 +5,280 @@ 'use strict'; -import { INextEditorService } from 'vs/workbench/services/editor/common/nextEditorService'; -import { NextEditorPart } from 'vs/workbench/browser/parts/editor2/nextEditorPart'; -// import { ResourceMap } from 'vs/base/common/map'; -// import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -// import { IFileEditorInput, IFileInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; -// import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput'; -// import { Registry } from 'vs/platform/registry/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorInput, IResourceInput, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditor, ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { GroupIdentifier, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor'; +import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ResourceMap } from 'vs/base/common/map'; +import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Schemas } from 'vs/base/common/network'; +import { getPathLabel } from 'vs/base/common/labels'; +import { once } from 'vs/base/common/event'; +import URI from 'vs/base/common/uri'; +import { basename } from 'vs/base/common/paths'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { localize } from 'vs/nls'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { INextEditorGroupsService, INextEditorGroup } from 'vs/workbench/services/editor/common/nextEditorGroupsService'; +import { INextEditorService, IResourceEditor, SIDE_BY_SIDE } from 'vs/workbench/services/editor/common/nextEditorService'; -// type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; +type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; export class NextEditorService implements INextEditorService { - public _serviceBrand: any; + _serviceBrand: any; - // private static CACHE: ResourceMap = new ResourceMap(); - // private fileInputFactory: IFileInputFactory; + private static CACHE: ResourceMap = new ResourceMap(); + + private fileInputFactory: IFileInputFactory; constructor( - /* private */ editorPart: NextEditorPart + @INextEditorGroupsService private nextEditorGroupsService: INextEditorGroupsService, + @IUntitledEditorService private untitledEditorService: IUntitledEditorService, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IInstantiationService private instantiationService: IInstantiationService, + @IEnvironmentService private environmentService: IEnvironmentService, + @IFileService private fileService: IFileService ) { - // this.fileInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileInputFactory(); + this.fileInputFactory = Registry.as(EditorExtensions.EditorInputFactories).getFileInputFactory(); + } + + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: GroupIdentifier | SIDE_BY_SIDE): Thenable; + openEditor(editor: IResourceEditor, group?: GroupIdentifier | SIDE_BY_SIDE): Thenable; + openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | GroupIdentifier, group?: GroupIdentifier): Thenable { + + // Typed Editor Support + if (editor instanceof EditorInput) { + return this.doOpenEditor(editor, this.toOptions(optionsOrGroup as IEditorOptions), group); + } + + // Throw error for well known foreign resources (such as a http link) (TODO@ben remove me after this has been adopted) + const resourceInput = editor; + if (resourceInput.resource instanceof URI) { + const schema = resourceInput.resource.scheme; + if (schema === Schemas.http || schema === Schemas.https) { + return TPromise.wrapError(new Error('Invalid scheme http/https to open resource as editor. Use IOpenerService instead.')); + } + } + + // Untyped Text Editor Support + const textInput = editor; + const typedInput = this.createInput(textInput); + if (typedInput) { + return this.doOpenEditor(typedInput, TextEditorOptions.from(textInput), optionsOrGroup as GroupIdentifier); + } + + return TPromise.wrap(null); } -} \ No newline at end of file + + private doOpenEditor(input: IEditorInput, options?: EditorOptions, group?: GroupIdentifier | SIDE_BY_SIDE): Thenable { + let targetGroup: INextEditorGroup; + + if (group === -1) { + group = void 0; // TODO@grid find correct side group based on active group + } + + if (typeof group === 'number') { + targetGroup = this.nextEditorGroupsService.getGroup(group); + } + + if (!targetGroup) { + targetGroup = this.nextEditorGroupsService.activeGroup; + } + + return targetGroup.openEditor(input, options).then(() => targetGroup.activeControl); + } + + 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); + } + + createInput(input: IEditorInput | IResourceEditor): EditorInput { + + // Typed Editor 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 || 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) as EditorInput; + } + + return null; + } + + private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string): ICachedEditorInput { + if (NextEditorService.CACHE.has(resource)) { + const input = NextEditorService.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); + } + + NextEditorService.CACHE.set(resource, input); + once(input.onDispose)(() => { + NextEditorService.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); + } +} + +//#region TODO@grid adopt legacy code to find a position based on options +// private findPosition(input: EditorInput, options ?: EditorOptions, sideBySide ?: boolean, ratio ?: number[]): Position; +// private findPosition(input: EditorInput, options ?: EditorOptions, desiredPosition ?: Position, ratio ?: number[]): Position; +// private findPosition(input: EditorInput, options ?: EditorOptions, arg1 ?: any, ratio ?: number[]): Position { + +// // With defined ratios, always trust the provided position +// if (ratio && types.isNumber(arg1)) { +// return arg1; +// } + +// // No editor open +// const visibleEditors = this.getVisibleEditors(); +// const activeEditor = this.getActiveEditor(); +// if (visibleEditors.length === 0 || !activeEditor) { +// return Position.ONE; // can only be ONE +// } + +// // Ignore revealIfVisible/revealIfOpened option if we got instructed explicitly to +// // * open at a specific index +// // * open to the side +// // * open in a specific group +// const skipReveal = (options && options.index) || arg1 === true /* open to side */ || typeof arg1 === 'number' /* open specific group */; + +// // Respect option to reveal an editor if it is already visible +// if (!skipReveal && options && options.revealIfVisible) { +// const group = this.stacks.findGroup(input, true); +// if (group) { +// return this.stacks.positionOfGroup(group); +// } +// } + +// // Respect option to reveal an editor if it is open (not necessarily visible) +// if (!skipReveal && (this.revealIfOpen /* workbench.editor.revealIfOpen */ || (options && options.revealIfOpened))) { +// const group = this.stacks.findGroup(input); +// if (group) { +// return this.stacks.positionOfGroup(group); +// } +// } + +// // Position is unknown: pick last active or ONE +// if (types.isUndefinedOrNull(arg1) || arg1 === false) { +// const lastActivePosition = this.editorGroupsControl.getActivePosition(); + +// return lastActivePosition || Position.ONE; +// } + +// // Position is sideBySide: Find position relative to active editor +// if (arg1 === true) { +// switch (activeEditor.position) { +// case Position.ONE: +// return Position.TWO; +// case Position.TWO: +// return Position.THREE; +// case Position.THREE: +// return null; // Cannot open to the side of the right/bottom most editor +// } + +// return null; // Prevent opening to the side +// } + +// // Position is provided, validate it +// if (arg1 === Position.THREE && visibleEditors.length === 1) { +// return Position.TWO; +// } + +// return arg1; +// } +//#endregion \ No newline at end of file diff --git a/src/vs/workbench/services/editor/common/nextEditorService.ts b/src/vs/workbench/services/editor/common/nextEditorService.ts index edd513f89d2..05b5dbd8213 100644 --- a/src/vs/workbench/services/editor/common/nextEditorService.ts +++ b/src/vs/workbench/services/editor/common/nextEditorService.ts @@ -6,81 +6,25 @@ 'use strict'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorInput, IResourceInput, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditor, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { GroupIdentifier } from 'vs/workbench/common/editor'; export const INextEditorService = createDecorator('nextEditorService'); +export type IResourceEditor = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput; + +export type SIDE_BY_SIDE = -1; + // TODO@grid this should provide convinience methods on top of INextEditorGroupsService to make the 99% // case of opening editors as simple as possible // Candidates: // - getVisibleEditors (text only?) export interface INextEditorService { _serviceBrand: ServiceIdentifier; -} - -// Legacy code to find a position based on options -// private findPosition(input: EditorInput, options ?: EditorOptions, sideBySide ?: boolean, ratio ?: number[]): Position; -// private findPosition(input: EditorInput, options ?: EditorOptions, desiredPosition ?: Position, ratio ?: number[]): Position; -// private findPosition(input: EditorInput, options ?: EditorOptions, arg1 ?: any, ratio ?: number[]): Position { - -// // With defined ratios, always trust the provided position -// if (ratio && types.isNumber(arg1)) { -// return arg1; -// } - -// // No editor open -// const visibleEditors = this.getVisibleEditors(); -// const activeEditor = this.getActiveEditor(); -// if (visibleEditors.length === 0 || !activeEditor) { -// return Position.ONE; // can only be ONE -// } - -// // Ignore revealIfVisible/revealIfOpened option if we got instructed explicitly to -// // * open at a specific index -// // * open to the side -// // * open in a specific group -// const skipReveal = (options && options.index) || arg1 === true /* open to side */ || typeof arg1 === 'number' /* open specific group */; - -// // Respect option to reveal an editor if it is already visible -// if (!skipReveal && options && options.revealIfVisible) { -// const group = this.stacks.findGroup(input, true); -// if (group) { -// return this.stacks.positionOfGroup(group); -// } -// } - -// // Respect option to reveal an editor if it is open (not necessarily visible) -// if (!skipReveal && (this.revealIfOpen /* workbench.editor.revealIfOpen */ || (options && options.revealIfOpened))) { -// const group = this.stacks.findGroup(input); -// if (group) { -// return this.stacks.positionOfGroup(group); -// } -// } - -// // Position is unknown: pick last active or ONE -// if (types.isUndefinedOrNull(arg1) || arg1 === false) { -// const lastActivePosition = this.editorGroupsControl.getActivePosition(); - -// return lastActivePosition || Position.ONE; -// } - -// // Position is sideBySide: Find position relative to active editor -// if (arg1 === true) { -// switch (activeEditor.position) { -// case Position.ONE: -// return Position.TWO; -// case Position.TWO: -// return Position.THREE; -// case Position.THREE: -// return null; // Cannot open to the side of the right/bottom most editor -// } - -// return null; // Prevent opening to the side -// } -// // Position is provided, validate it -// if (arg1 === Position.THREE && visibleEditors.length === 1) { -// return Position.TWO; -// } + // TODO@grid think about a better return type, is the IEditor needed always? Should it be ITextEditor? + openEditor(editor: IEditorInput, options?: IEditorOptions, group?: GroupIdentifier | SIDE_BY_SIDE): Thenable; + openEditor(editor: IResourceEditor, group?: GroupIdentifier | SIDE_BY_SIDE): Thenable; -// return arg1; -// } \ No newline at end of file + createInput(input: IResourceEditor): IEditorInput; +} \ No newline at end of file -- GitLab