@@ -8,18 +8,19 @@ import { $, DragAndDropObserver } from '../../../../base/browser/dom.js';
8
8
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js' ;
9
9
import { coalesce } from '../../../../base/common/arrays.js' ;
10
10
import { Codicon } from '../../../../base/common/codicons.js' ;
11
+ import { IDisposable } from '../../../../base/common/lifecycle.js' ;
11
12
import { Mimes } from '../../../../base/common/mime.js' ;
12
- import { basename } from '../../../../base/common/resources.js' ;
13
+ import { basename , joinPath } from '../../../../base/common/resources.js' ;
13
14
import { URI } from '../../../../base/common/uri.js' ;
14
15
import { localize } from '../../../../nls.js' ;
15
16
import { containsDragType , extractEditorsDropData , IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js' ;
16
- import { IFileService } from '../../../../platform/files/common/files.js' ;
17
+ import { FileType , IFileService , IFileSystemProvider } from '../../../../platform/files/common/files.js' ;
17
18
import { IThemeService , Themable } from '../../../../platform/theme/common/themeService.js' ;
18
19
import { EditorInput } from '../../../common/editor/editorInput.js' ;
19
20
import { IExtensionService , isProposedApiEnabled } from '../../../services/extensions/common/extensions.js' ;
20
21
import { IChatRequestVariableEntry } from '../common/chatModel.js' ;
21
- import { ChatInputPart } from './chatInputPart .js' ;
22
- import { IChatWidgetStyles } from './chatWidget .js' ;
22
+ import { ChatAttachmentModel } from './chatAttachmentModel .js' ;
23
+ import { IChatInputStyles } from './chatInputPart .js' ;
23
24
24
25
enum ChatDragAndDropType {
25
26
FILE_INTERNAL ,
@@ -30,66 +31,101 @@ enum ChatDragAndDropType {
30
31
31
32
export class ChatDragAndDrop extends Themable {
32
33
33
- private readonly overlay : HTMLElement ;
34
+ private readonly overlays : Map < HTMLElement , { overlay : HTMLElement ; disposable : IDisposable } > = new Map ( ) ;
34
35
private overlayText ?: HTMLElement ;
35
36
private overlayTextBackground : string = '' ;
36
37
37
38
constructor (
38
- private readonly contianer : HTMLElement ,
39
- private readonly inputPart : ChatInputPart ,
40
- private readonly styles : IChatWidgetStyles ,
39
+ protected readonly attachmentModel : ChatAttachmentModel ,
40
+ private readonly styles : IChatInputStyles ,
41
41
@IThemeService themeService : IThemeService ,
42
42
@IExtensionService private readonly extensionService : IExtensionService ,
43
- @IFileService private readonly fileService : IFileService
43
+ @IFileService protected readonly fileService : IFileService
44
44
) {
45
45
super ( themeService ) ;
46
46
47
- // If the mouse enters and leaves the overlay quickly,
48
- // the overlay may stick around due to too many drag enter events
49
- // Make sure the mouse enters only once
50
- let mouseInside = false ;
51
- this . _register ( new DragAndDropObserver ( this . contianer , {
52
- onDragEnter : ( e ) => {
53
- if ( ! mouseInside ) {
54
- mouseInside = true ;
55
- this . onDragEnter ( e ) ;
56
- }
57
- } ,
47
+ this . updateStyles ( ) ;
48
+ }
49
+
50
+ addOverlay ( target : HTMLElement , overlayContainer : HTMLElement ) : void {
51
+ this . removeOverlay ( target ) ;
52
+
53
+ const { overlay, disposable } = this . createOverlay ( target , overlayContainer ) ;
54
+ this . overlays . set ( target , { overlay, disposable } ) ;
55
+ }
56
+
57
+ removeOverlay ( target : HTMLElement ) : void {
58
+ if ( this . currentActiveTarget === target ) {
59
+ this . currentActiveTarget = undefined ;
60
+ }
61
+
62
+ const existingOverlay = this . overlays . get ( target ) ;
63
+ if ( existingOverlay ) {
64
+ existingOverlay . overlay . remove ( ) ;
65
+ existingOverlay . disposable . dispose ( ) ;
66
+ this . overlays . delete ( target ) ;
67
+ }
68
+ }
69
+
70
+ private currentActiveTarget : HTMLElement | undefined = undefined ;
71
+ private createOverlay ( target : HTMLElement , overlayContainer : HTMLElement ) : { overlay : HTMLElement ; disposable : IDisposable } {
72
+ const overlay = document . createElement ( 'div' ) ;
73
+ overlay . classList . add ( 'chat-dnd-overlay' ) ;
74
+ this . updateOverlayStyles ( overlay ) ;
75
+ overlayContainer . appendChild ( overlay ) ;
76
+
77
+ const disposable = new DragAndDropObserver ( target , {
58
78
onDragOver : ( e ) => {
59
79
e . stopPropagation ( ) ;
80
+ e . preventDefault ( ) ;
81
+
82
+ if ( target === this . currentActiveTarget ) {
83
+ return ;
84
+ }
85
+
86
+ if ( this . currentActiveTarget ) {
87
+ this . setOverlay ( this . currentActiveTarget , undefined ) ;
88
+ }
89
+
90
+ this . currentActiveTarget = target ;
91
+
92
+ this . onDragEnter ( e , target ) ;
93
+
60
94
} ,
61
95
onDragLeave : ( e ) => {
62
- this . onDragLeave ( e ) ;
63
- mouseInside = false ;
96
+ if ( target === this . currentActiveTarget ) {
97
+ this . currentActiveTarget = undefined ;
98
+ }
99
+
100
+ this . onDragLeave ( e , target ) ;
64
101
} ,
65
102
onDrop : ( e ) => {
66
- this . onDrop ( e ) ;
67
- mouseInside = false ;
68
- } ,
69
- } ) ) ;
103
+ e . stopPropagation ( ) ;
104
+ e . preventDefault ( ) ;
70
105
71
- this . overlay = document . createElement ( 'div' ) ;
72
- this . overlay . classList . add ( 'chat-dnd-overlay' ) ;
73
- this . contianer . appendChild ( this . overlay ) ;
106
+ if ( target !== this . currentActiveTarget ) {
107
+ return ;
108
+ }
74
109
75
- this . updateStyles ( ) ;
110
+ this . currentActiveTarget = undefined ;
111
+ this . onDrop ( e , target ) ;
112
+ } ,
113
+ } ) ;
114
+
115
+ return { overlay, disposable } ;
76
116
}
77
117
78
- private onDragEnter ( e : DragEvent ) : void {
118
+ private onDragEnter ( e : DragEvent , target : HTMLElement ) : void {
79
119
const estimatedDropType = this . guessDropType ( e ) ;
80
- if ( estimatedDropType !== undefined ) {
81
- e . stopPropagation ( ) ;
82
- e . preventDefault ( ) ;
83
- }
84
- this . updateDropFeedback ( e , estimatedDropType ) ;
120
+ this . updateDropFeedback ( e , target , estimatedDropType ) ;
85
121
}
86
122
87
- private onDragLeave ( e : DragEvent ) : void {
88
- this . updateDropFeedback ( e , undefined ) ;
123
+ private onDragLeave ( e : DragEvent , target : HTMLElement ) : void {
124
+ this . updateDropFeedback ( e , target , undefined ) ;
89
125
}
90
126
91
- private onDrop ( e : DragEvent ) : void {
92
- this . updateDropFeedback ( e , undefined ) ;
127
+ private onDrop ( e : DragEvent , target : HTMLElement ) : void {
128
+ this . updateDropFeedback ( e , target , undefined ) ;
93
129
this . drop ( e ) ;
94
130
}
95
131
@@ -99,19 +135,20 @@ export class ChatDragAndDrop extends Themable {
99
135
return ;
100
136
}
101
137
102
- e . stopPropagation ( ) ;
103
- e . preventDefault ( ) ;
138
+ this . handleDrop ( contexts ) ;
139
+ }
104
140
105
- this . inputPart . attachmentModel . addContext ( ...contexts ) ;
141
+ protected handleDrop ( contexts : IChatRequestVariableEntry [ ] ) : void {
142
+ this . attachmentModel . addContext ( ...contexts ) ;
106
143
}
107
144
108
- private updateDropFeedback ( e : DragEvent , dropType : ChatDragAndDropType | undefined ) : void {
145
+ private updateDropFeedback ( e : DragEvent , target : HTMLElement , dropType : ChatDragAndDropType | undefined ) : void {
109
146
const showOverlay = dropType !== undefined ;
110
147
if ( e . dataTransfer ) {
111
148
e . dataTransfer . dropEffect = showOverlay ? 'copy' : 'none' ;
112
149
}
113
150
114
- this . setOverlay ( dropType ) ;
151
+ this . setOverlay ( target , dropType ) ;
115
152
}
116
153
117
154
private guessDropType ( e : DragEvent ) : ChatDragAndDropType | undefined {
@@ -135,7 +172,7 @@ export class ChatDragAndDrop extends Themable {
135
172
return dropType !== undefined ;
136
173
}
137
174
138
- private getDropTypeName ( type : ChatDragAndDropType ) : string {
175
+ protected getDropTypeName ( type : ChatDragAndDropType ) : string {
139
176
switch ( type ) {
140
177
case ChatDragAndDropType . FILE_INTERNAL : return localize ( 'file' , 'File' ) ;
141
178
case ChatDragAndDropType . FILE_EXTERNAL : return localize ( 'file' , 'File' ) ;
@@ -208,16 +245,16 @@ export class ChatDragAndDrop extends Themable {
208
245
return getResourceAttachContext ( editor . resource , stat . isDirectory ) ;
209
246
}
210
247
211
- private setOverlay ( type : ChatDragAndDropType | undefined ) : void {
248
+ private setOverlay ( target : HTMLElement , type : ChatDragAndDropType | undefined ) : void {
212
249
// Remove any previous overlay text
213
250
this . overlayText ?. remove ( ) ;
214
251
this . overlayText = undefined ;
215
252
253
+ const { overlay } = this . overlays . get ( target ) ! ;
216
254
if ( type !== undefined ) {
217
255
// Render the overlay text
218
- const typeName = this . getDropTypeName ( type ) ;
219
256
220
- const iconAndtextElements = renderLabelWithIcons ( `$(${ Codicon . attach . id } ) ${ localize ( 'attach as context' , 'Attach {0} as Context' , typeName ) } ` ) ;
257
+ const iconAndtextElements = renderLabelWithIcons ( `$(${ Codicon . attach . id } ) ${ this . getOverlayText ( type ) } ` ) ;
221
258
const htmlElements = iconAndtextElements . map ( element => {
222
259
if ( typeof element === 'string' ) {
223
260
return $ ( 'span.overlay-text' , undefined , element ) ;
@@ -227,19 +264,99 @@ export class ChatDragAndDrop extends Themable {
227
264
228
265
this . overlayText = $ ( 'span.attach-context-overlay-text' , undefined , ...htmlElements ) ;
229
266
this . overlayText . style . backgroundColor = this . overlayTextBackground ;
230
- this . overlay . appendChild ( this . overlayText ) ;
267
+ overlay . appendChild ( this . overlayText ) ;
231
268
}
232
269
233
- this . overlay . classList . toggle ( 'visible' , type !== undefined ) ;
270
+ overlay . classList . toggle ( 'visible' , type !== undefined ) ;
271
+ }
272
+
273
+ protected getOverlayText ( type : ChatDragAndDropType ) : string {
274
+ const typeName = this . getDropTypeName ( type ) ;
275
+ return localize ( 'attacAsContext' , 'Attach {0} as Context' , typeName ) ;
276
+ }
277
+
278
+ private updateOverlayStyles ( overlay : HTMLElement ) : void {
279
+ overlay . style . backgroundColor = this . getColor ( this . styles . overlayBackground ) || '' ;
280
+ overlay . style . color = this . getColor ( this . styles . listForeground ) || '' ;
234
281
}
235
282
236
283
override updateStyles ( ) : void {
237
- this . overlay . style . backgroundColor = this . getColor ( this . styles . overlayBackground ) || '' ;
238
- this . overlay . style . color = this . getColor ( this . styles . listForeground ) || '' ;
284
+ this . overlays . forEach ( overlay => this . updateOverlayStyles ( overlay . overlay ) ) ;
239
285
this . overlayTextBackground = this . getColor ( this . styles . listBackground ) || '' ;
240
286
}
241
287
}
242
288
289
+ export class EditsDragAndDrop extends ChatDragAndDrop {
290
+
291
+ constructor (
292
+ attachmentModel : ChatAttachmentModel ,
293
+ styles : IChatInputStyles ,
294
+ @IThemeService themeService : IThemeService ,
295
+ @IExtensionService extensionService : IExtensionService ,
296
+ @IFileService fileService : IFileService
297
+ ) {
298
+ super ( attachmentModel , styles , themeService , extensionService , fileService ) ;
299
+ }
300
+
301
+ protected override handleDrop ( context : IChatRequestVariableEntry [ ] ) : void {
302
+ this . handleDropAsync ( context ) ;
303
+ }
304
+
305
+ protected async handleDropAsync ( context : IChatRequestVariableEntry [ ] ) : Promise < void > {
306
+ const nonDirectoryContext = context . filter ( context => ! context . isDirectory ) ;
307
+ const directories = context
308
+ . filter ( context => context . isDirectory )
309
+ . map ( context => context . value )
310
+ . filter ( value => ! ! value && URI . isUri ( value ) ) ;
311
+
312
+ // If there are directories, we need to resolve the files and add them to the working set
313
+ for ( const directory of directories ) {
314
+ const fileSystemProvider = this . fileService . getProvider ( directory . scheme ) ;
315
+ if ( ! fileSystemProvider ) {
316
+ continue ;
317
+ }
318
+
319
+ const resolvedFiles = await resolveFilesInDirectory ( directory , fileSystemProvider , false ) ;
320
+ const resolvedFileContext = resolvedFiles . map ( file => getResourceAttachContext ( file , false ) ) . filter ( context => ! ! context ) ;
321
+ nonDirectoryContext . push ( ...resolvedFileContext ) ;
322
+ }
323
+
324
+ super . handleDrop ( nonDirectoryContext ) ;
325
+ }
326
+
327
+ protected override getOverlayText ( type : ChatDragAndDropType ) : string {
328
+ const typeName = this . getDropTypeName ( type ) ;
329
+ switch ( type ) {
330
+ case ChatDragAndDropType . FILE_INTERNAL :
331
+ case ChatDragAndDropType . FILE_EXTERNAL :
332
+ return localize ( 'addToWorkingSet' , 'Add {0} to Working Set' , typeName ) ;
333
+ case ChatDragAndDropType . FOLDER :
334
+ return localize ( 'addToWorkingSet' , 'Add {0} to Working Set' , localize ( 'files' , 'Files' ) ) ;
335
+ default :
336
+ return super . getOverlayText ( type ) ;
337
+ }
338
+ }
339
+ }
340
+
341
+ async function resolveFilesInDirectory ( resource : URI , fileSystemProvider : IFileSystemProvider , shouldRecurse : boolean ) : Promise < URI [ ] > {
342
+ const entries = await fileSystemProvider . readdir ( resource ) ;
343
+
344
+ const files : URI [ ] = [ ] ;
345
+ const folders : URI [ ] = [ ] ;
346
+
347
+ for ( const [ name , type ] of entries ) {
348
+ const entryResource = joinPath ( resource , name ) ;
349
+ if ( type === FileType . File ) {
350
+ files . push ( entryResource ) ;
351
+ } else if ( type === FileType . Directory && shouldRecurse ) {
352
+ folders . push ( entryResource ) ;
353
+ }
354
+ }
355
+
356
+ const subFiles = await Promise . all ( folders . map ( folder => resolveFilesInDirectory ( folder , fileSystemProvider , shouldRecurse ) ) ) ;
357
+
358
+ return [ ...files , ...subFiles . flat ( ) ] ;
359
+ }
243
360
244
361
function getResourceAttachContext ( resource : URI , isDirectory : boolean ) : IChatRequestVariableEntry | undefined {
245
362
return {
0 commit comments