Skip to content

Commit 0f50837

Browse files
authored
enhane attaching files and folders (#298133)
* enhane attaching files and folders * feedback
1 parent 3c0e1a1 commit 0f50837

File tree

3 files changed

+121
-64
lines changed

3 files changed

+121
-64
lines changed

src/vs/sessions/contrib/chat/browser/media/chatWidget.css

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
flex-direction: column;
99
height: 100%;
1010
width: 100%;
11+
position: relative;
1112
}
1213

1314
/* Welcome container fills available space and centers content */
@@ -250,17 +251,37 @@
250251
}
251252

252253
/* Drag and drop */
253-
.sessions-chat-drop-overlay {
254-
display: none;
254+
.sessions-chat-dnd-overlay {
255255
position: absolute;
256256
top: 0;
257257
left: 0;
258-
right: 0;
259-
bottom: 0;
258+
width: 100%;
259+
height: 100%;
260+
box-sizing: border-box;
261+
display: none;
260262
z-index: 10;
263+
background-color: var(--vscode-sideBar-dropBackground, var(--vscode-list-dropBackground));
264+
}
265+
266+
.sessions-chat-dnd-overlay.visible {
267+
display: flex;
268+
align-items: center;
269+
justify-content: center;
261270
}
262271

263-
.sessions-chat-input-area.sessions-chat-drop-active {
264-
border-color: var(--vscode-focusBorder);
265-
background-color: var(--vscode-list-dropBackground);
272+
.sessions-chat-dnd-overlay .attach-context-overlay-text {
273+
padding: 0.6em;
274+
margin: 0.2em;
275+
line-height: 12px;
276+
height: 12px;
277+
display: flex;
278+
align-items: center;
279+
text-align: center;
280+
background-color: var(--vscode-sideBar-background, var(--vscode-editor-background));
281+
}
282+
283+
.sessions-chat-dnd-overlay .attach-context-overlay-text .codicon {
284+
height: 12px;
285+
font-size: 12px;
286+
margin-right: 3px;
266287
}

src/vs/sessions/contrib/chat/browser/newChatContextAttachments.ts

Lines changed: 92 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from '../../../../base/browser/dom.js';
7+
import { DragAndDropObserver } from '../../../../base/browser/dom.js';
78
import { Codicon } from '../../../../base/common/codicons.js';
89
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
910
import { URI } from '../../../../base/common/uri.js';
1011
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
1112
import { Emitter } from '../../../../base/common/event.js';
12-
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
13+
import { renderIcon, renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
1314
import { localize } from '../../../../nls.js';
1415
import { ThemeIcon } from '../../../../base/common/themables.js';
1516
import { registerOpenEditorListeners } from '../../../../platform/editor/browser/editor.js';
@@ -29,7 +30,8 @@ import { IChatRequestVariableEntry, OmittedState } from '../../../../workbench/c
2930
import { isLocation } from '../../../../editor/common/languages.js';
3031
import { resizeImage } from '../../../../workbench/contrib/chat/browser/chatImageUtils.js';
3132
import { imageToHash, isImage } from '../../../../workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.js';
32-
import { getPathForFile } from '../../../../platform/dnd/browser/dnd.js';
33+
import { CodeDataTransfers, containsDragType, extractEditorsDropData, getPathForFile } from '../../../../platform/dnd/browser/dnd.js';
34+
import { DataTransfers } from '../../../../base/browser/dnd.js';
3335
import { getExcludes, ISearchConfiguration, ISearchService, QueryType } from '../../../../workbench/services/search/common/search.js';
3436

3537
/**
@@ -101,7 +103,7 @@ export class NewChatContextAttachments extends Disposable {
101103
const pill = dom.append(this._container, dom.$('.sessions-chat-attachment-pill'));
102104
pill.tabIndex = 0;
103105
pill.role = 'button';
104-
const icon = entry.kind === 'image' ? Codicon.fileMedia : Codicon.file;
106+
const icon = entry.kind === 'image' ? Codicon.fileMedia : entry.kind === 'directory' ? Codicon.folder : Codicon.file;
105107
dom.append(pill, renderIcon(icon));
106108
dom.append(pill, dom.$('span.sessions-chat-attachment-name', undefined, entry.name));
107109

@@ -127,68 +129,85 @@ export class NewChatContextAttachments extends Disposable {
127129

128130
// --- Drag and drop ---
129131

130-
registerDropTarget(element: HTMLElement): void {
131-
// Use a transparent overlay during drag to capture events over the Monaco editor
132-
const overlay = dom.append(element, dom.$('.sessions-chat-drop-overlay'));
132+
registerDropTarget(dndContainer: HTMLElement): void {
133+
const overlay = dom.append(dndContainer, dom.$('.sessions-chat-dnd-overlay'));
134+
let overlayText: HTMLElement | undefined;
133135

134-
// Use capture phase to intercept drag events before Monaco editor handles them
135-
this._register(dom.addDisposableListener(element, dom.EventType.DRAG_ENTER, (e: DragEvent) => {
136-
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) {
137-
e.preventDefault();
138-
e.dataTransfer.dropEffect = 'copy';
139-
overlay.style.display = 'block';
140-
element.classList.add('sessions-chat-drop-active');
141-
}
142-
}, true));
136+
const isDropSupported = (e: DragEvent): boolean => {
137+
return containsDragType(e, DataTransfers.FILES, CodeDataTransfers.EDITORS, CodeDataTransfers.FILES, DataTransfers.RESOURCES, DataTransfers.INTERNAL_URI_LIST);
138+
};
143139

144-
this._register(dom.addDisposableListener(element, dom.EventType.DRAG_OVER, (e: DragEvent) => {
145-
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) {
146-
e.preventDefault();
147-
e.dataTransfer.dropEffect = 'copy';
148-
if (overlay.style.display !== 'block') {
149-
overlay.style.display = 'block';
150-
element.classList.add('sessions-chat-drop-active');
151-
}
140+
const showOverlay = () => {
141+
overlay.classList.add('visible');
142+
if (!overlayText) {
143+
const label = localize('attachAsContext', "Attach as Context");
144+
const iconAndTextElements = renderLabelWithIcons(`$(${Codicon.attach.id}) ${label}`);
145+
const htmlElements = iconAndTextElements.map(element => {
146+
if (typeof element === 'string') {
147+
return dom.$('span.overlay-text', undefined, element);
148+
}
149+
return element;
150+
});
151+
overlayText = dom.$('span.attach-context-overlay-text', undefined, ...htmlElements);
152+
overlay.appendChild(overlayText);
152153
}
153-
}, true));
154-
155-
this._register(dom.addDisposableListener(overlay, dom.EventType.DRAG_OVER, (e) => {
156-
e.preventDefault();
157-
e.dataTransfer!.dropEffect = 'copy';
158-
}));
154+
};
159155

160-
this._register(dom.addDisposableListener(overlay, dom.EventType.DRAG_LEAVE, (e) => {
161-
if (e.relatedTarget && element.contains(e.relatedTarget as Node)) {
162-
return;
163-
}
164-
overlay.style.display = 'none';
165-
element.classList.remove('sessions-chat-drop-active');
166-
}));
156+
const hideOverlay = () => {
157+
overlay.classList.remove('visible');
158+
overlayText?.remove();
159+
overlayText = undefined;
160+
};
167161

168-
this._register(dom.addDisposableListener(overlay, dom.EventType.DROP, async (e) => {
169-
e.preventDefault();
170-
e.stopPropagation();
171-
overlay.style.display = 'none';
172-
element.classList.remove('sessions-chat-drop-active');
173-
174-
// Try items first (for URI-based drops from VS Code tree views)
175-
const items = e.dataTransfer?.items;
176-
if (items) {
177-
for (const item of Array.from(items)) {
178-
if (item.kind === 'file') {
179-
const file = item.getAsFile();
180-
if (!file) {
181-
continue;
162+
this._register(new DragAndDropObserver(dndContainer, {
163+
onDragOver: (e) => {
164+
if (isDropSupported(e)) {
165+
e.preventDefault();
166+
e.stopPropagation();
167+
if (e.dataTransfer) {
168+
e.dataTransfer.dropEffect = 'copy';
169+
}
170+
showOverlay();
171+
}
172+
},
173+
onDragLeave: () => {
174+
hideOverlay();
175+
},
176+
onDrop: async (e) => {
177+
e.preventDefault();
178+
e.stopPropagation();
179+
hideOverlay();
180+
181+
// Extract editor data from VS Code internal drags (e.g., explorer view)
182+
const editorDropData = extractEditorsDropData(e);
183+
if (editorDropData.length > 0) {
184+
for (const editor of editorDropData) {
185+
if (editor.resource) {
186+
await this._attachFileUri(editor.resource, basename(editor.resource));
182187
}
183-
const filePath = getPathForFile(file);
184-
if (!filePath) {
185-
continue;
188+
}
189+
return;
190+
}
191+
192+
// Fallback: try native file items
193+
const items = e.dataTransfer?.items;
194+
if (items) {
195+
for (const item of Array.from(items)) {
196+
if (item.kind === 'file') {
197+
const file = item.getAsFile();
198+
if (!file) {
199+
continue;
200+
}
201+
const filePath = getPathForFile(file);
202+
if (!filePath) {
203+
continue;
204+
}
205+
const uri = URI.file(filePath);
206+
await this._attachFileUri(uri, file.name);
186207
}
187-
const uri = URI.file(filePath);
188-
await this._attachFileUri(uri, file.name);
189208
}
190209
}
191-
}
210+
},
192211
}));
193212
}
194213

@@ -446,6 +465,23 @@ export class NewChatContextAttachments extends Disposable {
446465
}
447466

448467
private async _attachFileUri(uri: URI, name: string): Promise<void> {
468+
let stat;
469+
try {
470+
stat = await this.fileService.stat(uri);
471+
} catch {
472+
return;
473+
}
474+
475+
if (stat.isDirectory) {
476+
this._addAttachments({
477+
kind: 'directory',
478+
id: uri.toString(),
479+
value: uri,
480+
name,
481+
});
482+
return;
483+
}
484+
449485
if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(uri.path)) {
450486
const readFile = await this.fileService.readFile(uri);
451487
const resizedImage = await resizeImage(readFile.value.buffer);

src/vs/sessions/contrib/chat/browser/newChatViewPane.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
241241

242242
// Input area inside the input slot
243243
const inputArea = dom.$('.sessions-chat-input-area');
244-
this._contextAttachments.registerDropTarget(inputArea);
244+
this._contextAttachments.registerDropTarget(wrapper);
245245
this._contextAttachments.registerPasteHandler(inputArea);
246246

247247
// Attachments row (pills only) inside input area, above editor

0 commit comments

Comments
 (0)