Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugChatIntegration.ts
5241 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 { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../base/common/codicons.js';
8
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
9
import { autorun, debouncedObservable, derived, IObservable, ISettableObservable, ObservablePromise, observableValue } from '../../../../base/common/observable.js';
10
import { basename } from '../../../../base/common/resources.js';
11
import { ThemeIcon } from '../../../../base/common/themables.js';
12
import { Range } from '../../../../editor/common/core/range.js';
13
import { localize } from '../../../../nls.js';
14
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
15
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
16
import { IWorkbenchContribution } from '../../../common/contributions.js';
17
import { IChatWidget, IChatWidgetService } from '../../chat/browser/chat.js';
18
import { ChatContextPick, IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from '../../chat/browser/attachments/chatContextPickService.js';
19
import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
20
import { IChatRequestFileEntry, IChatRequestVariableEntry, IDebugVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js';
21
import { IDebugService, IExpression, IScope, IStackFrame, State } from '../common/debug.js';
22
import { Variable } from '../common/debugModel.js';
23
24
const enum PickerMode {
25
Main = 'main',
26
Expression = 'expression',
27
}
28
29
class DebugSessionContextPick implements IChatContextPickerItem {
30
readonly type = 'pickerPick';
31
readonly label = localize('chatContext.debugSession', 'Debug Session...');
32
readonly icon = Codicon.debug;
33
readonly ordinal = -200;
34
35
constructor(
36
@IDebugService private readonly debugService: IDebugService,
37
) { }
38
39
isEnabled(): boolean {
40
// Only enabled when there's a focused session that is stopped (paused)
41
const viewModel = this.debugService.getViewModel();
42
const focusedSession = viewModel.focusedSession;
43
return !!focusedSession && focusedSession.state === State.Stopped;
44
}
45
46
asPicker(_widget: IChatWidget): IChatContextPicker {
47
const store = new DisposableStore();
48
const mode: ISettableObservable<PickerMode> = observableValue('debugPicker.mode', PickerMode.Main);
49
const query: ISettableObservable<string> = observableValue('debugPicker.query', '');
50
51
const picksObservable = this.createPicksObservable(mode, query, store);
52
53
return {
54
placeholder: localize('selectDebugData', 'Select debug data to attach'),
55
picks: (_queryObs: IObservable<string>, token: CancellationToken) => {
56
// Connect the external query observable to our internal one
57
store.add(autorun(reader => {
58
query.set(_queryObs.read(reader), undefined);
59
}));
60
61
const cts = new CancellationTokenSource(token);
62
store.add(toDisposable(() => cts.dispose(true)));
63
64
return picksObservable;
65
},
66
goBack: () => {
67
if (mode.get() === PickerMode.Expression) {
68
mode.set(PickerMode.Main, undefined);
69
return true; // Stay in picker
70
}
71
return false; // Go back to main context menu
72
},
73
dispose: () => store.dispose(),
74
};
75
}
76
77
private createPicksObservable(
78
mode: ISettableObservable<PickerMode>,
79
query: IObservable<string>,
80
store: DisposableStore
81
): IObservable<{ busy: boolean; picks: ChatContextPick[] }> {
82
const debouncedQuery = debouncedObservable(query, 300);
83
84
return derived(reader => {
85
const currentMode = mode.read(reader);
86
87
if (currentMode === PickerMode.Expression) {
88
return this.getExpressionPicks(debouncedQuery, store);
89
} else {
90
return this.getMainPicks(mode);
91
}
92
}).flatten();
93
}
94
95
private getMainPicks(mode: ISettableObservable<PickerMode>): IObservable<{ busy: boolean; picks: ChatContextPick[] }> {
96
// Return an observable that resolves to the main picks
97
const promise = derived(_reader => {
98
return new ObservablePromise(this.buildMainPicks(mode));
99
});
100
101
return promise.map((value, reader) => {
102
const result = value.promiseResult.read(reader);
103
return { picks: result?.data || [], busy: result === undefined };
104
});
105
}
106
107
private async buildMainPicks(mode: ISettableObservable<PickerMode>): Promise<ChatContextPick[]> {
108
const picks: ChatContextPick[] = [];
109
const viewModel = this.debugService.getViewModel();
110
const stackFrame = viewModel.focusedStackFrame;
111
const session = viewModel.focusedSession;
112
113
if (!session || !stackFrame) {
114
return picks;
115
}
116
117
// Add "Expression Value..." option at the top
118
picks.push({
119
label: localize('expressionValue', 'Expression Value...'),
120
iconClass: ThemeIcon.asClassName(Codicon.symbolVariable),
121
asAttachment: () => {
122
// Switch to expression mode
123
mode.set(PickerMode.Expression, undefined);
124
return 'noop';
125
},
126
});
127
128
// Add watch expressions section
129
const watches = this.debugService.getModel().getWatchExpressions();
130
if (watches.length > 0) {
131
picks.push({ type: 'separator', label: localize('watchExpressions', 'Watch Expressions') });
132
for (const watch of watches) {
133
picks.push({
134
label: watch.name,
135
description: watch.value,
136
iconClass: ThemeIcon.asClassName(Codicon.eye),
137
asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createDebugVariableEntry(watch)),
138
});
139
}
140
}
141
142
// Add scopes and their variables
143
let scopes: IScope[] = [];
144
try {
145
scopes = await stackFrame.getScopes();
146
} catch {
147
// Ignore errors when fetching scopes
148
}
149
150
for (const scope of scopes) {
151
// Include variables from non-expensive scopes
152
if (scope.expensive && !scope.childrenHaveBeenLoaded) {
153
continue;
154
}
155
156
picks.push({ type: 'separator', label: scope.name });
157
try {
158
const variables = await scope.getChildren();
159
if (variables.length > 1) {
160
picks.push({
161
label: localize('allVariablesInScope', 'All variables in {0}', scope.name),
162
iconClass: ThemeIcon.asClassName(Codicon.symbolNamespace),
163
asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createScopeEntry(scope, variables)),
164
});
165
}
166
for (const variable of variables) {
167
picks.push({
168
label: variable.name,
169
description: formatVariableDescription(variable),
170
iconClass: ThemeIcon.asClassName(Codicon.symbolVariable),
171
asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createDebugVariableEntry(variable)),
172
});
173
}
174
} catch {
175
// Ignore errors when fetching variables
176
}
177
}
178
179
return picks;
180
}
181
182
private getExpressionPicks(
183
query: IObservable<string>,
184
_store: DisposableStore
185
): IObservable<{ busy: boolean; picks: ChatContextPick[] }> {
186
const promise = derived((reader) => {
187
const queryValue = query.read(reader);
188
const cts = new CancellationTokenSource();
189
reader.store.add(toDisposable(() => cts.dispose(true)));
190
return new ObservablePromise(this.evaluateExpression(queryValue, cts.token));
191
});
192
193
return promise.map((value, r) => {
194
const result = value.promiseResult.read(r);
195
return { picks: result?.data || [], busy: result === undefined };
196
});
197
}
198
199
private async evaluateExpression(expression: string, token: CancellationToken): Promise<ChatContextPick[]> {
200
if (!expression.trim()) {
201
return [{
202
label: localize('typeExpression', 'Type an expression to evaluate...'),
203
disabled: true,
204
asAttachment: () => 'noop',
205
}];
206
}
207
208
const viewModel = this.debugService.getViewModel();
209
const session = viewModel.focusedSession;
210
const stackFrame = viewModel.focusedStackFrame;
211
212
if (!session || !stackFrame) {
213
return [{
214
label: localize('noDebugSession', 'No active debug session'),
215
disabled: true,
216
asAttachment: () => 'noop',
217
}];
218
}
219
220
try {
221
const response = await session.evaluate(expression, stackFrame.frameId, 'watch');
222
223
if (token.isCancellationRequested) {
224
return [];
225
}
226
227
if (response?.body) {
228
const resultValue = response.body.result;
229
const resultType = response.body.type;
230
return [{
231
label: expression,
232
description: formatExpressionResult(resultValue, resultType),
233
iconClass: ThemeIcon.asClassName(Codicon.symbolVariable),
234
asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, {
235
kind: 'debugVariable',
236
id: `debug-expression:${expression}`,
237
name: expression,
238
fullName: expression,
239
icon: Codicon.debug,
240
value: resultValue,
241
expression: expression,
242
type: resultType,
243
modelDescription: formatModelDescription(expression, resultValue, resultType),
244
}),
245
}];
246
} else {
247
return [{
248
label: expression,
249
description: localize('noResult', 'No result'),
250
disabled: true,
251
asAttachment: () => 'noop',
252
}];
253
}
254
} catch (err) {
255
return [{
256
label: expression,
257
description: err instanceof Error ? err.message : localize('evaluationError', 'Evaluation error'),
258
disabled: true,
259
asAttachment: () => 'noop',
260
}];
261
}
262
}
263
}
264
265
function createDebugVariableEntry(expression: IExpression): IDebugVariableEntry {
266
return {
267
kind: 'debugVariable',
268
id: `debug-variable:${expression.getId()}`,
269
name: expression.name,
270
fullName: expression.name,
271
icon: Codicon.debug,
272
value: expression.value,
273
expression: expression.name,
274
type: expression.type,
275
modelDescription: formatModelDescription(expression.name, expression.value, expression.type),
276
};
277
}
278
279
function createPausedLocationEntry(stackFrame: IStackFrame): IChatRequestFileEntry {
280
const uri = stackFrame.source.uri;
281
let range = Range.lift(stackFrame.range);
282
if (range.isEmpty()) {
283
range = range.setEndPosition(range.startLineNumber + 1, 1);
284
}
285
286
return {
287
kind: 'file',
288
value: { uri, range },
289
id: `debug-paused-location:${uri.toString()}:${range.startLineNumber}`,
290
name: basename(uri),
291
modelDescription: 'The debugger is currently paused at this location',
292
};
293
}
294
295
function createDebugAttachments(stackFrame: IStackFrame, variableEntry: IDebugVariableEntry): IChatRequestVariableEntry[] {
296
return [
297
createPausedLocationEntry(stackFrame),
298
variableEntry,
299
];
300
}
301
302
function createScopeEntry(scope: IScope, variables: IExpression[]): IDebugVariableEntry {
303
const variablesSummary = variables.map(v => `${v.name}: ${v.value}`).join('\n');
304
return {
305
kind: 'debugVariable',
306
id: `debug-scope:${scope.name}`,
307
name: `Scope: ${scope.name}`,
308
fullName: `Scope: ${scope.name}`,
309
icon: Codicon.debug,
310
value: variablesSummary,
311
expression: scope.name,
312
type: 'scope',
313
modelDescription: `Debug scope "${scope.name}" with ${variables.length} variables:\n${variablesSummary}`,
314
};
315
}
316
317
function formatVariableDescription(expression: IExpression): string {
318
const value = expression.value;
319
const type = expression.type;
320
if (type && value) {
321
return `${type}: ${value}`;
322
}
323
return value || type || '';
324
}
325
326
function formatExpressionResult(value: string, type?: string): string {
327
if (type && value) {
328
return `${type}: ${value}`;
329
}
330
return value || type || '';
331
}
332
333
function formatModelDescription(name: string, value: string, type?: string): string {
334
let description = `Debug variable "${name}"`;
335
if (type) {
336
description += ` of type ${type}`;
337
}
338
description += ` with value: ${value}`;
339
return description;
340
}
341
342
export class DebugChatContextContribution extends Disposable implements IWorkbenchContribution {
343
static readonly ID = 'workbench.contrib.chat.debugChatContextContribution';
344
345
constructor(
346
@IChatContextPickService contextPickService: IChatContextPickService,
347
@IInstantiationService instantiationService: IInstantiationService,
348
) {
349
super();
350
this._register(contextPickService.registerChatContextItem(instantiationService.createInstance(DebugSessionContextPick)));
351
}
352
}
353
354
// Context menu action: Add variable to chat
355
registerAction2(class extends Action2 {
356
constructor() {
357
super({
358
id: 'workbench.debug.action.addVariableToChat',
359
title: localize('addToChat', 'Add to Chat'),
360
f1: false,
361
menu: {
362
id: MenuId.DebugVariablesContext,
363
group: 'z_commands',
364
order: 110,
365
when: ChatContextKeys.enabled
366
}
367
});
368
}
369
370
override async run(accessor: ServicesAccessor, context: unknown): Promise<void> {
371
const chatWidgetService = accessor.get(IChatWidgetService);
372
const debugService = accessor.get(IDebugService);
373
const widget = await chatWidgetService.revealWidget();
374
if (!widget) {
375
return;
376
}
377
378
// Context is the variable from the variables view
379
const entry = createDebugVariableEntryFromContext(context);
380
if (entry) {
381
const stackFrame = debugService.getViewModel().focusedStackFrame;
382
if (stackFrame) {
383
widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame));
384
}
385
widget.attachmentModel.addContext(entry);
386
}
387
}
388
});
389
390
// Context menu action: Add watch expression to chat
391
registerAction2(class extends Action2 {
392
constructor() {
393
super({
394
id: 'workbench.debug.action.addWatchExpressionToChat',
395
title: localize('addToChat', 'Add to Chat'),
396
f1: false,
397
menu: {
398
id: MenuId.DebugWatchContext,
399
group: 'z_commands',
400
order: 110,
401
when: ChatContextKeys.enabled
402
}
403
});
404
}
405
406
override async run(accessor: ServicesAccessor, context: IExpression): Promise<void> {
407
const chatWidgetService = accessor.get(IChatWidgetService);
408
const debugService = accessor.get(IDebugService);
409
const widget = await chatWidgetService.revealWidget();
410
if (!context || !widget) {
411
return;
412
}
413
414
// Context is the expression (watch expression or variable under it)
415
const stackFrame = debugService.getViewModel().focusedStackFrame;
416
if (stackFrame) {
417
widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame));
418
}
419
widget.attachmentModel.addContext(createDebugVariableEntry(context));
420
}
421
});
422
423
// Context menu action: Add scope to chat
424
registerAction2(class extends Action2 {
425
constructor() {
426
super({
427
id: 'workbench.debug.action.addScopeToChat',
428
title: localize('addToChat', 'Add to Chat'),
429
f1: false,
430
menu: {
431
id: MenuId.DebugScopesContext,
432
group: 'z_commands',
433
order: 1,
434
when: ChatContextKeys.enabled
435
}
436
});
437
}
438
439
override async run(accessor: ServicesAccessor, context: IScopesContext): Promise<void> {
440
const chatWidgetService = accessor.get(IChatWidgetService);
441
const debugService = accessor.get(IDebugService);
442
const widget = await chatWidgetService.revealWidget();
443
if (!context || !widget) {
444
return;
445
}
446
447
// Get the actual scope and its variables
448
const viewModel = debugService.getViewModel();
449
const stackFrame = viewModel.focusedStackFrame;
450
if (!stackFrame) {
451
return;
452
}
453
454
try {
455
const scopes = await stackFrame.getScopes();
456
const scope = scopes.find(s => s.name === context.scope.name);
457
if (scope) {
458
const variables = await scope.getChildren();
459
widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame));
460
widget.attachmentModel.addContext(createScopeEntry(scope, variables));
461
}
462
} catch {
463
// Ignore errors
464
}
465
}
466
});
467
468
interface IScopesContext {
469
scope: { name: string };
470
}
471
472
interface IVariablesContext {
473
sessionId: string | undefined;
474
variable: { name: string; value: string; type?: string; evaluateName?: string };
475
}
476
477
function isVariablesContext(context: unknown): context is IVariablesContext {
478
return typeof context === 'object' && context !== null && 'variable' in context && 'sessionId' in context;
479
}
480
481
function createDebugVariableEntryFromContext(context: unknown): IDebugVariableEntry | undefined {
482
// The context can be either a Variable directly, or an IVariablesContext object
483
if (context instanceof Variable) {
484
return createDebugVariableEntry(context);
485
}
486
487
// Handle IVariablesContext format from the variables view
488
if (isVariablesContext(context)) {
489
const variable = context.variable;
490
return {
491
kind: 'debugVariable',
492
id: `debug-variable:${variable.name}`,
493
name: variable.name,
494
fullName: variable.evaluateName ?? variable.name,
495
icon: Codicon.debug,
496
value: variable.value,
497
expression: variable.evaluateName ?? variable.name,
498
type: variable.type,
499
modelDescription: formatModelDescription(variable.evaluateName || variable.name, variable.value, variable.type),
500
};
501
}
502
503
return undefined;
504
}
505
506