Path: blob/main/src/vs/workbench/contrib/debug/common/debugVisualizers.ts
3296 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 } from '../../../../base/common/cancellation.js';6import { IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';7import { isDefined } from '../../../../base/common/types.js';8import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';9import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';10import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';11import { ILogService } from '../../../../platform/log/common/log.js';12import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem, IDebugSession } from './debug.js';13import { getContextForVariable } from './debugContext.js';14import { Scope, Variable, VisualizedExpression } from './debugModel.js';15import { IExtensionService } from '../../../services/extensions/common/extensions.js';16import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';1718export const IDebugVisualizerService = createDecorator<IDebugVisualizerService>('debugVisualizerService');1920interface VisualizerHandle {21id: string;22extensionId: ExtensionIdentifier;23provideDebugVisualizers(context: IDebugVisualizationContext, token: CancellationToken): Promise<IDebugVisualization[]>;24resolveDebugVisualizer(viz: IDebugVisualization, token: CancellationToken): Promise<MainThreadDebugVisualization>;25executeDebugVisualizerCommand(id: number): Promise<void>;26disposeDebugVisualizers(ids: number[]): void;27}2829interface VisualizerTreeHandle {30getTreeItem(element: IDebugVisualizationContext): Promise<IDebugVisualizationTreeItem | undefined>;31getChildren(element: number): Promise<IDebugVisualizationTreeItem[]>;32disposeItem(element: number): void;33editItem?(item: number, value: string): Promise<IDebugVisualizationTreeItem | undefined>;34}3536export class DebugVisualizer {37public get name() {38return this.viz.name;39}4041public get iconPath() {42return this.viz.iconPath;43}4445public get iconClass() {46return this.viz.iconClass;47}4849constructor(private readonly handle: VisualizerHandle, private readonly viz: IDebugVisualization) { }5051public async resolve(token: CancellationToken) {52return this.viz.visualization ??= await this.handle.resolveDebugVisualizer(this.viz, token);53}5455public async execute() {56await this.handle.executeDebugVisualizerCommand(this.viz.id);57}58}5960export interface IDebugVisualizerService {61_serviceBrand: undefined;6263/**64* Gets visualizers applicable for the given Expression.65*/66getApplicableFor(expression: IExpression, token: CancellationToken): Promise<IReference<DebugVisualizer[]>>;6768/**69* Registers a new visualizer (called from the main thread debug service)70*/71register(handle: VisualizerHandle): IDisposable;7273/**74* Registers a new visualizer tree.75*/76registerTree(treeId: string, handle: VisualizerTreeHandle): IDisposable;7778/**79* Sets that a certa tree should be used for the visualized node80*/81getVisualizedNodeFor(treeId: string, expr: IExpression): Promise<VisualizedExpression | undefined>;8283/**84* Gets children for a visualized tree node.85*/86getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise<IExpression[]>;8788/**89* Gets children for a visualized tree node.90*/91editTreeItem(treeId: string, item: IDebugVisualizationTreeItem, newValue: string): Promise<void>;92}9394const emptyRef: IReference<DebugVisualizer[]> = { object: [], dispose: () => { } };9596export class DebugVisualizerService implements IDebugVisualizerService {97declare public readonly _serviceBrand: undefined;9899private readonly handles = new Map</* extId + \0 + vizId */ string, VisualizerHandle>();100private readonly trees = new Map</* extId + \0 + treeId */ string, VisualizerTreeHandle>();101private readonly didActivate = new Map<string, Promise<void>>();102private registrations: { expr: ContextKeyExpression; id: string; extensionId: ExtensionIdentifier }[] = [];103104constructor(105@IContextKeyService private readonly contextKeyService: IContextKeyService,106@IExtensionService private readonly extensionService: IExtensionService,107@ILogService private readonly logService: ILogService,108) {109visualizersExtensionPoint.setHandler((_, { added, removed }) => {110this.registrations = this.registrations.filter(r =>111!removed.some(e => ExtensionIdentifier.equals(e.description.identifier, r.extensionId)));112added.forEach(e => this.processExtensionRegistration(e.description));113});114}115116/** @inheritdoc */117public async getApplicableFor(variable: IExpression, token: CancellationToken): Promise<IReference<DebugVisualizer[]>> {118if (!(variable instanceof Variable)) {119return emptyRef;120}121const threadId = variable.getThreadId();122if (threadId === undefined) { // an expression, not a variable123return emptyRef;124}125126const context = this.getVariableContext(threadId, variable);127const overlay = getContextForVariable(this.contextKeyService, variable, [128[CONTEXT_VARIABLE_NAME.key, variable.name],129[CONTEXT_VARIABLE_VALUE.key, variable.value],130[CONTEXT_VARIABLE_TYPE.key, variable.type],131]);132133const maybeVisualizers = await Promise.all(this.registrations.map(async registration => {134if (!overlay.contextMatchesRules(registration.expr)) {135return;136}137138let prom = this.didActivate.get(registration.id);139if (!prom) {140prom = this.extensionService.activateByEvent(`onDebugVisualizer:${registration.id}`);141this.didActivate.set(registration.id, prom);142}143144await prom;145if (token.isCancellationRequested) {146return;147}148149const handle = this.handles.get(toKey(registration.extensionId, registration.id));150return handle && { handle, result: await handle.provideDebugVisualizers(context, token) };151}));152153const ref = {154object: maybeVisualizers.filter(isDefined).flatMap(v => v.result.map(r => new DebugVisualizer(v.handle, r))),155dispose: () => {156for (const viz of maybeVisualizers) {157viz?.handle.disposeDebugVisualizers(viz.result.map(r => r.id));158}159},160};161162if (token.isCancellationRequested) {163ref.dispose();164}165166return ref;167}168169/** @inheritdoc */170public register(handle: VisualizerHandle): IDisposable {171const key = toKey(handle.extensionId, handle.id);172this.handles.set(key, handle);173return toDisposable(() => this.handles.delete(key));174}175176/** @inheritdoc */177public registerTree(treeId: string, handle: VisualizerTreeHandle): IDisposable {178this.trees.set(treeId, handle);179return toDisposable(() => this.trees.delete(treeId));180}181182/** @inheritdoc */183public async getVisualizedNodeFor(treeId: string, expr: IExpression): Promise<VisualizedExpression | undefined> {184if (!(expr instanceof Variable)) {185return;186}187188const threadId = expr.getThreadId();189if (threadId === undefined) {190return;191}192193const tree = this.trees.get(treeId);194if (!tree) {195return;196}197198try {199const treeItem = await tree.getTreeItem(this.getVariableContext(threadId, expr));200if (!treeItem) {201return;202}203204return new VisualizedExpression(expr.getSession(), this, treeId, treeItem, expr);205} catch (e) {206this.logService.warn('Failed to get visualized node', e);207return;208}209}210211/** @inheritdoc */212public async getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise<IExpression[]> {213const node = this.trees.get(treeId);214const children = await node?.getChildren(treeElementId) || [];215return children.map(c => new VisualizedExpression(session, this, treeId, c, undefined));216}217218/** @inheritdoc */219public async editTreeItem(treeId: string, treeItem: IDebugVisualizationTreeItem, newValue: string): Promise<void> {220const newItem = await this.trees.get(treeId)?.editItem?.(treeItem.id, newValue);221if (newItem) {222Object.assign(treeItem, newItem); // replace in-place so rerenders work223}224}225226private getVariableContext(threadId: number, variable: Variable) {227const context: IDebugVisualizationContext = {228sessionId: variable.getSession()?.getId() || '',229containerId: (variable.parent instanceof Variable ? variable.reference : undefined),230threadId,231variable: {232name: variable.name,233value: variable.value,234type: variable.type,235evaluateName: variable.evaluateName,236variablesReference: variable.reference || 0,237indexedVariables: variable.indexedVariables,238memoryReference: variable.memoryReference,239namedVariables: variable.namedVariables,240presentationHint: variable.presentationHint,241}242};243244for (let p: IExpressionContainer = variable; p instanceof Variable; p = p.parent) {245if (p.parent instanceof Scope) {246context.frameId = p.parent.stackFrame.frameId;247}248}249250return context;251}252253private processExtensionRegistration(ext: IExtensionDescription) {254const viz = ext.contributes?.debugVisualizers;255if (!(viz instanceof Array)) {256return;257}258259for (const { when, id } of viz) {260try {261const expr = ContextKeyExpr.deserialize(when);262if (expr) {263this.registrations.push({ expr, id, extensionId: ext.identifier });264}265} catch (e) {266this.logService.error(`Error processing debug visualizer registration from extension '${ext.identifier.value}'`, e);267}268}269}270}271272const toKey = (extensionId: ExtensionIdentifier, id: string) => `${ExtensionIdentifier.toKey(extensionId)}\0${id}`;273274const visualizersExtensionPoint = ExtensionsRegistry.registerExtensionPoint<{ id: string; when: string }[]>({275extensionPoint: 'debugVisualizers',276jsonSchema: {277type: 'array',278items: {279type: 'object',280properties: {281id: {282type: 'string',283description: 'Name of the debug visualizer'284},285when: {286type: 'string',287description: 'Condition when the debug visualizer is applicable'288}289},290required: ['id', 'when']291}292},293activationEventsGenerator: (contribs, result: { push(item: string): void }) => {294for (const contrib of contribs) {295if (contrib.id) {296result.push(`onDebugVisualizer:${contrib.id}`);297}298}299}300});301302303