Skip to content

Commit 81f8ee0

Browse files
author
Benjamin Pasero
committed
improve cross window tab dnd (support dirty, restore UI state)
1 parent 9e64b03 commit 81f8ee0

14 files changed

Lines changed: 290 additions & 183 deletions

File tree

src/vs/base/browser/dnd.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,23 @@ export class DelayedDragHandler {
3939
public dispose(): void {
4040
this.clearDragTimeout();
4141
}
42-
}
42+
}
43+
44+
// Common data transfers
45+
export const DataTransfers = {
46+
47+
/**
48+
* Application specific resource transfer type.
49+
*/
50+
URL: 'URL',
51+
52+
/**
53+
* Browser specific transfer type to download.
54+
*/
55+
DOWNLOAD_URL: 'DownloadURL',
56+
57+
/**
58+
* Typicaly transfer type for copy/paste transfers.
59+
*/
60+
TEXT: 'text/plain'
61+
};

src/vs/base/parts/tree/browser/treeDnd.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults';
1010
import URI from 'vs/base/common/uri';
1111
import { basename } from 'vs/base/common/paths';
1212
import { getPathLabel } from 'vs/base/common/labels';
13+
import { DataTransfers } from 'vs/base/browser/dnd';
1314

1415
export class ElementsDragAndDropData implements _.IDragAndDropData {
1516

@@ -116,7 +117,7 @@ export class SimpleFileResourceDragAndDrop extends DefaultDragAndDrop {
116117
// Apply some datatransfer types to allow for dragging the element outside of the application
117118
const resource = this.toResource(source);
118119
if (resource) {
119-
originalEvent.dataTransfer.setData('text/plain', getPathLabel(resource));
120+
originalEvent.dataTransfer.setData(DataTransfers.TEXT, getPathLabel(resource));
120121
}
121122
}
122123
}

src/vs/base/parts/tree/browser/treeView.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import _ = require('vs/base/parts/tree/browser/tree');
2424
import { KeyCode } from 'vs/base/common/keyCodes';
2525
import Event, { Emitter } from 'vs/base/common/event';
2626
import { IDomNodePagePosition } from 'vs/base/browser/dom';
27+
import { DataTransfers } from 'vs/base/browser/dnd';
2728

2829
export interface IRow {
2930
element: HTMLElement;
@@ -1291,7 +1292,7 @@ export class TreeView extends HeightMap {
12911292
}
12921293

12931294
e.dataTransfer.effectAllowed = 'copyMove';
1294-
e.dataTransfer.setData('URL', item.uri);
1295+
e.dataTransfer.setData(DataTransfers.URL, item.uri);
12951296
if (e.dataTransfer.setDragImage) {
12961297
let label: string;
12971298

src/vs/workbench/browser/editor.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
1212
import { IConstructorSignature0, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1313
import { isArray } from 'vs/base/common/types';
1414
import URI from 'vs/base/common/uri';
15+
import { DataTransfers } from 'vs/base/browser/dnd';
16+
import { IEditorViewState } from 'vs/editor/common/editorCommon';
1517

1618
export interface IEditorDescriptor {
1719
instantiate(instantiationService: IInstantiationService): BaseEditor;
@@ -204,20 +206,50 @@ export interface IDraggedResource {
204206
isExternal: boolean;
205207
}
206208

207-
export function extractResources(e: DragEvent, externalOnly?: boolean): IDraggedResource[] {
208-
const resources: IDraggedResource[] = [];
209+
export interface IDraggedEditor extends IDraggedResource {
210+
backupResource?: URI;
211+
viewState?: IEditorViewState;
212+
}
213+
214+
export interface ISerializedDraggedEditor {
215+
resource: string;
216+
backupResource: string;
217+
viewState: IEditorViewState;
218+
}
219+
220+
export const CodeDataTransfers = {
221+
EDITOR: 'CodeEditor'
222+
};
223+
224+
export function extractResources(e: DragEvent, externalOnly?: boolean): (IDraggedResource | IDraggedEditor)[] {
225+
const resources: (IDraggedResource | IDraggedEditor)[] = [];
209226
if (e.dataTransfer.types.length > 0) {
210227

211-
// Check for in-app DND
228+
// Check for window-to-window DND
212229
if (!externalOnly) {
213-
const rawData = e.dataTransfer.getData('URL');
214-
if (rawData) {
230+
231+
// Data Transfer: Code Editor
232+
const rawEditorData = e.dataTransfer.getData(CodeDataTransfers.EDITOR);
233+
if (rawEditorData) {
215234
try {
216-
resources.push({ resource: URI.parse(rawData), isExternal: false });
235+
const draggedEditor = JSON.parse(rawEditorData) as ISerializedDraggedEditor;
236+
resources.push({ resource: URI.parse(draggedEditor.resource), backupResource: URI.parse(draggedEditor.backupResource), viewState: draggedEditor.viewState, isExternal: false });
217237
} catch (error) {
218238
// Invalid URI
219239
}
220240
}
241+
242+
// Data Transfer: URL
243+
else {
244+
const rawURLData = e.dataTransfer.getData(DataTransfers.URL);
245+
if (rawURLData) {
246+
try {
247+
resources.push({ resource: URI.parse(rawURLData), isExternal: false });
248+
} catch (error) {
249+
// Invalid URI
250+
}
251+
}
252+
}
221253
}
222254

223255
// Check for native file transfer
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
import { IDraggedResource, IDraggedEditor, extractResources } from 'vs/workbench/browser/editor';
9+
import { WORKSPACE_EXTENSION, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
10+
import { extname } from 'vs/base/common/paths';
11+
import { IFileService } from 'vs/platform/files/common/files';
12+
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
13+
import URI from 'vs/base/common/uri';
14+
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
15+
import { BACKUP_FILE_RESOLVE_OPTIONS, IBackupFileService } from 'vs/workbench/services/backup/common/backup';
16+
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
17+
import { TPromise } from 'vs/base/common/winjs.base';
18+
import { Schemas } from 'vs/base/common/network';
19+
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
20+
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
21+
import { Position } from 'vs/platform/editor/common/editor';
22+
import { onUnexpectedError } from 'vs/base/common/errors';
23+
24+
/**
25+
* Shared function across some editor components to handle drag & drop of external resources. E.g. of folders and workspace files
26+
* to open them in the window instead of the editor or to handle dirty editors being dropped between instances of Code.
27+
*/
28+
export class EditorAreaDropHandler {
29+
30+
constructor(
31+
@IFileService private fileService: IFileService,
32+
@IWindowsService private windowsService: IWindowsService,
33+
@IWindowService private windowService: IWindowService,
34+
@IWorkspacesService private workspacesService: IWorkspacesService,
35+
@ITextFileService private textFileService: ITextFileService,
36+
@IBackupFileService private backupFileService: IBackupFileService,
37+
@IEditorGroupService private groupService: IEditorGroupService,
38+
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
39+
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
40+
) {
41+
}
42+
43+
public handleDrop(event: DragEvent, afterDrop: () => void, targetPosition: Position, targetIndex?: number): void {
44+
const resources = extractResources(event).filter(r => r.resource.scheme === Schemas.file || r.resource.scheme === Schemas.untitled);
45+
if (!resources.length) {
46+
return;
47+
}
48+
49+
return this.doHandleDrop(resources).then(isWorkspaceOpening => {
50+
if (isWorkspaceOpening) {
51+
return void 0; // return early if the drop operation resulted in this window changing to a workspace
52+
}
53+
54+
// Add external ones to recently open list unless dropped resource is a workspace
55+
const externalResources = resources.filter(d => d.isExternal).map(d => d.resource);
56+
if (externalResources.length) {
57+
this.windowsService.addRecentlyOpened(externalResources.map(resource => resource.fsPath));
58+
}
59+
60+
// Open in Editor
61+
return this.windowService.focusWindow()
62+
.then(() => this.editorService.openEditors(resources.map(r => {
63+
return {
64+
input: {
65+
resource: r.resource,
66+
options: {
67+
pinned: true,
68+
index: targetIndex,
69+
viewState: (r as IDraggedEditor).viewState
70+
}
71+
},
72+
position: targetPosition
73+
};
74+
}))).then(() => {
75+
76+
// Finish with provided function
77+
afterDrop();
78+
});
79+
}).done(null, onUnexpectedError);
80+
}
81+
82+
private doHandleDrop(resources: (IDraggedResource | IDraggedEditor)[]): TPromise<boolean> {
83+
84+
// Check for dirty editor being dropped
85+
if (resources.length === 1 && !resources[0].isExternal && (resources[0] as IDraggedEditor).backupResource) {
86+
return this.handleDirtyEditorDrop(resources[0]);
87+
}
88+
89+
// Check for workspace file being dropped
90+
if (resources.some(r => r.isExternal)) {
91+
return this.handleWorkspaceFileDrop(resources);
92+
}
93+
94+
return TPromise.as(false);
95+
}
96+
97+
private handleDirtyEditorDrop(droppedDirtyEditor: IDraggedEditor): TPromise<boolean> {
98+
99+
// Untitled: always ensure that we open a new untitled for each file we drop
100+
if (droppedDirtyEditor.resource.scheme === Schemas.untitled) {
101+
droppedDirtyEditor.resource = this.untitledEditorService.createOrGet().getResource();
102+
}
103+
104+
// Return early if the resource is already dirty in target or opened already
105+
if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.groupService.getStacksModel().isOpen(droppedDirtyEditor.resource)) {
106+
return TPromise.as(false);
107+
}
108+
109+
// Resolve the contents of the dropped dirty resource from source
110+
return this.textFileService.resolveTextContent(droppedDirtyEditor.backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(content => {
111+
112+
// Set the contents of to the resource to the target
113+
return this.backupFileService.backupResource(droppedDirtyEditor.resource, this.backupFileService.parseBackupContent(content.value));
114+
}).then(() => false, () => false /* ignore any error */);
115+
}
116+
117+
private handleWorkspaceFileDrop(resources: (IDraggedResource | IDraggedEditor)[]): TPromise<boolean> {
118+
const externalResources = resources.filter(d => d.isExternal).map(d => d.resource);
119+
120+
const externalWorkspaceResources: { workspaces: URI[], folders: URI[] } = {
121+
workspaces: [],
122+
folders: []
123+
};
124+
125+
return TPromise.join(externalResources.map(resource => {
126+
127+
// Check for Workspace
128+
if (extname(resource.fsPath) === `.${WORKSPACE_EXTENSION}`) {
129+
externalWorkspaceResources.workspaces.push(resource);
130+
131+
return void 0;
132+
}
133+
134+
// Check for Folder
135+
return this.fileService.resolveFile(resource).then(stat => {
136+
if (stat.isDirectory) {
137+
externalWorkspaceResources.folders.push(stat.resource);
138+
}
139+
}, error => void 0);
140+
})).then(_ => {
141+
const { workspaces, folders } = externalWorkspaceResources;
142+
143+
// Return early if no external resource is a folder or workspace
144+
if (workspaces.length === 0 && folders.length === 0) {
145+
return false;
146+
}
147+
148+
// Pass focus to window
149+
this.windowService.focusWindow();
150+
151+
let workspacesToOpen: TPromise<string[]>;
152+
153+
// Open in separate windows if we drop workspaces or just one folder
154+
if (workspaces.length > 0 || folders.length === 1) {
155+
workspacesToOpen = TPromise.as([...workspaces, ...folders].map(resources => resources.fsPath));
156+
}
157+
158+
// Multiple folders: Create new workspace with folders and open
159+
else if (folders.length > 1) {
160+
workspacesToOpen = this.workspacesService.createWorkspace(folders.map(folder => ({ uri: folder }))).then(workspace => [workspace.configPath]);
161+
}
162+
163+
// Open
164+
workspacesToOpen.then(workspaces => {
165+
this.windowsService.openWindow(workspaces, { forceReuseWindow: true });
166+
});
167+
168+
return true;
169+
});
170+
}
171+
}

src/vs/workbench/browser/parts/editor/editorGroupsControl.ts

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,16 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
2626
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2727
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
2828
import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl';
29-
import { TitleControl, ITitleAreaControl, handleWorkspaceExternalDrop } from 'vs/workbench/browser/parts/editor/titleControl';
29+
import { TitleControl, ITitleAreaControl } from 'vs/workbench/browser/parts/editor/titleControl';
3030
import { NoTabsTitleControl } from 'vs/workbench/browser/parts/editor/noTabsTitleControl';
3131
import { IEditorStacksModel, IStacksModelChangeEvent, IEditorGroup, EditorOptions, TextEditorOptions, IEditorIdentifier } from 'vs/workbench/common/editor';
32-
import { extractResources } from 'vs/workbench/browser/editor';
33-
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
3432
import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService';
3533
import { IThemeService } from 'vs/platform/theme/common/themeService';
3634
import { editorBackground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
3735
import { Themable, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_GROUP_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme';
3836
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
39-
import { IMessageService } from 'vs/platform/message/common/message';
40-
import { IFileService } from 'vs/platform/files/common/files';
41-
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
4237
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
38+
import { EditorAreaDropHandler } from 'vs/workbench/browser/parts/editor/editorAreaDropHandler';
4339

4440
export enum Rochade {
4541
NONE,
@@ -148,12 +144,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
148144
@IContextKeyService private contextKeyService: IContextKeyService,
149145
@IExtensionService private extensionService: IExtensionService,
150146
@IInstantiationService private instantiationService: IInstantiationService,
151-
@IWindowService private windowService: IWindowService,
152-
@IWindowsService private windowsService: IWindowsService,
153-
@IThemeService themeService: IThemeService,
154-
@IFileService private fileService: IFileService,
155-
@IMessageService private messageService: IMessageService,
156-
@IWorkspacesService private workspacesService: IWorkspacesService
147+
@IThemeService themeService: IThemeService
157148
) {
158149
super(themeService);
159150

@@ -1121,36 +1112,14 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
11211112

11221113
// Check for URI transfer
11231114
else {
1124-
const droppedResources = extractResources(e).filter(r => r.resource.scheme === 'file' || r.resource.scheme === 'untitled');
1125-
if (droppedResources.length) {
1126-
handleWorkspaceExternalDrop(droppedResources, $this.fileService, $this.messageService, $this.windowsService, $this.windowService, $this.workspacesService).then(handled => {
1127-
if (handled) {
1128-
return;
1129-
}
1130-
1131-
// Add external ones to recently open list
1132-
const externalResources = droppedResources.filter(d => d.isExternal).map(d => d.resource);
1133-
if (externalResources.length) {
1134-
$this.windowsService.addRecentlyOpened(externalResources.map(resource => resource.fsPath));
1135-
}
1136-
1137-
// Open in Editor
1138-
$this.windowService.focusWindow()
1139-
.then(() => editorService.openEditors(droppedResources.map(d => {
1140-
return {
1141-
input: { resource: d.resource, options: { pinned: true } },
1142-
position: splitEditor ? freeGroup : position
1143-
};
1144-
}))).then(() => {
1145-
if (splitEditor && splitTo !== freeGroup) {
1146-
groupService.moveGroup(freeGroup, splitTo);
1147-
}
1115+
const dropHandler = $this.instantiationService.createInstance(EditorAreaDropHandler);
1116+
dropHandler.handleDrop(e, () => {
1117+
if (splitEditor && splitTo !== freeGroup) {
1118+
groupService.moveGroup(freeGroup, splitTo);
1119+
}
11481120

1149-
groupService.focusGroup(splitEditor ? splitTo : position);
1150-
})
1151-
.done(null, errors.onUnexpectedError);
1152-
});
1153-
}
1121+
groupService.focusGroup(splitEditor ? splitTo : position);
1122+
}, splitEditor ? freeGroup : position);
11541123
}
11551124
}
11561125

0 commit comments

Comments
 (0)