From a7c466d64983d71b066cf6ce50a4559e41b3042a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 20 Jul 2021 17:16:41 -0500 Subject: [PATCH] dnd terminals bw windows in panel and fix moving from editor to panel (#128875) --- src/vs/platform/terminal/common/terminal.ts | 5 + src/vs/workbench/browser/dnd.ts | 14 ++- .../contrib/terminal/browser/terminal.ts | 16 ++- .../terminal/browser/terminalEditorInput.ts | 89 ++++++++++----- .../browser/terminalEditorSerializer.ts | 15 ++- .../terminal/browser/terminalEditorService.ts | 108 +++++++++++++----- .../terminal/browser/terminalGroupService.ts | 14 +++ .../terminal/browser/terminalInstance.ts | 15 +-- .../browser/terminalInstanceService.ts | 9 +- .../terminal/browser/terminalService.ts | 88 +++++++------- .../terminal/browser/terminalTabsList.ts | 28 ++--- .../contrib/terminal/browser/terminalUri.ts | 28 +++++ .../contrib/terminal/browser/terminalView.ts | 2 +- .../test/browser/workbenchTestServices.ts | 4 +- 14 files changed, 300 insertions(+), 135 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalUri.ts diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 403b26e084d..67063115ab4 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -401,6 +401,11 @@ export interface ICreateTerminalOptions { * Where to create the terminal, when not specified the default target will be used. */ target?: TerminalLocation; + + /** + * The terminal's resource, passed when the terminal has moved windows. + */ + resource?: URI; } export interface ICreateContributedTerminalProfileOptions { diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 306e517de2f..97523d84e34 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -113,8 +113,20 @@ export function extractEditorsDropData(e: DragEvent, externalOnly?: boolean): Ar // Invalid transfer } } - } + // Check for terminals transfer + const terminals = e.dataTransfer.getData(DataTransfers.TERMINALS); + if (terminals) { + try { + const terminalEditors: string[] = JSON.parse(terminals); + for (const terminalEditor of terminalEditors) { + editors.push({ resource: URI.parse(terminalEditor), isExternal: true }); + } + } catch (error) { + // Invalid transfer + } + } + } return editors; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 3e317e67905..2ea9dcb8762 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -19,7 +19,7 @@ import { ICompleteTerminalConfiguration } from 'vs/workbench/contrib/terminal/co import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IEditableData } from 'vs/workbench/common/views'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; -import { SerializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; +import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); @@ -54,7 +54,7 @@ export interface ITerminalInstanceService { */ preparePathForTerminalAsync(path: string, executable: string | undefined, title: string, shellType: TerminalShellType, isRemote: boolean): Promise; - createInstance(launchConfig: IShellLaunchConfig, target?: TerminalLocation): ITerminalInstance; + createInstance(launchConfig: IShellLaunchConfig, target?: TerminalLocation, resource?: URI): ITerminalInstance; } export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper { @@ -140,7 +140,8 @@ export interface ITerminalService extends ITerminalInstanceHost { */ getInstanceFromId(terminalId: number): ITerminalInstance | undefined; getInstanceFromIndex(terminalIndex: number): ITerminalInstance; - getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined; + + getActiveOrCreateInstance(): ITerminalInstance; splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig, cwd?: string | URI): ITerminalInstance | null; splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null; @@ -198,7 +199,7 @@ export interface ITerminalEditorService extends ITerminalInstanceHost, ITerminal readonly instances: readonly ITerminalInstance[]; openEditor(instance: ITerminalInstance): Promise; - getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput): TerminalEditorInput; + getOrCreateEditorInput(instance: ITerminalInstance | DeserializedTerminalEditorInput | URI): TerminalEditorInput; detachActiveEditorInstance(): ITerminalInstance; detachInstance(instance: ITerminalInstance): void; splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance; @@ -271,6 +272,11 @@ export interface ITerminalInstanceHost { readonly onDidChangeInstances: Event; setActiveInstance(instance: ITerminalInstance): void; + /** + * Gets an instance from a resource if it exists. This MUST be used instead of getInstanceFromId + * when you only know about a terminal's URI. (a URI's instance ID may not be this window's instance ID) + */ + getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined; } export interface ITerminalFindHost { @@ -341,6 +347,8 @@ export interface ITerminalInstance { * A unique URI for this terminal instance with the following encoding: * path: // * fragment: Title + * Note that when dragging terminals across windows, this will retain the original workspace ID /instance ID + * from the other window. */ readonly resource: URI; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts index df72c6a8cb7..89927714bbb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts @@ -19,9 +19,13 @@ import { ConfirmOnKill } from 'vs/workbench/contrib/terminal/common/terminal'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { Emitter } from 'vs/base/common/event'; export class TerminalEditorInput extends EditorInput { + protected readonly _onDidRequestAttach = this._register(new Emitter()); + readonly onDidRequestAttach = this._onDidRequestAttach.event; + static readonly ID = 'workbench.editors.terminal'; private _isDetached = false; @@ -47,11 +51,26 @@ export class TerminalEditorInput extends EditorInput { return TerminalEditor.ID; } + setTerminalInstance(instance: ITerminalInstance): void { + if (this._terminalInstance) { + throw new Error('cannot set instance that has already been set'); + } + this._terminalInstance = instance; + this._setupInstanceListeners(); + + // Refresh dirty state when the confirm on kill setting is changed + this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.ConfirmOnKill)) { + this._onDidChangeDirty.fire(); + } + }); + } + override copy(): IEditorInput { const instance = this._copyInstance || this._terminalInstanceService.createInstance({}, TerminalLocation.Editor); instance.focusWhenReady(); this._copyInstance = undefined; - return this._instantiationService.createInstance(TerminalEditorInput, instance); + return this._instantiationService.createInstance(TerminalEditorInput, instance.resource, instance); } /** @@ -69,47 +88,27 @@ export class TerminalEditorInput extends EditorInput { return this._isDetached ? undefined : this._terminalInstance; } - get resource(): URI { - return this._terminalInstance.resource; - } - override isDirty(): boolean { const confirmOnKill = this._configurationService.getValue(TerminalSettingId.ConfirmOnKill); if (confirmOnKill === 'editor' || confirmOnKill === 'always') { - return this._terminalInstance.hasChildProcesses; + return this._terminalInstance?.hasChildProcesses || false; } return false; } constructor( - private readonly _terminalInstance: ITerminalInstance, + public readonly resource: URI, + private _terminalInstance: ITerminalInstance | undefined, @IThemeService private readonly _themeService: IThemeService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ILifecycleService lifecycleService: ILifecycleService, - @IContextKeyService contextKeyService: IContextKeyService + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IContextKeyService _contextKeyService: IContextKeyService ) { super(); - this._terminalEditorFocusContextKey = TerminalContextKeys.editorFocus.bindTo(contextKeyService); - - this._register(toDisposable(() => { - if (!this._isDetached && !this._isShuttingDown) { - this._terminalInstance.dispose(); - } - })); - - const disposeListeners = [ - this._terminalInstance.onExit(() => this.dispose()), - this._terminalInstance.onDisposed(() => this.dispose()), - this._terminalInstance.onTitleChanged(() => this._onDidChangeLabel.fire()), - this._terminalInstance.onIconChanged(() => this._onDidChangeLabel.fire()), - this._terminalInstance.onDidFocus(() => this._terminalEditorFocusContextKey.set(true)), - this._terminalInstance.onDidBlur(() => this._terminalEditorFocusContextKey.reset()), - this._terminalInstance.onDidChangeHasChildProcesses(() => this._onDidChangeDirty.fire()), - this._terminalInstance.statusList.onDidChangePrimaryStatus(() => this._onDidChangeLabel.fire()) - ]; + this._terminalEditorFocusContextKey = TerminalContextKeys.editorFocus.bindTo(_contextKeyService); // Refresh dirty state when the confirm on kill setting is changed this._configurationService.onDidChangeConfiguration(e => { @@ -117,20 +116,50 @@ export class TerminalEditorInput extends EditorInput { this._onDidChangeDirty.fire(); } }); + if (_terminalInstance) { + this._setupInstanceListeners(); + } + } + + private _setupInstanceListeners(): void { + const instance = this._terminalInstance; + if (!instance) { + return; + } + + this._register(toDisposable(() => { + if (!this._isDetached && !this._isShuttingDown) { + instance.dispose(); + } + })); + + const disposeListeners = [ + instance.onExit(() => this.dispose()), + instance.onDisposed(() => this.dispose()), + instance.onTitleChanged(() => this._onDidChangeLabel.fire()), + instance.onIconChanged(() => this._onDidChangeLabel.fire()), + instance.onDidFocus(() => this._terminalEditorFocusContextKey.set(true)), + instance.onDidBlur(() => this._terminalEditorFocusContextKey.reset()), + instance.onDidChangeHasChildProcesses(() => this._onDidChangeDirty.fire()), + instance.statusList.onDidChangePrimaryStatus(() => this._onDidChangeLabel.fire()) + ]; // Don't dispose editor when instance is torn down on shutdown to avoid extra work and so // the editor/tabs don't disappear - lifecycleService.onWillShutdown(() => { + this._lifecycleService.onWillShutdown(() => { this._isShuttingDown = true; dispose(disposeListeners); }); } override getName() { - return this._terminalInstance.title; + return this._terminalInstance?.title || this.resource.fragment; } override getLabelExtraClasses(): string[] { + if (!this._terminalInstance) { + return []; + } const extraClasses: string[] = ['terminal-tab']; const colorClass = getColorClass(this._terminalInstance); if (colorClass) { @@ -152,7 +181,7 @@ export class TerminalEditorInput extends EditorInput { */ detachInstance() { if (!this._isShuttingDown) { - this._terminalInstance.detachFromElement(); + this._terminalInstance?.detachFromElement(); this._isDetached = true; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts index db41361aa71..0981d57a04a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IEditorSerializer } from 'vs/workbench/common/editor'; @@ -29,6 +30,7 @@ export class TerminalInputSerializer implements IEditorSerializer { public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { const terminalInstance = JSON.parse(serializedEditorInput); + terminalInstance.resource = URI.parse(terminalInstance.resource); const editor = this._terminalEditorService.getOrCreateEditorInput(terminalInstance); return editor; } @@ -41,12 +43,13 @@ export class TerminalInputSerializer implements IEditorSerializer { titleSource: instance.titleSource, cwd: '', icon: instance.icon, - color: instance.color + color: instance.color, + resource: instance.resource.toString() }; } } -export interface SerializedTerminalEditorInput { +interface TerminalEditorInputObject { readonly id: number; readonly pid: number; readonly title: string; @@ -55,3 +58,11 @@ export interface SerializedTerminalEditorInput { readonly icon: TerminalIcon | undefined; readonly color: string | undefined; } + +export interface SerializedTerminalEditorInput extends TerminalEditorInputObject { + readonly resource: string +} + +export interface DeserializedTerminalEditorInput extends TerminalEditorInputObject { + readonly resource: URI +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index 5a2511f857b..399419fd63f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -5,17 +5,21 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { EditorActivation } from 'vs/platform/editor/common/editor'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IShellLaunchConfig, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; -import { ITerminalEditorService, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, ITerminalEditorService, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; -import { SerializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; +import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; +import { parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; +import { ILocalTerminalService, IOffProcessTerminalService } from 'vs/workbench/contrib/terminal/common/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; export class TerminalEditorService extends Disposable implements ITerminalEditorService { @@ -25,8 +29,10 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor private _activeInstanceIndex: number = -1; private _isShuttingDown = false; - private _editorInputs: Map = new Map(); - private _instanceDisposables: Map = new Map(); + private _editorInputs: Map = new Map(); + private _instanceDisposables: Map = new Map(); + + private readonly _primaryOffProcessTerminalService: IOffProcessTerminalService; private readonly _onDidDisposeInstance = new Emitter(); readonly onDidDisposeInstance = this._onDidDisposeInstance.event; @@ -42,9 +48,13 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor @IEditorService private readonly _editorService: IEditorService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILifecycleService lifecycleService: ILifecycleService + @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, + @ILifecycleService lifecycleService: ILifecycleService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @optional(ILocalTerminalService) private readonly _localTerminalService: ILocalTerminalService ) { super(); + this._primaryOffProcessTerminalService = !!environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService); this._register(toDisposable(() => { for (const d of this._instanceDisposables.values()) { dispose(d); @@ -65,7 +75,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor const terminalEditors = this._getActiveTerminalEditors(); const unknownEditor = terminalEditors.find(input => !knownIds.includes((input as any).terminalInstance.instanceId)); if (unknownEditor instanceof TerminalEditorInput && unknownEditor.terminalInstance) { - this._editorInputs.set(unknownEditor.terminalInstance.instanceId, unknownEditor); + this._editorInputs.set(unknownEditor.terminalInstance.resource.path, unknownEditor); this.instances.push(unknownEditor.terminalInstance); } })); @@ -153,31 +163,72 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor input.setGroup(editorPane?.group); } - getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput, isFutureSplit: boolean = false): TerminalEditorInput { - let cachedEditor; - if ('id' in instance) { - cachedEditor = this._editorInputs.get(instance.id); - } else if ('instanceId' in instance) { - cachedEditor = this._editorInputs.get(instance.instanceId); - } + getOrCreateEditorInput(instanceOrUri: ITerminalInstance | DeserializedTerminalEditorInput | URI, isFutureSplit: boolean = false): TerminalEditorInput { + const resource: URI = URI.isUri(instanceOrUri) ? instanceOrUri : instanceOrUri.resource; + const inputKey = resource.path; + const cachedEditor = this._editorInputs.get(inputKey); if (cachedEditor) { return cachedEditor; } - if ('pid' in instance) { - instance = this._terminalInstanceService.createInstance({ attachPersistentProcess: instance }, TerminalLocation.Editor); + if ('pid' in instanceOrUri) { + instanceOrUri = this._terminalInstanceService.createInstance({ attachPersistentProcess: instanceOrUri }, TerminalLocation.Editor); } - const input = this._instantiationService.createInstance(TerminalEditorInput, instance); - instance.target = TerminalLocation.Editor; - this._editorInputs.set(instance.instanceId, input); - this._instanceDisposables.set(instance.instanceId, [ - instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance), - instance.onDidFocus(this._onDidFocusInstance.fire, this._onDidFocusInstance) + // Terminal from a different window + if (URI.isUri(instanceOrUri)) { + const terminalIdentifier = parseTerminalUri(instanceOrUri); + if (terminalIdentifier.instanceId) { + this._primaryOffProcessTerminalService.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId).then(attachPersistentProcess => { + const instance = this._terminalInstanceService.createInstance({ attachPersistentProcess }, TerminalLocation.Editor, resource); + input.setTerminalInstance(instance); + // trigger setInput on TerminalEditor setInput + // which attaches to the element and updates the input + this._editorService.openEditor(input, { + pinned: true, + forceReload: true + }, + input.group + ); + this._registerInstance(inputKey, input, instance); + }); + } + } + + let input: TerminalEditorInput; + if ('instanceId' in instanceOrUri) { + instanceOrUri.target = TerminalLocation.Editor; + input = this._instantiationService.createInstance(TerminalEditorInput, resource, instanceOrUri); + this._registerInstance(inputKey, input, instanceOrUri); + } else { + input = this._instantiationService.createInstance(TerminalEditorInput, instanceOrUri, undefined); + this._editorInputs.set(inputKey, input); + } + return input; + } + + private _registerInstance(inputKey: string, input: TerminalEditorInput, instance: ITerminalInstance): void { + this._editorInputs.set(inputKey, input); + this._instanceDisposables.set(inputKey, [ + instance.onDidFocus(this._onDidFocusInstance.fire, this._onDidFocusInstance), + toDisposable(() => this._editorInputs.delete(inputKey)), + instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance) ]); this.instances.push(instance); this._onDidChangeInstances.fire(); - return input; + } + + getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined { + if (URI.isUri(resource)) { + // note that the uri and and instance id might + // not match this window + for (const instance of this.instances) { + if (instance.resource.path === resource.path) { + return instance; + } + } + } + return undefined; } splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): ITerminalInstance { @@ -202,9 +253,10 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor } detachInstance(instance: ITerminalInstance) { - const editorInput = this._editorInputs.get(instance.instanceId); + const inputKey = instance.resource.path; + const editorInput = this._editorInputs.get(inputKey); editorInput?.detachInstance(); - this._editorInputs.delete(instance.instanceId); + this._editorInputs.delete(inputKey); const instanceIndex = this.instances.findIndex(e => e === instance); if (instanceIndex !== -1) { this.instances.splice(instanceIndex, 1); @@ -213,8 +265,8 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor if (!this._isShuttingDown) { editorInput?.dispose(); } - const disposables = this._instanceDisposables.get(instance.instanceId); - this._instanceDisposables.delete(instance.instanceId); + const disposables = this._instanceDisposables.get(inputKey); + this._instanceDisposables.delete(inputKey); if (disposables) { dispose(disposables); } @@ -227,7 +279,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor return; } - const editorInput = this._editorInputs.get(instance.instanceId)!; + const editorInput = this._editorInputs.get(instance.resource.path)!; this._editorService.openEditor( editorInput, { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index f89f603bf4a..04a252eab81 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -7,6 +7,7 @@ import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { timeout } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -175,6 +176,19 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe } } + getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined { + if (URI.isUri(resource)) { + // note that the uri and and instance id might + // not match this window + for (const instance of this.instances) { + if (instance.resource.path === resource.path) { + return instance; + } + } + } + return undefined; + } + findNext(): void { const pane = this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID); if (pane?.terminalTabbedView) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 7fab8284a3e..b089faafcdd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -66,6 +66,7 @@ import { Color } from 'vs/base/common/color'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { getTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -152,6 +153,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined; private _dndObserver: IDisposable | undefined; + private readonly _resource: URI; + private _lastLayoutDimensions: dom.Dimension | undefined; private _hasHadInput: boolean; @@ -160,13 +163,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { disableLayout: boolean = false; target?: TerminalLocation; get instanceId(): number { return this._instanceId; } - get resource(): URI { - return URI.from({ - scheme: Schemas.vscodeTerminal, - path: `/${this._workspaceContextService.getWorkspace().id}/${this.instanceId}`, - fragment: this.title, - }); - } + get resource(): URI { return this._resource; } get cols(): number { if (this._dimensionsOverride && this._dimensionsOverride.cols) { if (this._dimensionsOverride.forceExactSize) { @@ -253,6 +250,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private readonly _terminalAltBufferActiveContextKey: IContextKey, private readonly _configHelper: TerminalConfigHelper, private _shellLaunchConfig: IShellLaunchConfig, + resource: URI | undefined, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -287,6 +285,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._titleReadyComplete = c; }); + // the resource is already set when it's been moved from another window + this._resource = resource || getTerminalUri(this._workspaceContextService.getWorkspace().id, this.instanceId, this.title); + this._terminalHasTextContextKey = TerminalContextKeys.textSelected.bindTo(this._contextKeyService); this._terminalA11yTreeFocusContextKey = TerminalContextKeys.a11yTreeFocus.bindTo(this._contextKeyService); this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(this._contextKeyService); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index 968bf8be707..87ded5521c0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -53,16 +53,17 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst this._configHelper = _instantiationService.createInstance(TerminalConfigHelper); } - createInstance(profile: ITerminalProfile, target?: TerminalLocation): ITerminalInstance; - createInstance(shellLaunchConfig: IShellLaunchConfig, target?: TerminalLocation): ITerminalInstance; - createInstance(config: IShellLaunchConfig | ITerminalProfile, target?: TerminalLocation): ITerminalInstance { + createInstance(profile: ITerminalProfile, target?: TerminalLocation, resource?: URI): ITerminalInstance; + createInstance(shellLaunchConfig: IShellLaunchConfig, target?: TerminalLocation, resource?: URI): ITerminalInstance; + createInstance(config: IShellLaunchConfig | ITerminalProfile, target?: TerminalLocation, resource?: URI): ITerminalInstance { const shellLaunchConfig = this._convertProfileToShellLaunchConfig(config); const instance = this._instantiationService.createInstance(TerminalInstance, this._terminalFocusContextKey, this._terminalShellTypeContextKey, this._terminalAltBufferActiveContextKey, this._configHelper, - shellLaunchConfig + shellLaunchConfig, + resource ); instance.target = target; this._onDidCreateInstance.fire(instance); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 0b0779e110e..08a4747f528 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -11,7 +11,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; -import { basename } from 'vs/base/common/path'; import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; @@ -30,14 +29,14 @@ import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { IconDefinition } from 'vs/platform/theme/common/iconRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { IRemoteTerminalService, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalProfileProvider, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, 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 { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; +import { getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { ILocalTerminalService, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileContribution, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -160,7 +159,6 @@ export class TerminalService implements ITerminalService { @IEditorResolverService editorResolverService: IEditorResolverService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @optional(ILocalTerminalService) localTerminalService: ILocalTerminalService ) { this._localTerminalService = localTerminalService; @@ -187,11 +185,9 @@ export class TerminalService implements ITerminalService { if (sourceGroup) { sourceGroup.removeInstance(instance); } - } else { - instance = _terminalInstanceService.createInstance({}); } return { - editor: this._terminalEditorService.getOrCreateEditorInput(instance), + editor: this._terminalEditorService.getOrCreateEditorInput(instance || resource), options: { ...options, pinned: true, @@ -262,10 +258,11 @@ export class TerminalService implements ITerminalService { : Promise.resolve(); this._primaryOffProcessTerminalService = !!this._environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService); this._primaryOffProcessTerminalService.onDidRequestDetach(async (e) => { - if (e.workspaceId && this._workspaceContextService.getWorkspace().id === e.workspaceId) { - const instanceToDetach = this.getInstanceFromId(e.instanceId); + const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId)); + if (instanceToDetach) { const persistentProcessId = instanceToDetach?.persistentProcessId; if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) { + this._terminalEditorService.detachInstance(instanceToDetach); await instanceToDetach.detachFromProcess(); await this._primaryOffProcessTerminalService?.acceptDetachInstanceReply(e.requestId, persistentProcessId); } else { @@ -274,6 +271,7 @@ export class TerminalService implements ITerminalService { } } }); + initPromise.then(() => this._setConnected()); // Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual @@ -609,25 +607,17 @@ export class TerminalService implements ITerminalService { getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined { if (URI.isUri(resource)) { - const instanceId = this._getInstanceIdFromUri(resource); - if (instanceId) { - return this.getInstanceFromId(instanceId); + // note that the uri and and instance id might + // not match this window + for (const instance of this.instances) { + if (instance.resource.path === resource.path) { + return instance; + } } } return undefined; } - private _getInstanceIdFromUri(resource: URI): number | undefined { - if (resource.scheme !== Schemas.vscodeTerminal) { - return undefined; - } - const base = basename(resource.path); - if (base === '') { - return undefined; - } - return parseInt(base); - } - isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean { return this.instances.some(term => term.processId === remoteTerm.pid); } @@ -673,7 +663,6 @@ export class TerminalService implements ITerminalService { break; } - this._initInstanceListeners(instance); if (instanceToSplit.target !== TerminalLocation.Editor) { this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex)); @@ -753,25 +742,41 @@ export class TerminalService implements ITerminalService { })); instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onDidMaxiumumDimensionsChange.fire(instance))); instance.addDisposable(instance.onDidFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance)); - instance.addDisposable(instance.onRequestAddInstanceToGroup(e => { - const instanceId = this._getInstanceIdFromUri(e.uri); - if (instanceId === undefined) { - return; - } + instance.addDisposable(instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e))); + } - // View terminals - let sourceInstance = this._terminalGroupService.instances.find(e => e.instanceId === instanceId); - if (sourceInstance) { + private async _addInstanceToGroup(instance: ITerminalInstance, e: IRequestAddInstanceToGroupEvent): Promise { + const terminalIdentifier = parseTerminalUri(e.uri); + if (terminalIdentifier.instanceId === undefined) { + return; + } + + let sourceInstance: ITerminalInstance | undefined = this.getInstanceFromResource(e.uri); + + // Terminal from a different window + if (!sourceInstance) { + const attachPersistentProcess = await this._primaryOffProcessTerminalService?.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId); + if (attachPersistentProcess) { + sourceInstance = this.createTerminal({ config: { attachPersistentProcess }, resource: e.uri }); this._terminalGroupService.moveInstance(sourceInstance, instance, e.side); return; } + } - // Terminal editors - sourceInstance = this._terminalEditorService.instances.find(e => e.instanceId === instanceId); - if (sourceInstance) { - this.moveToTerminalView(sourceInstance, instance, e.side); - } - })); + // View terminals + sourceInstance = this._terminalGroupService.getInstanceFromResource(e.uri); + if (sourceInstance) { + this._terminalGroupService.moveInstance(sourceInstance, instance, e.side); + return; + } + + // Terminal editors + sourceInstance = this._terminalEditorService.getInstanceFromResource(e.uri); + if (sourceInstance) { + this.moveToTerminalView(sourceInstance, instance, e.side); + return; + } + return; } registerProcessSupport(isSupported: boolean): void { @@ -1073,12 +1078,11 @@ export class TerminalService implements ITerminalService { throw new Error('Could not create terminal when process support is not registered'); } if (shellLaunchConfig.hideFromUser) { - const instance = this._terminalInstanceService.createInstance(shellLaunchConfig); + const instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource); this._backgroundedTerminalInstances.push(instance); this._backgroundedTerminalDisposables.set(instance.instanceId, [ instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance) ]); - this._initInstanceListeners(instance); return instance; } @@ -1087,14 +1091,14 @@ export class TerminalService implements ITerminalService { let instance: ITerminalInstance; const target = options?.target || this.configHelper.config.defaultLocation; if (target === TerminalLocation.Editor) { - instance = this._terminalInstanceService.createInstance(shellLaunchConfig); + instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource); instance.target = TerminalLocation.Editor; this._terminalEditorService.openEditor(instance); } else { + // TODO: pass resource? const group = this._terminalGroupService.createGroup(shellLaunchConfig); instance = group.terminalInstances[0]; } - this._initInstanceListeners(instance); return instance; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 0df97fa24b3..a5aed5ef78e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -47,6 +47,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; const $ = DOM.$; @@ -577,8 +578,8 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { // Attach terminals type to event const terminals: ITerminalInstance[] = dndData.filter(e => 'instanceId' in (e as any)); if (terminals.length > 0) { + originalEvent.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminals.map(e => e.resource.toString()))); originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(terminals.map(e => e.resource.toString()))); - originalEvent.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminals.map(e => e.instanceId))); } } @@ -618,21 +619,22 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { this._autoFocusInstance = undefined; let sourceInstances: ITerminalInstance[] | undefined; - const terminalResources = originalEvent.dataTransfer?.getData(DataTransfers.RESOURCES); - const terminals = originalEvent.dataTransfer?.getData(DataTransfers.TERMINALS); let promises: Promise[] = []; - if (terminals && terminalResources) { - const json = JSON.parse(terminalResources); + const resources = originalEvent.dataTransfer?.getData(DataTransfers.TERMINALS); + if (resources) { + const json = JSON.parse(resources); for (const entry of json) { const uri = URI.parse(entry); - const [, workspaceId, instanceId] = uri.path.split('/'); - if (workspaceId && instanceId) { - const instance = this._terminalService.instances.find(e => e.resource.path === instanceId); - if (instance) { - sourceInstances = [instance]; - this._terminalService.moveToTerminalView(instance); - } else if (this._offProcessTerminalService && workspaceId !== this._workspaceContextService.getWorkspace().id) { - promises.push(this._offProcessTerminalService.requestDetachInstance(workspaceId, Number.parseInt(instanceId))); + + const instance = this._terminalService.getInstanceFromResource(uri); + if (instance) { + sourceInstances = [instance]; + this._terminalService.moveToTerminalView(instance); + } else if (this._offProcessTerminalService) { + // why is this undefined + const terminalIdentifier = parseTerminalUri(uri); + if (terminalIdentifier.instanceId) { + promises.push(this._offProcessTerminalService.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId)); } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalUri.ts b/src/vs/workbench/contrib/terminal/browser/terminalUri.ts new file mode 100644 index 00000000000..d6a20ae6269 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalUri.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; + +export function parseTerminalUri(resource: URI): ITerminalIdentifier { + const [, workspaceId, instanceId] = resource.path.split('/'); + if (!workspaceId || !Number.parseInt(instanceId)) { + throw new Error(`Could not parse terminal uri for resource ${resource}`); + } + return { workspaceId, instanceId: Number.parseInt(instanceId) }; +} + +export function getTerminalUri(workspaceId: string, instanceId: number, title?: string): URI { + return URI.from({ + scheme: Schemas.vscodeTerminal, + path: `/${workspaceId}/${instanceId}`, + fragment: title || undefined, + }); +} + +export interface ITerminalIdentifier { + workspaceId: string; + instanceId: number | undefined; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 3e0190e849a..2099080122f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -481,7 +481,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { const instance = this._terminalGroupService.activeInstance; if (e.dataTransfer && instance) { e.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([instance.resource.toString()])); - e.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify([instance.instanceId])); + e.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify([instance.resource.toString()])); } })); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 1e868cc2956..61f49a9ab20 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1667,9 +1667,7 @@ export class TestLocalTerminalService implements ILocalTerminalService { onDidMoveWindowInstance = Event.None; onDidRequestDetach = Event.None; - async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { - return new TestTerminalChildProcess(shouldPersist); - } + async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { return new TestTerminalChildProcess(shouldPersist); } async attachToProcess(id: number): Promise { throw new Error('Method not implemented.'); } async listProcesses(): Promise { throw new Error('Method not implemented.'); } getDefaultSystemShell(osOverride?: OperatingSystem): Promise { throw new Error('Method not implemented.'); }