Path: blob/main/src/vs/workbench/contrib/debug/browser/replViewer.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 { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';7import { HighlightedLabel, IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';8import { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';9import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';10import { CachedListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';11import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';12import { IAsyncDataSource, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js';13import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';14import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';15import { basename } from '../../../../base/common/path.js';16import severity from '../../../../base/common/severity.js';17import { ThemeIcon } from '../../../../base/common/themables.js';18import { localize } from '../../../../nls.js';19import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';20import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';21import { IHoverService } from '../../../../platform/hover/browser/hover.js';22import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';23import { ILabelService } from '../../../../platform/label/common/label.js';24import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';25import { IEditorService } from '../../../services/editor/common/editorService.js';26import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpressionContainer, INestingReplElement, IReplElement, IReplElementSource, IReplOptions } from '../common/debug.js';27import { Variable } from '../common/debugModel.js';28import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from '../common/replModel.js';29import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions } from './baseDebugView.js';30import { DebugExpressionRenderer } from './debugExpressionRenderer.js';31import { debugConsoleEvaluationInput } from './debugIcons.js';3233const $ = dom.$;3435interface IReplEvaluationInputTemplateData {36label: HighlightedLabel;37}3839interface IReplGroupTemplateData {40label: HTMLElement;41source: SourceWidget;42elementDisposable?: IDisposable;43}4445interface IReplEvaluationResultTemplateData {46value: HTMLElement;47elementStore: DisposableStore;48}4950interface IOutputReplElementTemplateData {51container: HTMLElement;52count: CountBadge;53countContainer: HTMLElement;54value: HTMLElement;55source: SourceWidget;56getReplElementSource(): IReplElementSource | undefined;57elementDisposable: DisposableStore;58}5960interface IRawObjectReplTemplateData {61container: HTMLElement;62expression: HTMLElement;63name: HTMLElement;64value: HTMLElement;65label: HighlightedLabel;66elementStore: DisposableStore;67}6869export class ReplEvaluationInputsRenderer implements ITreeRenderer<ReplEvaluationInput, FuzzyScore, IReplEvaluationInputTemplateData> {70static readonly ID = 'replEvaluationInput';7172get templateId(): string {73return ReplEvaluationInputsRenderer.ID;74}7576renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData {77dom.append(container, $('span.arrow' + ThemeIcon.asCSSSelector(debugConsoleEvaluationInput)));78const input = dom.append(container, $('.expression'));79const label = new HighlightedLabel(input);80return { label };81}8283renderElement(element: ITreeNode<ReplEvaluationInput, FuzzyScore>, index: number, templateData: IReplEvaluationInputTemplateData): void {84const evaluation = element.element;85templateData.label.set(evaluation.value, createMatches(element.filterData));86}8788disposeTemplate(templateData: IReplEvaluationInputTemplateData): void {89templateData.label.dispose();90}91}9293export class ReplGroupRenderer implements ITreeRenderer<ReplGroup, FuzzyScore, IReplGroupTemplateData> {94static readonly ID = 'replGroup';9596constructor(97private readonly expressionRenderer: DebugExpressionRenderer,98@IInstantiationService private readonly instaService: IInstantiationService,99) { }100101get templateId(): string {102return ReplGroupRenderer.ID;103}104105renderTemplate(container: HTMLElement): IReplGroupTemplateData {106container.classList.add('group');107const expression = dom.append(container, $('.output.expression.value-and-source'));108const label = dom.append(expression, $('span.label'));109const source = this.instaService.createInstance(SourceWidget, expression);110return { label, source };111}112113renderElement(element: ITreeNode<ReplGroup, FuzzyScore>, _index: number, templateData: IReplGroupTemplateData): void {114115templateData.elementDisposable?.dispose();116const replGroup = element.element;117dom.clearNode(templateData.label);118templateData.elementDisposable = this.expressionRenderer.renderValue(templateData.label, replGroup.name, { wasANSI: true, session: element.element.session });119templateData.source.setSource(replGroup.sourceData);120}121122disposeTemplate(templateData: IReplGroupTemplateData): void {123templateData.elementDisposable?.dispose();124templateData.source.dispose();125}126}127128export class ReplEvaluationResultsRenderer implements ITreeRenderer<ReplEvaluationResult | Variable, FuzzyScore, IReplEvaluationResultTemplateData> {129static readonly ID = 'replEvaluationResult';130131get templateId(): string {132return ReplEvaluationResultsRenderer.ID;133}134135constructor(136private readonly expressionRenderer: DebugExpressionRenderer,137) { }138139renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData {140const output = dom.append(container, $('.evaluation-result.expression'));141const value = dom.append(output, $('span.value'));142143return { value, elementStore: new DisposableStore() };144}145146renderElement(element: ITreeNode<ReplEvaluationResult | Variable, FuzzyScore>, index: number, templateData: IReplEvaluationResultTemplateData): void {147templateData.elementStore.clear();148const expression = element.element;149templateData.elementStore.add(this.expressionRenderer.renderValue(templateData.value, expression, {150colorize: true,151hover: false,152session: element.element.getSession(),153}));154}155156disposeTemplate(templateData: IReplEvaluationResultTemplateData): void {157templateData.elementStore.dispose();158}159}160161export class ReplOutputElementRenderer implements ITreeRenderer<ReplOutputElement, FuzzyScore, IOutputReplElementTemplateData> {162static readonly ID = 'outputReplElement';163164constructor(165private readonly expressionRenderer: DebugExpressionRenderer,166@IInstantiationService private readonly instaService: IInstantiationService,167) { }168169get templateId(): string {170return ReplOutputElementRenderer.ID;171}172173renderTemplate(container: HTMLElement): IOutputReplElementTemplateData {174const data: IOutputReplElementTemplateData = Object.create(null);175container.classList.add('output');176const expression = dom.append(container, $('.output.expression.value-and-source'));177178data.container = container;179data.countContainer = dom.append(expression, $('.count-badge-wrapper'));180data.count = new CountBadge(data.countContainer, {}, defaultCountBadgeStyles);181data.value = dom.append(expression, $('span.value.label'));182data.source = this.instaService.createInstance(SourceWidget, expression);183data.elementDisposable = new DisposableStore();184185return data;186}187188renderElement({ element }: ITreeNode<ReplOutputElement, FuzzyScore>, index: number, templateData: IOutputReplElementTemplateData): void {189templateData.elementDisposable.clear();190this.setElementCount(element, templateData);191templateData.elementDisposable.add(element.onDidChangeCount(() => this.setElementCount(element, templateData)));192// value193dom.clearNode(templateData.value);194// Reset classes to clear ansi decorations since templates are reused195templateData.value.className = 'value';196197const locationReference = element.expression?.valueLocationReference;198templateData.elementDisposable.add(this.expressionRenderer.renderValue(templateData.value, element.value, {199wasANSI: true,200session: element.session,201locationReference,202hover: false,203}));204205templateData.value.classList.add((element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info');206templateData.source.setSource(element.sourceData);207templateData.getReplElementSource = () => element.sourceData;208}209210private setElementCount(element: ReplOutputElement, templateData: IOutputReplElementTemplateData): void {211if (element.count >= 2) {212templateData.count.setCount(element.count);213templateData.countContainer.hidden = false;214} else {215templateData.countContainer.hidden = true;216}217}218219disposeTemplate(templateData: IOutputReplElementTemplateData): void {220templateData.source.dispose();221templateData.elementDisposable.dispose();222templateData.count.dispose();223}224225disposeElement(_element: ITreeNode<ReplOutputElement, FuzzyScore>, _index: number, templateData: IOutputReplElementTemplateData): void {226templateData.elementDisposable.clear();227}228}229230export class ReplVariablesRenderer extends AbstractExpressionsRenderer<IExpression | ReplVariableElement> {231232static readonly ID = 'replVariable';233234get templateId(): string {235return ReplVariablesRenderer.ID;236}237238constructor(239private readonly expressionRenderer: DebugExpressionRenderer,240@IDebugService debugService: IDebugService,241@IContextViewService contextViewService: IContextViewService,242@IHoverService hoverService: IHoverService,243) {244super(debugService, contextViewService, hoverService);245}246247public renderElement(node: ITreeNode<IExpression | ReplVariableElement, FuzzyScore>, _index: number, data: IExpressionTemplateData): void {248const element = node.element;249data.elementDisposable.clear();250super.renderExpressionElement(element instanceof ReplVariableElement ? element.expression : element, node, data);251}252253protected renderExpression(expression: IExpression | ReplVariableElement, data: IExpressionTemplateData, highlights: IHighlight[]): void {254const isReplVariable = expression instanceof ReplVariableElement;255if (isReplVariable || !expression.name) {256data.label.set('');257const value = isReplVariable ? expression.expression : expression;258data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, value, { colorize: true, hover: false, session: expression.getSession() }));259data.expression.classList.remove('nested-variable');260} else {261data.elementDisposable.add(this.expressionRenderer.renderVariable(data, expression as Variable, { showChanged: true, highlights }));262data.expression.classList.toggle('nested-variable', isNestedVariable(expression));263}264}265266protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {267return undefined;268}269}270271export class ReplRawObjectsRenderer implements ITreeRenderer<RawObjectReplElement, FuzzyScore, IRawObjectReplTemplateData> {272static readonly ID = 'rawObject';273274constructor(275private readonly expressionRenderer: DebugExpressionRenderer,276) { }277278get templateId(): string {279return ReplRawObjectsRenderer.ID;280}281282renderTemplate(container: HTMLElement): IRawObjectReplTemplateData {283container.classList.add('output');284285const expression = dom.append(container, $('.output.expression'));286const name = dom.append(expression, $('span.name'));287const label = new HighlightedLabel(name);288const value = dom.append(expression, $('span.value'));289290return { container, expression, name, label, value, elementStore: new DisposableStore() };291}292293renderElement(node: ITreeNode<RawObjectReplElement, FuzzyScore>, index: number, templateData: IRawObjectReplTemplateData): void {294templateData.elementStore.clear();295296// key297const element = node.element;298templateData.label.set(element.name ? `${element.name}:` : '', createMatches(node.filterData));299if (element.name) {300templateData.name.textContent = `${element.name}:`;301} else {302templateData.name.textContent = '';303}304305// value306templateData.elementStore.add(this.expressionRenderer.renderValue(templateData.value, element.value, {307hover: false,308session: node.element.getSession(),309}));310}311312disposeTemplate(templateData: IRawObjectReplTemplateData): void {313templateData.elementStore.dispose();314templateData.label.dispose();315}316}317318function isNestedVariable(element: IReplElement) {319return element instanceof Variable && (element.parent instanceof ReplEvaluationResult || element.parent instanceof Variable);320}321322export class ReplDelegate extends CachedListVirtualDelegate<IReplElement> {323324constructor(325private readonly configurationService: IConfigurationService,326private readonly replOptions: IReplOptions327) {328super();329}330331override getHeight(element: IReplElement): number {332const config = this.configurationService.getValue<IDebugConfiguration>('debug');333334if (!config.console.wordWrap) {335return this.estimateHeight(element, true);336}337338return super.getHeight(element);339}340341/**342* With wordWrap enabled, this is an estimate. With wordWrap disabled, this is the real height that the list will use.343*/344protected estimateHeight(element: IReplElement, ignoreValueLength = false): number {345const lineHeight = this.replOptions.replConfiguration.lineHeight;346const countNumberOfLines = (str: string) => str.match(/\n/g)?.length ?? 0;347const hasValue = (e: any): e is { value: string } => typeof e.value === 'string';348349if (hasValue(element) && !isNestedVariable(element)) {350const value = element.value;351const valueRows = countNumberOfLines(value)352+ (ignoreValueLength ? 0 : Math.floor(value.length / 70)) // Make an estimate for wrapping353+ (element instanceof ReplOutputElement ? 0 : 1); // A SimpleReplElement ends in \n if it's a complete line354355return Math.max(valueRows, 1) * lineHeight;356}357358return lineHeight;359}360361getTemplateId(element: IReplElement): string {362if (element instanceof Variable || element instanceof ReplVariableElement) {363return ReplVariablesRenderer.ID;364}365if (element instanceof ReplEvaluationResult) {366return ReplEvaluationResultsRenderer.ID;367}368if (element instanceof ReplEvaluationInput) {369return ReplEvaluationInputsRenderer.ID;370}371if (element instanceof ReplOutputElement) {372return ReplOutputElementRenderer.ID;373}374if (element instanceof ReplGroup) {375return ReplGroupRenderer.ID;376}377378return ReplRawObjectsRenderer.ID;379}380381hasDynamicHeight(element: IReplElement): boolean {382if (isNestedVariable(element)) {383// Nested variables should always be in one line #111843384return false;385}386// Empty elements should not have dynamic height since they will be invisible387return element.toString().length > 0;388}389}390391function isDebugSession(obj: any): obj is IDebugSession {392return typeof obj.getReplElements === 'function';393}394395export class ReplDataSource implements IAsyncDataSource<IDebugSession, IReplElement> {396397hasChildren(element: IReplElement | IDebugSession): boolean {398if (isDebugSession(element)) {399return true;400}401402return !!(<IExpressionContainer | INestingReplElement>element).hasChildren;403}404405getChildren(element: IReplElement | IDebugSession): Promise<IReplElement[]> {406if (isDebugSession(element)) {407return Promise.resolve(element.getReplElements());408}409410return Promise.resolve((<IExpression | INestingReplElement>element).getChildren());411}412}413414export class ReplAccessibilityProvider implements IListAccessibilityProvider<IReplElement> {415416getWidgetAriaLabel(): string {417return localize('debugConsole', "Debug Console");418}419420getAriaLabel(element: IReplElement): string {421if (element instanceof Variable) {422return localize('replVariableAriaLabel', "Variable {0}, value {1}", element.name, element.value);423}424if (element instanceof ReplOutputElement || element instanceof ReplEvaluationInput || element instanceof ReplEvaluationResult) {425return element.value + (element instanceof ReplOutputElement && element.count > 1 ? localize({ key: 'occurred', comment: ['Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] },426", occurred {0} times", element.count) : '');427}428if (element instanceof RawObjectReplElement) {429return localize('replRawObjectAriaLabel', "Debug console variable {0}, value {1}", element.name, element.value);430}431if (element instanceof ReplGroup) {432return localize('replGroup', "Debug console group {0}", element.name);433}434435return '';436}437}438439class SourceWidget extends Disposable {440private readonly el: HTMLElement;441private source?: IReplElementSource;442private hover?: IManagedHover;443444constructor(container: HTMLElement,445@IEditorService editorService: IEditorService,446@IHoverService private readonly hoverService: IHoverService,447@ILabelService private readonly labelService: ILabelService,448) {449super();450this.el = dom.append(container, $('.source'));451this._register(dom.addDisposableListener(this.el, 'click', e => {452e.preventDefault();453e.stopPropagation();454if (this.source) {455this.source.source.openInEditor(editorService, {456startLineNumber: this.source.lineNumber,457startColumn: this.source.column,458endLineNumber: this.source.lineNumber,459endColumn: this.source.column460});461}462}));463464}465466public setSource(source?: IReplElementSource) {467this.source = source;468this.el.textContent = source ? `${basename(source.source.name)}:${source.lineNumber}` : '';469470this.hover ??= this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.el, ''));471this.hover.update(source ? `${this.labelService.getUriLabel(source.source.uri)}:${source.lineNumber}` : '');472}473}474475476