diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index d299d8fabd9..f437285b00d 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -63,20 +63,20 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._onInstanceDimensionsChanged(instance); })); - this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); + this._toDispose.add(_terminalService.onDidDisposeInstance(instance => this._onTerminalDisposed(instance))); this._toDispose.add(_terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance))); this._toDispose.add(_terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance))); this._toDispose.add(_terminalService.onInstanceMaximumDimensionsChanged(instance => this._onInstanceMaximumDimensionsChanged(instance))); this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e))); - this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); + this._toDispose.add(_terminalService.onDidChangeActiveInstance(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); // Set initial ext host state - this._terminalService.terminalInstances.forEach(t => { + this._terminalService.instances.forEach(t => { this._onTerminalOpened(t); t.processReady.then(() => this._onTerminalProcessIdReady(t)); }); - const activeInstance = this._terminalService.getActiveInstance(); + const activeInstance = this._terminalService.activeInstance; if (activeInstance) { this._proxy.$acceptActiveTerminalChanged(activeInstance.instanceId); } @@ -145,7 +145,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape }; let terminal: ITerminalInstance | undefined; if (launchConfig.isSplitTerminal) { - const activeInstance = this._terminalService.getActiveInstance(); + const activeInstance = this._terminalService.activeInstance; if (activeInstance) { terminal = withNullAsUndefined(this._terminalService.splitInstance(activeInstance, shellLaunchConfig)); } @@ -166,7 +166,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape public $hide(id: TerminalIdentifier): void { const rendererId = this._getTerminalId(id); - const instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.activeInstance; if (instance && instance.instanceId === rendererId) { this._terminalService.hidePanel(); } @@ -186,7 +186,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._onTerminalData(id, data); }); // Send initial events if they exist - this._terminalService.terminalInstances.forEach(t => { + this._terminalService.instances.forEach(t => { t.initialDataEvents?.forEach(d => this._onTerminalData(t.instanceId, d)); }); } @@ -382,9 +382,9 @@ class TerminalDataEventTracker extends Disposable { this._register(this._bufferer = new TerminalDataBufferer(this._callback)); - this._terminalService.terminalInstances.forEach(instance => this._registerInstance(instance)); + this._terminalService.instances.forEach(instance => this._registerInstance(instance)); this._register(this._terminalService.onInstanceCreated(instance => this._registerInstance(instance))); - this._register(this._terminalService.onInstanceDisposed(instance => this._bufferer.stopBuffering(instance.instanceId))); + this._register(this._terminalService.onDidDisposeInstance(instance => this._bufferer.stopBuffering(instance.instanceId))); } private _registerInstance(instance: ITerminalInstance): void { diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index f29bdac9e6a..5a47cc54c3b 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -9,7 +9,7 @@ import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/q import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewDescriptorService, IViewsService, ViewContainer } from 'vs/workbench/common/views'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IPanelService, IPanelIdentifier } from 'vs/workbench/services/panel/common/panelService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; @@ -37,6 +37,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider { + this.terminalGroupService.groups.forEach((group, groupIndex) => { group.terminalInstances.forEach((terminal, terminalIndex) => { const label = localize('terminalTitle', "{0}: {1}", `${groupIndex + 1}.${terminalIndex + 1}`, terminal.title); viewEntries.push({ diff --git a/src/vs/workbench/contrib/remote/browser/urlFinder.ts b/src/vs/workbench/contrib/remote/browser/urlFinder.ts index b66495b0157..89352aae11e 100644 --- a/src/vs/workbench/contrib/remote/browser/urlFinder.ts +++ b/src/vs/workbench/contrib/remote/browser/urlFinder.ts @@ -33,13 +33,13 @@ export class UrlFinder extends Disposable { constructor(terminalService: ITerminalService, debugService: IDebugService) { super(); // Terminal - terminalService.terminalInstances.forEach(instance => { + terminalService.instances.forEach(instance => { this.registerTerminalInstance(instance); }); this._register(terminalService.onInstanceCreated(instance => { this.registerTerminalInstance(instance); })); - this._register(terminalService.onInstanceDisposed(instance => { + this._register(terminalService.onDidDisposeInstance(instance => { this.listeners.get(instance)?.dispose(); this.listeners.delete(instance); })); diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 01a8407e50e..31e420ae126 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -309,7 +309,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (!terminalData) { return false; } - const activeTerminalInstance = this.terminalService.getActiveInstance(); + const activeTerminalInstance = this.terminalService.activeInstance; const isPanelShowingTerminal = !!this.viewsService.getActiveViewWithId(TERMINAL_VIEW_ID); return isPanelShowingTerminal && (activeTerminalInstance?.instanceId === terminalData.terminal.instanceId); } @@ -336,7 +336,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (isTerminalInPanel) { this.previousPanelId = this.panelService.getActivePanel()?.getId(); if (this.previousPanelId === TERMINAL_VIEW_ID) { - this.previousTerminalInstance = this.terminalService.getActiveInstance() ?? undefined; + this.previousTerminalInstance = this.terminalService.activeInstance ?? undefined; } } this.terminalService.setActiveInstance(terminalData.terminal); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index f034d9babd8..26c26d0145e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -23,7 +23,7 @@ import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalCol import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { TerminalService } from 'vs/workbench/contrib/terminal/browser/terminalService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IRemoteTerminalService, ITerminalEditorService, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; @@ -43,10 +43,12 @@ import { TerminalInputSerializer, TerminalEditor } from 'vs/workbench/contrib/te import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { TerminalEditorService } from 'vs/workbench/contrib/terminal/browser/terminalEditorService'; +import { TerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminalGroupService'; // Register services registerSingleton(ITerminalService, TerminalService, true); registerSingleton(ITerminalEditorService, TerminalEditorService, true); +registerSingleton(ITerminalGroupService, TerminalGroupService, true); registerSingleton(IRemoteTerminalService, RemoteTerminalService); registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 024ae525949..5543c983c9c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -22,6 +22,7 @@ import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/termi export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); +export const ITerminalGroupService = createDecorator('terminalGroupService'); export const ITerminalInstanceService = createDecorator('terminalInstanceService'); export const IRemoteTerminalService = createDecorator('remoteTerminalService'); @@ -63,10 +64,11 @@ export const enum Direction { } export interface ITerminalGroup { - activeInstance: ITerminalInstance | null; + activeInstance: ITerminalInstance | undefined; terminalInstances: ITerminalInstance[]; title: string; + readonly onDidDisposeInstance: Event; readonly onDisposed: Event; readonly onInstancesChanged: Event; readonly onPanelOrientationChanged: Event; @@ -109,44 +111,32 @@ export interface ICreateTerminalOptions { target?: TerminalLocation; } -export interface ITerminalService { +export interface ITerminalService extends ITerminalInstanceHost { readonly _serviceBrand: undefined; - activeGroupIndex: number; + /** Gets all terminal instances, including editor and terminal view (group) instances. */ + readonly instances: readonly ITerminalInstance[]; configHelper: ITerminalConfigHelper; - terminalInstances: ITerminalInstance[]; - terminalGroups: ITerminalGroup[]; isProcessSupportRegistered: boolean; readonly connectionState: TerminalConnectionState; readonly availableProfiles: ITerminalProfile[]; readonly profilesReady: Promise; initializeTerminals(): Promise; - onActiveGroupChanged: Event; + onActiveGroupChanged: Event; onGroupDisposed: Event; - /** - * An event that fires when a terminal group is created, disposed of, or shown (in the case of a background group) - */ - onGroupsChanged: Event; onInstanceCreated: Event; - onInstanceDisposed: Event; onInstanceProcessIdReady: Event; onInstanceDimensionsChanged: Event; onInstanceMaximumDimensionsChanged: Event; onInstanceRequestStartExtensionTerminal: Event; - /** - * An event that fires when a terminal instance is created, disposed of, or shown (in the case of a background terminal) - */ - onInstancesChanged: Event; onInstanceTitleChanged: Event; onInstanceIconChanged: Event; onInstanceColorChanged: Event; onInstancePrimaryStatusChanged: Event; - onActiveInstanceChanged: Event; onDidRegisterProcessSupport: Event; onDidChangeConnectionState: Event; onDidChangeAvailableProfiles: Event; - onPanelOrientationChanged: Event; /** * Creates a terminal. @@ -163,20 +153,9 @@ export interface ITerminalService { createInstance(shellLaunchConfig: IShellLaunchConfig): ITerminalInstance; getInstanceFromId(terminalId: number): ITerminalInstance | undefined; getInstanceFromIndex(terminalIndex: number): ITerminalInstance; - getGroupLabels(): string[]; - getActiveInstance(): ITerminalInstance | null; - setActiveInstance(terminalInstance: ITerminalInstance): void; - setActiveInstanceByIndex(terminalIndex: number): void; getActiveOrCreateInstance(): ITerminalInstance; splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig, cwd?: string | URI): ITerminalInstance | null; splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null; - unsplitInstance(instance: ITerminalInstance): void; - joinInstances(instances: ITerminalInstance[]): void; - /** - * Moves a terminal instance's group to the target instance group's position. - */ - moveGroup(source: ITerminalInstance, target: ITerminalInstance): void; - moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'before' | 'after'): void; moveToEditor(source: ITerminalInstance): void; moveToTerminalView(source?: ITerminalInstance): Promise; @@ -187,11 +166,6 @@ export interface ITerminalService { */ doWithActiveInstance(callback: (terminal: ITerminalInstance) => T): T | void; - getActiveGroup(): ITerminalGroup | null; - setActiveGroupToNext(): void; - setActiveGroupToPrevious(): void; - setActiveGroupByIndex(groupIndex: number): void; - /** * Fire the onActiveTabChanged event, this will trigger the terminal dropdown to be updated, * among other things. @@ -221,15 +195,12 @@ export interface ITerminalService { showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise; - getGroupForInstance(instance: ITerminalInstance): ITerminalGroup | undefined; - setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise; isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean; getEditableData(instance: ITerminalInstance): IEditableData | undefined; setEditable(instance: ITerminalInstance, data: IEditableData | null): Promise; - instanceIsSplit(instance: ITerminalInstance): boolean; safeDisposeTerminal(instance: ITerminalInstance): Promise; } @@ -237,10 +208,11 @@ export interface ITerminalService { * This service is responsible for integrating with the editor service and managing terminal * editors. */ -export interface ITerminalEditorService { +export interface ITerminalEditorService extends ITerminalInstanceHost { readonly _serviceBrand: undefined; - readonly terminalEditorInstances: ITerminalInstance[]; + /** Gets all _terminal editor_ instances. */ + readonly instances: readonly ITerminalInstance[]; createEditor(instance: ITerminalInstance): Promise; createEditorInput(instance: ITerminalInstance): TerminalEditorInput; @@ -248,6 +220,67 @@ export interface ITerminalEditorService { detachInstance(instance: ITerminalInstance): void; } +/** + * This service is responsible for managing terminal groups, that is the terminals that are hosted + * within the terminal panel, not in an editor. + */ +export interface ITerminalGroupService extends ITerminalInstanceHost { + readonly _serviceBrand: undefined; + + /** Gets all _terminal view_ instances, ie. instances contained within terminal groups. */ + readonly instances: readonly ITerminalInstance[]; + readonly groups: readonly ITerminalGroup[]; + activeGroup: ITerminalGroup | undefined; + readonly activeGroupIndex: number; + readonly activeInstanceIndex: number; + + readonly onDidChangeActiveGroup: Event; + readonly onDidDisposeGroup: Event; + /** Fires when a group is created, disposed of, or shown (in the case of a background group). */ + readonly onDidChangeGroups: Event; + + readonly onPanelOrientationChanged: Event; + + createGroup(slcOrInstance?: IShellLaunchConfig | ITerminalInstance): ITerminalGroup; + getGroupForInstance(instance: ITerminalInstance): ITerminalGroup | undefined; + + /** + * Moves a terminal instance's group to the target instance group's position. + * @param source The source instance to move. + * @param target The target instance to move the source instance to. + */ + moveGroup(source: ITerminalInstance, target: ITerminalInstance): void; + + moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'before' | 'after'): void; + unsplitInstance(instance: ITerminalInstance): void; + joinInstances(instances: ITerminalInstance[]): void; + instanceIsSplit(instance: ITerminalInstance): boolean; + + getGroupLabels(): string[]; + setActiveGroupByIndex(index: number): void; + setActiveGroupToNext(): void; + setActiveGroupToPrevious(): void; + + setActiveInstanceByIndex(terminalIndex: number): void; + + setContainer(container: HTMLElement): void; +} + +/** + * An interface that indicates the implementer hosts terminal instances, exposing a common set of + * properties and events. + */ +export interface ITerminalInstanceHost { + readonly activeInstance: ITerminalInstance | undefined; + readonly instances: readonly ITerminalInstance[]; + + readonly onDidDisposeInstance: Event; + readonly onDidChangeActiveInstance: Event; + readonly onDidChangeInstances: Event; + + setActiveInstance(instance: ITerminalInstance): void; +} + export interface IRemoteTerminalService extends IOffProcessTerminalService { createProcess(shellLaunchConfig: IShellLaunchConfig, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 26b98cdaa77..a9035572261 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -30,7 +30,7 @@ import { ILocalTerminalService, ITerminalProfile, TerminalSettingId, TitleEventS import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; -import { Direction, IRemoteTerminalService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { Direction, IRemoteTerminalService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TerminalCommandId, TerminalLocation, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; @@ -153,7 +153,7 @@ export function registerTerminalActions() { const commandService = accessor.get(ICommandService); const folders = workspaceContextService.getWorkspace().folders; if (event instanceof MouseEvent && (event.altKey || event.ctrlKey)) { - const activeInstance = terminalService.getActiveInstance(); + const activeInstance = terminalService.activeInstance; if (activeInstance) { const cwd = await getCwdForSplit(terminalService.configHelper, activeInstance); terminalService.splitInstance(activeInstance, profile, cwd); @@ -276,9 +276,8 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); - terminalService.getActiveGroup()?.focusPreviousPane(); - await terminalService.showPanel(true); + accessor.get(ITerminalGroupService).activeGroup?.focusPreviousPane(); + await accessor.get(ITerminalService).showPanel(true); } }); registerAction2(class extends Action2 { @@ -302,9 +301,8 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); - terminalService.getActiveGroup()?.focusNextPane(); - await terminalService.showPanel(true); + accessor.get(ITerminalGroupService).activeGroup?.focusNextPane(); + await accessor.get(ITerminalService).showPanel(true); } }); registerAction2(class extends Action2 { @@ -324,7 +322,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveGroup()?.resizePane(Direction.Left); + accessor.get(ITerminalGroupService).activeGroup?.resizePane(Direction.Left); } }); registerAction2(class extends Action2 { @@ -344,7 +342,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveGroup()?.resizePane(Direction.Right); + accessor.get(ITerminalGroupService).activeGroup?.resizePane(Direction.Right); } }); registerAction2(class extends Action2 { @@ -363,7 +361,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveGroup()?.resizePane(Direction.Up); + accessor.get(ITerminalGroupService).activeGroup?.resizePane(Direction.Up); } }); registerAction2(class extends Action2 { @@ -382,7 +380,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveGroup()?.resizePane(Direction.Down); + accessor.get(ITerminalGroupService).activeGroup?.resizePane(Direction.Down); } }); registerAction2(class extends Action2 { @@ -443,9 +441,8 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); - terminalService.setActiveGroupToNext(); - await terminalService.showPanel(true); + accessor.get(ITerminalGroupService).setActiveGroupToNext(); + await accessor.get(ITerminalService).showPanel(true); } }); registerAction2(class extends Action2 { @@ -467,9 +464,8 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); - terminalService.setActiveGroupToPrevious(); - await terminalService.showPanel(true); + accessor.get(ITerminalGroupService).setActiveGroupToPrevious(); + await accessor.get(ITerminalService).showPanel(true); } }); registerAction2(class extends Action2 { @@ -553,7 +549,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.scrollDownLine(); + accessor.get(ITerminalService).activeInstance?.scrollDownLine(); } }); registerAction2(class extends Action2 { @@ -573,7 +569,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.scrollDownPage(); + accessor.get(ITerminalService).activeInstance?.scrollDownPage(); } }); registerAction2(class extends Action2 { @@ -593,7 +589,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.scrollToBottom(); + accessor.get(ITerminalService).activeInstance?.scrollToBottom(); } }); registerAction2(class extends Action2 { @@ -613,7 +609,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.scrollUpLine(); + accessor.get(ITerminalService).activeInstance?.scrollUpLine(); } }); registerAction2(class extends Action2 { @@ -633,7 +629,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.scrollUpPage(); + accessor.get(ITerminalService).activeInstance?.scrollUpPage(); } }); registerAction2(class extends Action2 { @@ -653,7 +649,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.scrollToTop(); + accessor.get(ITerminalService).activeInstance?.scrollToTop(); } }); registerAction2(class extends Action2 { @@ -672,7 +668,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.navigationMode?.exitNavigationMode(); + accessor.get(ITerminalService).activeInstance?.navigationMode?.exitNavigationMode(); } }); registerAction2(class extends Action2 { @@ -694,7 +690,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.navigationMode?.focusPreviousLine(); + accessor.get(ITerminalService).activeInstance?.navigationMode?.focusPreviousLine(); } }); registerAction2(class extends Action2 { @@ -716,7 +712,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.navigationMode?.focusNextLine(); + accessor.get(ITerminalService).activeInstance?.navigationMode?.focusNextLine(); } }); registerAction2(class extends Action2 { @@ -735,7 +731,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - const terminalInstance = accessor.get(ITerminalService).getActiveInstance(); + const terminalInstance = accessor.get(ITerminalService).activeInstance; if (terminalInstance && terminalInstance.hasSelection()) { terminalInstance.clearSelection(); } @@ -752,7 +748,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - return accessor.get(ITerminalService).getActiveInstance()?.changeIcon(); + return accessor.get(ITerminalService).activeInstance?.changeIcon(); } }); registerAction2(class extends Action2 { @@ -780,7 +776,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - return accessor.get(ITerminalService).getActiveInstance()?.changeColor(); + return accessor.get(ITerminalService).activeInstance?.changeColor(); } }); registerAction2(class extends Action2 { @@ -808,7 +804,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - return accessor.get(ITerminalService).getActiveInstance()?.rename(); + return accessor.get(ITerminalService).activeInstance?.rename(); } }); registerAction2(class extends Action2 { @@ -905,7 +901,7 @@ export function registerTerminalActions() { } async run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - await terminalService.getActiveInstance()?.detachFromProcess(); + await terminalService.activeInstance?.detachFromProcess(); } }); registerAction2(class extends Action2 { @@ -1101,7 +1097,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.toggleEscapeSequenceLogging(); + accessor.get(ITerminalService).activeInstance?.toggleEscapeSequenceLogging(); } }); registerAction2(class extends Action2 { @@ -1205,7 +1201,7 @@ export function registerTerminalActions() { notificationService.warn(localize('workbench.action.terminal.renameWithArg.noName', "No name argument provided")); return; } - accessor.get(ITerminalService).getActiveInstance()?.setTitle(args.name, TitleEventSource.Api); + accessor.get(ITerminalService).activeInstance?.setTitle(args.name, TitleEventSource.Api); } }); registerAction2(class extends Action2 { @@ -1343,7 +1339,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - const query = accessor.get(ITerminalService).getActiveInstance()?.selection; + const query = accessor.get(ITerminalService).activeInstance?.selection; FindInFilesCommand(accessor, { query } as IFindInFilesArgs); } }); @@ -1358,7 +1354,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.relaunch(); + accessor.get(ITerminalService).activeInstance?.relaunch(); } }); registerAction2(class extends Action2 { @@ -1372,7 +1368,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.showEnvironmentInfoHover(); + accessor.get(ITerminalService).activeInstance?.showEnvironmentInfoHover(); } }); registerAction2(class extends Action2 { @@ -1461,8 +1457,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); - await terminalService.doWithActiveInstance(async t => terminalService.unsplitInstance(t)); + await accessor.get(ITerminalService).doWithActiveInstance(async t => accessor.get(ITerminalGroupService).unsplitInstance(t)); } }); registerAction2(class extends Action2 { @@ -1476,14 +1471,14 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); + const terminalGroupService = accessor.get(ITerminalGroupService); const instances = getSelectedInstances(accessor); // should not even need this check given the context key // but TS complains if (instances?.length === 1) { - const group = terminalService.getGroupForInstance(instances[0]); + const group = terminalGroupService.getGroupForInstance(instances[0]); if (group && group?.terminalInstances.length > 1) { - terminalService.unsplitInstance(instances[0]); + terminalGroupService.unsplitInstance(instances[0]); } } } @@ -1498,10 +1493,9 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - const terminalService = accessor.get(ITerminalService); const instances = getSelectedInstances(accessor); if (instances) { - terminalService.joinInstances(instances); + accessor.get(ITerminalGroupService).joinInstances(instances); } } }); @@ -1546,7 +1540,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).getActiveInstance()?.selectAll(); + accessor.get(ITerminalService).activeInstance?.selectAll(); } }); registerAction2(class extends Action2 { @@ -1571,7 +1565,7 @@ export function registerTerminalActions() { const commandService = accessor.get(ICommandService); const folders = workspaceContextService.getWorkspace().folders; if (event instanceof MouseEvent && (event.altKey || event.ctrlKey)) { - const activeInstance = terminalService.getActiveInstance(); + const activeInstance = terminalService.activeInstance; if (activeInstance) { const cwd = await getCwdForSplit(terminalService.configHelper, activeInstance); terminalService.splitInstance(activeInstance, { cwd }); @@ -1620,7 +1614,7 @@ export function registerTerminalActions() { const terminalService = accessor.get(ITerminalService); await terminalService.doWithActiveInstance(async t => { t.dispose(true); - if (terminalService.terminalInstances.length > 0) { + if (terminalService.instances.length > 0) { await terminalService.showPanel(true); } }); @@ -1655,7 +1649,7 @@ export function registerTerminalActions() { for (const instance of selectedInstances) { terminalService.safeDisposeTerminal(instance); } - if (terminalService.terminalInstances.length > 0) { + if (terminalService.instances.length > 0) { terminalService.focusTabs(); focusNext(accessor); } @@ -1748,7 +1742,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - await accessor.get(ITerminalService).getActiveInstance()?.copySelection(); + await accessor.get(ITerminalService).activeInstance?.copySelection(); } }); } @@ -1772,7 +1766,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - await accessor.get(ITerminalService).getActiveInstance()?.paste(); + await accessor.get(ITerminalService).activeInstance?.paste(); } }); } @@ -1794,7 +1788,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor) { - await accessor.get(ITerminalService).getActiveInstance()?.pasteSelection(); + await accessor.get(ITerminalService).activeInstance?.pasteSelection(); } }); } @@ -1825,7 +1819,7 @@ export function registerTerminalActions() { } const indexMatches = terminalIndexRe.exec(item); if (indexMatches) { - terminalService.setActiveGroupByIndex(Number(indexMatches[1]) - 1); + accessor.get(ITerminalGroupService).setActiveGroupByIndex(Number(indexMatches[1]) - 1); return terminalService.showPanel(true); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalCommands.ts b/src/vs/workbench/contrib/terminal/browser/terminalCommands.ts index 8f0f92efe05..359167fa331 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalCommands.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalCommands.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; export function setupTerminalCommands(): void { registerOpenTerminalAtIndexCommands(); @@ -21,9 +21,8 @@ function registerOpenTerminalAtIndexCommands(): void { when: undefined, primary: 0, handler: accessor => { - const terminalService = accessor.get(ITerminalService); - terminalService.setActiveInstanceByIndex(terminalIndex); - return terminalService.showPanel(true); + accessor.get(ITerminalGroupService).setActiveInstanceByIndex(terminalIndex); + return accessor.get(ITerminalService).showPanel(true); } }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index f43c571f761..e445e846524 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ITerminalEditorService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { TerminalLocation } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -12,16 +13,57 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic export class TerminalEditorService extends Disposable implements ITerminalEditorService { declare _serviceBrand: undefined; - terminalEditorInstances: ITerminalInstance[] = []; + instances: ITerminalInstance[] = []; + private _activeInstanceIndex: number = -1; private _editorInputs: Map = new Map(); + private _instanceDisposables: Map = new Map(); + + private readonly _onDidDisposeInstance = new Emitter(); + get onDidDisposeInstance(): Event { return this._onDidDisposeInstance.event; } + private readonly _onDidChangeActiveInstance = new Emitter(); + get onDidChangeActiveInstance(): Event { return this._onDidChangeActiveInstance.event; } + private readonly _onDidChangeInstances = new Emitter(); + get onDidChangeInstances(): Event { return this._onDidChangeInstances.event; } constructor( @IEditorService private readonly _editorService: IEditorService ) { super(); - // TODO: Multiplex instance events + this._register(toDisposable(() => { + for (const d of this._instanceDisposables.values()) { + dispose(d); + } + })); + this._register(this._editorService.onDidActiveEditorChange(() => { + const activeEditor = this._editorService.activeEditor; + this._setActiveInstance(activeEditor instanceof TerminalEditorInput ? activeEditor?.terminalInstance : undefined); + })); + } + + get activeInstance(): ITerminalInstance | undefined { + if (this.instances.length === 0 || this._activeInstanceIndex === -1) { + return undefined; + } + return this.instances[this._activeInstanceIndex]; + } + + setActiveInstance(instance: ITerminalInstance): void { + this._setActiveInstance(instance); + } + + private _setActiveInstance(instance: ITerminalInstance | undefined): void { + const oldActiveInstance = this.activeInstance; + if (instance === undefined) { + this._activeInstanceIndex = -1; + } else { + this._activeInstanceIndex = this.instances.findIndex(e => e === instance); + } + const newActiveInstance = this.activeInstance; + if (oldActiveInstance !== newActiveInstance) { + this._onDidChangeActiveInstance.fire(newActiveInstance); + } } async createEditor(instance: ITerminalInstance): Promise { @@ -30,13 +72,17 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor pinned: true, forceReload: true }); - this.terminalEditorInstances.push(instance); } createEditorInput(instance: ITerminalInstance): TerminalEditorInput { const input = new TerminalEditorInput(instance); instance.target = TerminalLocation.Editor; this._editorInputs.set(instance.instanceId, input); + this._instanceDisposables.set(instance.instanceId, [ + instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance) + ]); + this.instances.push(instance); + this._onDidChangeInstances.fire(); return input; } @@ -57,10 +103,16 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor const editorInput = this._editorInputs.get(instance.instanceId); editorInput?.detachInstance(); this._editorInputs.delete(instance.instanceId); - const instanceIndex = this.terminalEditorInstances.findIndex(e => e === instance); + const instanceIndex = this.instances.findIndex(e => e === instance); if (instanceIndex !== -1) { - this.terminalEditorInstances.splice(instanceIndex, 1); + this.instances.splice(instanceIndex, 1); } editorInput?.dispose(); + const disposables = this._instanceDisposables.get(instance.instanceId); + this._instanceDisposables.delete(instance.instanceId); + if (disposables) { + dispose(disposables); + } + this._onDidChangeInstances.fire(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 4d9ac4ed1f1..1f14339bb71 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -29,19 +29,20 @@ export class TerminalFindWidget extends SimpleFindWidget { } find(previous: boolean) { - const instance = this._terminalService.getActiveInstance(); - if (instance !== null) { - if (previous) { - instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); - } else { - instance.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); - } + const instance = this._terminalService.activeInstance; + if (!instance) { + return; + } + if (previous) { + instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + } else { + instance.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } } override hide() { super.hide(); - const instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.activeInstance; if (instance) { instance.focus(); } @@ -49,15 +50,15 @@ export class TerminalFindWidget extends SimpleFindWidget { protected _onInputChanged() { // Ignore input changes for now - const instance = this._terminalService.getActiveInstance(); - if (instance !== null) { + const instance = this._terminalService.activeInstance; + if (instance) { return instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: true }); } return false; } protected _onFocusTrackerFocus() { - const instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.activeInstance; if (instance) { instance.notifyFindWidgetFocusChanged(true); } @@ -65,7 +66,7 @@ export class TerminalFindWidget extends SimpleFindWidget { } protected _onFocusTrackerBlur() { - const instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.activeInstance; if (instance) { instance.notifyFindWidgetFocusChanged(false); } @@ -81,7 +82,7 @@ export class TerminalFindWidget extends SimpleFindWidget { } findFirst() { - const instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.activeInstance; if (instance) { if (instance.hasSelection()) { instance.clearSelection(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index b7b915f93c9..ad355995d14 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -235,19 +235,23 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { private _terminalLocation: ViewContainerLocation = ViewContainerLocation.Panel; private _instanceDisposables: Map = new Map(); - private _activeInstanceIndex: number; + private _activeInstanceIndex: number = -1; private _isVisible: boolean = false; get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } private _initialRelativeSizes: number[] | undefined; + private readonly _onDidDisposeInstance: Emitter = this._register(new Emitter()); + readonly onDidDisposeInstance = this._onDidDisposeInstance.event; private readonly _onDisposed: Emitter = this._register(new Emitter()); - public readonly onDisposed: Event = this._onDisposed.event; + readonly onDisposed = this._onDisposed.event; private readonly _onInstancesChanged: Emitter = this._register(new Emitter()); - readonly onInstancesChanged: Event = this._onInstancesChanged.event; + readonly onInstancesChanged = this._onInstancesChanged.event; + private readonly _onDidChangeActiveInstance = new Emitter(); + readonly onDidChangeActiveInstance = this._onDidChangeActiveInstance.event; private readonly _onPanelOrientationChanged = new Emitter(); - get onPanelOrientationChanged(): Event { return this._onPanelOrientationChanged.event; } + readonly onPanelOrientationChanged = this._onPanelOrientationChanged.event; constructor( private _container: HTMLElement | undefined, @@ -261,7 +265,6 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { if (shellLaunchConfigOrInstance) { this.addInstance(shellLaunchConfigOrInstance); } - this._activeInstanceIndex = 0; if (this._container) { this.attachToElement(this._container); } @@ -301,9 +304,9 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._onInstancesChanged.fire(); } - get activeInstance(): ITerminalInstance | null { + get activeInstance(): ITerminalInstance | undefined { if (this._terminalInstances.length === 0) { - return null; + return undefined; } return this._terminalInstances[this._activeInstanceIndex]; } @@ -326,12 +329,15 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { private _initInstanceListeners(instance: ITerminalInstance) { this._instanceDisposables.set(instance.instanceId, [ - instance.onDisposed(instance => this._onInstanceDisposed(instance)), + instance.onDisposed(instance => { + this._onDidDisposeInstance.fire(instance); + this._handleOnDidDisposeInstance(instance); + }), instance.onFocused(instance => this._setActiveInstance(instance)) ]); } - private _onInstanceDisposed(instance: ITerminalInstance) { + private _handleOnDidDisposeInstance(instance: ITerminalInstance) { this._removeInstance(instance); } @@ -416,12 +422,9 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { return; } - const didInstanceChange = this._activeInstanceIndex !== index; this._activeInstanceIndex = index; - - if (didInstanceChange) { - this._onInstancesChanged.fire(); - } + this._onInstancesChanged.fire(); + this._onDidChangeActiveInstance.fire(this.activeInstance); } attachToElement(element: HTMLElement): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts new file mode 100644 index 00000000000..dd6423c2fac --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -0,0 +1,321 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; +import { ITerminalGroup, ITerminalGroupService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalGroup } from 'vs/workbench/contrib/terminal/browser/terminalGroup'; +import { KEYBINDING_CONTEXT_TERMINAL_GROUP_COUNT } from 'vs/workbench/contrib/terminal/common/terminal'; + +export class TerminalGroupService extends Disposable implements ITerminalGroupService { + declare _serviceBrand: undefined; + + groups: ITerminalGroup[] = []; + activeGroupIndex: number = -1; + get instances(): ITerminalInstance[] { + return this.groups.reduce((p, c) => p.concat(c.terminalInstances), [] as ITerminalInstance[]); + } + activeInstanceIndex: number = -1; + + private _terminalGroupCountContextKey: IContextKey; + private _container: HTMLElement | undefined; + + private readonly _onDidChangeActiveGroup = new Emitter(); + get onDidChangeActiveGroup(): Event { return this._onDidChangeActiveGroup.event; } + private readonly _onDidDisposeGroup = new Emitter(); + get onDidDisposeGroup(): Event { return this._onDidDisposeGroup.event; } + private readonly _onDidChangeGroups = new Emitter(); + get onDidChangeGroups(): Event { return this._onDidChangeGroups.event; } + + private readonly _onDidDisposeInstance = new Emitter(); + get onDidDisposeInstance(): Event { return this._onDidDisposeInstance.event; } + private readonly _onDidChangeActiveInstance = new Emitter(); + get onDidChangeActiveInstance(): Event { return this._onDidChangeActiveInstance.event; } + private readonly _onDidChangeInstances = new Emitter(); + get onDidChangeInstances(): Event { return this._onDidChangeInstances.event; } + + private readonly _onPanelOrientationChanged = new Emitter(); + get onPanelOrientationChanged(): Event { return this._onPanelOrientationChanged.event; } + + constructor( + @IContextKeyService private _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + + this.onDidDisposeGroup(group => this._removeGroup(group)); + + this._terminalGroupCountContextKey = KEYBINDING_CONTEXT_TERMINAL_GROUP_COUNT.bindTo(this._contextKeyService); + this.onDidChangeGroups(() => this._terminalGroupCountContextKey.set(this.groups.length)); + } + + get activeGroup(): ITerminalGroup | undefined { + if (this.activeGroupIndex < 0 || this.activeGroupIndex >= this.groups.length) { + return undefined; + } + return this.groups[this.activeGroupIndex]; + } + set activeGroup(value: ITerminalGroup | undefined) { + if (value === undefined) { + this.activeGroupIndex = -1; + return; + } + this.activeGroupIndex = this.groups.findIndex(e => e === value); + } + + get activeInstance(): ITerminalInstance | undefined { + return this.activeGroup?.activeInstance; + } + + setActiveInstance(instance: ITerminalInstance) { + this.setActiveInstanceByIndex(this._getIndexFromId(instance.instanceId)); + } + + private _getIndexFromId(terminalId: number): number { + let terminalIndex = this.instances.findIndex(e => e.instanceId === terminalId); + if (terminalIndex === -1) { + throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`); + } + return terminalIndex; + } + + setContainer(container: HTMLElement) { + this._container = container; + this.groups.forEach(group => group.attachToElement(container)); + } + + createGroup(slcOrInstance?: IShellLaunchConfig | ITerminalInstance): ITerminalGroup { + const group = this._instantiationService.createInstance(TerminalGroup, this._container, slcOrInstance); + // TODO: Move panel orientation change into this file so it's not fired many times + group.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); + this.groups.push(group); + group.addDisposable(group.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)); + group.addDisposable(group.onDidChangeActiveInstance(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance)); + group.addDisposable(group.onInstancesChanged(this._onDidChangeInstances.fire, this._onDidChangeInstances)); + group.addDisposable(group.onDisposed(this._onDidDisposeGroup.fire, this._onDidDisposeGroup)); + if (group.terminalInstances.length > 0) { + this._onDidChangeInstances.fire(); + } + this._onDidChangeGroups.fire(); + return group; + } + + private _removeGroup(group: ITerminalGroup) { + // Get the index of the group and remove it from the list + const activeGroup = this.activeGroup; + const wasActiveGroup = group === activeGroup; + const index = this.groups.indexOf(group); + if (index !== -1) { + this.groups.splice(index, 1); + this._onDidChangeGroups.fire(); + } + + // Adjust focus if the group was active + if (wasActiveGroup && this.groups.length > 0) { + const newIndex = index < this.groups.length ? index : this.groups.length - 1; + this.setActiveGroupByIndex(newIndex); + this.activeInstance?.focus(true); + } else if (this.activeGroupIndex >= this.groups.length) { + const newIndex = this.groups.length - 1; + this.setActiveGroupByIndex(newIndex); + } + + this._onDidChangeInstances.fire(); + if (this.groups.length === 0) { + this._onDidChangeActiveInstance.fire(undefined); + } + + this._onDidChangeGroups.fire(); + if (wasActiveGroup) { + this._onDidChangeActiveGroup.fire(this.activeGroup); + } + } + + setActiveGroupByIndex(index: number) { + if (index >= this.groups.length) { + return; + } + + this.activeGroupIndex = index; + + this.groups.forEach((g, i) => g.setVisible(i === this.activeGroupIndex)); + this._onDidChangeActiveGroup.fire(this.activeGroup); + } + + + private _getInstanceLocation(index: number): IInstanceLocation | undefined { + let currentGroupIndex = 0; + while (index >= 0 && currentGroupIndex < this.groups.length) { + const group = this.groups[currentGroupIndex]; + const count = group.terminalInstances.length; + if (index < count) { + return { + group, + groupIndex: currentGroupIndex, + instance: group.terminalInstances[index], + instanceIndex: index + }; + } + index -= count; + currentGroupIndex++; + } + return undefined; + } + + setActiveInstanceByIndex(index: number) { + const instanceLocation = this._getInstanceLocation(index); + if (!instanceLocation || (this.activeInstanceIndex > 0 && this.activeInstanceIndex === index)) { + return; + } + + this.activeInstanceIndex = instanceLocation.instanceIndex; + this.activeGroupIndex = instanceLocation.groupIndex; + + if (this.activeGroupIndex !== instanceLocation.groupIndex) { + this._onDidChangeActiveGroup.fire(this.activeGroup); + } + this.groups.forEach((g, i) => g.setVisible(i === instanceLocation.groupIndex)); + + instanceLocation.group.setActiveInstanceByIndex(this.activeInstanceIndex); + } + + setActiveGroupToNext() { + if (this.groups.length <= 1) { + return; + } + let newIndex = this.activeGroupIndex + 1; + if (newIndex >= this.groups.length) { + newIndex = 0; + } + this.setActiveGroupByIndex(newIndex); + } + + setActiveGroupToPrevious() { + if (this.groups.length <= 1) { + return; + } + let newIndex = this.activeGroupIndex - 1; + if (newIndex < 0) { + newIndex = this.groups.length - 1; + } + this.setActiveGroupByIndex(newIndex); + } + + moveGroup(source: ITerminalInstance, target: ITerminalInstance) { + const sourceGroup = this.getGroupForInstance(source); + const targetGroup = this.getGroupForInstance(target); + if (!sourceGroup || !targetGroup) { + return; + } + const sourceGroupIndex = this.groups.indexOf(sourceGroup); + const targetGroupIndex = this.groups.indexOf(targetGroup); + this.groups.splice(sourceGroupIndex, 1); + this.groups.splice(targetGroupIndex, 0, sourceGroup); + this._onDidChangeInstances.fire(); + } + + moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'before' | 'after') { + const sourceGroup = this.getGroupForInstance(source); + const targetGroup = this.getGroupForInstance(target); + if (!sourceGroup || !targetGroup) { + return; + } + + // Move from the source group to the target group + if (sourceGroup !== targetGroup) { + // Move groups + sourceGroup.removeInstance(source); + targetGroup.addInstance(source); + } + + // Rearrange within the target group + const index = targetGroup.terminalInstances.indexOf(target) + (side === 'after' ? 1 : 0); + targetGroup.moveInstance(source, index); + } + + unsplitInstance(instance: ITerminalInstance) { + const oldGroup = this.getGroupForInstance(instance); + if (!oldGroup || oldGroup.terminalInstances.length < 2) { + return; + } + + oldGroup.removeInstance(instance); + this.createGroup(instance); + } + + joinInstances(instances: ITerminalInstance[]) { + // Find the group of the first instance that is the only instance in the group, if one exists + let candidateInstance: ITerminalInstance | undefined = undefined; + let candidateGroup: ITerminalGroup | undefined = undefined; + for (const instance of instances) { + const group = this.getGroupForInstance(instance); + if (group?.terminalInstances.length === 1) { + candidateInstance = instance; + candidateGroup = group; + break; + } + } + + // Create a new group if needed + if (!candidateGroup) { + candidateGroup = this.createGroup(); + } + + const wasActiveGroup = this.activeGroup === candidateGroup; + + // Unsplit all other instances and add them to the new group + for (const instance of instances) { + if (instance === candidateInstance) { + continue; + } + + const oldGroup = this.getGroupForInstance(instance); + if (!oldGroup) { + // Something went wrong, don't join this one + continue; + } + oldGroup.removeInstance(instance); + candidateGroup.addInstance(instance); + } + + // Set the active terminal + this.setActiveInstance(instances[0]); + + // Fire events + this._onDidChangeInstances.fire(); + if (!wasActiveGroup) { + this._onDidChangeActiveGroup.fire(this.activeGroup); + } + } + + instanceIsSplit(instance: ITerminalInstance): boolean { + const group = this.getGroupForInstance(instance); + if (!group) { + return false; + } + return group.terminalInstances.length > 1; + } + + getGroupForInstance(instance: ITerminalInstance): ITerminalGroup | undefined { + return this.groups.find(group => group.terminalInstances.indexOf(instance) !== -1); + } + + getGroupLabels(): string[] { + return this.groups.filter(group => group.terminalInstances.length > 0).map((group, index) => { + return `${index + 1}: ${group.title ? group.title : ''}`; + }); + } +} + +interface IInstanceLocation { + group: ITerminalGroup, + groupIndex: number, + instance: ITerminalInstance, + instanceIndex: number +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts index 4842e563fc0..476611da45d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickAccess.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { matchesFuzzy } from 'vs/base/common/filters'; -import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -21,6 +21,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider { const terminalPicks: Array = []; - const terminalGroups = this._terminalService.terminalGroups; + const terminalGroups = this._terminalGroupService.groups; for (let groupIndex = 0; groupIndex < terminalGroups.length; groupIndex++) { const terminalGroup = terminalGroups[groupIndex]; for (let terminalIndex = 0; terminalIndex < terminalGroup.terminalInstances.length; terminalIndex++) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 84dc25c621d..1ead85ecc45 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { AutoOpenBarrier, timeout } from 'vs/base/common/async'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { debounce, throttle } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; @@ -28,14 +27,13 @@ import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/termina import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { IEditableData, IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IRemoteTerminalService, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalGroup, ITerminalInstance, ITerminalProfileProvider, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, IRemoteTerminalService, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalProfileProvider, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; -import { TerminalGroup } from 'vs/workbench/contrib/terminal/browser/terminalGroup'; import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileContribution, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_COUNT, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_GROUP_COUNT, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, KEYBINDING_CONTEXT_TERMINAL_TABS_MOUSE, TerminalLocation, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileContribution, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_COUNT, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, KEYBINDING_CONTEXT_TERMINAL_TABS_MOUSE, TerminalLocation, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; import { formatMessageForTerminal, terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IEditorOverrideService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; @@ -48,17 +46,16 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; + private _hostActiveTerminals: Map = new Map(); + private _isShuttingDown: boolean; private _terminalFocusContextKey: IContextKey; private _terminalCountContextKey: IContextKey; - private _terminalGroupCountContextKey: IContextKey; private _terminalShellTypeContextKey: IContextKey; private _terminalAltBufferActiveContextKey: IContextKey; - private _terminalGroups: ITerminalGroup[] = []; private _backgroundedTerminalInstances: ITerminalInstance[] = []; + private _backgroundedTerminalDisposables: Map = new Map(); private _findState: FindReplaceState; - private _activeGroupIndex: number; - private _activeInstanceIndex: number; private readonly _profileProviders: Map> = new Map(); private _linkProviders: Set = new Set(); private _linkProviderDisposables: Map = new Map(); @@ -68,16 +65,13 @@ export class TerminalService implements ITerminalService { private _profilesReadyBarrier: AutoOpenBarrier; private _availableProfiles: ITerminalProfile[] | undefined; private _configHelper: TerminalConfigHelper; - private _terminalContainer: HTMLElement | undefined; private _remoteTerminalsInitPromise: Promise | undefined; private _localTerminalsInitPromise: Promise | undefined; private _connectionState: TerminalConnectionState; private _editable: { instance: ITerminalInstance, data: IEditableData } | undefined; - public get activeGroupIndex(): number { return this._activeGroupIndex; } - public get terminalGroups(): ITerminalGroup[] { return this._terminalGroups; } - public get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); } + get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); } get connectionState(): TerminalConnectionState { return this._connectionState; } get profilesReady(): Promise { return this._profilesReadyBarrier.wait().then(() => { }); } get availableProfiles(): ITerminalProfile[] { @@ -85,19 +79,19 @@ export class TerminalService implements ITerminalService { return this._availableProfiles || []; } get configHelper(): ITerminalConfigHelper { return this._configHelper; } - private get _terminalInstances(): ITerminalInstance[] { - return this._terminalGroups.reduce((p, c) => p.concat(c.terminalInstances), []); - } - get terminalInstances(): ITerminalInstance[] { - return this._terminalInstances; + get instances(): ITerminalInstance[] { + return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); } - private readonly _onActiveGroupChanged = new Emitter(); - get onActiveGroupChanged(): Event { return this._onActiveGroupChanged.event; } + private _activeInstance: ITerminalInstance | undefined; + get activeInstance(): ITerminalInstance | undefined { return this._activeInstance; } + + private readonly _onActiveGroupChanged = new Emitter(); + get onActiveGroupChanged(): Event { return this._onActiveGroupChanged.event; } private readonly _onInstanceCreated = new Emitter(); get onInstanceCreated(): Event { return this._onInstanceCreated.event; } - private readonly _onInstanceDisposed = new Emitter(); - get onInstanceDisposed(): Event { return this._onInstanceDisposed.event; } + private readonly _onDidDisposeInstance = new Emitter(); + get onDidDisposeInstance(): Event { return this._onDidDisposeInstance.event; } private readonly _onInstanceProcessIdReady = new Emitter(); get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } private readonly _onInstanceLinksReady = new Emitter(); @@ -108,20 +102,20 @@ export class TerminalService implements ITerminalService { get onInstanceDimensionsChanged(): Event { return this._onInstanceDimensionsChanged.event; } private readonly _onInstanceMaximumDimensionsChanged = new Emitter(); get onInstanceMaximumDimensionsChanged(): Event { return this._onInstanceMaximumDimensionsChanged.event; } - private readonly _onInstancesChanged = new Emitter(); - get onInstancesChanged(): Event { return this._onInstancesChanged.event; } + private readonly _onDidChangeInstances = new Emitter(); + get onDidChangeInstances(): Event { return this._onDidChangeInstances.event; } private readonly _onInstanceTitleChanged = new Emitter(); get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } private readonly _onInstanceIconChanged = new Emitter(); get onInstanceIconChanged(): Event { return this._onInstanceIconChanged.event; } private readonly _onInstanceColorChanged = new Emitter(); get onInstanceColorChanged(): Event { return this._onInstanceColorChanged.event; } - private readonly _onActiveInstanceChanged = new Emitter(); - get onActiveInstanceChanged(): Event { return this._onActiveInstanceChanged.event; } + private readonly _onDidChangeActiveInstance = new Emitter(); + get onDidChangeActiveInstance(): Event { return this._onDidChangeActiveInstance.event; } private readonly _onInstancePrimaryStatusChanged = new Emitter(); - public get onInstancePrimaryStatusChanged(): Event { return this._onInstancePrimaryStatusChanged.event; } + get onInstancePrimaryStatusChanged(): Event { return this._onInstancePrimaryStatusChanged.event; } private readonly _onGroupDisposed = new Emitter(); - public get onGroupDisposed(): Event { return this._onGroupDisposed.event; } + get onGroupDisposed(): Event { return this._onGroupDisposed.event; } private readonly _onGroupsChanged = new Emitter(); get onGroupsChanged(): Event { return this._onGroupsChanged.event; } private readonly _onDidRegisterProcessSupport = new Emitter(); @@ -130,8 +124,6 @@ export class TerminalService implements ITerminalService { get onDidChangeConnectionState(): Event { return this._onDidChangeConnectionState.event; } private readonly _onDidChangeAvailableProfiles = new Emitter(); get onDidChangeAvailableProfiles(): Event { return this._onDidChangeAvailableProfiles.event; } - private readonly _onPanelOrientationChanged = new Emitter(); - get onPanelOrientationChanged(): Event { return this._onPanelOrientationChanged.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -150,19 +142,17 @@ export class TerminalService implements ITerminalService { @ITelemetryService private readonly _telemetryService: ITelemetryService, @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IEditorOverrideService editorOverrideService: IEditorOverrideService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, @optional(ILocalTerminalService) localTerminalService: ILocalTerminalService ) { this._localTerminalService = localTerminalService; - this._activeGroupIndex = 0; - this._activeInstanceIndex = 0; this._isShuttingDown = false; this._findState = new FindReplaceState(); this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); this._terminalCountContextKey = KEYBINDING_CONTEXT_TERMINAL_COUNT.bindTo(this._contextKeyService); - this._terminalGroupCountContextKey = KEYBINDING_CONTEXT_TERMINAL_GROUP_COUNT.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE.bindTo(this._contextKeyService); this._terminalAltBufferActiveContextKey = KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE.bindTo(this._contextKeyService); this._configHelper = _instantiationService.createInstance(TerminalConfigHelper); @@ -183,14 +173,13 @@ export class TerminalService implements ITerminalService { const instanceId = TerminalInstance.getInstanceIdFromUri(resource); let instance = instanceId === undefined ? undefined : this.getInstanceFromId(instanceId); if (instance) { - const sourceGroup = this.getGroupForInstance(instance); + const sourceGroup = this._terminalGroupService.getGroupForInstance(instance); if (sourceGroup) { sourceGroup.removeInstance(instance); } } else { instance = this.createInstance({}); } - this._terminalEditorService.terminalEditorInstances.push(instance); return { editor: this._terminalEditorService.createEditorInput(instance), options: { @@ -202,16 +191,26 @@ export class TerminalService implements ITerminalService { } ); + this._forwardInstanceHostEvents(this._terminalGroupService); + this._forwardInstanceHostEvents(this._terminalEditorService); + this._terminalGroupService.onDidChangeActiveGroup(this._onActiveGroupChanged.fire, this._onActiveGroupChanged); // the below avoids having to poll routinely. // we update detected profiles when an instance is created so that, // for example, we detect if you've installed a pwsh this.onInstanceCreated(() => this._refreshAvailableProfiles()); - this.onGroupDisposed(group => this._removeGroup(group)); - this.onInstancesChanged(() => this._terminalCountContextKey.set(this._terminalInstances.length)); - this.onGroupsChanged(() => this._terminalGroupCountContextKey.set(this._terminalGroups.length)); + this.onDidChangeInstances(() => this._terminalCountContextKey.set(this.instances.length)); this.onInstanceLinksReady(instance => this._setInstanceLinkProviders(instance)); + // Hide the panel if there are no more instances, provided that VS Code is not shutting + // down. When shutting down the panel is locked in place so that it is restored upon next + // launch. + this._terminalGroupService.onDidChangeActiveInstance(instance => { + if (!instance && !this._isShuttingDown) { + this.hidePanel(); + } + }); + this._handleInstanceContextKeys(); this._processSupportContextKey = KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); @@ -258,6 +257,41 @@ export class TerminalService implements ITerminalService { this._refreshAvailableProfiles(); } + private _forwardInstanceHostEvents(host: ITerminalInstanceHost) { + host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances); + host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance); + // Track the latest active terminal for each host so that when one becomes undefined, the + // TerminalService's active terminal is set to the last active terminal from the other host. + // This means if the last terminal editor is closed such that it becomes undefined, the last + // active group's terminal will be used as the active terminal if available. + this._hostActiveTerminals.set(host, undefined); + host.onDidChangeActiveInstance(instance => { + this._hostActiveTerminals.set(host, instance); + if (instance === undefined) { + for (const active of this._hostActiveTerminals.values()) { + if (active) { + instance = active; + } + } + } + this._activeInstance = instance; + this._onDidChangeActiveInstance.fire(instance); + }); + } + + setActiveInstance(value: ITerminalInstance) { + // If this was a hideFromUser terminal created by the API this was triggered by show, + // in which case we need to create the terminal group + if (value.shellLaunchConfig.hideFromUser) { + this._showBackgroundTerminal(value); + } + if (value.target === TerminalLocation.Editor) { + this._terminalEditorService.setActiveInstance(value); + } else { + this._terminalGroupService.setActiveInstance(value); + } + } + async safeDisposeTerminal(instance: ITerminalInstance): Promise { if (this.configHelper.config.confirmOnExit) { const notConfirmed = await this._showTerminalCloseConfirmation(true); @@ -319,7 +353,7 @@ export class TerminalService implements ITerminalService { // create group and terminal const config = { attachPersistentProcess: terminalLayout.terminal! } as IShellLaunchConfig; terminalInstance = this.createTerminal(config); - group = this.getGroupForInstance(terminalInstance); + group = this._terminalGroupService.getGroupForInstance(terminalInstance); if (groupLayout.isActive) { activeGroup = group; } @@ -328,7 +362,7 @@ export class TerminalService implements ITerminalService { this.splitInstance(terminalInstance, { attachPersistentProcess: terminalLayout.terminal! }); } }); - const activeInstance = this.terminalInstances.find(t => { + const activeInstance = this.instances.find(t => { return t.shellLaunchConfig.attachPersistentProcess?.id === groupLayout.activePersistentProcessId; }); if (activeInstance) { @@ -338,7 +372,7 @@ export class TerminalService implements ITerminalService { } }); if (layoutInfo.tabs.length) { - this.setActiveGroupByIndex(activeGroup ? this.terminalGroups.indexOf(activeGroup) : 0); + this._terminalGroupService.activeGroup = activeGroup; } } return reconnectCounter; @@ -346,8 +380,8 @@ export class TerminalService implements ITerminalService { private _attachProcessLayoutListeners(): void { this.onActiveGroupChanged(() => this._saveState()); - this.onActiveInstanceChanged(() => this._saveState()); - this.onInstancesChanged(() => this._saveState()); + this.onDidChangeActiveInstance(() => this._saveState()); + this.onDidChangeInstances(() => this._saveState()); // The state must be updated when the terminal is relaunched, otherwise the persistent // terminal ID will be stale and the process will be leaked. this.onInstanceProcessIdReady(() => this._saveState()); @@ -358,14 +392,13 @@ export class TerminalService implements ITerminalService { private _handleInstanceContextKeys(): void { const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); const updateTerminalContextKeys = () => { - terminalIsOpenContext.set(this.terminalInstances.length > 0); + terminalIsOpenContext.set(this.instances.length > 0); }; - this.onInstancesChanged(() => updateTerminalContextKeys()); + this.onDidChangeInstances(() => updateTerminalContextKeys()); } getActiveOrCreateInstance(): ITerminalInstance { - const activeInstance = this.getActiveInstance(); - return activeInstance ? activeInstance : this.createTerminal(); + return this.activeInstance || this.createTerminal(); } async setEditable(instance: ITerminalInstance, data?: IEditableData | null): Promise { @@ -423,7 +456,7 @@ export class TerminalService implements ITerminalService { } private _onBeforeShutdown(reason: ShutdownReason): boolean | Promise { - if (this.terminalInstances.length === 0) { + if (this.instances.length === 0) { // No terminal instances, don't veto return false; } @@ -451,22 +484,16 @@ export class TerminalService implements ITerminalService { // Don't touch processes if the shutdown was a result of reload as they will be reattached const shouldPersistTerminals = this._configHelper.config.enablePersistentSessions && e.reason === ShutdownReason.RELOAD; if (shouldPersistTerminals) { - this.terminalInstances.forEach(instance => instance.detachFromProcess()); + this.instances.forEach(instance => instance.detachFromProcess()); return; } // Force dispose of all terminal instances - this.terminalInstances.forEach(instance => instance.dispose(true)); + this.instances.forEach(instance => instance.dispose(true)); this._localTerminalService?.setTerminalLayoutInfo(undefined); } - public getGroupLabels(): string[] { - return this._terminalGroups.filter(group => group.terminalInstances.length > 0).map((group, index) => { - return `${index + 1}: ${group.title ? group.title : ''}`; - }); - } - getFindState(): FindReplaceState { return this._findState; } @@ -476,9 +503,8 @@ export class TerminalService implements ITerminalService { if (!this.configHelper.config.enablePersistentSessions) { return; } - const state: ITerminalsLayoutInfoById = { - tabs: this.terminalGroups.map(g => g.getLayoutInfo(g === this.getActiveGroup())) - }; + const tabs = this._terminalGroupService.groups.map(g => g.getLayoutInfo(g === this._terminalGroupService.activeGroup)); + const state: ITerminalsLayoutInfoById = { tabs }; this._primaryOffProcessTerminalService?.setTerminalLayoutInfo(state); } @@ -498,72 +524,12 @@ export class TerminalService implements ITerminalService { this._primaryOffProcessTerminalService?.updateIcon(instance.persistentProcessId, instance.icon, instance.color); } - private _removeGroup(group: ITerminalGroup): void { - const wasActiveGroup = this._removeGroupAndAdjustFocus(group); - - this._onInstancesChanged.fire(); - this._onGroupsChanged.fire(); - if (wasActiveGroup) { - this._onActiveGroupChanged.fire(); - } - } - - private _removeGroupAndAdjustFocus(group: ITerminalGroup): boolean { - // Get the index of the group and remove it from the list - const index = this._terminalGroups.indexOf(group); - const activeGroup = this.getActiveGroup(); - const activeGroupIndex = activeGroup ? this._terminalGroups.indexOf(activeGroup) : -1; - const wasActiveGroup = group === activeGroup; - if (index !== -1) { - this._terminalGroups.splice(index, 1); - this._onGroupsChanged.fire(); - } - - // Adjust focus if the group was active - if (wasActiveGroup && this._terminalGroups.length > 0) { - const newIndex = index < this._terminalGroups.length ? index : this._terminalGroups.length - 1; - this.setActiveGroupByIndex(newIndex); - const activeInstance = this.getActiveInstance(); - if (activeInstance) { - activeInstance.focus(true); - } - } else if (activeGroupIndex >= this._terminalGroups.length) { - const newIndex = this._terminalGroups.length - 1; - this.setActiveGroupByIndex(newIndex); - } - - // Hide the panel if there are no more instances, provided that VS Code is not shutting - // down. When shutting down the panel is locked in place so that it is restored upon next - // launch. - if (this._terminalGroups.length === 0 && !this._isShuttingDown) { - this.hidePanel(); - this._onActiveInstanceChanged.fire(undefined); - } - - return wasActiveGroup; - } - refreshActiveGroup(): void { - this._onActiveGroupChanged.fire(); - } - - public getActiveGroup(): ITerminalGroup | null { - if (this._activeGroupIndex < 0 || this._activeGroupIndex >= this._terminalGroups.length) { - return null; - } - return this._terminalGroups[this._activeGroupIndex]; - } - - public getActiveInstance(): ITerminalInstance | null { - const group = this.getActiveGroup(); - if (!group) { - return null; - } - return group.activeInstance; + this._onActiveGroupChanged.fire(this._terminalGroupService.activeGroup); } doWithActiveInstance(callback: (terminal: ITerminalInstance) => T): T | void { - const instance = this.getActiveInstance(); + const instance = this.activeInstance; if (instance) { return callback(instance); } @@ -580,41 +546,18 @@ export class TerminalService implements ITerminalService { return this._backgroundedTerminalInstances[bgIndex]; } try { - return this.terminalInstances[this._getIndexFromId(terminalId)]; + return this.instances[this._getIndexFromId(terminalId)]; } catch { return undefined; } } getInstanceFromIndex(terminalIndex: number): ITerminalInstance { - return this.terminalInstances[terminalIndex]; - } - - setActiveInstance(terminalInstance: ITerminalInstance): void { - if (this.configHelper.config.defaultLocation === TerminalLocation.Editor) { - return; - } - // If this was a hideFromUser terminal created by the API this was triggered by show, - // in which case we need to create the terminal group - if (terminalInstance.shellLaunchConfig.hideFromUser) { - this._showBackgroundTerminal(terminalInstance); - } - this.setActiveInstanceByIndex(this._getIndexFromId(terminalInstance.instanceId)); - } - - setActiveGroupByIndex(index: number): void { - if (index >= this._terminalGroups.length) { - return; - } - - this._activeGroupIndex = index; - - this._terminalGroups.forEach((g, i) => g.setVisible(i === this._activeGroupIndex)); - this._onActiveGroupChanged.fire(); + return this.instances[terminalIndex]; } isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean { - return this.terminalInstances.some(term => term.processId === remoteTerm.pid); + return this.instances.some(term => term.processId === remoteTerm.pid); } async initializeTerminals(): Promise { @@ -623,74 +566,15 @@ export class TerminalService implements ITerminalService { } else if (this._localTerminalsInitPromise) { await this._localTerminalsInitPromise; } - if (this.terminalGroups.length === 0 && this.isProcessSupportRegistered) { + if (this._terminalGroupService.groups.length === 0 && this.isProcessSupportRegistered) { this.createTerminal(); } } - private _getInstanceLocation(index: number): IInstanceLocation | undefined { - let currentGroupIndex = 0; - while (index >= 0 && currentGroupIndex < this._terminalGroups.length) { - const group = this._terminalGroups[currentGroupIndex]; - const count = group.terminalInstances.length; - if (index < count) { - return { - group, - groupIndex: currentGroupIndex, - instance: group.terminalInstances[index], - instanceIndex: index - }; - } - index -= count; - currentGroupIndex++; - } - return undefined; - } - - setActiveInstanceByIndex(index: number): void { - const instanceLocation = this._getInstanceLocation(index); - if (!instanceLocation || (this._activeInstanceIndex > 0 && this._activeInstanceIndex === index)) { - return; - } - - this._activeInstanceIndex = instanceLocation.instanceIndex; - this._activeGroupIndex = instanceLocation.groupIndex; - - instanceLocation.group.setActiveInstanceByIndex(this._activeInstanceIndex); - this._terminalGroups.forEach((g, i) => g.setVisible(i === instanceLocation.groupIndex)); - - if (this._activeGroupIndex !== instanceLocation.groupIndex) { - this._onActiveGroupChanged.fire(); - } - this._onActiveInstanceChanged.fire(instanceLocation.instance); - } - - setActiveGroupToNext(): void { - if (this._terminalGroups.length <= 1) { - return; - } - let newIndex = this._activeGroupIndex + 1; - if (newIndex >= this._terminalGroups.length) { - newIndex = 0; - } - this.setActiveGroupByIndex(newIndex); - } - - setActiveGroupToPrevious(): void { - if (this._terminalGroups.length <= 1) { - return; - } - let newIndex = this._activeGroupIndex - 1; - if (newIndex < 0) { - newIndex = this._terminalGroups.length - 1; - } - this.setActiveGroupByIndex(newIndex); - } - splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance | null; splitInstance(instanceToSplit: ITerminalInstance, profile: ITerminalProfile, cwd?: string | URI): ITerminalInstance | null splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile = {}, cwd?: string | URI): ITerminalInstance | null { - const group = this.getGroupForInstance(instanceToSplit); + const group = this._terminalGroupService.getGroupForInstance(instanceToSplit); if (!group) { return null; } @@ -699,115 +583,15 @@ export class TerminalService implements ITerminalService { this._initInstanceListeners(instance); - this._terminalGroups.forEach((g, i) => g.setVisible(i === this._activeGroupIndex)); + this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex)); return instance; } - unsplitInstance(instance: ITerminalInstance): void { - const oldGroup = this.getGroupForInstance(instance); - if (!oldGroup || oldGroup.terminalInstances.length < 2) { - return; - } - - oldGroup.removeInstance(instance); - - const newGroup = this._instantiationService.createInstance(TerminalGroup, this._terminalContainer, instance); - newGroup.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); - this._terminalGroups.push(newGroup); - - newGroup.addDisposable(newGroup.onDisposed(this._onGroupDisposed.fire, this._onGroupDisposed)); - newGroup.addDisposable(newGroup.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); - this._onInstancesChanged.fire(); - this._onGroupsChanged.fire(); - } - - joinInstances(instances: ITerminalInstance[]): void { - // Find the group of the first instance that is the only instance in the group, if one exists - let candidateInstance: ITerminalInstance | undefined = undefined; - let candidateGroup: ITerminalGroup | undefined = undefined; - for (const instance of instances) { - const group = this.getGroupForInstance(instance); - if (group?.terminalInstances.length === 1) { - candidateInstance = instance; - candidateGroup = group; - break; - } - } - - // Create a new group if needed - if (!candidateGroup) { - candidateGroup = this._instantiationService.createInstance(TerminalGroup, this._terminalContainer, undefined); - candidateGroup.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); - this._terminalGroups.push(candidateGroup); - candidateGroup.addDisposable(candidateGroup.onDisposed(this._onGroupDisposed.fire, this._onGroupDisposed)); - candidateGroup.addDisposable(candidateGroup.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); - this._onGroupsChanged.fire(); - } - - const wasActiveGroup = this.getActiveGroup() === candidateGroup; - - // Unsplit all other instances and add them to the new group - for (const instance of instances) { - if (instance === candidateInstance) { - continue; - } - - const oldGroup = this.getGroupForInstance(instance); - if (!oldGroup) { - // Something went wrong, don't join this one - continue; - } - oldGroup.removeInstance(instance); - candidateGroup.addInstance(instance); - } - - // Set the active terminal - this.setActiveInstance(instances[0]); - - // Fire events - this._onInstancesChanged.fire(); - if (!wasActiveGroup) { - this._onActiveGroupChanged.fire(); - } - } - - moveGroup(source: ITerminalInstance, target: ITerminalInstance): void { - const sourceGroup = this.getGroupForInstance(source); - const targetGroup = this.getGroupForInstance(target); - if (!sourceGroup || !targetGroup) { - return; - } - const sourceGroupIndex = this._terminalGroups.indexOf(sourceGroup); - const targetGroupIndex = this._terminalGroups.indexOf(targetGroup); - this._terminalGroups.splice(sourceGroupIndex, 1); - this._terminalGroups.splice(targetGroupIndex, 0, sourceGroup); - this._onInstancesChanged.fire(); - } - - moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'before' | 'after'): void { - const sourceGroup = this.getGroupForInstance(source); - const targetGroup = this.getGroupForInstance(target); - if (!sourceGroup || !targetGroup) { - return; - } - - // Move from the source group to the target group - if (sourceGroup !== targetGroup) { - // Move groups - sourceGroup.removeInstance(source); - targetGroup.addInstance(source); - } - - // Rearrange within the target group - const index = targetGroup.terminalInstances.indexOf(target) + (side === 'after' ? 1 : 0); - targetGroup.moveInstance(source, index); - } - moveToEditor(source: ITerminalInstance): void { if (source.target === TerminalLocation.Editor) { return; } - const sourceGroup = this.getGroupForInstance(source); + const sourceGroup = this._terminalGroupService.getGroupForInstance(source); if (!sourceGroup) { return; } @@ -832,16 +616,11 @@ export class TerminalService implements ITerminalService { let group: ITerminalGroup | undefined; if (target) { - group = this.getGroupForInstance(target); + group = this._terminalGroupService.getGroupForInstance(target); } - // TODO: Share code with joinInstances - move into terminal group service if (!group) { - group = this._instantiationService.createInstance(TerminalGroup, this._terminalContainer, undefined); - group.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); - this._terminalGroups.push(group); - group.addDisposable(group.onDisposed(this._onGroupDisposed.fire, this._onGroupDisposed)); - group.addDisposable(group.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); + group = this._terminalGroupService.createGroup(); } group.addInstance(source); @@ -856,13 +635,11 @@ export class TerminalService implements ITerminalService { } // Fire events - this._onInstancesChanged.fire(); - this._onGroupsChanged.fire(); - this._onActiveGroupChanged.fire(); + this._onDidChangeInstances.fire(); + this._onActiveGroupChanged.fire(this._terminalGroupService.activeGroup); } protected _initInstanceListeners(instance: ITerminalInstance): void { - instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); instance.addDisposable(instance.onIconChanged(this._onInstanceIconChanged.fire, this._onInstanceIconChanged)); instance.addDisposable(instance.onIconChanged(this._onInstanceColorChanged.fire, this._onInstanceColorChanged)); @@ -876,7 +653,7 @@ export class TerminalService implements ITerminalService { } })); instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onInstanceMaximumDimensionsChanged.fire(instance))); - instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); + instance.addDisposable(instance.onFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance)); instance.addDisposable(instance.onRequestAddInstanceToGroup(e => { const instanceId = TerminalInstance.getInstanceIdFromUri(e.uri); if (instanceId === undefined) { @@ -886,11 +663,12 @@ export class TerminalService implements ITerminalService { // View terminals let sourceInstance = this.getInstanceFromId(instanceId); if (sourceInstance) { - this.moveInstance(sourceInstance, instance, e.side); + this._terminalGroupService.moveInstance(sourceInstance, instance, e.side); + return; } // Terminal editors - sourceInstance = this._terminalEditorService.terminalEditorInstances.find(instance => instance.resource.path === e.uri.path); + sourceInstance = this._terminalEditorService.instances.find(instance => instance.resource.path === e.uri.path); if (sourceInstance) { this.moveToTerminalView(sourceInstance, instance, e.side); } @@ -908,7 +686,7 @@ export class TerminalService implements ITerminalService { registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable { const disposables: IDisposable[] = []; this._linkProviders.add(linkProvider); - for (const instance of this.terminalInstances) { + for (const instance of this.instances) { if (instance.areLinksReady) { disposables.push(instance.registerLinkProvider(linkProvider)); } @@ -943,18 +721,6 @@ export class TerminalService implements ITerminalService { } } - instanceIsSplit(instance: ITerminalInstance): boolean { - const group = this.getGroupForInstance(instance); - if (!group) { - return false; - } - return group.terminalInstances.length > 1; - } - - getGroupForInstance(instance: ITerminalInstance): ITerminalGroup | undefined { - return this._terminalGroups.find(group => group.terminalInstances.indexOf(instance) !== -1); - } - async showPanel(focus?: boolean): Promise { if (this.configHelper.config.defaultLocation === TerminalLocation.Editor) { return; @@ -967,7 +733,7 @@ export class TerminalService implements ITerminalService { // Do the focus call asynchronously as going through the // command palette will force editor focus await timeout(0); - const instance = this.getActiveInstance(); + const instance = this.activeInstance; if (instance) { await instance.focusWhenReady(true); } @@ -975,7 +741,7 @@ export class TerminalService implements ITerminalService { } async focusTabs(): Promise { - if (this._terminalInstances.length === 0) { + if (this.instances.length === 0) { return; } await this.showPanel(true); @@ -987,9 +753,10 @@ export class TerminalService implements ITerminalService { this._configurationService.updateValue(TerminalSettingId.TabsEnabled, true); } + // TODO: Remove this, it should live in group/editor servioce private _getIndexFromId(terminalId: number): number { let terminalIndex = -1; - this.terminalInstances.forEach((terminalInstance, i) => { + this.instances.forEach((terminalInstance, i) => { if (terminalInstance.instanceId === terminalId) { terminalIndex = i; } @@ -1002,10 +769,10 @@ export class TerminalService implements ITerminalService { protected async _showTerminalCloseConfirmation(singleTerminal?: boolean): Promise { let message: string; - if (this.terminalInstances.length === 1 || singleTerminal) { + if (this.instances.length === 1 || singleTerminal) { message = nls.localize('terminalService.terminalCloseConfirmationSingular', "There is an active terminal session, do you want to kill it?"); } else { - message = nls.localize('terminalService.terminalCloseConfirmationPlural', "There are {0} active terminal sessions, do you want to kill them?", this.terminalInstances.length); + message = nls.localize('terminalService.terminalCloseConfirmationPlural', "There are {0} active terminal sessions, do you want to kill them?", this.instances.length); } const res = await this._dialogService.confirm({ message, @@ -1092,7 +859,7 @@ export class TerminalService implements ITerminalService { return; } if (type === 'createInstance') { - const activeInstance = this.getActiveInstance(); + const activeInstance = this.activeInstance; let instance; if ('id' in value.profile) { @@ -1147,8 +914,8 @@ export class TerminalService implements ITerminalService { } try { await profileProvider.createContributedTerminalProfile(isSplitTerminal); - this.setActiveInstanceByIndex(this._terminalInstances.length - 1); - await this.getActiveInstance()?.focusWhenReady(); + this._terminalGroupService.setActiveInstanceByIndex(this.instances.length - 1); + await this.activeInstance?.focusWhenReady(); } catch (e) { this._notificationService.error(e.message); } @@ -1228,6 +995,9 @@ export class TerminalService implements ITerminalService { if (shellLaunchConfig.hideFromUser) { const instance = this.createInstance(shellLaunchConfig); this._backgroundedTerminalInstances.push(instance); + this._backgroundedTerminalDisposables.set(instance.instanceId, [ + instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance) + ]); this._initInstanceListeners(instance); return instance; } @@ -1249,42 +1019,38 @@ export class TerminalService implements ITerminalService { instance = this.createInstance(shellLaunchConfig); this._terminalEditorService.createEditor(instance); this._initInstanceListeners(instance); - this._onInstancesChanged.fire(); + this._onDidChangeInstances.fire(); } else { - const terminalGroup = this._instantiationService.createInstance(TerminalGroup, this._terminalContainer, shellLaunchConfig); - this._terminalGroups.push(terminalGroup); - terminalGroup.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); - - instance = terminalGroup.terminalInstances[0]; - - terminalGroup.addDisposable(terminalGroup.onDisposed(this._onGroupDisposed.fire, this._onGroupDisposed)); - terminalGroup.addDisposable(terminalGroup.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); + const group = this._terminalGroupService.createGroup(shellLaunchConfig); + instance = group.terminalInstances[0]; this._initInstanceListeners(instance); - this._onInstancesChanged.fire(); - this._onGroupsChanged.fire(); + this._onDidChangeInstances.fire(); } - if (this.terminalInstances.length === 1) { + if (this.instances.length === 1) { // It's the first instance so it should be made active automatically, this must fire // after onInstancesChanged so consumers can react to the instance being added first - this.setActiveInstanceByIndex(0); + this._terminalGroupService.setActiveInstanceByIndex(0); } return instance; } protected _showBackgroundTerminal(instance: ITerminalInstance): void { this._backgroundedTerminalInstances.splice(this._backgroundedTerminalInstances.indexOf(instance), 1); - instance.shellLaunchConfig.hideFromUser = false; - const terminalGroup = this._instantiationService.createInstance(TerminalGroup, this._terminalContainer, instance); - this._terminalGroups.push(terminalGroup); - terminalGroup.onPanelOrientationChanged((orientation) => this._onPanelOrientationChanged.fire(orientation)); - terminalGroup.addDisposable(terminalGroup.onDisposed(this._onGroupDisposed.fire, this._onGroupDisposed)); - terminalGroup.addDisposable(terminalGroup.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); - if (this.terminalInstances.length === 1) { - // It's the first instance so it should be made active automatically - this.setActiveInstanceByIndex(0); + const disposables = this._backgroundedTerminalDisposables.get(instance.instanceId); + if (disposables) { + dispose(disposables); } - this._onInstancesChanged.fire(); + this._backgroundedTerminalDisposables.delete(instance.instanceId); + instance.shellLaunchConfig.hideFromUser = false; + this._terminalGroupService.createGroup(instance); + + // Make active automatically if it's the first instance + if (this.instances.length === 1) { + this._terminalGroupService.setActiveInstanceByIndex(0); + } + + this._onDidChangeInstances.fire(); this._onGroupsChanged.fire(); } @@ -1317,8 +1083,7 @@ export class TerminalService implements ITerminalService { async setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): Promise { this._configHelper.panelContainer = panelContainer; - this._terminalContainer = terminalContainer; - this._terminalGroups.forEach(group => group.attachToElement(terminalContainer)); + this._terminalGroupService.setContainer(terminalContainer); } hidePanel(): void { @@ -1337,10 +1102,3 @@ export class TerminalService implements ITerminalService { interface IProfileQuickPickItem extends IQuickPickItem { profile: ITerminalProfile | (ITerminalProfileContribution & { extensionIdentifier: string }); } - -interface IInstanceLocation { - group: ITerminalGroup, - groupIndex: number, - instance: ITerminalInstance, - instanceIndex: number -} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 69a0e7f6d62..38b52d5d0fa 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -7,7 +7,7 @@ import { LayoutPriority, Orientation, Sizing, SplitView } from 'vs/base/browser/ import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; import { TerminalTabsListSizes, TerminalTabList } from 'vs/workbench/contrib/terminal/browser/terminalTabsList'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; @@ -71,6 +71,7 @@ export class TerminalTabbedView extends Disposable { constructor( parentElement: HTMLElement, @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotificationService private readonly _notificationService: INotificationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @@ -128,8 +129,8 @@ export class TerminalTabbedView extends Disposable { } } }); - this._register(this._terminalService.onInstancesChanged(() => this._refreshShowTabs())); - this._register(this._terminalService.onGroupsChanged(() => this._refreshShowTabs())); + this._register(this._terminalGroupService.onDidChangeInstances(() => this._refreshShowTabs())); + this._register(this._terminalGroupService.onDidChangeGroups(() => this._refreshShowTabs())); this._register(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); this._updateTheme(); @@ -138,7 +139,7 @@ export class TerminalTabbedView extends Disposable { this._attachEventListeners(parentElement, this._terminalContainer); - this._terminalService.onPanelOrientationChanged((orientation) => { + this._terminalGroupService.onPanelOrientationChanged((orientation) => { this._panelOrientation = orientation; }); @@ -158,11 +159,11 @@ export class TerminalTabbedView extends Disposable { return true; } - if (hide === 'singleTerminal' && this._terminalService.terminalInstances.length > 1) { + if (hide === 'singleTerminal' && this._terminalGroupService.instances.length > 1) { return true; } - if (hide === 'singleGroup' && this._terminalService.terminalGroups.length > 1) { + if (hide === 'singleGroup' && this._terminalGroupService.groups.length > 1) { return true; } @@ -208,7 +209,7 @@ export class TerminalTabbedView extends Disposable { if (ctx) { const style = window.getComputedStyle(this._tabListElement); ctx.font = `${style.fontStyle} ${style.fontSize} ${style.fontFamily}`; - const maxInstanceWidth = this._terminalService.terminalInstances.reduce((p, c) => { + const maxInstanceWidth = this._terminalGroupService.instances.reduce((p, c) => { return Math.max(p, ctx.measureText(c.title + (c.shellLaunchConfig.description || '')).width + this._getAdditionalWidth(c)); }, 0); idealWidth = Math.ceil(Math.max(maxInstanceWidth, TerminalTabsListSizes.WideViewMinimumWidth)); @@ -226,7 +227,7 @@ export class TerminalTabbedView extends Disposable { // Size to include padding, icon, status icon (if any), split annotation (if any), + a little more const additionalWidth = 30; const statusIconWidth = instance.statusList.statuses.length > 0 ? STATUS_ICON_WIDTH : 0; - const splitAnnotationWidth = (this._terminalService.getGroupForInstance(instance)?.terminalInstances.length || 0) > 1 ? SPLIT_ANNOTATION_WIDTH : 0; + const splitAnnotationWidth = (this._terminalGroupService.getGroupForInstance(instance)?.terminalInstances.length || 0) > 1 ? SPLIT_ANNOTATION_WIDTH : 0; return additionalWidth + splitAnnotationWidth + statusIconWidth; } @@ -260,7 +261,7 @@ export class TerminalTabbedView extends Disposable { } this._splitView.addView({ element: terminalOuterContainer, - layout: width => this._terminalService.terminalGroups.forEach(tab => tab.layout(width, this._height || 0)), + layout: width => this._terminalGroupService.groups.forEach(tab => tab.layout(width, this._height || 0)), minimumSize: 120, maximumSize: Number.POSITIVE_INFINITY, onDidChange: () => Disposable.None, @@ -342,21 +343,21 @@ export class TerminalTabbedView extends Disposable { event.stopPropagation(); })); this._register(dom.addDisposableListener(terminalContainer, 'mousedown', async (event: MouseEvent) => { - if (this._terminalService.terminalInstances.length === 0) { + if (this._terminalGroupService.instances.length === 0) { return; } if (event.which === 2 && isLinux) { // Drop selection and focus terminal on Linux to enable middle button paste when click // occurs on the selection itself. - const terminal = this._terminalService.getActiveInstance(); + const terminal = this._terminalGroupService.activeInstance; if (terminal) { terminal.focus(); } } else if (event.which === 3) { const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { - const terminal = this._terminalService.getActiveInstance(); + const terminal = this._terminalGroupService.activeInstance; if (!terminal) { return; } @@ -490,7 +491,7 @@ export class TerminalTabbedView extends Disposable { focusFindWidget() { this._findWidgetVisible.set(true); - const activeInstance = this._terminalService.getActiveInstance(); + const activeInstance = this._terminalGroupService.activeInstance; if (activeInstance && activeInstance.hasSelection() && activeInstance.selection!.indexOf('\n') === -1) { this._findWidget!.reveal(activeInstance.selection); } else { @@ -505,7 +506,7 @@ export class TerminalTabbedView extends Disposable { } showFindWidget() { - const activeInstance = this._terminalService.getActiveInstance(); + const activeInstance = this._terminalGroupService.activeInstance; if (activeInstance && activeInstance.hasSelection() && activeInstance.selection!.indexOf('\n') === -1) { this._findWidget!.show(activeInstance.selection); } else { @@ -536,6 +537,6 @@ export class TerminalTabbedView extends Disposable { } private _focus() { - this._terminalService.getActiveInstance()?.focusWhenReady(); + this._terminalGroupService.activeInstance?.focusWhenReady(); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 138eb99d3ae..2c8fceb5959 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -9,14 +9,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IS_SPLIT_TERMINAL_CONTEXT_KEY, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, TerminalCommandId, TerminalLocation } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IS_SPLIT_TERMINAL_CONTEXT_KEY, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { Codicon } from 'vs/base/common/codicons'; import { Action } from 'vs/base/common/actions'; @@ -68,7 +68,8 @@ export class TerminalTabList extends WorkbenchList { @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, - @ITerminalService private _terminalService: ITerminalService, + @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @ITerminalInstanceService _terminalInstanceService: ITerminalInstanceService, @IInstantiationService instantiationService: IInstantiationService, @IDecorationsService _decorationsService: IDecorationsService, @@ -99,16 +100,16 @@ export class TerminalTabList extends WorkbenchList { configurationService, keybindingService, ); - this._terminalService.onInstancesChanged(() => this.refresh()); - this._terminalService.onGroupsChanged(() => this.refresh()); + this._terminalGroupService.onDidChangeInstances(() => this.refresh()); + this._terminalGroupService.onDidChangeGroups(() => this.refresh()); this._terminalService.onInstanceTitleChanged(() => this.refresh()); this._terminalService.onInstanceIconChanged(() => this.refresh()); this._terminalService.onInstancePrimaryStatusChanged(() => this.refresh()); this._terminalService.onDidChangeConnectionState(() => this.refresh()); this._themeService.onDidColorThemeChange(() => this.refresh()); - this._terminalService.onActiveInstanceChanged(e => { - if (e && e.target !== TerminalLocation.Editor) { - const i = this._terminalService.terminalInstances.indexOf(e); + this._terminalGroupService.onDidChangeActiveInstance(e => { + if (e) { + const i = this._terminalGroupService.instances.indexOf(e); this.setSelection([i]); this.reveal(i); } @@ -117,7 +118,7 @@ export class TerminalTabList extends WorkbenchList { this.onMouseDblClick(async () => { if (this.getFocus().length === 0) { const instance = this._terminalService.createTerminal(); - this._terminalService.setActiveInstance(instance); + this._terminalGroupService.setActiveInstance(instance); await instance.focusWhenReady(); } }); @@ -162,7 +163,7 @@ export class TerminalTabList extends WorkbenchList { if (e.editorOptions.pinned) { return; } - this._terminalService.setActiveInstance(instance); + this._terminalGroupService.setActiveInstance(instance); if (!e.editorOptions.preserveFocus) { await instance.focusWhenReady(); } @@ -175,13 +176,13 @@ export class TerminalTabList extends WorkbenchList { } refresh(): void { - this.splice(0, this.length, this._terminalService.terminalInstances); + this.splice(0, this.length, this._terminalGroupService.instances.slice()); } private _updateContextKey() { this._terminalTabsSingleSelectedContextKey.set(this.getSelectedElements().length === 1); const instance = this.getFocusedElements(); - this._isSplitContextKey.set(instance.length > 0 && this._terminalService.instanceIsSplit(instance[0])); + this._isSplitContextKey.set(instance.length > 0 && this._terminalGroupService.instanceIsSplit(instance[0])); } } @@ -194,6 +195,7 @@ class TerminalTabsRenderer implements IListRenderer ITerminalInstance[], @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IHoverService private readonly _hoverService: IHoverService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @@ -250,7 +252,7 @@ class TerminalTabsRenderer implements IListRenderer { - constructor(@ITerminalService private readonly _terminalService: ITerminalService) { } + constructor( + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + ) { } getWidgetAriaLabel(): string { return localize('terminal.tabs', "Terminal tabs"); @@ -488,7 +492,7 @@ class TerminalTabsAccessibilityProvider implements IListAccessibilityProvider 1) { const terminalIndex = tab.terminalInstances.indexOf(instance); ariaLabel = localize({ @@ -519,6 +523,7 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { constructor( @ITerminalService private _terminalService: ITerminalService, + @ITerminalGroupService private _terminalGroupService: ITerminalGroupService, @ITerminalInstanceService private _terminalInstanceService: ITerminalInstanceService ) { } @@ -603,13 +608,13 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { if (!targetInstance) { for (const instance of sourceInstances) { - this._terminalService.unsplitInstance(instance); + this._terminalGroupService.unsplitInstance(instance); } return; } for (const instance of sourceInstances) { - this._terminalService.moveGroup(instance, targetInstance); + this._terminalGroupService.moveGroup(instance, targetInstance); if (!focused) { this._terminalService.setActiveInstance(instance); focused = true; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index f8f55275b8b..8fd7c0f33d7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -14,7 +14,7 @@ import { IThemeService, IColorTheme, registerThemingParticipant, ICssStyleCollec import { switchTerminalActionViewItemSeparator, switchTerminalShowTabsTitle } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR, TERMINAL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -66,6 +66,7 @@ export class TerminalViewPane extends ViewPane { @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @INotificationService private readonly _notificationService: INotificationService, @@ -130,7 +131,7 @@ export class TerminalViewPane extends ViewPane { this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { - const hadTerminals = !!this._terminalService.terminalGroups.length; + const hadTerminals = !!this._terminalGroupService.groups.length; if (this._terminalService.isProcessSupportRegistered) { if (this._terminalsInitialized) { if (!hadTerminals) { @@ -143,14 +144,14 @@ export class TerminalViewPane extends ViewPane { } if (hadTerminals) { - this._terminalService.getActiveGroup()?.setVisible(visible); + this._terminalGroupService.activeGroup?.setVisible(visible); } else { // TODO@Tyriar - this call seems unnecessary this.layoutBody(this._bodyDimensions.height, this._bodyDimensions.width); } this._terminalService.showPanel(true); } else { - this._terminalService.getActiveGroup()?.setVisible(false); + this._terminalGroupService.activeGroup?.setVisible(false); } })); this.layoutBody(this._parentDomElement.offsetHeight, this._parentDomElement.offsetWidth); @@ -282,11 +283,11 @@ export class TerminalViewPane extends ViewPane { } private _focus() { - this._terminalService.getActiveInstance()?.focusWhenReady(); + this._terminalService.activeInstance?.focusWhenReady(); } override shouldShowWelcome(): boolean { - this._isWelcomeShowing = !this._terminalService.isProcessSupportRegistered && this._terminalService.terminalInstances.length === 0; + this._isWelcomeShowing = !this._terminalService.isProcessSupportRegistered && this._terminalService.instances.length === 0; return this._isWelcomeShowing; } } @@ -315,16 +316,16 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem { constructor( action: IAction, @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, @IThemeService private readonly _themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, getTerminalSelectOpenItems(_terminalService), _terminalService.activeGroupIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Open Terminals.'), optionsAsChildren: true }); - this._register(_terminalService.onInstancesChanged(() => this._updateItems(), this)); - this._register(_terminalService.onGroupsChanged(() => this._updateItems(), this)); + super(null, action, getTerminalSelectOpenItems(_terminalService, _terminalGroupService), _terminalGroupService.activeGroupIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Open Terminals.'), optionsAsChildren: true }); + this._register(_terminalService.onDidChangeInstances(() => this._updateItems(), this)); this._register(_terminalService.onActiveGroupChanged(() => this._updateItems(), this)); - this._register(_terminalService.onActiveInstanceChanged(() => this._updateItems(), this)); + this._register(_terminalService.onDidChangeActiveInstance(() => this._updateItems(), this)); this._register(_terminalService.onInstanceTitleChanged(() => this._updateItems(), this)); - this._register(_terminalService.onGroupDisposed(() => this._updateItems(), this)); + this._register(_terminalGroupService.onDidChangeGroups(() => this._updateItems(), this)); this._register(_terminalService.onDidChangeConnectionState(() => this._updateItems(), this)); this._register(_terminalService.onDidChangeAvailableProfiles(() => this._updateItems(), this)); this._register(attachSelectBoxStyler(this.selectBox, this._themeService)); @@ -339,15 +340,15 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem { } private _updateItems(): void { - const options = getTerminalSelectOpenItems(this._terminalService); - this.setOptions(options, this._terminalService.activeGroupIndex); + const options = getTerminalSelectOpenItems(this._terminalService, this._terminalGroupService); + this.setOptions(options, this._terminalGroupService.activeGroupIndex); } } -function getTerminalSelectOpenItems(terminalService: ITerminalService): ISelectOptionItem[] { +function getTerminalSelectOpenItems(terminalService: ITerminalService, terminalGroupService: ITerminalGroupService): ISelectOptionItem[] { let items: ISelectOptionItem[]; if (terminalService.connectionState === TerminalConnectionState.Connected) { - items = terminalService.getGroupLabels().map(label => { + items = terminalGroupService.getGroupLabels().map(label => { return { text: label }; }); } else { @@ -377,8 +378,8 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { super(new MenuItemAction( { id: action.id, - title: getSingleTabLabel(_terminalService.getActiveInstance()), - tooltip: getSingleTabTooltip(_terminalService.getActiveInstance()) + title: getSingleTabLabel(_terminalService.activeInstance), + tooltip: getSingleTabTooltip(_terminalService.activeInstance) }, { id: TerminalCommandId.Split, @@ -391,15 +392,15 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { ), keybindingService, notificationService); this._register(this._terminalService.onInstancePrimaryStatusChanged(() => this.updateLabel())); - this._register(this._terminalService.onActiveInstanceChanged(() => this.updateLabel())); + this._register(this._terminalService.onDidChangeActiveInstance(() => this.updateLabel())); this._register(this._terminalService.onInstanceTitleChanged(e => { - if (e === this._terminalService.getActiveInstance()) { + if (e === this._terminalService.activeInstance) { this._action.tooltip = getSingleTabTooltip(e); this.updateLabel(); } })); this._register(this._terminalService.onInstanceIconChanged(e => { - if (e === this._terminalService.getActiveInstance()) { + if (e === this._terminalService.activeInstance) { this.updateLabel(); } })); @@ -426,14 +427,14 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { // Middle click kills this._elementDisposables.push(dom.addDisposableListener(this.element!, dom.EventType.AUXCLICK, e => { if (e.button === 1) { - this._terminalService.getActiveInstance()?.dispose(); + this._terminalService.activeInstance?.dispose(); e.preventDefault(); } })); } if (this.label) { const label = this.label; - const instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.activeInstance; if (!instance) { dom.reset(label, ''); return; @@ -492,7 +493,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { } } -function getSingleTabLabel(instance: ITerminalInstance | null, icon?: ThemeIcon) { +function getSingleTabLabel(instance: ITerminalInstance | undefined, icon?: ThemeIcon) { // Don't even show the icon if there is no title as the icon would shift around when the title // is added if (!instance || !instance.title) { @@ -508,7 +509,7 @@ function getSingleTabLabel(instance: ITerminalInstance | null, icon?: ThemeIcon) return `${label} $(${primaryStatus.icon.id})`; } -function getSingleTabTooltip(instance: ITerminalInstance | null): string { +function getSingleTabTooltip(instance: ITerminalInstance | undefined): string { if (!instance) { return ''; } @@ -523,7 +524,8 @@ class TerminalThemeIconStyle extends Themable { constructor( container: HTMLElement, @IThemeService private readonly _themeService: IThemeService, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService ) { super(_themeService); this._registerListeners(); @@ -536,8 +538,8 @@ class TerminalThemeIconStyle extends Themable { private _registerListeners(): void { this._register(this._terminalService.onInstanceIconChanged(() => this.updateStyles())); this._register(this._terminalService.onInstanceColorChanged(() => this.updateStyles())); - this._register(this._terminalService.onInstancesChanged(() => this.updateStyles())); - this._register(this._terminalService.onGroupsChanged(() => this.updateStyles())); + this._register(this._terminalService.onDidChangeInstances(() => this.updateStyles())); + this._register(this._terminalGroupService.onDidChangeGroups(() => this.updateStyles())); } override updateStyles(): void { @@ -548,7 +550,7 @@ class TerminalThemeIconStyle extends Themable { let css = ''; // Add icons - for (const instance of this._terminalService.terminalInstances) { + for (const instance of this._terminalService.instances) { const icon = instance.icon; if (!icon) { continue; @@ -567,7 +569,7 @@ class TerminalThemeIconStyle extends Themable { } // Add colors - for (const instance of this._terminalService.terminalInstances) { + for (const instance of this._terminalService.instances) { const colorClass = getColorClass(instance); if (!colorClass || !instance.color) { continue; diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts index 651030fe136..ce9cd074027 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalNativeContribution.ts @@ -37,7 +37,7 @@ export class TerminalNativeContribution extends Disposable implements IWorkbench } private _onOsResume(): void { - this._terminalService.terminalInstances.forEach(instance => instance.forceRedraw()); + this._terminalService.instances.forEach(instance => instance.forceRedraw()); } private async _onOpenFileRequest(request: INativeOpenFileRequest): Promise { @@ -49,7 +49,7 @@ export class TerminalNativeContribution extends Disposable implements IWorkbench await this._whenFileDeleted(waitMarkerFileUri); // Focus active terminal - this._terminalService.getActiveInstance()?.focus(); + this._terminalService.activeInstance?.focus(); } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts b/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts index 3e2cbafbb0c..ad63dc72cdf 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputTerminalService.ts @@ -56,7 +56,7 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi // If a result terminal is currently active and we start a new test run, // stream live results there automatically. resultService.onResultsChanged(evt => { - const active = this.terminalService.getActiveInstance(); + const active = this.terminalService.activeInstance; if (!('started' in evt) || !active) { return; } @@ -77,7 +77,7 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi * @inheritdoc */ public async open(result: ITestResult | undefined): Promise { - const testOutputPtys = this.terminalService.terminalInstances + const testOutputPtys = this.terminalService.instances .map(t => { const output = this.outputTerminals.get(t); return output ? [t, output] as const : undefined;