Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts
3296 views
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
import * as dom from '../../../../base/browser/dom.js';
7
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
8
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
9
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
10
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
13
import { IRange } from '../../../../editor/common/core/range.js';
14
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
15
import { Location, SymbolKinds } from '../../../../editor/common/languages.js';
16
import { ILanguageService } from '../../../../editor/common/languages/language.js';
17
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
18
import { IModelService } from '../../../../editor/common/services/model.js';
19
import { DefinitionAction } from '../../../../editor/contrib/gotoSymbol/browser/goToCommands.js';
20
import * as nls from '../../../../nls.js';
21
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
22
import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
23
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
24
import { ICommandService } from '../../../../platform/commands/common/commands.js';
25
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
26
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
27
import { IResourceStat } from '../../../../platform/dnd/browser/dnd.js';
28
import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js';
29
import { FileKind, IFileService } from '../../../../platform/files/common/files.js';
30
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
31
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
32
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
33
import { ILabelService } from '../../../../platform/label/common/label.js';
34
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
35
import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';
36
import { fillEditorsDragData } from '../../../browser/dnd.js';
37
import { ResourceContextKey } from '../../../common/contextkeys.js';
38
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
39
import { INotebookDocumentService } from '../../../services/notebook/common/notebookDocumentService.js';
40
import { ExplorerFolderContext } from '../../files/common/files.js';
41
import { IWorkspaceSymbol } from '../../search/common/search.js';
42
import { IChatContentInlineReference } from '../common/chatService.js';
43
import { IChatWidgetService } from './chat.js';
44
import { chatAttachmentResourceContextKey, hookUpSymbolAttachmentDragAndContextMenu } from './chatAttachmentWidgets.js';
45
import { IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js';
46
47
type ContentRefData =
48
| { readonly kind: 'symbol'; readonly symbol: IWorkspaceSymbol }
49
| {
50
readonly kind?: undefined;
51
readonly uri: URI;
52
readonly range?: IRange;
53
};
54
55
export function renderFileWidgets(element: HTMLElement, instantiationService: IInstantiationService, chatMarkdownAnchorService: IChatMarkdownAnchorService, disposables: DisposableStore) {
56
const links = element.querySelectorAll('a');
57
links.forEach(a => {
58
// Empty link text -> render file widget
59
if (!a.textContent?.trim()) {
60
const href = a.getAttribute('data-href');
61
const uri = href ? URI.parse(href) : undefined;
62
if (uri?.scheme) {
63
const widget = instantiationService.createInstance(InlineAnchorWidget, a, { kind: 'inlineReference', inlineReference: uri });
64
disposables.add(chatMarkdownAnchorService.register(widget));
65
disposables.add(widget);
66
}
67
}
68
});
69
}
70
71
export class InlineAnchorWidget extends Disposable {
72
73
public static readonly className = 'chat-inline-anchor-widget';
74
75
private readonly _chatResourceContext: IContextKey<string>;
76
77
readonly data: ContentRefData;
78
79
private _isDisposed = false;
80
81
constructor(
82
private readonly element: HTMLAnchorElement | HTMLElement,
83
public readonly inlineReference: IChatContentInlineReference,
84
@IContextKeyService originalContextKeyService: IContextKeyService,
85
@IContextMenuService contextMenuService: IContextMenuService,
86
@IFileService fileService: IFileService,
87
@IHoverService hoverService: IHoverService,
88
@IInstantiationService instantiationService: IInstantiationService,
89
@ILabelService labelService: ILabelService,
90
@ILanguageService languageService: ILanguageService,
91
@IMenuService menuService: IMenuService,
92
@IModelService modelService: IModelService,
93
@ITelemetryService telemetryService: ITelemetryService,
94
@IThemeService themeService: IThemeService,
95
@INotebookDocumentService private readonly notebookDocumentService: INotebookDocumentService,
96
) {
97
super();
98
99
// TODO: Make sure we handle updates from an inlineReference being `resolved` late
100
101
this.data = 'uri' in inlineReference.inlineReference
102
? inlineReference.inlineReference
103
: 'name' in inlineReference.inlineReference
104
? { kind: 'symbol', symbol: inlineReference.inlineReference }
105
: { uri: inlineReference.inlineReference };
106
107
const contextKeyService = this._register(originalContextKeyService.createScoped(element));
108
this._chatResourceContext = chatAttachmentResourceContextKey.bindTo(contextKeyService);
109
110
element.classList.add(InlineAnchorWidget.className, 'show-file-icons');
111
112
let iconText: string;
113
let iconClasses: string[];
114
115
let location: { readonly uri: URI; readonly range?: IRange };
116
117
let updateContextKeys: (() => Promise<void>) | undefined;
118
if (this.data.kind === 'symbol') {
119
const symbol = this.data.symbol;
120
121
location = this.data.symbol.location;
122
iconText = this.data.symbol.name;
123
iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(symbol.kind))];
124
125
this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, contextKeyService, { value: symbol.location, name: symbol.name, kind: symbol.kind }, MenuId.ChatInlineSymbolAnchorContext)));
126
} else {
127
location = this.data;
128
129
const label = labelService.getUriBasenameLabel(location.uri);
130
iconText = location.range && this.data.kind !== 'symbol' ?
131
`${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` :
132
location.uri.scheme === 'vscode-notebook-cell' && this.data.kind !== 'symbol' ?
133
`${label} • cell${this.getCellIndex(location.uri)}` :
134
label;
135
136
let fileKind = location.uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;
137
const recomputeIconClasses = () => getIconClasses(modelService, languageService, location.uri, fileKind, fileKind === FileKind.FOLDER && !themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined);
138
139
iconClasses = recomputeIconClasses();
140
141
const refreshIconClasses = () => {
142
iconEl.classList.remove(...iconClasses);
143
iconClasses = recomputeIconClasses();
144
iconEl.classList.add(...iconClasses);
145
};
146
147
this._register(themeService.onDidFileIconThemeChange(() => {
148
refreshIconClasses();
149
}));
150
151
const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService);
152
fileService.stat(location.uri)
153
.then(stat => {
154
isFolderContext.set(stat.isDirectory);
155
if (stat.isDirectory) {
156
fileKind = FileKind.FOLDER;
157
refreshIconClasses();
158
}
159
})
160
.catch(() => { });
161
162
// Context menu
163
this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, async domEvent => {
164
const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
165
dom.EventHelper.stop(domEvent, true);
166
167
try {
168
await updateContextKeys?.();
169
} catch (e) {
170
console.error(e);
171
}
172
173
if (this._isDisposed) {
174
return;
175
}
176
177
contextMenuService.showContextMenu({
178
contextKeyService,
179
getAnchor: () => event,
180
getActions: () => {
181
const menu = menuService.getMenuActions(MenuId.ChatInlineResourceAnchorContext, contextKeyService, { arg: location.uri });
182
return getFlatContextMenuActions(menu);
183
},
184
});
185
}));
186
}
187
188
const resourceContextKey = this._register(new ResourceContextKey(contextKeyService, fileService, languageService, modelService));
189
resourceContextKey.set(location.uri);
190
this._chatResourceContext.set(location.uri.toString());
191
192
const iconEl = dom.$('span.icon');
193
iconEl.classList.add(...iconClasses);
194
element.replaceChildren(iconEl, dom.$('span.icon-label', {}, iconText));
195
196
const fragment = location.range ? `${location.range.startLineNumber},${location.range.startColumn}` : '';
197
element.setAttribute('data-href', (fragment ? location.uri.with({ fragment }) : location.uri).toString());
198
199
// Hover
200
const relativeLabel = labelService.getUriLabel(location.uri, { relative: true });
201
this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel));
202
203
// Drag and drop
204
if (this.data.kind !== 'symbol') {
205
element.draggable = true;
206
this._register(dom.addDisposableListener(element, 'dragstart', e => {
207
const stat: IResourceStat = {
208
resource: location.uri,
209
selection: location.range,
210
};
211
instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [stat], e));
212
213
214
e.dataTransfer?.setDragImage(element, 0, 0);
215
}));
216
}
217
}
218
219
override dispose(): void {
220
this._isDisposed = true;
221
super.dispose();
222
}
223
224
getHTMLElement(): HTMLElement {
225
return this.element;
226
}
227
228
private getCellIndex(location: URI) {
229
const notebook = this.notebookDocumentService.getNotebook(location);
230
const index = notebook?.getCellIndex(location) ?? -1;
231
return index >= 0 ? ` ${index + 1}` : '';
232
}
233
}
234
235
//#region Resource context menu
236
237
registerAction2(class AddFileToChatAction extends Action2 {
238
239
static readonly id = 'chat.inlineResourceAnchor.addFileToChat';
240
241
constructor() {
242
super({
243
id: AddFileToChatAction.id,
244
title: nls.localize2('actions.attach.label', "Add File to Chat"),
245
menu: [{
246
id: MenuId.ChatInlineResourceAnchorContext,
247
group: 'chat',
248
order: 1,
249
when: ExplorerFolderContext.negate(),
250
}]
251
});
252
}
253
254
override async run(accessor: ServicesAccessor, resource: URI): Promise<void> {
255
const chatWidgetService = accessor.get(IChatWidgetService);
256
257
const widget = chatWidgetService.lastFocusedWidget;
258
if (widget) {
259
widget.attachmentModel.addFile(resource);
260
261
}
262
}
263
});
264
265
//#endregion
266
267
//#region Resource keybindings
268
269
registerAction2(class CopyResourceAction extends Action2 {
270
271
static readonly id = 'chat.inlineResourceAnchor.copyResource';
272
273
constructor() {
274
super({
275
id: CopyResourceAction.id,
276
title: nls.localize2('actions.copy.label', "Copy"),
277
f1: false,
278
precondition: chatAttachmentResourceContextKey,
279
keybinding: {
280
weight: KeybindingWeight.WorkbenchContrib,
281
primary: KeyMod.CtrlCmd | KeyCode.KeyC,
282
}
283
});
284
}
285
286
override async run(accessor: ServicesAccessor): Promise<void> {
287
const chatWidgetService = accessor.get(IChatMarkdownAnchorService);
288
const clipboardService = accessor.get(IClipboardService);
289
290
const anchor = chatWidgetService.lastFocusedAnchor;
291
if (!anchor) {
292
return;
293
}
294
295
// TODO: we should also write out the standard mime types so that external programs can use them
296
// like how `fillEditorsDragData` works but without having an event to work with.
297
const resource = anchor.data.kind === 'symbol' ? anchor.data.symbol.location.uri : anchor.data.uri;
298
clipboardService.writeResources([resource]);
299
}
300
});
301
302
registerAction2(class OpenToSideResourceAction extends Action2 {
303
304
static readonly id = 'chat.inlineResourceAnchor.openToSide';
305
306
constructor() {
307
super({
308
id: OpenToSideResourceAction.id,
309
title: nls.localize2('actions.openToSide.label', "Open to the Side"),
310
f1: false,
311
precondition: chatAttachmentResourceContextKey,
312
keybinding: {
313
weight: KeybindingWeight.ExternalExtension + 2,
314
primary: KeyMod.CtrlCmd | KeyCode.Enter,
315
mac: {
316
primary: KeyMod.WinCtrl | KeyCode.Enter
317
},
318
},
319
menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
320
id: id,
321
group: 'navigation',
322
order: 1
323
}))
324
});
325
}
326
327
override async run(accessor: ServicesAccessor, arg?: Location | URI): Promise<void> {
328
const editorService = accessor.get(IEditorService);
329
330
const target = this.getTarget(accessor, arg);
331
if (!target) {
332
return;
333
}
334
335
const input: ITextResourceEditorInput = URI.isUri(target)
336
? { resource: target }
337
: {
338
resource: target.uri, options: {
339
selection: {
340
startColumn: target.range.startColumn,
341
startLineNumber: target.range.startLineNumber,
342
}
343
}
344
};
345
346
await editorService.openEditors([input], SIDE_GROUP);
347
}
348
349
private getTarget(accessor: ServicesAccessor, arg: URI | Location | undefined): Location | URI | undefined {
350
const chatWidgetService = accessor.get(IChatMarkdownAnchorService);
351
352
if (arg) {
353
return arg;
354
}
355
356
const anchor = chatWidgetService.lastFocusedAnchor;
357
if (!anchor) {
358
return undefined;
359
}
360
361
return anchor.data.kind === 'symbol' ? anchor.data.symbol.location : anchor.data.uri;
362
}
363
});
364
365
//#endregion
366
367
//#region Symbol context menu
368
369
registerAction2(class GoToDefinitionAction extends Action2 {
370
371
static readonly id = 'chat.inlineSymbolAnchor.goToDefinition';
372
373
constructor() {
374
super({
375
id: GoToDefinitionAction.id,
376
title: {
377
...nls.localize2('actions.goToDecl.label', "Go to Definition"),
378
mnemonicTitle: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition"),
379
},
380
menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
381
id,
382
group: '4_symbol_nav',
383
order: 1.1,
384
when: EditorContextKeys.hasDefinitionProvider,
385
}))
386
});
387
}
388
389
override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
390
const editorService = accessor.get(ICodeEditorService);
391
const instantiationService = accessor.get(IInstantiationService);
392
393
await openEditorWithSelection(editorService, location);
394
395
const action = new DefinitionAction({ openToSide: false, openInPeek: false, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined });
396
return instantiationService.invokeFunction(accessor => action.run(accessor));
397
}
398
});
399
400
async function openEditorWithSelection(editorService: ICodeEditorService, location: Location) {
401
await editorService.openCodeEditor({
402
resource: location.uri, options: {
403
selection: {
404
startColumn: location.range.startColumn,
405
startLineNumber: location.range.startLineNumber,
406
}
407
}
408
}, null);
409
}
410
411
async function runGoToCommand(accessor: ServicesAccessor, command: string, location: Location) {
412
const editorService = accessor.get(ICodeEditorService);
413
const commandService = accessor.get(ICommandService);
414
415
await openEditorWithSelection(editorService, location);
416
417
return commandService.executeCommand(command);
418
}
419
420
registerAction2(class GoToTypeDefinitionsAction extends Action2 {
421
422
static readonly id = 'chat.inlineSymbolAnchor.goToTypeDefinitions';
423
424
constructor() {
425
super({
426
id: GoToTypeDefinitionsAction.id,
427
title: {
428
...nls.localize2('goToTypeDefinitions.label', "Go to Type Definitions"),
429
mnemonicTitle: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definitions"),
430
},
431
menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
432
id,
433
group: '4_symbol_nav',
434
order: 1.1,
435
when: EditorContextKeys.hasTypeDefinitionProvider,
436
})),
437
});
438
}
439
440
override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
441
return runGoToCommand(accessor, 'editor.action.goToTypeDefinition', location);
442
}
443
});
444
445
registerAction2(class GoToImplementations extends Action2 {
446
447
static readonly id = 'chat.inlineSymbolAnchor.goToImplementations';
448
449
constructor() {
450
super({
451
id: GoToImplementations.id,
452
title: {
453
...nls.localize2('goToImplementations.label', "Go to Implementations"),
454
mnemonicTitle: nls.localize({ key: 'miGotoImplementations', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementations"),
455
},
456
menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
457
id,
458
group: '4_symbol_nav',
459
order: 1.2,
460
when: EditorContextKeys.hasImplementationProvider,
461
})),
462
});
463
}
464
465
override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
466
return runGoToCommand(accessor, 'editor.action.goToImplementation', location);
467
}
468
});
469
470
registerAction2(class GoToReferencesAction extends Action2 {
471
472
static readonly id = 'chat.inlineSymbolAnchor.goToReferences';
473
474
constructor() {
475
super({
476
id: GoToReferencesAction.id,
477
title: {
478
...nls.localize2('goToReferences.label', "Go to References"),
479
mnemonicTitle: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References"),
480
},
481
menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
482
id,
483
group: '4_symbol_nav',
484
order: 1.3,
485
when: EditorContextKeys.hasReferenceProvider,
486
})),
487
});
488
}
489
490
override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
491
return runGoToCommand(accessor, 'editor.action.goToReferences', location);
492
}
493
});
494
495
//#endregion
496
497