diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 222f6b91861d8..413c1db06f189 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -162,6 +162,11 @@ export interface INotebookRendererContribution { readonly mimeTypes: string[]; } +export interface IDebugVisualizationContribution { + readonly id: string; + readonly when: string; +} + export interface ITranslation { id: string; path: string; @@ -199,6 +204,7 @@ export interface IExtensionContributions { startEntries?: IStartEntry[]; readonly notebooks?: INotebookEntry[]; readonly notebookRenderer?: INotebookRendererContribution[]; + readonly debugVisualizers?: IDebugVisualizationContribution[]; } export interface IExtensionCapabilities { diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index c98dd8af07217..283c0e783728e 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI as uri, UriComponents } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto @@ -16,6 +16,8 @@ import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstract import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { convertToVSCPaths, convertToDAPaths, isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; +import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -27,10 +29,12 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private readonly _debugConfigurationProviders: Map; private readonly _debugAdapterDescriptorFactories: Map; private readonly _extHostKnownSessions: Set; + private readonly _visualizerHandles = new Map(); constructor( extHostContext: IExtHostContext, - @IDebugService private readonly debugService: IDebugService + @IDebugService private readonly debugService: IDebugService, + @IDebugVisualizerService private readonly visualizerService: IDebugVisualizerService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService); @@ -108,6 +112,24 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb this.sendBreakpointsAndListen(); } + $registerDebugVisualizer(extensionId: string, id: string): void { + const handle = this.visualizerService.register({ + extensionId: new ExtensionIdentifier(extensionId), + id, + disposeDebugVisualizers: ids => this._proxy.$disposeDebugVisualizers(ids), + executeDebugVisualizerCommand: id => this._proxy.$executeDebugVisualizerCommand(id), + provideDebugVisualizers: (context, token) => this._proxy.$provideDebugVisualizers(extensionId, id, context, token).then(r => r.map(IDebugVisualization.deserialize)), + resolveDebugVisualizer: (viz, token) => this._proxy.$resolveDebugVisualizer(viz.id, token), + }); + this._visualizerHandles.set(`${extensionId}/${id}`, handle); + } + + $unregisterDebugVisualizer(extensionId: string, id: string): void { + const key = `${extensionId}/${id}`; + this._visualizerHandles.get(key)?.dispose(); + this._visualizerHandles.delete(key); + } + private sendBreakpointsAndListen(): void { // set up a handler to send more this._toDispose.add(this.debugService.getModel().onDidChangeBreakpoints(e => { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index dd36b43f34f41..d3982e71c82bf 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1211,6 +1211,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get stackFrameFocus() { return extHostDebugService.stackFrameFocus; }, + registerDebugVisualizationProvider(id, provider) { + checkProposedApiEnabled(extension, 'debugVisualization'); + return extHostDebugService.registerDebugVisualizationProvider(extension, id, provider); + }, onDidStartDebugSession(listener, thisArg?, disposables?) { return _asExtensionEvent(extHostDebugService.onDidStartDebugSession)(listener, thisArg, disposables); }, @@ -1465,6 +1469,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I DebugAdapterServer: extHostTypes.DebugAdapterServer, DebugConfigurationProviderTriggerKind: DebugConfigurationProviderTriggerKind, DebugConsoleMode: extHostTypes.DebugConsoleMode, + DebugVisualization: extHostTypes.DebugVisualization, DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior, Diagnostic: extHostTypes.Diagnostic, DiagnosticRelatedInformation: extHostTypes.DiagnosticRelatedInformation, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 978e0179e80e1..70049a74f73e3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -55,7 +55,7 @@ import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/c import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider'; import { IChatAsyncContent, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; +import { DebugConfigurationProviderTriggerKind, MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -1567,6 +1567,8 @@ export interface MainThreadDebugServiceShape extends IDisposable { $appendDebugConsole(value: string): void; $registerBreakpoints(breakpoints: Array): Promise; $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise; + $registerDebugVisualizer(extensionId: string, id: string): void; + $unregisterDebugVisualizer(extensionId: string, id: string): void; } export interface IOpenUriOptions { @@ -2357,6 +2359,10 @@ export interface ExtHostDebugServiceShape { $acceptBreakpointsDelta(delta: IBreakpointsDeltaDto): void; $acceptDebugSessionNameChanged(session: IDebugSessionDto, name: string): void; $acceptStackFrameFocus(focus: IThreadFocusDto | IStackFrameFocusDto | undefined): void; + $provideDebugVisualizers(extensionId: string, id: string, context: IDebugVisualizationContext, token: CancellationToken): Promise; + $resolveDebugVisualizer(id: number, token: CancellationToken): Promise; + $executeDebugVisualizerCommand(id: number): Promise; + $disposeDebugVisualizers(ids: number[]): void; } diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 8eacd6f83b24f..ae9ba7e1b42a5 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -7,7 +7,7 @@ import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ISignService } from 'vs/platform/sign/common/sign'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -15,16 +15,19 @@ import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThre import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, ThreadFocus, StackFrameFocus } from 'vs/workbench/api/common/extHostTypes'; +import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, ThreadFocus, StackFrameFocus, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution } from 'vs/workbench/contrib/debug/common/debug'; +import { MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebugVisualization, IDebugVisualizationContext, IDebuggerContribution, DebugVisualizationType } from 'vs/workbench/contrib/debug/common/debug'; import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import { IExtHostConfiguration } from '../common/extHostConfiguration'; import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; +import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -50,6 +53,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider, trigger: vscode.DebugConfigurationProviderTriggerKind): vscode.Disposable; registerDebugAdapterDescriptorFactory(extension: IExtensionDescription, type: string, factory: vscode.DebugAdapterDescriptorFactory): vscode.Disposable; registerDebugAdapterTrackerFactory(type: string, factory: vscode.DebugAdapterTrackerFactory): vscode.Disposable; + registerDebugVisualizationProvider(extension: IExtensionDescription, id: string, provider: vscode.DebugVisualizationProvider): vscode.Disposable; asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri; } @@ -96,9 +100,13 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private _debugAdapters: Map; private _debugAdaptersTrackers: Map; + private readonly _debugVisualizationProviders = new Map(); private _signService: ISignService | undefined; + private readonly _visualizers = new Map(); + private _visualizerIdCounter = 0; + constructor( @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace protected _workspaceService: IExtHostWorkspace, @@ -106,6 +114,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E @IExtHostConfiguration protected _configurationService: IExtHostConfiguration, @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, + @IExtHostCommands private _commands: IExtHostCommands, ) { this._configProviderHandleCounter = 0; this._configProviders = []; @@ -209,6 +218,96 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return result; } + public async $resolveDebugVisualizer(id: number, token: CancellationToken): Promise { + const visualizer = this._visualizers.get(id); + if (!visualizer) { + throw new Error(`No debug visualizer found with id '${id}'`); + } + + let { v, provider } = visualizer; + if (!v.visualization) { + v = await provider.resolveDebugVisualization?.(v, token) || v; + visualizer.v = v; + } + + if (!v.visualization) { + throw new Error(`No visualization returned from resolveDebugVisualization in '${provider}'`); + } + + return this.serializeVisualization(v.visualization)!; + } + + public async $executeDebugVisualizerCommand(id: number): Promise { + const visualizer = this._visualizers.get(id); + if (!visualizer) { + throw new Error(`No debug visualizer found with id '${id}'`); + } + + const command = visualizer.v.visualization; + if (command && 'command' in command) { + this._commands.executeCommand(command.command, ...(command.arguments || [])); + } + } + + public async $provideDebugVisualizers(extensionId: string, id: string, context: IDebugVisualizationContext, token: CancellationToken): Promise { + const session = this._debugSessions.get(context.sessionId); + const key = this.extensionVisKey(extensionId, id); + const provider = this._debugVisualizationProviders.get(key); + if (!session || !provider) { + return []; // probably ended in the meantime + } + + const visualizations = await provider.provideDebugVisualization({ + session, + variable: context.variable, + containerId: context.containerId, + frameId: context.frameId, + threadId: context.threadId, + }, token); + + if (!visualizations) { + return []; + } + + return visualizations.map(v => { + const id = ++this._visualizerIdCounter; + this._visualizers.set(id, { v, provider }); + const icon = v.iconPath ? this.getIconPathOrClass(v.iconPath) : undefined; + return { + id, + name: v.name, + iconClass: icon?.iconClass, + iconPath: icon?.iconPath, + visualization: this.serializeVisualization(v.visualization), + }; + }); + } + + public $disposeDebugVisualizers(ids: number[]): void { + for (const id of ids) { + this._visualizers.delete(id); + } + } + + public registerDebugVisualizationProvider(manifest: IExtensionDescription, id: string, provider: vscode.DebugVisualizationProvider): vscode.Disposable { + if (!manifest.contributes?.debugVisualizers?.some(r => r.id === id)) { + throw new Error(`Extensions may only call registerDebugVisualizationProvider() for renderers they contribute (got ${id})`); + } + + const extensionId = ExtensionIdentifier.toKey(manifest.identifier); + const key = this.extensionVisKey(extensionId, id); + if (this._debugVisualizationProviders.has(key)) { + throw new Error(`A debug visualization provider with id '${id}' is already registered`); + } + + this._debugVisualizationProviders.set(key, provider); + this._debugServiceProxy.$registerDebugVisualizer(extensionId, id); + return toDisposable(() => { + this._debugServiceProxy.$unregisterDebugVisualizer(extensionId, id); + this._debugVisualizationProviders.delete(id); + }); + } + public addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise { // filter only new breakpoints const breakpoints = breakpoints0.filter(bp => { @@ -858,6 +957,50 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } return Promise.resolve(undefined); } + + private extensionVisKey(extensionId: string, id: string) { + return `${extensionId}\0${id}`; + } + + private serializeVisualization(viz: vscode.DebugVisualization['visualization']): MainThreadDebugVisualization | undefined { + if (!viz) { + return undefined; + } + + if ('title' in viz && 'command' in viz) { + return { type: DebugVisualizationType.Command }; + } + + throw new Error('Unsupported debug visualization type'); + } + + private getIconPathOrClass(icon: vscode.DebugVisualization['iconPath']) { + const iconPathOrIconClass = this.getIconUris(icon); + let iconPath: { dark: URI; light?: URI | undefined } | undefined; + let iconClass: string | undefined; + if ('id' in iconPathOrIconClass) { + iconClass = ThemeIconUtils.asClassName(iconPathOrIconClass); + } else { + iconPath = iconPathOrIconClass; + } + + return { + iconPath, + iconClass + }; + } + + private getIconUris(iconPath: vscode.DebugVisualization['iconPath']): { dark: URI; light?: URI } | { id: string } { + if (iconPath instanceof ThemeIcon) { + return { id: iconPath.id }; + } + const dark = typeof iconPath === 'object' && 'dark' in iconPath ? iconPath.dark : iconPath; + const light = typeof iconPath === 'object' && 'light' in iconPath ? iconPath.light : iconPath; + return { + dark: (typeof dark === 'string' ? URI.file(dark) : dark) as URI, + light: (typeof light === 'string' ? URI.file(light) : light) as URI, + }; + } } export class ExtHostDebugSession implements vscode.DebugSession { @@ -1013,8 +1156,9 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostExtensionService extensionService: IExtHostExtensionService, @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, - @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, + @IExtHostCommands commands: IExtHostCommands ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4c45b385fa867..1307d23e7056d 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3448,6 +3448,13 @@ export enum DebugConsoleMode { MergeWithParent = 1 } +export class DebugVisualization { + iconPath?: URI | { light: URI; dark: URI } | ThemeIcon; + visualization?: vscode.Command | vscode.TreeDataProvider; + + constructor(public name: string) { } +} + //#endregion @es5ClassCompat diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 55cd060d3646a..07bb7582839f2 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -26,6 +26,7 @@ import { hasChildProcesses, prepareCommand } from 'vs/workbench/contrib/debug/no import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import type * as vscode from 'vscode'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; +import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -42,8 +43,9 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostTerminalService private _terminalService: IExtHostTerminalService, @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, + @IExtHostCommands commands: IExtHostCommands, ) { - super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index a3e99830ac38e..058167334d83a 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -138,7 +138,7 @@ export interface IExpressionTemplateData { value: HTMLSpanElement; inputBoxContainer: HTMLElement; actionBar?: ActionBar; - elementDisposable: IDisposable[]; + elementDisposable: DisposableStore; templateDisposable: IDisposable; label: HighlightedLabel; lazyButton: HTMLElement; @@ -174,7 +174,7 @@ export abstract class AbstractExpressionsRenderer implements IT actionBar = templateDisposable.add(new ActionBar(expression)); } - const template: IExpressionTemplateData = { expression, name, value, label, inputBoxContainer, actionBar, elementDisposable: [], templateDisposable, lazyButton, currentElement: undefined }; + const template: IExpressionTemplateData = { expression, name, value, label, inputBoxContainer, actionBar, elementDisposable: new DisposableStore(), templateDisposable, lazyButton, currentElement: undefined }; templateDisposable.add(dom.addDisposableListener(lazyButton, dom.EventType.CLICK, () => { if (template.currentElement) { @@ -188,6 +188,7 @@ export abstract class AbstractExpressionsRenderer implements IT public abstract renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void; protected renderExpressionElement(element: IExpression, node: ITreeNode, data: IExpressionTemplateData): void { + data.elementDisposable.clear(); data.currentElement = element; this.renderExpression(node.element, data, createMatches(node.filterData)); if (data.actionBar) { @@ -197,7 +198,7 @@ export abstract class AbstractExpressionsRenderer implements IT if (element === selectedExpression?.expression || (element instanceof Variable && element.errorMessage)) { const options = this.getInputBoxOptions(element, !!selectedExpression?.settingWatch); if (options) { - data.elementDisposable.push(this.renderInputBox(data.name, data.value, data.inputBoxContainer, options)); + data.elementDisposable.add(this.renderInputBox(data.name, data.value, data.inputBoxContainer, options)); } } } @@ -259,12 +260,11 @@ export abstract class AbstractExpressionsRenderer implements IT protected renderActionBar?(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData): void; disposeElement(node: ITreeNode, index: number, templateData: IExpressionTemplateData): void { - dispose(templateData.elementDisposable); - templateData.elementDisposable = []; + templateData.elementDisposable.clear(); } disposeTemplate(templateData: IExpressionTemplateData): void { - dispose(templateData.elementDisposable); + templateData.elementDisposable.dispose(); templateData.templateDisposable.dispose(); } } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 7324f4bedfe58..76ca42eeadd35 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -51,6 +51,7 @@ import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_ITEM_TYPE, DEBUG_PANEL_ID, DISASSEMBLY_VIEW_ID, EDITOR_CONTRIBUTION_ID, IDebugService, INTERNAL_CONSOLE_OPTIONS_SCHEMA, LOADED_SCRIPTS_VIEW_ID, REPL_VIEW_ID, State, VARIABLES_VIEW_ID, VIEWLET_ID, WATCH_VIEW_ID, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { DebugLifecycle } from 'vs/workbench/contrib/debug/common/debugLifecycle'; +import { DebugVisualizerService, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -58,6 +59,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); registerSingleton(IDebugService, DebugService, InstantiationType.Delayed); +registerSingleton(IDebugVisualizerService, DebugVisualizerService, InstantiationType.Delayed); // Register Debug Workbench Contributions Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugStatusContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index a0f887673ae1c..df67a66260a13 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -10,12 +10,14 @@ import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; -import { IAction } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { coalesce } from 'vs/base/common/arrays'; import { RunOnceScheduler, timeout } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; -import { createMatches, FuzzyScore } from 'vs/base/common/filters'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { localize } from 'vs/nls'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -37,8 +39,10 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, CONTEXT_VARIABLES_FOCUSED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_VARIABLE_IS_READONLY, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { ErrorScope, Expression, getUriForDebugMemory, Scope, StackFrame, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; +import { ErrorScope, Expression, Scope, StackFrame, Variable, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DebugVisualizer, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -254,7 +258,7 @@ const getVariablesContext = (variable: Variable): IVariablesContext => ({ async function getContextForVariableMenuWithDataAccess(parentContext: IContextKeyService, variable: Variable) { const session = variable.getSession(); if (!session || !session.capabilities.supportsDataBreakpoints) { - return getContextForVariableMenu(parentContext, variable); + return getContextForVariableMenuBase(parentContext, variable); } const contextKeys: [string, unknown][] = []; @@ -280,25 +284,15 @@ async function getContextForVariableMenuWithDataAccess(parentContext: IContextKe } } - return getContextForVariableMenu(parentContext, variable, contextKeys); + return getContextForVariableMenuBase(parentContext, variable, contextKeys); } /** * Gets a context key overlay that has context for the given variable. */ -function getContextForVariableMenu(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) { - const session = variable.getSession(); - const contextKeys: [string, unknown][] = [ - [CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.key, variable.variableMenuContext || ''], - [CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.key, !!variable.evaluateName], - [CONTEXT_CAN_VIEW_MEMORY.key, !!session?.capabilities.supportsReadMemoryRequest && variable.memoryReference !== undefined], - [CONTEXT_VARIABLE_IS_READONLY.key, !!variable.presentationHint?.attributes?.includes('readOnly') || variable.presentationHint?.lazy], - ...additionalContext, - ]; - +function getContextForVariableMenuBase(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) { variableInternalContext = variable; - - return parentContext.createOverlay(contextKeys); + return getContextForVariable(parentContext, variable, additionalContext); } function isStackFrame(obj: any): obj is IStackFrame { @@ -410,6 +404,8 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { private readonly linkDetector: LinkDetector, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IDebugVisualizerService private readonly visualization: IDebugVisualizerService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, ) { @@ -452,9 +448,9 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { }; } - protected override renderActionBar(actionBar: ActionBar, expression: IExpression) { + protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) { const variable = expression as Variable; - const contextKeyService = getContextForVariableMenu(this.contextKeyService, variable); + const contextKeyService = getContextForVariableMenuBase(this.contextKeyService, variable); const menu = this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); const primary: IAction[] = []; @@ -464,6 +460,43 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { actionBar.clear(); actionBar.context = context; actionBar.push(primary, { icon: true, label: false }); + + const cts = new CancellationTokenSource(); + data.elementDisposable.add(toDisposable(() => cts.dispose(true))); + this.visualization.getApplicableFor(expression, cts.token).then(result => { + data.elementDisposable.add(result); + + const actions = result.object.map(v => new Action('debugViz', v.name, v.iconClass || 'debug-viz-icon', undefined, this.useVisualizer(v, cts.token))); + if (actions.length === 0) { + // no-op + } else if (actions.length === 1) { + actionBar.push(actions[0], { icon: true, label: false }); + } else { + actionBar.push(new Action('debugViz', localize('useVisualizer', 'Visualize Variable...'), ThemeIcon.asClassName(Codicon.eye), undefined, () => this.pickVisualizer(actions, expression, data)), { icon: true, label: false }); + } + }); + } + + private pickVisualizer(actions: IAction[], expression: IExpression, data: IExpressionTemplateData) { + this.contextMenuService.showContextMenu({ + getAnchor: () => data.actionBar!.getContainer(), + getActions: () => actions, + }); + } + + private useVisualizer(viz: DebugVisualizer, token: CancellationToken) { + return async () => { + const resolved = await viz.resolve(token); + if (token.isCancellationRequested) { + return; + } + + if (resolved.type === DebugVisualizationType.Command) { + viz.execute(); + } else { + throw new Error('not implemented, yet'); + } + }; } } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 308b0ac9bc0b9..2bdd7fa756c46 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -11,7 +11,7 @@ import { Event } from 'vs/base/common/event'; import { IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IDisposable } from 'vs/base/common/lifecycle'; import severity from 'vs/base/common/severity'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI, UriComponents, URI as uri } from 'vs/base/common/uri'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; @@ -84,6 +84,9 @@ export const CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED = new RawContextKey(' export const CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED = new RawContextKey('suspendDebuggeeSupported', false, { type: 'boolean', description: nls.localize('suspendDebuggeeSupported', "True when the focused session supports the suspend debuggee capability.") }); export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey('variableEvaluateNamePresent', false, { type: 'boolean', description: nls.localize('variableEvaluateNamePresent', "True when the focused variable has an 'evalauteName' field set.") }); export const CONTEXT_VARIABLE_IS_READONLY = new RawContextKey('variableIsReadonly', false, { type: 'boolean', description: nls.localize('variableIsReadonly', "True when the focused variable is read-only.") }); +export const CONTEXT_VARIABLE_VALUE = new RawContextKey('variableValue', false, { type: 'string', description: nls.localize('variableValue', "Value of the variable, present for debug visualization clauses.") }); +export const CONTEXT_VARIABLE_TYPE = new RawContextKey('variableType', false, { type: 'string', description: nls.localize('variableType', "Type of the variable, present for debug visualization clauses.") }); +export const CONTEXT_VARIABLE_NAME = new RawContextKey('variableName', false, { type: 'string', description: nls.localize('variableName', "Name of the variable, present for debug visualization clauses.") }); export const CONTEXT_EXCEPTION_WIDGET_VISIBLE = new RawContextKey('exceptionWidgetVisible', false, { type: 'boolean', description: nls.localize('exceptionWidgetVisible', "True when the exception widget is visible.") }); export const CONTEXT_MULTI_SESSION_REPL = new RawContextKey('multiSessionRepl', false, { type: 'boolean', description: nls.localize('multiSessionRepl', "True when there is more than 1 debug console.") }); export const CONTEXT_MULTI_SESSION_DEBUG = new RawContextKey('multiSessionDebug', false, { type: 'boolean', description: nls.localize('multiSessionDebug', "True when there is more than 1 active debug session.") }); @@ -1247,3 +1250,48 @@ export interface IReplConfiguration { export interface IReplOptions { readonly replConfiguration: IReplConfiguration; } + +export interface IDebugVisualizationContext { + variable: DebugProtocol.Variable; + containerId?: string; + frameId?: number; + threadId: number; + sessionId: string; +} + +export const enum DebugVisualizationType { + Command, + Tree, +} + +export type MainThreadDebugVisualization = { + type: DebugVisualizationType.Command; +}; // todo: tree + +export interface IDebugVisualization { + id: number; + name: string; + iconPath: { light?: URI; dark: URI } | undefined; + iconClass: string | undefined; + visualization: MainThreadDebugVisualization | undefined; +} + +export namespace IDebugVisualization { + export interface Serialized { + id: number; + name: string; + iconPath?: { light?: UriComponents; dark: UriComponents }; + iconClass?: string; + visualization?: MainThreadDebugVisualization; + } + + export const deserialize = (v: Serialized): IDebugVisualization => ({ + id: v.id, + name: v.name, + iconPath: v.iconPath && { light: URI.revive(v.iconPath.light), dark: URI.revive(v.iconPath.dark) }, + iconClass: v.iconClass, + visualization: v.visualization, + }); + + export const serialize = (visualizer: IDebugVisualization): Serialized => visualizer; +} diff --git a/src/vs/workbench/contrib/debug/common/debugContext.ts b/src/vs/workbench/contrib/debug/common/debugContext.ts new file mode 100644 index 0000000000000..26a6953310701 --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/debugContext.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_DEBUG_TYPE } from 'vs/workbench/contrib/debug/common/debug'; +import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; + + +/** + * Gets a context key overlay that has context for the given variable. + */ +export function getContextForVariable(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) { + const session = variable.getSession(); + const contextKeys: [string, unknown][] = [ + [CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.key, variable.variableMenuContext || ''], + [CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.key, !!variable.evaluateName], + [CONTEXT_CAN_VIEW_MEMORY.key, !!session?.capabilities.supportsReadMemoryRequest && variable.memoryReference !== undefined], + [CONTEXT_VARIABLE_IS_READONLY.key, !!variable.presentationHint?.attributes?.includes('readOnly') || variable.presentationHint?.lazy], + [CONTEXT_DEBUG_TYPE.key, session?.configuration.type], + ...additionalContext, + ]; + + return parentContext.createOverlay(contextKeys); +} diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index c4f5c811018da..5a33aecfbebbf 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -305,6 +305,10 @@ export class Variable extends ExpressionContainer implements IExpression { this.type = type; } + getThreadId() { + return this.threadId; + } + async setVariable(value: string, stackFrame: IStackFrame): Promise { if (!this.session) { return; @@ -354,7 +358,7 @@ export class Variable extends ExpressionContainer implements IExpression { export class Scope extends ExpressionContainer implements IScope { constructor( - stackFrame: IStackFrame, + public readonly stackFrame: IStackFrame, id: number, public readonly name: string, reference: number, diff --git a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts new file mode 100644 index 0000000000000..9a5cf6885c29e --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; +import { isDefined } from 'vs/base/common/types'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug'; +import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; +import { Scope, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +export const IDebugVisualizerService = createDecorator('debugVisualizerService'); + +interface VisualizerHandle { + id: string; + extensionId: ExtensionIdentifier; + provideDebugVisualizers(context: IDebugVisualizationContext, token: CancellationToken): Promise; + resolveDebugVisualizer(viz: IDebugVisualization, token: CancellationToken): Promise; + executeDebugVisualizerCommand(id: number): Promise; + disposeDebugVisualizers(ids: number[]): void; +} + +export class DebugVisualizer { + public get name() { + return this.viz.name; + } + + public get iconPath() { + return this.viz.iconPath; + } + + public get iconClass() { + return this.viz.iconClass; + } + + constructor(private readonly handle: VisualizerHandle, private readonly viz: IDebugVisualization) { } + + public async resolve(token: CancellationToken) { + return this.viz.visualization ??= await this.handle.resolveDebugVisualizer(this.viz, token); + } + + public async execute() { + await this.handle.executeDebugVisualizerCommand(this.viz.id); + } +} + +export interface IDebugVisualizerService { + _serviceBrand: undefined; + + /** + * Gets visualizers applicable for the given Expression. + */ + getApplicableFor(expression: IExpression, token: CancellationToken): Promise>; + + /** + * Registers a new visualizer (called from the main thread debug service) + */ + register(handle: VisualizerHandle): IDisposable; +} + +export class DebugVisualizerService implements IDebugVisualizerService { + declare public readonly _serviceBrand: undefined; + + private readonly handles = new Map(); + private readonly didActivate = new Map>(); + private registrations: { expr: ContextKeyExpression; id: string; extensionId: ExtensionIdentifier }[] = []; + + constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IExtensionService private readonly extensionService: IExtensionService, + @ILogService private readonly logService: ILogService, + ) { + visualizersExtensionPoint.setHandler((_, { added, removed }) => { + this.registrations = this.registrations.filter(r => + !removed.some(e => ExtensionIdentifier.equals(e.description.identifier, r.extensionId))); + added.forEach(e => this.processExtensionRegistration(e.description)); + }); + } + + /** @inheritdoc */ + public async getApplicableFor(variable: Variable, token: CancellationToken): Promise> { + const threadId = variable.getThreadId(); + if (threadId === undefined) { // an expression, not a variable + return { object: [], dispose: () => { } }; + } + + const context: IDebugVisualizationContext = { + sessionId: variable.getSession()?.getId() || '', + containerId: variable.parent.getId(), + threadId, + variable: { + name: variable.name, + value: variable.value, + type: variable.type, + evaluateName: variable.evaluateName, + variablesReference: variable.reference || 0, + indexedVariables: variable.indexedVariables, + memoryReference: variable.memoryReference, + namedVariables: variable.namedVariables, + presentationHint: variable.presentationHint, + } + }; + + for (let p: IExpressionContainer = variable; p instanceof Variable; p = p.parent) { + if (p.parent instanceof Scope) { + context.frameId = p.parent.stackFrame.frameId; + } + } + + const overlay = getContextForVariable(this.contextKeyService, variable, [ + [CONTEXT_VARIABLE_NAME.key, variable.name], + [CONTEXT_VARIABLE_VALUE.key, variable.value], + [CONTEXT_VARIABLE_TYPE.key, variable.type], + ]); + + const maybeVisualizers = await Promise.all(this.registrations.map(async registration => { + if (!overlay.contextMatchesRules(registration.expr)) { + return; + } + + let prom = this.didActivate.get(registration.id); + if (!prom) { + prom = this.extensionService.activateByEvent(`onDebugVisualizer:${registration.id}`); + this.didActivate.set(registration.id, prom); + } + + await prom; + if (token.isCancellationRequested) { + return; + } + + const handle = this.handles.get(toKey(registration.extensionId, registration.id)); + return handle && { handle, result: await handle.provideDebugVisualizers(context, token) }; + })); + + const ref = { + object: maybeVisualizers.filter(isDefined).flatMap(v => v.result.map(r => new DebugVisualizer(v.handle, r))), + dispose: () => { + for (const viz of maybeVisualizers) { + viz?.handle.disposeDebugVisualizers(viz.result.map(r => r.id)); + } + }, + }; + + if (token.isCancellationRequested) { + ref.dispose(); + } + + return ref; + } + + /** @inheritdoc */ + public register(handle: VisualizerHandle): IDisposable { + const key = toKey(handle.extensionId, handle.id); + this.handles.set(key, handle); + return toDisposable(() => this.handles.delete(key)); + } + + private processExtensionRegistration(ext: Readonly) { + const viz = ext.contributes?.debugVisualizers; + if (!(viz instanceof Array)) { + return; + } + + for (const { when, id } of viz) { + try { + const expr = ContextKeyExpr.deserialize(when); + if (expr) { + this.registrations.push({ expr, id, extensionId: ext.identifier }); + } + } catch (e) { + this.logService.error(`Error processing debug visualizer registration from extension '${ext.identifier.value}'`, e); + } + } + } +} + +const toKey = (extensionId: ExtensionIdentifier, id: string) => `${ExtensionIdentifier.toKey(extensionId)}\0${id}`; + +const visualizersExtensionPoint = ExtensionsRegistry.registerExtensionPoint<{ id: string; when: string }[]>({ + extensionPoint: 'debugVisualizers', + jsonSchema: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Name of the debug visualizer' + }, + when: { + type: 'string', + description: 'Condition when the debug visualizer is applicable' + } + }, + required: ['id', 'when'] + } + }, + activationEventsGenerator: (contribs, result: { push(item: string): void }) => { + for (const contrib of contribs) { + if (contrib.id) { + result.push(`onDebugVisualizer:${contrib.id}`); + } + } + } +}); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 225fd88658980..91297cf16255a 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -40,6 +40,7 @@ export const allApiProposals = Object.freeze({ createFileSystemWatcher: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', debugFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugFocus.d.ts', + debugVisualization: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', defaultChatAgent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatAgent.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', diffContentOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', diff --git a/src/vscode-dts/vscode.proposed.debugVisualization.d.ts b/src/vscode-dts/vscode.proposed.debugVisualization.d.ts new file mode 100644 index 0000000000000..29a3c22d83240 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.debugVisualization.d.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +declare module 'vscode' { + export namespace debug { + /** + * Registers a custom data visualization for variables when debugging. + * + * @param id The corresponding ID in the package.json `debugVisualizers` contribution point. + * @param provider The {@link DebugVisualizationProvider} to register + */ + export function registerDebugVisualizationProvider( + id: string, + provider: DebugVisualizationProvider + ): Disposable; + } + + export class DebugVisualization { + /** + * The name of the visualization to show to the user. + */ + name: string; + + /** + * An icon for the view when it's show in inline actions. + */ + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + + /** + * Visualization to use for the variable. This may be either: + * - A command to run when the visualization is selected for a variable. + * - A {@link TreeDataProvider} which is used to display the data in-line + * where the variable is shown. If a single root item is returned from + * the data provider, it will replace the variable in its tree. + * Otherwise, the items will be shown as children of the variable. + */ + visualization?: Command | TreeDataProvider; + + /** + * Creates a new debug visualization object. + * @param name Name of the visualization to show to the user. + */ + constructor(name: string); + } + + export interface DebugVisualizationProvider { + /** + * Called for each variable when the debug session stops. It should return + * any visualizations the extension wishes to show to the user. + * + * Note that this is only called when its `when` clause defined under the + * `debugVisualizers` contribution point in the `package.json` evaluates + * to true. + */ + provideDebugVisualization(context: DebugVisualizationContext, token: CancellationToken): ProviderResult; + + /** + * Invoked for a variable when a user picks the visualizer. + * + * It may return a {@link TreeView} that's shown in the Debug Console or + * inline in a hover. A visualizer may choose to return `undefined` from + * this function and instead trigger other actions in the UI, such as opening + * a custom {@link WebviewView}. + */ + resolveDebugVisualization?(visualization: T, token: CancellationToken): ProviderResult; + } + + export interface DebugVisualizationContext { + /** + * The Debug Adapter Protocol Variable to be visualized. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable + */ + variable: any; + /** + * The Debug Adapter Protocol variable reference the type (such as a scope + * or another variable) that contained this one. Empty for variables + * that came from user evaluations in the Debug Console. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable + */ + containerId?: string; + /** + * The ID of the Debug Adapter Protocol StackFrame in which the variable was found, + * for variables that came from scopes in a stack frame. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame + */ + frameId?: number; + /** + * The ID of the Debug Adapter Protocol Thread in which the variable was found. + * @see https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame + */ + threadId: number; + /** + * The debug session the variable belongs to. + */ + session: DebugSession; + } +}