Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/replViewer.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';
8
import { HighlightedLabel, IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
9
import { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';
10
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
11
import { CachedListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
12
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
13
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js';
14
import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';
15
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
16
import { basename } from '../../../../base/common/path.js';
17
import severity from '../../../../base/common/severity.js';
18
import { ThemeIcon } from '../../../../base/common/themables.js';
19
import { localize } from '../../../../nls.js';
20
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
21
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
22
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
23
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
24
import { ILabelService } from '../../../../platform/label/common/label.js';
25
import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';
26
import { IEditorService } from '../../../services/editor/common/editorService.js';
27
import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpressionContainer, INestingReplElement, IReplElement, IReplElementSource, IReplOptions } from '../common/debug.js';
28
import { Variable } from '../common/debugModel.js';
29
import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from '../common/replModel.js';
30
import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions } from './baseDebugView.js';
31
import { DebugExpressionRenderer } from './debugExpressionRenderer.js';
32
import { debugConsoleEvaluationInput } from './debugIcons.js';
33
34
const $ = dom.$;
35
36
interface IReplEvaluationInputTemplateData {
37
label: HighlightedLabel;
38
}
39
40
interface IReplGroupTemplateData {
41
label: HTMLElement;
42
source: SourceWidget;
43
elementDisposable?: IDisposable;
44
}
45
46
interface IReplEvaluationResultTemplateData {
47
value: HTMLElement;
48
elementStore: DisposableStore;
49
}
50
51
interface IOutputReplElementTemplateData {
52
container: HTMLElement;
53
count: CountBadge;
54
countContainer: HTMLElement;
55
value: HTMLElement;
56
source: SourceWidget;
57
getReplElementSource(): IReplElementSource | undefined;
58
elementDisposable: DisposableStore;
59
}
60
61
interface IRawObjectReplTemplateData {
62
container: HTMLElement;
63
expression: HTMLElement;
64
name: HTMLElement;
65
value: HTMLElement;
66
label: HighlightedLabel;
67
elementStore: DisposableStore;
68
}
69
70
export class ReplEvaluationInputsRenderer implements ITreeRenderer<ReplEvaluationInput, FuzzyScore, IReplEvaluationInputTemplateData> {
71
static readonly ID = 'replEvaluationInput';
72
73
get templateId(): string {
74
return ReplEvaluationInputsRenderer.ID;
75
}
76
77
renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData {
78
dom.append(container, $('span.arrow' + ThemeIcon.asCSSSelector(debugConsoleEvaluationInput)));
79
const input = dom.append(container, $('.expression'));
80
const label = new HighlightedLabel(input);
81
return { label };
82
}
83
84
renderElement(element: ITreeNode<ReplEvaluationInput, FuzzyScore>, index: number, templateData: IReplEvaluationInputTemplateData): void {
85
const evaluation = element.element;
86
templateData.label.set(evaluation.value, createMatches(element.filterData));
87
}
88
89
disposeTemplate(templateData: IReplEvaluationInputTemplateData): void {
90
templateData.label.dispose();
91
}
92
}
93
94
export class ReplGroupRenderer implements ITreeRenderer<ReplGroup, FuzzyScore, IReplGroupTemplateData> {
95
static readonly ID = 'replGroup';
96
97
constructor(
98
private readonly expressionRenderer: DebugExpressionRenderer,
99
@IInstantiationService private readonly instaService: IInstantiationService,
100
) { }
101
102
get templateId(): string {
103
return ReplGroupRenderer.ID;
104
}
105
106
renderTemplate(container: HTMLElement): IReplGroupTemplateData {
107
container.classList.add('group');
108
const expression = dom.append(container, $('.output.expression.value-and-source'));
109
const label = dom.append(expression, $('span.label'));
110
const source = this.instaService.createInstance(SourceWidget, expression);
111
return { label, source };
112
}
113
114
renderElement(element: ITreeNode<ReplGroup, FuzzyScore>, _index: number, templateData: IReplGroupTemplateData): void {
115
116
templateData.elementDisposable?.dispose();
117
const replGroup = element.element;
118
dom.clearNode(templateData.label);
119
templateData.elementDisposable = this.expressionRenderer.renderValue(templateData.label, replGroup.name, { wasANSI: true, session: element.element.session });
120
templateData.source.setSource(replGroup.sourceData);
121
}
122
123
disposeTemplate(templateData: IReplGroupTemplateData): void {
124
templateData.elementDisposable?.dispose();
125
templateData.source.dispose();
126
}
127
}
128
129
export class ReplEvaluationResultsRenderer implements ITreeRenderer<ReplEvaluationResult | Variable, FuzzyScore, IReplEvaluationResultTemplateData> {
130
static readonly ID = 'replEvaluationResult';
131
132
get templateId(): string {
133
return ReplEvaluationResultsRenderer.ID;
134
}
135
136
constructor(
137
private readonly expressionRenderer: DebugExpressionRenderer,
138
) { }
139
140
renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData {
141
const output = dom.append(container, $('.evaluation-result.expression'));
142
const value = dom.append(output, $('span.value'));
143
144
return { value, elementStore: new DisposableStore() };
145
}
146
147
renderElement(element: ITreeNode<ReplEvaluationResult | Variable, FuzzyScore>, index: number, templateData: IReplEvaluationResultTemplateData): void {
148
templateData.elementStore.clear();
149
const expression = element.element;
150
templateData.elementStore.add(this.expressionRenderer.renderValue(templateData.value, expression, {
151
colorize: true,
152
hover: false,
153
session: element.element.getSession(),
154
}));
155
}
156
157
disposeTemplate(templateData: IReplEvaluationResultTemplateData): void {
158
templateData.elementStore.dispose();
159
}
160
}
161
162
export class ReplOutputElementRenderer implements ITreeRenderer<ReplOutputElement, FuzzyScore, IOutputReplElementTemplateData> {
163
static readonly ID = 'outputReplElement';
164
165
constructor(
166
private readonly expressionRenderer: DebugExpressionRenderer,
167
@IInstantiationService private readonly instaService: IInstantiationService,
168
) { }
169
170
get templateId(): string {
171
return ReplOutputElementRenderer.ID;
172
}
173
174
renderTemplate(container: HTMLElement): IOutputReplElementTemplateData {
175
const data: IOutputReplElementTemplateData = Object.create(null);
176
container.classList.add('output');
177
const expression = dom.append(container, $('.output.expression.value-and-source'));
178
179
data.container = container;
180
data.countContainer = dom.append(expression, $('.count-badge-wrapper'));
181
data.count = new CountBadge(data.countContainer, {}, defaultCountBadgeStyles);
182
data.value = dom.append(expression, $('span.value.label'));
183
data.source = this.instaService.createInstance(SourceWidget, expression);
184
data.elementDisposable = new DisposableStore();
185
186
return data;
187
}
188
189
renderElement({ element }: ITreeNode<ReplOutputElement, FuzzyScore>, index: number, templateData: IOutputReplElementTemplateData): void {
190
templateData.elementDisposable.clear();
191
this.setElementCount(element, templateData);
192
templateData.elementDisposable.add(element.onDidChangeCount(() => this.setElementCount(element, templateData)));
193
// value
194
dom.clearNode(templateData.value);
195
// Reset classes to clear ansi decorations since templates are reused
196
templateData.value.className = 'value';
197
198
const locationReference = element.expression?.valueLocationReference;
199
templateData.elementDisposable.add(this.expressionRenderer.renderValue(templateData.value, element.value, {
200
wasANSI: true,
201
session: element.session,
202
locationReference,
203
hover: false,
204
}));
205
206
templateData.value.classList.add((element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info');
207
templateData.source.setSource(element.sourceData);
208
templateData.getReplElementSource = () => element.sourceData;
209
}
210
211
private setElementCount(element: ReplOutputElement, templateData: IOutputReplElementTemplateData): void {
212
if (element.count >= 2) {
213
templateData.count.setCount(element.count);
214
templateData.countContainer.hidden = false;
215
} else {
216
templateData.countContainer.hidden = true;
217
}
218
}
219
220
disposeTemplate(templateData: IOutputReplElementTemplateData): void {
221
templateData.source.dispose();
222
templateData.elementDisposable.dispose();
223
templateData.count.dispose();
224
}
225
226
disposeElement(_element: ITreeNode<ReplOutputElement, FuzzyScore>, _index: number, templateData: IOutputReplElementTemplateData): void {
227
templateData.elementDisposable.clear();
228
}
229
}
230
231
export class ReplVariablesRenderer extends AbstractExpressionsRenderer<IExpression | ReplVariableElement> {
232
233
static readonly ID = 'replVariable';
234
235
get templateId(): string {
236
return ReplVariablesRenderer.ID;
237
}
238
239
constructor(
240
private readonly expressionRenderer: DebugExpressionRenderer,
241
@IDebugService debugService: IDebugService,
242
@IContextViewService contextViewService: IContextViewService,
243
@IHoverService hoverService: IHoverService,
244
) {
245
super(debugService, contextViewService, hoverService);
246
}
247
248
public renderElement(node: ITreeNode<IExpression | ReplVariableElement, FuzzyScore>, _index: number, data: IExpressionTemplateData): void {
249
const element = node.element;
250
data.elementDisposable.clear();
251
super.renderExpressionElement(element instanceof ReplVariableElement ? element.expression : element, node, data);
252
}
253
254
protected renderExpression(expression: IExpression | ReplVariableElement, data: IExpressionTemplateData, highlights: IHighlight[]): void {
255
const isReplVariable = expression instanceof ReplVariableElement;
256
if (isReplVariable || !expression.name) {
257
data.label.set('');
258
const value = isReplVariable ? expression.expression : expression;
259
data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, value, { colorize: true, hover: false, session: expression.getSession() }));
260
data.expression.classList.remove('nested-variable');
261
} else {
262
data.elementDisposable.add(this.expressionRenderer.renderVariable(data, expression as Variable, { showChanged: true, highlights }));
263
data.expression.classList.toggle('nested-variable', isNestedVariable(expression));
264
}
265
}
266
267
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
268
return undefined;
269
}
270
}
271
272
export class ReplRawObjectsRenderer implements ITreeRenderer<RawObjectReplElement, FuzzyScore, IRawObjectReplTemplateData> {
273
static readonly ID = 'rawObject';
274
275
constructor(
276
private readonly expressionRenderer: DebugExpressionRenderer,
277
) { }
278
279
get templateId(): string {
280
return ReplRawObjectsRenderer.ID;
281
}
282
283
renderTemplate(container: HTMLElement): IRawObjectReplTemplateData {
284
container.classList.add('output');
285
286
const expression = dom.append(container, $('.output.expression'));
287
const name = dom.append(expression, $('span.name'));
288
const label = new HighlightedLabel(name);
289
const value = dom.append(expression, $('span.value'));
290
291
return { container, expression, name, label, value, elementStore: new DisposableStore() };
292
}
293
294
renderElement(node: ITreeNode<RawObjectReplElement, FuzzyScore>, index: number, templateData: IRawObjectReplTemplateData): void {
295
templateData.elementStore.clear();
296
297
// key
298
const element = node.element;
299
templateData.label.set(element.name ? `${element.name}:` : '', createMatches(node.filterData));
300
if (element.name) {
301
templateData.name.textContent = `${element.name}:`;
302
} else {
303
templateData.name.textContent = '';
304
}
305
306
// value
307
templateData.elementStore.add(this.expressionRenderer.renderValue(templateData.value, element.value, {
308
hover: false,
309
session: node.element.getSession(),
310
}));
311
}
312
313
disposeTemplate(templateData: IRawObjectReplTemplateData): void {
314
templateData.elementStore.dispose();
315
templateData.label.dispose();
316
}
317
}
318
319
function isNestedVariable(element: IReplElement) {
320
return element instanceof Variable && (element.parent instanceof ReplEvaluationResult || element.parent instanceof Variable);
321
}
322
323
export class ReplDelegate extends CachedListVirtualDelegate<IReplElement> {
324
325
constructor(
326
private readonly configurationService: IConfigurationService,
327
private readonly replOptions: IReplOptions
328
) {
329
super();
330
}
331
332
override getHeight(element: IReplElement): number {
333
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
334
335
if (!config.console.wordWrap) {
336
return this.estimateHeight(element, true);
337
}
338
339
return super.getHeight(element);
340
}
341
342
/**
343
* With wordWrap enabled, this is an estimate. With wordWrap disabled, this is the real height that the list will use.
344
*/
345
protected estimateHeight(element: IReplElement, ignoreValueLength = false): number {
346
const lineHeight = this.replOptions.replConfiguration.lineHeight;
347
const countNumberOfLines = (str: string) => str.match(/\n/g)?.length ?? 0;
348
const hasValue = (e: any): e is { value: string } => typeof e.value === 'string';
349
350
if (hasValue(element) && !isNestedVariable(element)) {
351
const value = element.value;
352
const valueRows = countNumberOfLines(value)
353
+ (ignoreValueLength ? 0 : Math.floor(value.length / 70)) // Make an estimate for wrapping
354
+ (element instanceof ReplOutputElement ? 0 : 1); // A SimpleReplElement ends in \n if it's a complete line
355
356
return Math.max(valueRows, 1) * lineHeight;
357
}
358
359
return lineHeight;
360
}
361
362
getTemplateId(element: IReplElement): string {
363
if (element instanceof Variable || element instanceof ReplVariableElement) {
364
return ReplVariablesRenderer.ID;
365
}
366
if (element instanceof ReplEvaluationResult) {
367
return ReplEvaluationResultsRenderer.ID;
368
}
369
if (element instanceof ReplEvaluationInput) {
370
return ReplEvaluationInputsRenderer.ID;
371
}
372
if (element instanceof ReplOutputElement) {
373
return ReplOutputElementRenderer.ID;
374
}
375
if (element instanceof ReplGroup) {
376
return ReplGroupRenderer.ID;
377
}
378
379
return ReplRawObjectsRenderer.ID;
380
}
381
382
hasDynamicHeight(element: IReplElement): boolean {
383
if (isNestedVariable(element)) {
384
// Nested variables should always be in one line #111843
385
return false;
386
}
387
// Empty elements should not have dynamic height since they will be invisible
388
return element.toString().length > 0;
389
}
390
}
391
392
function isDebugSession(obj: any): obj is IDebugSession {
393
return typeof obj.getReplElements === 'function';
394
}
395
396
export class ReplDataSource implements IAsyncDataSource<IDebugSession, IReplElement> {
397
398
hasChildren(element: IReplElement | IDebugSession): boolean {
399
if (isDebugSession(element)) {
400
return true;
401
}
402
403
return !!(<IExpressionContainer | INestingReplElement>element).hasChildren;
404
}
405
406
getChildren(element: IReplElement | IDebugSession): Promise<IReplElement[]> {
407
if (isDebugSession(element)) {
408
return Promise.resolve(element.getReplElements());
409
}
410
411
return Promise.resolve((<IExpression | INestingReplElement>element).getChildren());
412
}
413
}
414
415
export class ReplAccessibilityProvider implements IListAccessibilityProvider<IReplElement> {
416
417
getWidgetAriaLabel(): string {
418
return localize('debugConsole', "Debug Console");
419
}
420
421
getAriaLabel(element: IReplElement): string {
422
if (element instanceof Variable) {
423
return localize('replVariableAriaLabel', "Variable {0}, value {1}", element.name, element.value);
424
}
425
if (element instanceof ReplOutputElement || element instanceof ReplEvaluationInput || element instanceof ReplEvaluationResult) {
426
return 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.'] },
427
", occurred {0} times", element.count) : '');
428
}
429
if (element instanceof RawObjectReplElement) {
430
return localize('replRawObjectAriaLabel', "Debug console variable {0}, value {1}", element.name, element.value);
431
}
432
if (element instanceof ReplGroup) {
433
return localize('replGroup', "Debug console group {0}", element.name);
434
}
435
436
return '';
437
}
438
}
439
440
class SourceWidget extends Disposable {
441
private readonly el: HTMLElement;
442
private source?: IReplElementSource;
443
private hover?: IManagedHover;
444
445
constructor(container: HTMLElement,
446
@IEditorService editorService: IEditorService,
447
@IHoverService private readonly hoverService: IHoverService,
448
@ILabelService private readonly labelService: ILabelService,
449
) {
450
super();
451
this.el = dom.append(container, $('.source'));
452
this._register(dom.addDisposableListener(this.el, 'click', e => {
453
e.preventDefault();
454
e.stopPropagation();
455
if (this.source) {
456
this.source.source.openInEditor(editorService, {
457
startLineNumber: this.source.lineNumber,
458
startColumn: this.source.column,
459
endLineNumber: this.source.lineNumber,
460
endColumn: this.source.column
461
});
462
}
463
}));
464
465
}
466
467
public setSource(source?: IReplElementSource) {
468
this.source = source;
469
this.el.textContent = source ? `${basename(source.source.name)}:${source.lineNumber}` : '';
470
471
this.hover ??= this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.el, ''));
472
this.hover.update(source ? `${this.labelService.getUriLabel(source.source.uri)}:${source.lineNumber}` : '');
473
}
474
}
475
476