Path: blob/main/src/vs/workbench/contrib/debug/browser/debugChatIntegration.ts
5223 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';6import { Codicon } from '../../../../base/common/codicons.js';7import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';8import { autorun, debouncedObservable, derived, IObservable, ISettableObservable, ObservablePromise, observableValue } from '../../../../base/common/observable.js';9import { basename } from '../../../../base/common/resources.js';10import { ThemeIcon } from '../../../../base/common/themables.js';11import { Range } from '../../../../editor/common/core/range.js';12import { localize } from '../../../../nls.js';13import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';14import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';15import { IWorkbenchContribution } from '../../../common/contributions.js';16import { IChatWidget, IChatWidgetService } from '../../chat/browser/chat.js';17import { ChatContextPick, IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from '../../chat/browser/attachments/chatContextPickService.js';18import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';19import { IChatRequestFileEntry, IChatRequestVariableEntry, IDebugVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js';20import { IDebugService, IExpression, IScope, IStackFrame, State } from '../common/debug.js';21import { Variable } from '../common/debugModel.js';2223const enum PickerMode {24Main = 'main',25Expression = 'expression',26}2728class DebugSessionContextPick implements IChatContextPickerItem {29readonly type = 'pickerPick';30readonly label = localize('chatContext.debugSession', 'Debug Session...');31readonly icon = Codicon.debug;32readonly ordinal = -200;3334constructor(35@IDebugService private readonly debugService: IDebugService,36) { }3738isEnabled(): boolean {39// Only enabled when there's a focused session that is stopped (paused)40const viewModel = this.debugService.getViewModel();41const focusedSession = viewModel.focusedSession;42return !!focusedSession && focusedSession.state === State.Stopped;43}4445asPicker(_widget: IChatWidget): IChatContextPicker {46const store = new DisposableStore();47const mode: ISettableObservable<PickerMode> = observableValue('debugPicker.mode', PickerMode.Main);48const query: ISettableObservable<string> = observableValue('debugPicker.query', '');4950const picksObservable = this.createPicksObservable(mode, query, store);5152return {53placeholder: localize('selectDebugData', 'Select debug data to attach'),54picks: (_queryObs: IObservable<string>, token: CancellationToken) => {55// Connect the external query observable to our internal one56store.add(autorun(reader => {57query.set(_queryObs.read(reader), undefined);58}));5960const cts = new CancellationTokenSource(token);61store.add(toDisposable(() => cts.dispose(true)));6263return picksObservable;64},65goBack: () => {66if (mode.get() === PickerMode.Expression) {67mode.set(PickerMode.Main, undefined);68return true; // Stay in picker69}70return false; // Go back to main context menu71},72dispose: () => store.dispose(),73};74}7576private createPicksObservable(77mode: ISettableObservable<PickerMode>,78query: IObservable<string>,79store: DisposableStore80): IObservable<{ busy: boolean; picks: ChatContextPick[] }> {81const debouncedQuery = debouncedObservable(query, 300);8283return derived(reader => {84const currentMode = mode.read(reader);8586if (currentMode === PickerMode.Expression) {87return this.getExpressionPicks(debouncedQuery, store);88} else {89return this.getMainPicks(mode);90}91}).flatten();92}9394private getMainPicks(mode: ISettableObservable<PickerMode>): IObservable<{ busy: boolean; picks: ChatContextPick[] }> {95// Return an observable that resolves to the main picks96const promise = derived(_reader => {97return new ObservablePromise(this.buildMainPicks(mode));98});99100return promise.map((value, reader) => {101const result = value.promiseResult.read(reader);102return { picks: result?.data || [], busy: result === undefined };103});104}105106private async buildMainPicks(mode: ISettableObservable<PickerMode>): Promise<ChatContextPick[]> {107const picks: ChatContextPick[] = [];108const viewModel = this.debugService.getViewModel();109const stackFrame = viewModel.focusedStackFrame;110const session = viewModel.focusedSession;111112if (!session || !stackFrame) {113return picks;114}115116// Add "Expression Value..." option at the top117picks.push({118label: localize('expressionValue', 'Expression Value...'),119iconClass: ThemeIcon.asClassName(Codicon.symbolVariable),120asAttachment: () => {121// Switch to expression mode122mode.set(PickerMode.Expression, undefined);123return 'noop';124},125});126127// Add watch expressions section128const watches = this.debugService.getModel().getWatchExpressions();129if (watches.length > 0) {130picks.push({ type: 'separator', label: localize('watchExpressions', 'Watch Expressions') });131for (const watch of watches) {132picks.push({133label: watch.name,134description: watch.value,135iconClass: ThemeIcon.asClassName(Codicon.eye),136asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createDebugVariableEntry(watch)),137});138}139}140141// Add scopes and their variables142let scopes: IScope[] = [];143try {144scopes = await stackFrame.getScopes();145} catch {146// Ignore errors when fetching scopes147}148149for (const scope of scopes) {150// Include variables from non-expensive scopes151if (scope.expensive && !scope.childrenHaveBeenLoaded) {152continue;153}154155picks.push({ type: 'separator', label: scope.name });156try {157const variables = await scope.getChildren();158if (variables.length > 1) {159picks.push({160label: localize('allVariablesInScope', 'All variables in {0}', scope.name),161iconClass: ThemeIcon.asClassName(Codicon.symbolNamespace),162asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createScopeEntry(scope, variables)),163});164}165for (const variable of variables) {166picks.push({167label: variable.name,168description: formatVariableDescription(variable),169iconClass: ThemeIcon.asClassName(Codicon.symbolVariable),170asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createDebugVariableEntry(variable)),171});172}173} catch {174// Ignore errors when fetching variables175}176}177178return picks;179}180181private getExpressionPicks(182query: IObservable<string>,183_store: DisposableStore184): IObservable<{ busy: boolean; picks: ChatContextPick[] }> {185const promise = derived((reader) => {186const queryValue = query.read(reader);187const cts = new CancellationTokenSource();188reader.store.add(toDisposable(() => cts.dispose(true)));189return new ObservablePromise(this.evaluateExpression(queryValue, cts.token));190});191192return promise.map((value, r) => {193const result = value.promiseResult.read(r);194return { picks: result?.data || [], busy: result === undefined };195});196}197198private async evaluateExpression(expression: string, token: CancellationToken): Promise<ChatContextPick[]> {199if (!expression.trim()) {200return [{201label: localize('typeExpression', 'Type an expression to evaluate...'),202disabled: true,203asAttachment: () => 'noop',204}];205}206207const viewModel = this.debugService.getViewModel();208const session = viewModel.focusedSession;209const stackFrame = viewModel.focusedStackFrame;210211if (!session || !stackFrame) {212return [{213label: localize('noDebugSession', 'No active debug session'),214disabled: true,215asAttachment: () => 'noop',216}];217}218219try {220const response = await session.evaluate(expression, stackFrame.frameId, 'watch');221222if (token.isCancellationRequested) {223return [];224}225226if (response?.body) {227const resultValue = response.body.result;228const resultType = response.body.type;229return [{230label: expression,231description: formatExpressionResult(resultValue, resultType),232iconClass: ThemeIcon.asClassName(Codicon.symbolVariable),233asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, {234kind: 'debugVariable',235id: `debug-expression:${expression}`,236name: expression,237fullName: expression,238icon: Codicon.debug,239value: resultValue,240expression: expression,241type: resultType,242modelDescription: formatModelDescription(expression, resultValue, resultType),243}),244}];245} else {246return [{247label: expression,248description: localize('noResult', 'No result'),249disabled: true,250asAttachment: () => 'noop',251}];252}253} catch (err) {254return [{255label: expression,256description: err instanceof Error ? err.message : localize('evaluationError', 'Evaluation error'),257disabled: true,258asAttachment: () => 'noop',259}];260}261}262}263264function createDebugVariableEntry(expression: IExpression): IDebugVariableEntry {265return {266kind: 'debugVariable',267id: `debug-variable:${expression.getId()}`,268name: expression.name,269fullName: expression.name,270icon: Codicon.debug,271value: expression.value,272expression: expression.name,273type: expression.type,274modelDescription: formatModelDescription(expression.name, expression.value, expression.type),275};276}277278function createPausedLocationEntry(stackFrame: IStackFrame): IChatRequestFileEntry {279const uri = stackFrame.source.uri;280let range = Range.lift(stackFrame.range);281if (range.isEmpty()) {282range = range.setEndPosition(range.startLineNumber + 1, 1);283}284285return {286kind: 'file',287value: { uri, range },288id: `debug-paused-location:${uri.toString()}:${range.startLineNumber}`,289name: basename(uri),290modelDescription: 'The debugger is currently paused at this location',291};292}293294function createDebugAttachments(stackFrame: IStackFrame, variableEntry: IDebugVariableEntry): IChatRequestVariableEntry[] {295return [296createPausedLocationEntry(stackFrame),297variableEntry,298];299}300301function createScopeEntry(scope: IScope, variables: IExpression[]): IDebugVariableEntry {302const variablesSummary = variables.map(v => `${v.name}: ${v.value}`).join('\n');303return {304kind: 'debugVariable',305id: `debug-scope:${scope.name}`,306name: `Scope: ${scope.name}`,307fullName: `Scope: ${scope.name}`,308icon: Codicon.debug,309value: variablesSummary,310expression: scope.name,311type: 'scope',312modelDescription: `Debug scope "${scope.name}" with ${variables.length} variables:\n${variablesSummary}`,313};314}315316function formatVariableDescription(expression: IExpression): string {317const value = expression.value;318const type = expression.type;319if (type && value) {320return `${type}: ${value}`;321}322return value || type || '';323}324325function formatExpressionResult(value: string, type?: string): string {326if (type && value) {327return `${type}: ${value}`;328}329return value || type || '';330}331332function formatModelDescription(name: string, value: string, type?: string): string {333let description = `Debug variable "${name}"`;334if (type) {335description += ` of type ${type}`;336}337description += ` with value: ${value}`;338return description;339}340341export class DebugChatContextContribution extends Disposable implements IWorkbenchContribution {342static readonly ID = 'workbench.contrib.chat.debugChatContextContribution';343344constructor(345@IChatContextPickService contextPickService: IChatContextPickService,346@IInstantiationService instantiationService: IInstantiationService,347) {348super();349this._register(contextPickService.registerChatContextItem(instantiationService.createInstance(DebugSessionContextPick)));350}351}352353// Context menu action: Add variable to chat354registerAction2(class extends Action2 {355constructor() {356super({357id: 'workbench.debug.action.addVariableToChat',358title: localize('addToChat', 'Add to Chat'),359f1: false,360menu: {361id: MenuId.DebugVariablesContext,362group: 'z_commands',363order: 110,364when: ChatContextKeys.enabled365}366});367}368369override async run(accessor: ServicesAccessor, context: unknown): Promise<void> {370const chatWidgetService = accessor.get(IChatWidgetService);371const debugService = accessor.get(IDebugService);372const widget = await chatWidgetService.revealWidget();373if (!widget) {374return;375}376377// Context is the variable from the variables view378const entry = createDebugVariableEntryFromContext(context);379if (entry) {380const stackFrame = debugService.getViewModel().focusedStackFrame;381if (stackFrame) {382widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame));383}384widget.attachmentModel.addContext(entry);385}386}387});388389// Context menu action: Add watch expression to chat390registerAction2(class extends Action2 {391constructor() {392super({393id: 'workbench.debug.action.addWatchExpressionToChat',394title: localize('addToChat', 'Add to Chat'),395f1: false,396menu: {397id: MenuId.DebugWatchContext,398group: 'z_commands',399order: 110,400when: ChatContextKeys.enabled401}402});403}404405override async run(accessor: ServicesAccessor, context: IExpression): Promise<void> {406const chatWidgetService = accessor.get(IChatWidgetService);407const debugService = accessor.get(IDebugService);408const widget = await chatWidgetService.revealWidget();409if (!context || !widget) {410return;411}412413// Context is the expression (watch expression or variable under it)414const stackFrame = debugService.getViewModel().focusedStackFrame;415if (stackFrame) {416widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame));417}418widget.attachmentModel.addContext(createDebugVariableEntry(context));419}420});421422// Context menu action: Add scope to chat423registerAction2(class extends Action2 {424constructor() {425super({426id: 'workbench.debug.action.addScopeToChat',427title: localize('addToChat', 'Add to Chat'),428f1: false,429menu: {430id: MenuId.DebugScopesContext,431group: 'z_commands',432order: 1,433when: ChatContextKeys.enabled434}435});436}437438override async run(accessor: ServicesAccessor, context: IScopesContext): Promise<void> {439const chatWidgetService = accessor.get(IChatWidgetService);440const debugService = accessor.get(IDebugService);441const widget = await chatWidgetService.revealWidget();442if (!context || !widget) {443return;444}445446// Get the actual scope and its variables447const viewModel = debugService.getViewModel();448const stackFrame = viewModel.focusedStackFrame;449if (!stackFrame) {450return;451}452453try {454const scopes = await stackFrame.getScopes();455const scope = scopes.find(s => s.name === context.scope.name);456if (scope) {457const variables = await scope.getChildren();458widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame));459widget.attachmentModel.addContext(createScopeEntry(scope, variables));460}461} catch {462// Ignore errors463}464}465});466467interface IScopesContext {468scope: { name: string };469}470471interface IVariablesContext {472sessionId: string | undefined;473variable: { name: string; value: string; type?: string; evaluateName?: string };474}475476function isVariablesContext(context: unknown): context is IVariablesContext {477return typeof context === 'object' && context !== null && 'variable' in context && 'sessionId' in context;478}479480function createDebugVariableEntryFromContext(context: unknown): IDebugVariableEntry | undefined {481// The context can be either a Variable directly, or an IVariablesContext object482if (context instanceof Variable) {483return createDebugVariableEntry(context);484}485486// Handle IVariablesContext format from the variables view487if (isVariablesContext(context)) {488const variable = context.variable;489return {490kind: 'debugVariable',491id: `debug-variable:${variable.name}`,492name: variable.name,493fullName: variable.evaluateName ?? variable.name,494icon: Codicon.debug,495value: variable.value,496expression: variable.evaluateName ?? variable.name,497type: variable.type,498modelDescription: formatModelDescription(variable.evaluateName || variable.name, variable.value, variable.type),499};500}501502return undefined;503}504505506