diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 138541a6ecfb7..066385368f863 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -242,6 +242,15 @@ export interface IEditorGroupTitleHeight { readonly offset: number; } +export interface IEditorGroupViewOptions { + + /** + * Whether the editor group should receive keyboard focus + * after creation or not. + */ + readonly preserveFocus?: boolean; +} + /** * A helper to access and mutate an editor group within an editor part. */ diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index d88cd67318658..6c8c42ec330a3 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -28,7 +28,7 @@ import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; -import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsView, IEditorGroupView, fillActiveEditorViewState, EditorServiceImpl, IEditorGroupTitleHeight, IInternalEditorOpenOptions, IInternalMoveCopyOptions, IInternalEditorCloseOptions, IInternalEditorTitleControlOptions, IEditorPartsView, IEditorGroupViewOptions } from 'vs/workbench/browser/parts/editor/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAction, SubmenuAction } from 'vs/base/common/actions'; @@ -63,16 +63,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region factory - static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex); + static createNew(editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, null, editorPartsView, groupsView, groupsLabel, groupIndex, options); } - static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex); + static createFromSerialized(serialized: ISerializedEditorGroupModel, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, serialized, editorPartsView, groupsView, groupsLabel, groupIndex, options); } - static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService): IEditorGroupView { - return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex); + static createCopy(copyFrom: IEditorGroupView, editorPartsView: IEditorPartsView, groupsView: IEditorGroupsView, groupsLabel: string, groupIndex: number, instantiationService: IInstantiationService, options?: IEditorGroupViewOptions): IEditorGroupView { + return instantiationService.createInstance(EditorGroupView, copyFrom, editorPartsView, groupsView, groupsLabel, groupIndex, options); } //#endregion @@ -145,6 +145,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { readonly groupsView: IEditorGroupsView, private groupsLabel: string, private _index: number, + options: IEditorGroupViewOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @@ -236,7 +237,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#endregion // Restore editors if provided - const restoreEditorsPromise = this.restoreEditors(from) ?? Promise.resolve(); + const restoreEditorsPromise = this.restoreEditors(from, options) ?? Promise.resolve(); // Signal restored once editors have restored restoreEditorsPromise.finally(() => { @@ -534,7 +535,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleContainer.classList.toggle('show-file-icons', this.groupsView.partOptions.showIcons); } - private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null): Promise | undefined { + private restoreEditors(from: IEditorGroupView | ISerializedEditorGroupModel | null, groupViewOptions?: IEditorGroupViewOptions): Promise | undefined { if (this.count === 0) { return; // nothing to show } @@ -571,7 +572,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // stolen accidentally on startup when the user already // clicked somewhere. - if (this.groupsView.activeGroup === this && activeElement && isActiveElement(activeElement)) { + if (this.groupsView.activeGroup === this && activeElement && isActiveElement(activeElement) && !groupViewOptions?.preserveFocus) { this.focus(); } }); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index e467ab0270ea3..6d2df54a86672 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -14,7 +14,7 @@ import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGr import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce } from 'vs/base/common/arrays'; -import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView, IEditorGroupViewOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -615,16 +615,16 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } } - private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroupModel | null): IEditorGroupView { + private doCreateGroupView(from?: IEditorGroupView | ISerializedEditorGroupModel | null, options?: IEditorGroupViewOptions): IEditorGroupView { // Create group view let groupView: IEditorGroupView; if (from instanceof EditorGroupView) { - groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService,); + groupView = EditorGroupView.createCopy(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } else if (isSerializedEditorGroupModel(from)) { - groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService); + groupView = EditorGroupView.createFromSerialized(from, this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } else { - groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService); + groupView = EditorGroupView.createNew(this.editorPartsView, this, this.groupsLabel, this.count, this.scopedInstantiationService, options); } // Keep in map @@ -1192,7 +1192,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { return true; // success } - private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + private doCreateGridControlWithState(serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[], options?: IEditorGroupViewOptions): void { // Determine group views to reuse if any let reuseGroupViews: IEditorGroupView[]; @@ -1210,7 +1210,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { if (reuseGroupViews.length > 0) { groupView = reuseGroupViews.shift()!; } else { - groupView = this.doCreateGroupView(serializedEditorGroup); + groupView = this.doCreateGroupView(serializedEditorGroup, options); } groupViews.push(groupView); @@ -1342,15 +1342,15 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { }; } - applyState(state: IEditorPartUIState | 'empty'): Promise { + applyState(state: IEditorPartUIState | 'empty', options?: IEditorGroupViewOptions): Promise { if (state === 'empty') { return this.doApplyEmptyState(); } else { - return this.doApplyState(state); + return this.doApplyState(state, options); } } - private async doApplyState(state: IEditorPartUIState): Promise { + private async doApplyState(state: IEditorPartUIState, options?: IEditorGroupViewOptions): Promise { const groups = await this.doPrepareApplyState(); const resumeEvents = this.disposeGroups(true /* suspress events for the duration of applying state */); @@ -1359,7 +1359,7 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { // Grid Widget try { - this.doApplyGridState(state.serializedGrid, state.activeGroup); + this.doApplyGridState(state.serializedGrid, state.activeGroup, undefined, options); } finally { resumeEvents(); } @@ -1396,10 +1396,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { return groups; } - private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + private doApplyGridState(gridState: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[], options?: IEditorGroupViewOptions): void { // Recreate grid widget from state - this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse); + this.doCreateGridControlWithState(gridState, activeGroupId, editorGroupViewsToReuse, options); // Layout this.doLayout(this._contentDimension); diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 574c97e41539e..564464012e0d2 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Emitter } from 'vs/base/common/event'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { GroupIdentifier } from 'vs/workbench/common/editor'; @@ -375,7 +375,7 @@ export class EditorParts extends MultiWindowParts implements IEditor } } - async applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise { + async applyWorkingSet(workingSet: IEditorWorkingSet | 'empty', options?: IEditorWorkingSetOptions): Promise { let workingSetState: IEditorWorkingSetState | 'empty' | undefined; if (workingSet === 'empty') { workingSetState = 'empty'; @@ -395,13 +395,15 @@ export class EditorParts extends MultiWindowParts implements IEditor if (!applied) { return false; } - await this.mainPart.applyState(workingSetState === 'empty' ? workingSetState : workingSetState.main); - - // Restore Focus - const mostRecentActivePart = firstOrDefault(this.mostRecentActiveParts); - if (mostRecentActivePart) { - await mostRecentActivePart.whenReady; - mostRecentActivePart.activeGroup.focus(); + await this.mainPart.applyState(workingSetState === 'empty' ? workingSetState : workingSetState.main, options); + + // Restore Focus unless instructed otherwise + if (!options?.preserveFocus) { + const mostRecentActivePart = firstOrDefault(this.mostRecentActiveParts); + if (mostRecentActivePart) { + await mostRecentActivePart.whenReady; + mostRecentActivePart.activeGroup.focus(); + } } return true; diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 498b667390e3b..3a023470ea207 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -11,6 +11,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { getProviderKey } from 'vs/workbench/contrib/scm/browser/util'; import { ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; import { IEditorGroupsService, IEditorWorkingSet } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; type ISCMSerializedWorkingSet = { readonly providerKey: string; @@ -35,7 +36,8 @@ export class SCMWorkingSetController implements IWorkbenchContribution { @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ISCMService private readonly scmService: ISCMService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.workingSets.enabled'), this._disposables); this._disposables.add(Event.runAndSubscribe(onDidChangeConfiguration, () => this._onDidChangeConfiguration())); @@ -147,7 +149,13 @@ export class SCMWorkingSetController implements IWorkbenchContribution { } if (editorWorkingSetId) { - await this.editorGroupsService.applyWorkingSet(editorWorkingSetId); + // Applying a working set can be the result of a user action that has been + // initiated from the terminal (ex: switching branches). As such, we want + // to preserve the focus in the terminal. This does not cover the scenario + // in which the terminal is in the editor part. + const preserveFocus = this.layoutService.hasFocus(Parts.PANEL_PART); + + await this.editorGroupsService.applyWorkingSet(editorWorkingSetId, { preserveFocus }); } } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 6a7acef2ae618..b90d84720f01c 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -491,6 +491,10 @@ export interface IEditorWorkingSet { readonly name: string; } +export interface IEditorWorkingSetOptions { + readonly preserveFocus?: boolean; +} + export interface IEditorGroupContextKeyProvider { /** @@ -573,7 +577,7 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { * * @returns `true` when the working set as applied. */ - applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise; + applyWorkingSet(workingSet: IEditorWorkingSet | 'empty', options?: IEditorWorkingSetOptions): Promise; /** * Deletes a working set. diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index cf3df1f56ce65..e7c8dbf262aff 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -52,7 +52,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet, IEditorGroupContextKeyProvider, IEditorWorkingSetOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; @@ -841,7 +841,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { getPart(group: number | IEditorGroup): IEditorPart { return this; } saveWorkingSet(name: string): IEditorWorkingSet { throw new Error('Method not implemented.'); } getWorkingSets(): IEditorWorkingSet[] { throw new Error('Method not implemented.'); } - applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise { throw new Error('Method not implemented.'); } + applyWorkingSet(workingSet: IEditorWorkingSet | 'empty', options?: IEditorWorkingSetOptions): Promise { throw new Error('Method not implemented.'); } deleteWorkingSet(workingSet: IEditorWorkingSet): Promise { throw new Error('Method not implemented.'); } getGroups(_order?: GroupsOrder): readonly IEditorGroup[] { return this.groups; } getGroup(identifier: number): IEditorGroup | undefined { return this.groups.find(group => group.id === identifier); } @@ -1841,7 +1841,7 @@ export class TestEditorPart extends MainEditorPart implements IEditorGroupsServi saveWorkingSet(name: string): IEditorWorkingSet { throw new Error('Method not implemented.'); } getWorkingSets(): IEditorWorkingSet[] { throw new Error('Method not implemented.'); } - applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise { throw new Error('Method not implemented.'); } + applyWorkingSet(workingSet: IEditorWorkingSet | 'empty', options?: IEditorWorkingSetOptions): Promise { throw new Error('Method not implemented.'); } deleteWorkingSet(workingSet: IEditorWorkingSet): Promise { throw new Error('Method not implemented.'); } registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { throw new Error('Method not implemented.'); }