diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 12ff01c047e6d..618fe4256bf51 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -289,3 +289,12 @@ margin-left: 4px; color: inherit; } + +.monaco-workbench .pane-body.integrated-terminal .drop-target::after { + background: rgba(128, 128, 255, .5); + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 3edf41f34a6c1..0e8e9204a310e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -150,6 +150,11 @@ export interface ITerminalService { 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(instance: ITerminalInstance, target: ITerminalInstance): void; + moveInstance(source: ITerminalInstance, target: ITerminalInstance, side: 'left' | 'right'): void; /** * Perform an action with the active terminal instance, if the terminal does @@ -317,6 +322,11 @@ export interface ITerminalInstance { onFocus: Event; + /** + * An event that fires when a terminal is dropped on this instance via drag and drop. + */ + onRequestAddInstanceToGroup: Event; + /** * Attach a listener to the raw data stream coming from the pty, including ANSI escape * sequences. @@ -595,6 +605,11 @@ export interface ITerminalInstance { changeColor(): Promise; } +export interface IRequestAddInstanceToGroupEvent { + uri: URI; + side: 'left' | 'right' +} + export const enum LinuxDistro { Unknown = 1, Fedora = 2, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 2ea850cfaccab..5e93ba564b84a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -29,7 +29,7 @@ import { ansiColorIdentifiers, ansiColorMap, TERMINAL_BACKGROUND_COLOR, TERMINAL import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ITerminalInstanceService, ITerminalInstance, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstanceService, ITerminalInstance, ITerminalExternalLinkProvider, IRequestAddInstanceToGroupEvent } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon, RendererType, ITheme } from 'xterm'; import type { SearchAddon, ISearchOptions } from 'xterm-addon-search'; @@ -216,6 +216,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get onMaximumDimensionsChanged(): Event { return this._onMaximumDimensionsChanged.event; } private readonly _onFocus = new Emitter(); get onFocus(): Event { return this._onFocus.event; } + private readonly _onRequestAddInstanceToGroup = new Emitter(); + get onRequestAddInstanceToGroup(): Event { return this._onRequestAddInstanceToGroup.event; } constructor( private readonly _terminalFocusContextKey: IContextKey, @@ -749,7 +751,20 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._refreshSelectionContextKey(); })); + this._register(dom.addDisposableListener(xterm.element, dom.EventType.DRAG_OVER, (dragEvent: DragEvent) => { + dragEvent.preventDefault(); + if (!dragEvent.dataTransfer) { + return; + } + if ((dragEvent.dataTransfer?.types || []).includes('terminals')) { + this._container?.parentElement?.classList.add('drop-target'); + } + })); + this._register(dom.addDisposableListener(xterm.element, dom.EventType.DRAG_LEAVE, (dragEvent: DragEvent) => { + this._container?.parentElement?.classList.remove('drop-target'); + })); this._register(dom.addDisposableListener(xterm.element, dom.EventType.DROP, async (dragEvent: DragEvent) => { + this._container?.parentElement?.classList.remove('drop-target'); if (!dragEvent.dataTransfer) { return; } @@ -758,7 +773,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { let path: string | undefined; const resources = dragEvent.dataTransfer.getData(DataTransfers.RESOURCES); if (resources) { - path = URI.parse(JSON.parse(resources)[0]).fsPath; + const uri = URI.parse(JSON.parse(resources)[0]); + if (uri.scheme === Schemas.vscodeTerminal) { + console.log('drop event', dragEvent); + this._onRequestAddInstanceToGroup.fire({ + uri, + // TODO: Get side + side: 'right' + }); + return; + } else { + path = uri.fsPath; + } } else if (dragEvent.dataTransfer.files?.[0].path /* Electron only */) { // Check if the file was dragged from the filesystem path = URI.file(dragEvent.dataTransfer.files[0].path).fsPath; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 3fb23678d6a53..a4aa765b3c868 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -687,6 +687,24 @@ export class TerminalService implements ITerminalService { } } + moveGroup(instance: ITerminalInstance, target: ITerminalInstance): void { + const sourceGroup = this.getGroupForInstance(instance); + 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: 'left' | 'right'): void { + // TODO: Implement properly + this.joinInstances([source, target]); + } + protected _initInstanceListeners(instance: ITerminalInstance): void { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); @@ -703,6 +721,14 @@ 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.onRequestAddInstanceToGroup(e => { + const sourceInstance = this.getInstanceFromId(parseInt(e.uri.path)); + console.log('source', sourceInstance, 'join', instance); + if (sourceInstance) { + // TODO: Pass in side + this.moveInstance(sourceInstance, instance, e.side); + } + })); } registerProcessSupport(isSupported: boolean): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index aeaa8b5fafb14..85c332eac6c78 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -27,11 +27,12 @@ import { IDecorationsService } from 'vs/workbench/services/decorations/browser/d import { IHoverAction, IHoverService } from 'vs/workbench/services/hover/browser/hover'; import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IListDragAndDrop, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { IListDragAndDrop, IListDragOverReaction, IListRenderer, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { disposableTimeout } from 'vs/base/common/async'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; const $ = DOM.$; @@ -78,7 +79,7 @@ export class TerminalTabList extends WorkbenchList { smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), multipleSelectionSupport: true, additionalScrollHeight: TerminalTabsListSizes.TabHeight, - dnd: new TerminalTabsDragAndDrop(_terminalService, _terminalInstanceService) + dnd: instantiationService.createInstance(TerminalTabsDragAndDrop) }, contextKeyService, listService, @@ -403,15 +404,33 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { private _autoFocusDisposable: IDisposable = Disposable.None; constructor( - private _terminalService: ITerminalService, - private _terminalInstanceService: ITerminalInstanceService + @ITerminalService private _terminalService: ITerminalService, + @ITerminalInstanceService private _terminalInstanceService: ITerminalInstanceService ) { } getDragURI(instance: ITerminalInstance): string | null { - return null; + return URI.from({ + scheme: Schemas.vscodeTerminal, + path: instance.instanceId.toString() + }).toString(); } - onDragOver(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean { + onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + if (!originalEvent.dataTransfer) { + return; + } + const dndData: unknown = data.getData(); + if (!Array.isArray(dndData)) { + return; + } + // Attach terminals type to event + const terminals: ITerminalInstance[] = dndData.filter(e => 'instanceId' in (e as any)); + if (terminals.length > 0) { + originalEvent.dataTransfer.setData('terminals', JSON.stringify(terminals.map(e => e.instanceId))); + } + } + + onDragOver(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction { let result = true; const didChangeAutoFocusInstance = this._autoFocusInstance !== targetInstance; @@ -424,24 +443,47 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop { return result; } - const isExternalDragOver = !(data instanceof ElementsDragAndDropData); - if (didChangeAutoFocusInstance && isExternalDragOver) { + if (didChangeAutoFocusInstance) { this._autoFocusDisposable = disposableTimeout(() => { this._terminalService.setActiveInstance(targetInstance); this._autoFocusInstance = undefined; }, 500); } - return result; + return { + feedback: targetIndex ? [targetIndex] : undefined, + accept: true, + effect: ListDragOverEffect.Move + }; } drop(data: IDragAndDropData, targetInstance: ITerminalInstance | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { this._autoFocusDisposable.dispose(); this._autoFocusInstance = undefined; - const isExternalDrop = !(data instanceof ElementsDragAndDropData); - if (isExternalDrop) { + if (!(data instanceof ElementsDragAndDropData)) { this._handleExternalDrop(targetInstance, originalEvent); + return; + } + + const draggedElement = data.getData(); + if (!draggedElement || !Array.isArray(draggedElement)) { + return; + } + let focused = false; + if (!targetInstance) { + // TODO: Support dropping on empty + return; + } + for (const e of draggedElement) { + if ('instanceId' in e) { + const instance = e as ITerminalInstance; + this._terminalService.moveGroup(instance, targetInstance); + if (!focused) { + this._terminalService.setActiveInstance(instance); + focused = true; + } + } } }