diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 1541403b4822fed0d40eca0b055ca66c38392728..94ab58c880eda7b686c40ad90386a0cc0934ca63 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -543,6 +543,50 @@ export class CloseEditorAction extends Action { } } +export class CloseOneEditorAction extends Action { + + public static readonly ID = 'workbench.action.closeActiveEditor'; + public static readonly LABEL = nls.localize('closeOneEditor', "Close"); + + constructor( + id: string, + label: string, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEditorGroupService private editorGroupService: IEditorGroupService + ) { + super(id, label, 'close-editor-action'); + } + + public run(context?: IEditorCommandsContext): TPromise { + const model = this.editorGroupService.getStacksModel(); + + const group = context ? model.getGroup(context.groupId) : null; + const position = group ? model.positionOfGroup(group) : null; + + // Close Active Editor + if (typeof position !== 'number') { + const activeEditor = this.editorService.getActiveEditor(); + if (activeEditor) { + return this.editorService.closeEditor(activeEditor.position, activeEditor.input); + } + } + + // Close Specific Editor + const editor = group && context && typeof context.editorIndex === 'number' ? group.getEditor(context.editorIndex) : null; + if (editor) { + return this.editorService.closeEditor(position, editor); + } + + // Close First Editor at Position + const visibleEditors = this.editorService.getVisibleEditors(); + if (visibleEditors[position]) { + return this.editorService.closeEditor(position, visibleEditors[position].input); + } + + return TPromise.as(false); + } +} + export class RevertAndCloseEditorAction extends Action { public static readonly ID = 'workbench.action.revertAndCloseActiveEditor'; diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 7a36e4d1930938e6644ae636cefdad313f1a4626..fa570c9cf6a1ab1ac74a30bf555284029ec1b06c 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -81,7 +81,7 @@ export class NoTabsTitleControl extends TitleControl { // Close editor on middle mouse click if (e instanceof MouseEvent && e.button === 1 /* Middle Button */) { - this.closeEditorAction.run({ groupId: group.id, editorIndex: group.indexOf(group.activeEditor) }).done(null, errors.onUnexpectedError); + this.closeOneEditorAction.run({ groupId: group.id, editorIndex: group.indexOf(group.activeEditor) }).done(null, errors.onUnexpectedError); } // Focus editor group unless: diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 049c0f4d0e9aa997f9edc37a317a79948b67451a..696d593bbf12faca9e78f78b3e07e88b7dada6a7 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -20,7 +20,7 @@ import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/brow import { KeyCode } from 'vs/base/common/keyCodes'; import { ResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -34,6 +34,7 @@ import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecyc import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_BACKGROUND, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; @@ -83,6 +84,34 @@ export class TabsTitleControl extends TitleControl { this.editorLabels = []; } + protected initActions(services: IInstantiationService): void { + super.initActions(this.createScopedInstantiationService()); + } + + 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 + // Specifically we want to move focus back to the editor when an inactive editor is closed from anywhere + // in the tabs title control (e.g. mouse middle click, context menu on tab). This is only needed for + // 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); + } + + this.blockRevealActiveTab = true; + + return TPromise.as(void 0); + }); + + return this.instantiationService.createChild(new ServiceCollection([IWorkbenchEditorService, delegatingEditorService])); + } + public create(parent: HTMLElement): void { super.create(parent); @@ -517,7 +546,7 @@ export class TabsTitleControl extends TitleControl { this.tabDisposeables.push(actionRunner); const bar = new ActionBar(tabCloseContainer, { ariaLabel: nls.localize('araLabelTabActions', "Tab actions"), actionRunner }); - bar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) }); + bar.push(this.closeOneEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeOneEditorAction) }); // Eventing const disposable = this.hookTabListeners(tabContainer, index); @@ -636,7 +665,7 @@ export class TabsTitleControl extends TitleControl { tab.blur(); if (e.button === 1 /* Middle Button*/ && !this.isTabActionBar((e.target || e.srcElement) as HTMLElement)) { - this.closeEditorAction.run({ groupId: this.context.id, editorIndex: index }).done(null, errors.onUnexpectedError); + this.closeOneEditorAction.run({ groupId: this.context.id, editorIndex: index }).done(null, errors.onUnexpectedError); } })); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index e393a66d667ea834921fd4b6d25bc7630595896c..fda09f5920b287402111b8c8fc1a1e1ed08234ce 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -28,7 +28,7 @@ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { SplitEditorAction, CloseEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { SplitEditorAction, CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createActionItem, fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IMenuService, MenuId, IMenu, ExecuteCommandAction } from 'vs/platform/actions/common/actions'; @@ -65,7 +65,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl protected dragged: boolean; - protected closeEditorAction: CloseEditorAction; + protected closeOneEditorAction: CloseOneEditorAction; protected splitEditorAction: SplitEditorAction; private parent: HTMLElement; @@ -75,7 +75,8 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl protected editorActionsToolbar: ToolBar; private mapActionsToEditors: { [editorId: string]: IToolbarActions; }; - private scheduler: RunOnceScheduler; + private titleAreaUpdateScheduler: RunOnceScheduler; + private titleAreaToolbarUpdateScheduler: RunOnceScheduler; private refreshScheduled: boolean; private resourceContext: ResourceContextKey; @@ -101,8 +102,11 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl this.stacks = editorGroupService.getStacksModel(); this.mapActionsToEditors = Object.create(null); - this.scheduler = new RunOnceScheduler(() => this.onSchedule(), 0); - this.toUnbind.push(this.scheduler); + this.titleAreaUpdateScheduler = new RunOnceScheduler(() => this.onSchedule(), 0); + this.toUnbind.push(this.titleAreaUpdateScheduler); + + this.titleAreaToolbarUpdateScheduler = new RunOnceScheduler(() => this.updateEditorActionsToolbar(), 0); + this.toUnbind.push(this.titleAreaToolbarUpdateScheduler); this.resourceContext = instantiationService.createInstance(ResourceContextKey); @@ -166,22 +170,26 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl public update(instant?: boolean): void { if (instant) { - this.scheduler.cancel(); + this.titleAreaUpdateScheduler.cancel(); this.onSchedule(); } else { - this.scheduler.schedule(); + this.titleAreaUpdateScheduler.schedule(); } + + this.titleAreaToolbarUpdateScheduler.cancel(); // a title area update will always refresh the toolbar too } public refresh(instant?: boolean) { this.refreshScheduled = true; if (instant) { - this.scheduler.cancel(); + this.titleAreaUpdateScheduler.cancel(); this.onSchedule(); } else { - this.scheduler.schedule(); + this.titleAreaUpdateScheduler.schedule(); } + + this.titleAreaToolbarUpdateScheduler.cancel(); // a title area update will always refresh the toolbar too } public create(parent: HTMLElement): void { @@ -207,7 +215,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl } protected initActions(services: IInstantiationService): void { - this.closeEditorAction = services.createInstance(CloseEditorAction, CloseEditorAction.ID, nls.localize('close', "Close")); + this.closeOneEditorAction = services.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL); this.splitEditorAction = services.createInstance(SplitEditorAction, SplitEditorAction.ID, SplitEditorAction.LABEL); } @@ -296,7 +304,14 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl 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); - this.disposeOnEditorActions.push(titleBarMenu, titleBarMenu.onDidChange(_ => this.update())); + this.disposeOnEditorActions.push(titleBarMenu, titleBarMenu.onDidChange(_ => { + // schedule the update for the title area toolbar only if no other + // update to the title area is scheduled which will always also + // update the toolbar + if (!this.titleAreaUpdateScheduler.isScheduled()) { + this.titleAreaToolbarUpdateScheduler.schedule(); + } + })); fillInActions(titleBarMenu, { arg: this.resourceContext.get(), shouldForwardArgs: true }, { primary, secondary }, this.contextMenuService); } @@ -334,11 +349,10 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl const primaryEditorActionIds = primaryEditorActions.map(a => a.id); if (!tabOptions.showTabs) { - primaryEditorActionIds.push(this.closeEditorAction.id); // always show "Close" when tabs are disabled + primaryEditorActionIds.push(this.closeOneEditorAction.id); // always show "Close" when tabs are disabled } const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id); - if ( !arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) || !arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds) || @@ -348,7 +362,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl this.editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions)(); if (!tabOptions.showTabs) { - this.editorActionsToolbar.addPrimaryAction(this.closeEditorAction)(); + this.editorActionsToolbar.addPrimaryAction(this.closeOneEditorAction)(); } this.currentPrimaryEditorActionIds = primaryEditorActionIds; @@ -416,7 +430,7 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl // Actions [ this.splitEditorAction, - this.closeEditorAction + this.closeOneEditorAction ].forEach((action) => { action.dispose(); }); diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 7a56d26f02bdea38d27d649f48da2fdc1697b9ee..5ddaced115d63c3d292077ab56e8b48f9491b8a6 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -268,6 +268,10 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { } 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); } @@ -396,6 +400,7 @@ export interface IEditorCloseHandler { */ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { private editorOpenHandler: IEditorOpenHandler; + private editorCloseHandler: IEditorCloseHandler; constructor( @IUntitledEditorService untitledEditorService: IUntitledEditorService, @@ -419,6 +424,10 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { 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 { @@ -432,4 +441,12 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { 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); + }); + } } diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index e2e3f68c14474a8b3b1e1ea907956695f10ebc01..e83cf741b00c0b78d984703e0fa1d56ef4292191 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -266,11 +266,18 @@ suite('WorkbenchEditorService', () => { delegate.setEditorOpenHandler((input: IEditorInput, options?: EditorOptions) => { assert.strictEqual(input, inp); + return TPromise.as(ed); + }); + + delegate.setEditorCloseHandler((position, input) => { + assert.strictEqual(input, inp); + done(); - return TPromise.as(ed); + return TPromise.as(void 0); }); delegate.openEditor(inp); + delegate.closeEditor(0, inp); }); });