Skip to content

Commit e592087

Browse files
authored
Support drag and drop for symbols in breadcrumbs, outline and chat (microsoft#234372)
* Support DnD for breadcrumbs and symbols * symbol data type * Refactor DocumentSymbolDragAndDrop to remove model provider dependency
1 parent cb2c473 commit e592087

File tree

5 files changed

+201
-10
lines changed

5 files changed

+201
-10
lines changed

src/vs/platform/dnd/browser/dnd.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import { Registry } from '../../registry/common/platform.js';
2929

3030
export const CodeDataTransfers = {
3131
EDITORS: 'CodeEditors',
32-
FILES: 'CodeFiles'
32+
FILES: 'CodeFiles',
33+
SYMBOLS: 'application/vnd.code.symbols'
3334
};
3435

3536
export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput {
@@ -400,6 +401,31 @@ export class LocalSelectionTransfer<T> {
400401
}
401402
}
402403

404+
export interface DocumentSymbolTransferData {
405+
name: string;
406+
fsPath: string;
407+
range: {
408+
startLineNumber: number;
409+
startColumn: number;
410+
endLineNumber: number;
411+
endColumn: number;
412+
};
413+
kind: number;
414+
}
415+
416+
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
417+
const rawSymbolsData = e.dataTransfer?.getData(CodeDataTransfers.SYMBOLS);
418+
if (rawSymbolsData) {
419+
try {
420+
return JSON.parse(rawSymbolsData);
421+
} catch (error) {
422+
// Invalid transfer
423+
}
424+
}
425+
426+
return [];
427+
}
428+
403429
/**
404430
* A helper to get access to Electrons `webUtils.getPathForFile` function
405431
* in a safe way without crashing the application when running in the web.

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
88
import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent, IBreadcrumbsWidgetStyles } from '../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js';
99
import { timeout } from '../../../../base/common/async.js';
1010
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
11-
import { combinedDisposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12-
import { extUri } from '../../../../base/common/resources.js';
11+
import { combinedDisposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12+
import { basename, extUri } from '../../../../base/common/resources.js';
1313
import { URI } from '../../../../base/common/uri.js';
1414
import './media/breadcrumbscontrol.css';
1515
import { localize, localize2 } from '../../../../nls.js';
@@ -40,6 +40,11 @@ import { defaultBreadcrumbsWidgetStyles } from '../../../../platform/theme/brows
4040
import { Emitter } from '../../../../base/common/event.js';
4141
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
4242
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
43+
import { DataTransfers } from '../../../../base/browser/dnd.js';
44+
import { $ } from '../../../../base/browser/dom.js';
45+
import { OutlineElement } from '../../../../editor/contrib/documentSymbols/browser/outlineModel.js';
46+
import { CodeDataTransfers, DocumentSymbolTransferData } from '../../../../platform/dnd/browser/dnd.js';
47+
import { withSelection } from '../../../../platform/opener/common/opener.js';
4348

4449
class OutlineItem extends BreadcrumbsItem {
4550

@@ -96,8 +101,22 @@ class OutlineItem extends BreadcrumbsItem {
96101
}, 0, template, undefined);
97102

98103
this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); }));
99-
}
100104

105+
if (element instanceof OutlineElement && outline.uri) {
106+
const symbolUri = withSelection(outline.uri, element.symbol.range);
107+
const symbolTransferData: DocumentSymbolTransferData = {
108+
name: element.symbol.name,
109+
fsPath: outline.uri.fsPath,
110+
range: element.symbol.range,
111+
kind: element.symbol.kind
112+
};
113+
const dataTransfers: DataTransfer[] = [
114+
[CodeDataTransfers.SYMBOLS, [symbolTransferData]],
115+
[DataTransfers.RESOURCES, [symbolUri]]
116+
];
117+
this._disposables.add(createBreadcrumbDndObserver(container, element.symbol.name, symbolUri.toString(), dataTransfers));
118+
}
119+
}
101120
}
102121

103122
class FileItem extends BreadcrumbsItem {
@@ -139,9 +158,53 @@ class FileItem extends BreadcrumbsItem {
139158
});
140159
container.classList.add(FileKind[this.element.kind].toLowerCase());
141160
this._disposables.add(label);
161+
162+
const dataTransfers: DataTransfer[] = [
163+
[CodeDataTransfers.FILES, [this.element.uri.fsPath]],
164+
[DataTransfers.RESOURCES, [this.element.uri.toString()]],
165+
];
166+
const dndObserver = createBreadcrumbDndObserver(container, basename(this.element.uri), this.element.uri.toString(), dataTransfers);
167+
this._disposables.add(dndObserver);
142168
}
143169
}
144170

171+
type DataTransfer = [string, any[]];
172+
173+
function createBreadcrumbDndObserver(container: HTMLElement, label: string, textData: string, dataTransfers: DataTransfer[]): IDisposable {
174+
container.draggable = true;
175+
176+
return new dom.DragAndDropObserver(container, {
177+
onDragStart: event => {
178+
if (!event.dataTransfer) {
179+
return;
180+
}
181+
182+
// Set data transfer
183+
event.dataTransfer.effectAllowed = 'copyMove';
184+
event.dataTransfer.setData(DataTransfers.TEXT, textData);
185+
for (const [type, data] of dataTransfers) {
186+
event.dataTransfer.setData(type, JSON.stringify(data));
187+
}
188+
189+
// Create drag image and remove when dropped
190+
const dragImage = $('.monaco-drag-image');
191+
dragImage.textContent = label;
192+
193+
const getDragImageContainer = (e: HTMLElement | null) => {
194+
while (e && !e.classList.contains('monaco-workbench')) {
195+
e = e.parentElement;
196+
}
197+
return e || container.ownerDocument;
198+
};
199+
200+
const dragContainer = getDragImageContainer(container);
201+
dragContainer.appendChild(dragImage);
202+
event.dataTransfer.setDragImage(dragImage, -10, -10);
203+
setTimeout(() => dragImage.remove(), 0);
204+
}
205+
});
206+
}
207+
145208
export interface IBreadcrumbsControlOptions {
146209
readonly showFileIcons: boolean;
147210
readonly showSymbolIcons: boolean;

src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import { IDisposable } from '../../../../base/common/lifecycle.js';
1212
import { Mimes } from '../../../../base/common/mime.js';
1313
import { basename, joinPath } from '../../../../base/common/resources.js';
1414
import { URI } from '../../../../base/common/uri.js';
15+
import { IRange } from '../../../../editor/common/core/range.js';
16+
import { SymbolKinds } from '../../../../editor/common/languages.js';
1517
import { localize } from '../../../../nls.js';
16-
import { containsDragType, extractEditorsDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js';
18+
import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractSymbolDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js';
1719
import { FileType, IFileService, IFileSystemProvider } from '../../../../platform/files/common/files.js';
1820
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
1921
import { EditorInput } from '../../../common/editor/editorInput.js';
@@ -26,7 +28,8 @@ enum ChatDragAndDropType {
2628
FILE_INTERNAL,
2729
FILE_EXTERNAL,
2830
FOLDER,
29-
IMAGE
31+
IMAGE,
32+
SYMBOL
3033
}
3134

3235
export class ChatDragAndDrop extends Themable {
@@ -155,6 +158,8 @@ export class ChatDragAndDrop extends Themable {
155158
// This is an esstimation based on the datatransfer types/items
156159
if (this.isImageDnd(e)) {
157160
return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined;
161+
} else if (containsDragType(e, CodeDataTransfers.SYMBOLS)) {
162+
return ChatDragAndDropType.SYMBOL;
158163
} else if (containsDragType(e, DataTransfers.FILES)) {
159164
return ChatDragAndDropType.FILE_EXTERNAL;
160165
} else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) {
@@ -178,6 +183,7 @@ export class ChatDragAndDrop extends Themable {
178183
case ChatDragAndDropType.FILE_EXTERNAL: return localize('file', 'File');
179184
case ChatDragAndDropType.FOLDER: return localize('folder', 'Folder');
180185
case ChatDragAndDropType.IMAGE: return localize('image', 'Image');
186+
case ChatDragAndDropType.SYMBOL: return localize('symbol', 'Symbol');
181187
}
182188
}
183189

@@ -209,6 +215,11 @@ export class ChatDragAndDrop extends Themable {
209215
return [];
210216
}
211217

218+
if (containsDragType(e, CodeDataTransfers.SYMBOLS)) {
219+
const data = extractSymbolDropData(e);
220+
return this.resolveSymbolsAttachContext(data);
221+
}
222+
212223
const data = extractEditorsDropData(e);
213224
return coalesce(await Promise.all(data.map(editorInput => {
214225
return this.resolveAttachContext(editorInput);
@@ -245,6 +256,20 @@ export class ChatDragAndDrop extends Themable {
245256
return getResourceAttachContext(editor.resource, stat.isDirectory);
246257
}
247258

259+
private resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): IChatRequestVariableEntry[] {
260+
return symbols.map(symbol => {
261+
const resource = URI.file(symbol.fsPath);
262+
return {
263+
kind: 'symbol',
264+
id: symbolId(resource, symbol.range),
265+
value: { uri: resource, range: symbol.range },
266+
fullName: `$(${SymbolKinds.toIcon(symbol.kind).id}) ${symbol.name}`,
267+
name: symbol.name,
268+
isDynamic: true
269+
};
270+
});
271+
}
272+
248273
private setOverlay(target: HTMLElement, type: ChatDragAndDropType | undefined): void {
249274
// Remove any previous overlay text
250275
this.overlayText?.remove();
@@ -390,3 +415,14 @@ function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput
390415

391416
return undefined;
392417
}
418+
419+
function symbolId(resource: URI, range?: IRange): string {
420+
let rangePart = '';
421+
if (range) {
422+
rangePart = `:${range.startLineNumber}`;
423+
if (range.startLineNumber !== range.endLineNumber) {
424+
rangePart += `-${range.endLineNumber}`;
425+
}
426+
}
427+
return resource.fsPath + rangePart;
428+
}

src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr
1010
import { Registry } from '../../../../../platform/registry/common/platform.js';
1111
import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js';
1212
import { IEditorPane } from '../../../../common/editor.js';
13-
import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate } from './documentSymbolsTree.js';
13+
import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate, DocumentSymbolDragAndDrop } from './documentSymbolsTree.js';
1414
import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js';
1515
import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker, IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js';
1616
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
@@ -169,7 +169,8 @@ class DocumentSymbolsOutline implements IOutline<DocumentSymbolItem> {
169169
? instantiationService.createInstance(DocumentSymbolFilter, 'outline')
170170
: target === OutlineTarget.Breadcrumbs
171171
? instantiationService.createInstance(DocumentSymbolFilter, 'breadcrumbs')
172-
: undefined
172+
: undefined,
173+
dnd: new DocumentSymbolDragAndDrop(),
173174
};
174175

175176
this.config = {

src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import '../../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; // Th
88
import * as dom from '../../../../../base/browser/dom.js';
99
import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
1010
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js';
11-
import { ITreeNode, ITreeRenderer, ITreeFilter } from '../../../../../base/browser/ui/tree/tree.js';
11+
import { ITreeNode, ITreeRenderer, ITreeFilter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js';
1212
import { createMatches, FuzzyScore } from '../../../../../base/common/filters.js';
1313
import { Range } from '../../../../../editor/common/core/range.js';
14-
import { SymbolKind, SymbolKinds, SymbolTag, getAriaLabelForSymbol, symbolKindNames } from '../../../../../editor/common/languages.js';
14+
import { DocumentSymbol, SymbolKind, SymbolKinds, SymbolTag, getAriaLabelForSymbol, symbolKindNames } from '../../../../../editor/common/languages.js';
1515
import { OutlineElement, OutlineGroup, OutlineModel } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js';
1616
import { localize } from '../../../../../nls.js';
1717
import { IconLabel, IIconLabelValueOptions } from '../../../../../base/browser/ui/iconLabel/iconLabel.js';
@@ -24,6 +24,11 @@ import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/
2424
import { IOutlineComparator, OutlineConfigKeys, OutlineTarget } from '../../../../services/outline/browser/outline.js';
2525
import { ThemeIcon } from '../../../../../base/common/themables.js';
2626
import { mainWindow } from '../../../../../base/browser/window.js';
27+
import { IDragAndDropData, DataTransfers } from '../../../../../base/browser/dnd.js';
28+
import { ElementsDragAndDropData } from '../../../../../base/browser/ui/list/listView.js';
29+
import { CodeDataTransfers } from '../../../../../platform/dnd/browser/dnd.js';
30+
import { withSelection } from '../../../../../platform/opener/common/opener.js';
31+
import { URI } from '../../../../../base/common/uri.js';
2732

2833
export type DocumentSymbolItem = OutlineGroup | OutlineElement;
2934

@@ -60,6 +65,66 @@ export class DocumentSymbolIdentityProvider implements IIdentityProvider<Documen
6065
}
6166
}
6267

68+
export class DocumentSymbolDragAndDrop implements ITreeDragAndDrop<DocumentSymbolItem> {
69+
70+
constructor() { }
71+
72+
getDragURI(element: DocumentSymbolItem): string | null {
73+
const resource = OutlineModel.get(element)?.uri;
74+
if (!resource) {
75+
return null;
76+
}
77+
78+
if (element instanceof OutlineElement) {
79+
return symbolRangeUri(resource, element.symbol).toString();
80+
} else {
81+
return resource.toString();
82+
}
83+
}
84+
85+
getDragLabel(elements: DocumentSymbolItem[], originalEvent: DragEvent): string | undefined {
86+
// Multi select not supported
87+
if (elements.length !== 1) {
88+
return undefined;
89+
}
90+
91+
const element = elements[0];
92+
return element instanceof OutlineElement ? element.symbol.name : element.label;
93+
}
94+
95+
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
96+
const elements = (data as ElementsDragAndDropData<DocumentSymbolItem, DocumentSymbolItem[]>).elements;
97+
const item = elements[0];
98+
if (!item || !originalEvent.dataTransfer) {
99+
return;
100+
}
101+
102+
const resource = OutlineModel.get(item)?.uri;
103+
if (!resource) {
104+
return;
105+
}
106+
107+
const outlineElements = item instanceof OutlineElement ? [item] : Array.from(item.children.values());
108+
const symbolsData = outlineElements.map(oe => ({
109+
name: oe.symbol.name,
110+
fsPath: resource.fsPath,
111+
range: oe.symbol.range,
112+
kind: oe.symbol.kind
113+
}));
114+
115+
originalEvent.dataTransfer.setData(CodeDataTransfers.SYMBOLS, JSON.stringify(symbolsData));
116+
originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(outlineElements.map(oe => symbolRangeUri(resource, oe.symbol))));
117+
}
118+
119+
onDragOver(): boolean | ITreeDragOverReaction { return false; }
120+
drop(): void { }
121+
dispose(): void { }
122+
}
123+
124+
function symbolRangeUri(resource: URI, symbol: DocumentSymbol): URI {
125+
return withSelection(resource, symbol.range);
126+
}
127+
63128
class DocumentSymbolGroupTemplate {
64129
static readonly id = 'DocumentSymbolGroupTemplate';
65130
constructor(

0 commit comments

Comments
 (0)