Path: blob/main/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.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 * as dom from '../../../../base/browser/dom.js';6import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';7import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';8import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';9import { IObservable } from '../../../../base/common/observable.js';10import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { IHoverService } from '../../../../platform/hover/browser/hover.js';13import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';14import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';15import { IDebugSession, IExpressionValue } from '../common/debug.js';16import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js';17import { ReplEvaluationResult } from '../common/replModel.js';18import { IVariableTemplateData, splitExpressionOrScopeHighlights } from './baseDebugView.js';19import { handleANSIOutput } from './debugANSIHandling.js';20import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js';21import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, ILinkDetector, LinkDetector } from './linkDetector.js';2223export interface IValueHoverOptions {24/** Commands to show in the hover footer. */25commands?: { id: string; args: unknown[] }[];26}2728export interface IRenderValueOptions {29showChanged?: boolean;30maxValueLength?: number;31/** If not false, a rich hover will be shown on the element. */32hover?: false | IValueHoverOptions;33colorize?: boolean;34highlights?: IHighlight[];3536/**37* Indicates areas where VS Code implicitly always supported ANSI escape38* sequences. These should be rendered as ANSI when the DA does not specify39* any value of `supportsANSIStyling`.40* @deprecated41*/42wasANSI?: boolean;43session?: IDebugSession;44locationReference?: number;45}4647export interface IRenderVariableOptions {48showChanged?: boolean;49highlights?: IHighlight[];50}515253const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;54const booleanRegex = /^(true|false)$/i;55const stringRegex = /^(['"]).*\1$/;5657const enum Cls {58Value = 'value',59Unavailable = 'unavailable',60Error = 'error',61Changed = 'changed',62Boolean = 'boolean',63String = 'string',64Number = 'number',65}6667const allClasses: readonly Cls[] = Object.keys({68[Cls.Value]: 0,69[Cls.Unavailable]: 0,70[Cls.Error]: 0,71[Cls.Changed]: 0,72[Cls.Boolean]: 0,73[Cls.String]: 0,74[Cls.Number]: 0,75} satisfies { [key in Cls]: unknown }) as Cls[];7677export class DebugExpressionRenderer {78private displayType: IObservable<boolean>;79private readonly linkDetector: LinkDetector;8081constructor(82@ICommandService private readonly commandService: ICommandService,83@IConfigurationService configurationService: IConfigurationService,84@IInstantiationService instantiationService: IInstantiationService,85@IHoverService private readonly hoverService: IHoverService,86) {87this.linkDetector = instantiationService.createInstance(LinkDetector);88this.displayType = observableConfigValue('debug.showVariableTypes', false, configurationService);89}9091renderVariable(data: IVariableTemplateData, variable: Variable, options: IRenderVariableOptions = {}): IDisposable {92const displayType = this.displayType.get();93const highlights = splitExpressionOrScopeHighlights(variable, options.highlights || []);9495if (variable.available) {96data.type.textContent = '';97let text = variable.name;98if (variable.value && typeof variable.name === 'string') {99if (variable.type && displayType) {100text += ': ';101data.type.textContent = variable.type + ' =';102} else {103text += ' =';104}105}106107data.label.set(text, highlights.name, variable.type && !displayType ? variable.type : variable.name);108data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual');109data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal');110} else if (variable.value && typeof variable.name === 'string' && variable.name) {111data.label.set(':');112}113114data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy);115const commands = [116{ id: COPY_VALUE_ID, args: [variable, [variable]] as unknown[] }117];118if (variable.evaluateName) {119commands.push({ id: COPY_EVALUATE_PATH_ID, args: [{ variable }] });120}121122return this.renderValue(data.value, variable, {123showChanged: options.showChanged,124maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,125hover: { commands },126highlights: highlights.value,127colorize: true,128session: variable.getSession(),129});130}131132renderValue(container: HTMLElement, expressionOrValue: IExpressionValue | string, options: IRenderValueOptions = {}): IDisposable {133const store = new DisposableStore();134// Use remembered capabilities so REPL elements can render even once a session ends135const supportsANSI: boolean = options.session?.rememberedCapabilities?.supportsANSIStyling ?? options.wasANSI ?? false;136137let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value;138139// remove stale classes140for (const cls of allClasses) {141container.classList.remove(cls);142}143container.classList.add(Cls.Value);144// when resolving expressions we represent errors from the server as a variable with name === null.145if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) {146container.classList.add(Cls.Unavailable);147if (value !== Expression.DEFAULT_VALUE) {148container.classList.add(Cls.Error);149}150} else {151if (typeof expressionOrValue !== 'string' && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) {152// value changed color has priority over other colors.153container.classList.add(Cls.Changed);154expressionOrValue.valueChanged = false;155}156157if (options.colorize && typeof expressionOrValue !== 'string') {158if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') {159container.classList.add(expressionOrValue.type);160} else if (!isNaN(+value)) {161container.classList.add(Cls.Number);162} else if (booleanRegex.test(value)) {163container.classList.add(Cls.Boolean);164} else if (stringRegex.test(value)) {165container.classList.add(Cls.String);166}167}168}169170if (options.maxValueLength && value && value.length > options.maxValueLength) {171value = value.substring(0, options.maxValueLength) + '...';172}173if (!value) {174value = '';175}176177const session = options.session ?? ((expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined);178// Only use hovers for links if thre's not going to be a hover for the value.179const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None };180dom.clearNode(container);181const locationReference = options.locationReference ?? (expressionOrValue instanceof ExpressionContainer && expressionOrValue.valueLocationReference);182183let linkDetector: ILinkDetector = this.linkDetector;184if (locationReference && session) {185linkDetector = this.linkDetector.makeReferencedLinkDetector(locationReference, session);186}187188if (supportsANSI) {189container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined, options.highlights));190} else {191container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior, options.highlights));192}193194if (options.hover !== false) {195const { commands = [] } = options.hover || {};196store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => {197const container = dom.$('div');198const markdownHoverElement = dom.$('div.hover-row');199const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents'));200const hoverContentsPre = dom.append(hoverContentsElement, dom.$('pre.debug-var-hover-pre'));201if (supportsANSI) {202// note: intentionally using `this.linkDetector` so we don't blindly linkify the203// entire contents and instead only link file paths that it contains.204hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined, options.highlights));205} else {206hoverContentsPre.textContent = value;207}208container.appendChild(markdownHoverElement);209return container;210}, {211actions: commands.map(({ id, args }) => {212const description = CommandsRegistry.getCommand(id)?.metadata?.description;213return {214label: typeof description === 'string' ? description : description ? description.value : id,215commandId: id,216run: () => this.commandService.executeCommand(id, ...args),217};218})219}));220}221222return store;223}224}225226227