diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 3806f25f5dda7..0e7dff20d2556 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -51,7 +51,7 @@ export const enum Direction { Down = 3 } -export interface ITerminalTab { +export interface ITerminalTab extends IDisposable { activeInstance: ITerminalInstance | null; terminalInstances: ITerminalInstance[]; title: string; @@ -68,6 +68,8 @@ export interface ITerminalTab { addDisposable(disposable: IDisposable): void; split(shellLaunchConfig: IShellLaunchConfig): ITerminalInstance; getLayoutInfo(isActive: boolean): ITerminalTabLayoutInfoById; + addInstance(instance: ITerminalInstance, targetPosition?: ITerminalInstance): void; + removeInstance(instance: ITerminalInstance): void; } export const enum TerminalConnectionState { @@ -129,6 +131,17 @@ export interface ITerminalService { splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig): ITerminalInstance | null; splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null; + /** + * Unsplits an instance, moving it into its own tab. + */ + unsplitInstance(instance: ITerminalInstance): void; + + /** + * Moves a terminal instance to the position of another, pushing the target instance to the + * right. + */ + moveInstance(instance: ITerminalInstance, target: ITerminalInstance): void; + /** * Perform an action with the active terminal instance, if the terminal does * not exist the callback will not be called. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 2c097abb47bfe..5943ad2ebd4c9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1232,6 +1232,21 @@ export function registerTerminalActions() { }); } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TERMINAL_COMMAND_ID.UNSPLIT, + title: { value: localize('workbench.action.terminal.unsplit', "Unsplit Terminal"), original: 'Unsplit Terminal' }, + f1: true, + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED + }); + } + async run(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + terminalService.doWithActiveInstance(t => terminalService.unsplitInstance(t)); + } + }); registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts b/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts index 763c52daa7997..0b90a02fe4b97 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts @@ -11,7 +11,7 @@ import { IDecorationData, IDecorationsProvider } from 'vs/workbench/services/dec import { Event, Emitter } from 'vs/base/common/event'; import { Codicon } from 'vs/base/common/codicons'; import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; -import { TERMINAL_DECORATIONS_SCHEME } from 'vs/workbench/contrib/terminal/common/terminal'; +import { terminalUrlScheme } from 'vs/workbench/contrib/terminal/common/terminal'; export interface ITerminalDecorationData { tooltip: string, @@ -33,7 +33,7 @@ export class TerminalDecorationsProvider implements IDecorationsProvider { } provideDecorations(resource: URI): IDecorationData | undefined { - if (resource.scheme !== TERMINAL_DECORATIONS_SCHEME || !parseInt(resource.path)) { + if (resource.scheme !== terminalUrlScheme || !parseInt(resource.path)) { return; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index e381469ee1233..8ed812af59ea8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -626,6 +626,41 @@ export class TerminalService implements ITerminalService { return instance; } + public unsplitInstance(instance: ITerminalInstance): void { + const oldTab = this.getTabForInstance(instance); + if (!oldTab || oldTab.terminalInstances.length < 2) { + return; + } + + oldTab.removeInstance(instance); + + const newTab = this._instantiationService.createInstance(TerminalTab, this._terminalContainer, instance); + this._terminalTabs.push(newTab); + + newTab.addDisposable(newTab.onDisposed(this._onTabDisposed.fire, this._onTabDisposed)); + newTab.addDisposable(newTab.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); + this._onInstancesChanged.fire(); + } + + public moveInstance(instance: ITerminalInstance, target: ITerminalInstance): void { + const oldTab = this.getTabForInstance(instance); + const newTab = this.getTabForInstance(target); + if (!oldTab || !newTab) { + return; + } + + // Remove from old tab, disposing of it if now empty + oldTab.removeInstance(instance); + if (oldTab.terminalInstances.length === 0) { + console.log('dispose oldTab'); + oldTab.dispose(); + } + + newTab.addInstance(instance, target); + + this._onInstancesChanged.fire(); + } + protected _initInstanceListeners(instance: ITerminalInstance): void { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts index 0f089c7c3daac..52033ebb9cb7a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts @@ -264,7 +264,7 @@ export class TerminalTab extends Disposable implements ITerminalTab { } } - public addInstance(shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance): void { + public addInstance(shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance, targetPosition?: ITerminalInstance): void { let instance: ITerminalInstance; if ('instanceId' in shellLaunchConfigOrInstance) { instance = shellLaunchConfigOrInstance; @@ -281,6 +281,21 @@ export class TerminalTab extends Disposable implements ITerminalTab { this._onInstancesChanged.fire(); } + public removeInstance(instance: ITerminalInstance): void { + const index = this._terminalInstances.indexOf(instance); + if (index === -1) { + return; + } + + this._terminalInstances.splice(index, 1); + this._splitPaneContainer?.remove(instance); + + // TODO: Remove listeners + + // TODO: Call from ITerminalService.moveInstance + this._onInstancesChanged.fire(); + } + public override dispose(): void { super.dispose(); if (this._container && this._tabElement) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index bd0834032bb90..6e95fb974d1c7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -88,8 +88,8 @@ export class TerminalTabbedView extends Disposable { this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalContext, contextKeyService)); // this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalTabsContext, contextKeyService)); - this._register(this._tabsWidget = this._instantiationService.createInstance(TerminalTabsWidget, this._terminalTabTree)); - this._register(this._findWidget = this._instantiationService.createInstance(TerminalFindWidget, this._terminalService.getFindState())); + this._tabsWidget = this._register(this._instantiationService.createInstance(TerminalTabsWidget, this._terminalTabTree)); + this._findWidget = this._register(this._instantiationService.createInstance(TerminalFindWidget, this._terminalService.getFindState())); parentElement.appendChild(this._findWidget.getDomNode()); this._terminalContainer = document.createElement('div'); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts index 58473ca762697..cb3b8329405cc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ITreeDragOverReaction, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -18,7 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti 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 { TERMINAL_COMMAND_ID, TERMINAL_DECORATIONS_SCHEME } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_COMMAND_ID, terminalUrlScheme } from 'vs/workbench/contrib/terminal/common/terminal'; import { Codicon } from 'vs/base/common/codicons'; import { Action } from 'vs/base/common/actions'; import { MarkdownString } from 'vs/base/common/htmlContent'; @@ -28,11 +28,13 @@ import { IDecorationsService } from 'vs/workbench/services/decorations/browser/d import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { URI } from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; +import { ListDragOverEffect } from 'vs/base/browser/ui/list/list'; const $ = DOM.$; export const MIN_TABS_WIDGET_WIDTH = 46; export const DEFAULT_TABS_WIDGET_WIDTH = 80; export const MIDPOINT_WIDGET_WIDTH = (MIN_TABS_WIDGET_WIDTH + DEFAULT_TABS_WIDGET_WIDTH) / 2; +const TAB_HEIGHT = 22; export class TerminalTabsWidget extends WorkbenchObjectTree { private _decorationsProvider: TerminalDecorationsProvider | undefined; @@ -51,7 +53,7 @@ export class TerminalTabsWidget extends WorkbenchObjectTree ) { super('TerminalTabsTree', container, { - getHeight: () => 22, + getHeight: () => TAB_HEIGHT, getTemplateId: () => 'terminal.tabs' }, [instantiationService.createInstance(TerminalTabsRenderer, container, instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER))], @@ -70,7 +72,43 @@ export class TerminalTabsWidget extends WorkbenchObjectTree smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), multipleSelectionSupport: false, expandOnlyOnTwistieClick: true, - selectionNavigation: true + selectionNavigation: true, + dnd: { + drop: (data, targetElement) => { + if (!targetElement) { + return; + } + const draggedElement = data.getData(); + if (!draggedElement || !Array.isArray(draggedElement)) { + return; + } + let focused = false; + for (const e of draggedElement) { + if ('instanceId' in e) { + const instance = e as ITerminalInstance; + this._terminalService.moveInstance(instance, targetElement); + if (!focused) { + this._terminalService.setActiveInstance(instance); + focused = true; + } + } + } + }, + getDragURI: (instance) => { + return URI.from({ + scheme: terminalUrlScheme, + path: instance.instanceId.toString() + }).path; + }, + onDragOver(data, targetElement, targetIndex, originalEvent): boolean | ITreeDragOverReaction { + return { + feedback: targetIndex ? [targetIndex] : undefined, + accept: true, + effect: ListDragOverEffect.Move + }; + } + }, + additionalScrollHeight: TAB_HEIGHT }, contextKeyService, listService, @@ -160,9 +198,7 @@ class TerminalTabsRenderer implements ITreeRenderer, index: number, template: ITerminalTabEntryTemplate): void { - let instance = node.element; const tab = this._terminalService.getTabForInstance(instance); @@ -216,7 +252,7 @@ class TerminalTabsRenderer implements ITreeRenderer