Skip to content

Commit

Permalink
Merge pull request #124196 from microsoft/tyriar/dnd_2
Browse files Browse the repository at this point in the history
Implement terminal drag and drop
  • Loading branch information
Tyriar authored May 20, 2021
2 parents e538fd9 + e91d547 commit aea7c1a
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 46 deletions.
17 changes: 17 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/media/terminal.css
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,20 @@
margin-left: 4px;
color: inherit;
}

.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay {
display: block;
position: absolute;
left: 0;
right: 0;
height: 100%;
pointer-events: none;
opacity: 0; /* hidden initially */
transition: left 70ms ease-out, right 70ms ease-out, opacity 150ms ease-out;
}
.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay.drop-left {
right: 50%;
}
.monaco-workbench .pane-body.integrated-terminal .terminal-drop-overlay.drop-right {
left: 50%
}
16 changes: 16 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface ITerminalGroup {
attachToElement(element: HTMLElement): void;
addInstance(instance: ITerminalInstance): void;
removeInstance(instance: ITerminalInstance): void;
moveInstance(instance: ITerminalInstance, index: number): void;
setVisible(visible: boolean): void;
layout(width: number, height: number): void;
addDisposable(disposable: IDisposable): void;
Expand Down Expand Up @@ -150,6 +151,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(source: 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
Expand Down Expand Up @@ -317,6 +323,11 @@ export interface ITerminalInstance {

onFocus: Event<ITerminalInstance>;

/**
* An event that fires when a terminal is dropped on this instance via drag and drop.
*/
onRequestAddInstanceToGroup: Event<IRequestAddInstanceToGroupEvent>;

/**
* Attach a listener to the raw data stream coming from the pty, including ANSI escape
* sequences.
Expand Down Expand Up @@ -595,6 +606,11 @@ export interface ITerminalInstance {
changeColor(): Promise<void>;
}

export interface IRequestAddInstanceToGroupEvent {
uri: URI;
side: 'left' | 'right'
}

export const enum LinuxDistro {
Unknown = 1,
Fedora = 2,
Expand Down
14 changes: 14 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,20 @@ export class TerminalGroup extends Disposable implements ITerminalGroup {
}
}

moveInstance(instance: ITerminalInstance, index: number): void {
const sourceIndex = this.terminalInstances.indexOf(instance);
if (sourceIndex === -1) {
return;
}
this._terminalInstances.splice(sourceIndex, 1);
this._terminalInstances.splice(index, 0, instance);
if (this._splitPaneContainer) {
this._splitPaneContainer.remove(instance);
this._splitPaneContainer.split(instance, sourceIndex < index ? index - 1 : index);
}
this._onInstancesChanged.fire();
}

private _setActiveInstance(instance: ITerminalInstance) {
this.setActiveInstanceByIndex(this._getIndexFromId(instance.instanceId));
}
Expand Down
169 changes: 137 additions & 32 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { debounce } from 'vs/base/common/decorators';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
Expand All @@ -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';
Expand Down Expand Up @@ -58,6 +58,7 @@ import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/plat
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { DataTransfers } from 'vs/base/browser/dnd';
import { DragAndDropObserver, IDragAndDropObserverCallbacks } from 'vs/workbench/browser/dnd';

// How long in milliseconds should an average frame take to render for a notification to appear
// which suggests the fallback DOM-based renderer
Expand Down Expand Up @@ -132,6 +133,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
private _webglAddon: WebglAddon | undefined;
private _commandTrackerAddon: CommandTrackerAddon | undefined;
private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined;
private _dndObserver: IDisposable | undefined;

private _lastLayoutDimensions: dom.Dimension | undefined;

Expand Down Expand Up @@ -216,6 +218,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
get onMaximumDimensionsChanged(): Event<void> { return this._onMaximumDimensionsChanged.event; }
private readonly _onFocus = new Emitter<ITerminalInstance>();
get onFocus(): Event<ITerminalInstance> { return this._onFocus.event; }
private readonly _onRequestAddInstanceToGroup = new Emitter<IRequestAddInstanceToGroupEvent>();
get onRequestAddInstanceToGroup(): Event<IRequestAddInstanceToGroupEvent> { return this._onRequestAddInstanceToGroup.event; }

constructor(
private readonly _terminalFocusContextKey: IContextKey<boolean>,
Expand Down Expand Up @@ -273,6 +277,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._initDimensions();
this._createProcessManager();


this._register(toDisposable(() => this._dndObserver?.dispose()));

this._containerReadyBarrier = new AutoOpenBarrier(Constants.WaitForContainerThreshold);
this._xtermReadyPromise = this._createXterm();
this._xtermReadyPromise.then(async () => {
Expand Down Expand Up @@ -306,13 +313,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
initialDataEventsTimeout = undefined;
this._initialDataEvents = undefined;
}, 10000);
this._register({
dispose: () => {
if (initialDataEventsTimeout) {
window.clearTimeout(initialDataEventsTimeout);
}
this._register(toDisposable(() => {
if (initialDataEventsTimeout) {
window.clearTimeout(initialDataEventsTimeout);
}
});
}));
}

private _getIcon(): Codicon | undefined {
Expand Down Expand Up @@ -602,6 +607,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._container?.removeChild(this._wrapperElement);
this._container = container;
this._container.appendChild(this._wrapperElement);
setTimeout(() => this._initDragAndDrop(container));
}

private async _attachToElement(container: HTMLElement): Promise<void> {
Expand Down Expand Up @@ -749,30 +755,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._refreshSelectionContextKey();
}));

this._register(dom.addDisposableListener(xterm.element, dom.EventType.DROP, async (dragEvent: DragEvent) => {
if (!dragEvent.dataTransfer) {
return;
}

// Check if files were dragged from the tree explorer
let path: string | undefined;
const resources = dragEvent.dataTransfer.getData(DataTransfers.RESOURCES);
if (resources) {
path = URI.parse(JSON.parse(resources)[0]).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;
}

if (!path) {
return;
}

const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote);

this.sendText(preparedPath, false);
this.focus();
}));
this._initDragAndDrop(container);

this._widgetManager.attachToElement(xterm.element);
this._processManager.onProcessReady(() => this._linkManager?.setWidgetManager(this._widgetManager));
Expand All @@ -794,6 +777,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
}
}

private _initDragAndDrop(container: HTMLElement) {
this._dndObserver?.dispose();
const dndController = new TerminalInstanceDropAndDropController(container);
dndController.onDropTerminal(e => this._onRequestAddInstanceToGroup.fire(e));
dndController.onDropFile(async path => {
const preparedPath = await this._terminalInstanceService.preparePathForTerminalAsync(path, this.shellLaunchConfig.executable, this.title, this.shellType, this.isRemote);
this.sendText(preparedPath, false);
this.focus();
});
this._dndObserver = new DragAndDropObserver(container.parentElement!, dndController);
}

private async _measureRenderTime(): Promise<void> {
await this._xtermReadyPromise;
const frameTimes: number[] = [];
Expand Down Expand Up @@ -1837,6 +1832,116 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
}
}

class TerminalInstanceDropAndDropController extends Disposable implements IDragAndDropObserverCallbacks {
private _dropOverlay?: HTMLElement;


private readonly _onDropFile = new Emitter<string>();
get onDropFile(): Event<string> { return this._onDropFile.event; }
private readonly _onDropTerminal = new Emitter<IRequestAddInstanceToGroupEvent>();
get onDropTerminal(): Event<IRequestAddInstanceToGroupEvent> { return this._onDropTerminal.event; }

constructor(
private readonly _container: HTMLElement
) {
super();
this._register(toDisposable(() => this._clearDropOverlay()));
}

private _clearDropOverlay() {
if (this._dropOverlay && this._dropOverlay.parentElement) {
this._dropOverlay.parentElement.removeChild(this._dropOverlay);
}
this._dropOverlay = undefined;
}

onDragEnter(e: DragEvent) {
if (!this._dropOverlay) {
this._dropOverlay = document.createElement('div');
this._dropOverlay.classList.add('terminal-drop-overlay');
}

const types = e.dataTransfer?.types || [];

// Dragging terminals
if (types.includes('terminals')) {
const side = this._getDropSide(e);
this._dropOverlay.classList.toggle('drop-left', side === 'left');
this._dropOverlay.classList.toggle('drop-right', side === 'right');
}

if (!this._dropOverlay.parentElement) {
this._container.appendChild(this._dropOverlay);
}
}
onDragLeave(e: DragEvent) {
this._clearDropOverlay();
}

onDragEnd(e: DragEvent) {
this._clearDropOverlay();
}

onDragOver(e: DragEvent) {
if (!e.dataTransfer || !this._dropOverlay) {
return;
}

const types = e.dataTransfer?.types || [];

// Dragging terminals
if (types.includes('terminals')) {
const side = this._getDropSide(e);
this._dropOverlay.classList.toggle('drop-left', side === 'left');
this._dropOverlay.classList.toggle('drop-right', side === 'right');
}

this._dropOverlay.style.opacity = '1';
}

async onDrop(e: DragEvent) {
this._clearDropOverlay();

if (!e.dataTransfer) {
return;
}

// Check if files were dragged from the tree explorer
let path: string | undefined;
const resources = e.dataTransfer.getData(DataTransfers.RESOURCES);
if (resources) {
const uri = URI.parse(JSON.parse(resources)[0]);
if (uri.scheme === Schemas.vscodeTerminal) {
this._onDropTerminal.fire({
uri,
side: this._getDropSide(e)
});
return;
} else {
path = uri.fsPath;
}
} else if (e.dataTransfer.files?.[0].path /* Electron only */) {
// Check if the file was dragged from the filesystem
path = URI.file(e.dataTransfer.files[0].path).fsPath;
}

if (!path) {
return;
}

this._onDropFile.fire(path);
}

private _getDropSide(e: DragEvent): 'left' | 'right' {
const target = this._container;
if (!target) {
return 'right';
}
const rect = target.getBoundingClientRect();
return e.clientX - rect.left < rect.width / 2 ? 'left' : 'right';
}
}

let colors: string[] = [];
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
// add icon colors
Expand Down
38 changes: 38 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,38 @@ export class TerminalService implements ITerminalService {
}
}

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: 'left' | 'right'): 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 === 'right' ? 1 : 0);
targetGroup.moveInstance(source, index);
}

protected _initInstanceListeners(instance: ITerminalInstance): void {
instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed));
instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged));
Expand All @@ -703,6 +735,12 @@ 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));
if (sourceInstance) {
this.moveInstance(sourceInstance, instance, e.side);
}
}));
}

registerProcessSupport(isSupported: boolean): void {
Expand Down
Loading

0 comments on commit aea7c1a

Please sign in to comment.