Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

debug: initial visualization extension points #202775

Merged
merged 1 commit into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/vs/platform/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -199,6 +204,7 @@ export interface IExtensionContributions {
startEntries?: IStartEntry[];
readonly notebooks?: INotebookEntry[];
readonly notebookRenderer?: INotebookRendererContribution[];
readonly debugVisualizers?: IDebugVisualizationContribution[];
}

export interface IExtensionCapabilities {
Expand Down
28 changes: 25 additions & 3 deletions src/vs/workbench/api/browser/mainThreadDebugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -27,10 +29,12 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
private readonly _debugConfigurationProviders: Map<number, IDebugConfigurationProvider>;
private readonly _debugAdapterDescriptorFactories: Map<number, IDebugAdapterDescriptorFactory>;
private readonly _extHostKnownSessions: Set<DebugSessionUUID>;
private readonly _visualizerHandles = new Map<string, IDisposable>();

constructor(
extHostContext: IExtHostContext,
@IDebugService private readonly debugService: IDebugService
@IDebugService private readonly debugService: IDebugService,
@IDebugVisualizerService private readonly visualizerService: IDebugVisualizerService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService);

Expand Down Expand Up @@ -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 => {
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1567,6 +1567,8 @@ export interface MainThreadDebugServiceShape extends IDisposable {
$appendDebugConsole(value: string): void;
$registerBreakpoints(breakpoints: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto>): Promise<void>;
$unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise<void>;
$registerDebugVisualizer(extensionId: string, id: string): void;
$unregisterDebugVisualizer(extensionId: string, id: string): void;
}

export interface IOpenUriOptions {
Expand Down Expand Up @@ -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<IDebugVisualization.Serialized[]>;
$resolveDebugVisualizer(id: number, token: CancellationToken): Promise<MainThreadDebugVisualization>;
$executeDebugVisualizerCommand(id: number): Promise<void>;
$disposeDebugVisualizers(ids: number[]): void;
}


Expand Down
154 changes: 149 additions & 5 deletions src/vs/workbench/api/common/extHostDebugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@ 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';
import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from 'vs/workbench/api/common/extHost.protocol';
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>('IExtHostDebugService');

Expand All @@ -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<T extends vscode.DebugVisualization>(extension: IExtensionDescription, id: string, provider: vscode.DebugVisualizationProvider<T>): vscode.Disposable;
asDebugSourceUri(source: vscode.DebugProtocolSource, session?: vscode.DebugSession): vscode.Uri;
}

Expand Down Expand Up @@ -96,16 +100,21 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E

private _debugAdapters: Map<number, IDebugAdapter>;
private _debugAdaptersTrackers: Map<number, vscode.DebugAdapterTracker>;
private readonly _debugVisualizationProviders = new Map<string, vscode.DebugVisualizationProvider>();

private _signService: ISignService | undefined;

private readonly _visualizers = new Map<number, { v: vscode.DebugVisualization; provider: vscode.DebugVisualizationProvider }>();
private _visualizerIdCounter = 0;

constructor(
@IExtHostRpcService extHostRpcService: IExtHostRpcService,
@IExtHostWorkspace protected _workspaceService: IExtHostWorkspace,
@IExtHostExtensionService private _extensionService: IExtHostExtensionService,
@IExtHostConfiguration protected _configurationService: IExtHostConfiguration,
@IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs,
@IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider,
@IExtHostCommands private _commands: IExtHostCommands,
) {
this._configProviderHandleCounter = 0;
this._configProviders = [];
Expand Down Expand Up @@ -209,6 +218,96 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E
return result;
}

public async $resolveDebugVisualizer(id: number, token: CancellationToken): Promise<MainThreadDebugVisualization> {
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<void> {
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<IDebugVisualization.Serialized[]> {
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<T extends vscode.DebugVisualization>(manifest: IExtensionDescription, id: string, provider: vscode.DebugVisualizationProvider<T>): 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<void> {
// filter only new breakpoints
const breakpoints = breakpoints0.filter(bp => {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3448,6 +3448,13 @@ export enum DebugConsoleMode {
MergeWithParent = 1
}

export class DebugVisualization {
iconPath?: URI | { light: URI; dark: URI } | ThemeIcon;
visualization?: vscode.Command | vscode.TreeDataProvider<unknown>;

constructor(public name: string) { }
}

//#endregion

@es5ClassCompat
Expand Down
Loading
Loading