Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/common/replModel.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 { Emitter, Event } from '../../../../base/common/event.js';
7
import severity from '../../../../base/common/severity.js';
8
import { isObject, isString } from '../../../../base/common/types.js';
9
import { generateUuid } from '../../../../base/common/uuid.js';
10
import * as nls from '../../../../nls.js';
11
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
12
import { IDebugConfiguration, IDebugSession, IExpression, INestingReplElement, IReplElement, IReplElementSource, IStackFrame } from './debug.js';
13
import { ExpressionContainer } from './debugModel.js';
14
15
let topReplElementCounter = 0;
16
const getUniqueId = () => `topReplElement:${topReplElementCounter++}`;
17
18
/**
19
* General case of data from DAP the `output` event. {@link ReplVariableElement}
20
* is used instead only if there is a `variablesReference` with no `output` text.
21
*/
22
export class ReplOutputElement implements INestingReplElement {
23
24
private _count = 1;
25
private _onDidChangeCount = new Emitter<void>();
26
27
constructor(
28
public session: IDebugSession,
29
private id: string,
30
public value: string,
31
public severity: severity,
32
public sourceData?: IReplElementSource,
33
public readonly expression?: IExpression,
34
) {
35
}
36
37
toString(includeSource = false): string {
38
let valueRespectCount = this.value;
39
for (let i = 1; i < this.count; i++) {
40
valueRespectCount += (valueRespectCount.endsWith('\n') ? '' : '\n') + this.value;
41
}
42
const sourceStr = (this.sourceData && includeSource) ? ` ${this.sourceData.source.name}` : '';
43
return valueRespectCount + sourceStr;
44
}
45
46
getId(): string {
47
return this.id;
48
}
49
50
getChildren(): Promise<IReplElement[]> {
51
return this.expression?.getChildren() || Promise.resolve([]);
52
}
53
54
set count(value: number) {
55
this._count = value;
56
this._onDidChangeCount.fire();
57
}
58
59
get count(): number {
60
return this._count;
61
}
62
63
get onDidChangeCount(): Event<void> {
64
return this._onDidChangeCount.event;
65
}
66
67
get hasChildren() {
68
return !!this.expression?.hasChildren;
69
}
70
}
71
72
/** Top-level variable logged via DAP output when there's no `output` string */
73
export class ReplVariableElement implements INestingReplElement {
74
public readonly hasChildren: boolean;
75
private readonly id = generateUuid();
76
77
constructor(
78
private readonly session: IDebugSession,
79
public readonly expression: IExpression,
80
public readonly severity: severity,
81
public readonly sourceData?: IReplElementSource,
82
) {
83
this.hasChildren = expression.hasChildren;
84
}
85
86
getSession() {
87
return this.session;
88
}
89
90
getChildren(): IReplElement[] | Promise<IReplElement[]> {
91
return this.expression.getChildren();
92
}
93
94
toString(): string {
95
return this.expression.toString();
96
}
97
98
getId(): string {
99
return this.id;
100
}
101
}
102
103
export class RawObjectReplElement implements IExpression, INestingReplElement {
104
105
private static readonly MAX_CHILDREN = 1000; // upper bound of children per value
106
107
constructor(private id: string, public name: string, public valueObj: any, public sourceData?: IReplElementSource, public annotation?: string) { }
108
109
getId(): string {
110
return this.id;
111
}
112
113
getSession(): IDebugSession | undefined {
114
return undefined;
115
}
116
117
get value(): string {
118
if (this.valueObj === null) {
119
return 'null';
120
} else if (Array.isArray(this.valueObj)) {
121
return `Array[${this.valueObj.length}]`;
122
} else if (isObject(this.valueObj)) {
123
return 'Object';
124
} else if (isString(this.valueObj)) {
125
return `"${this.valueObj}"`;
126
}
127
128
return String(this.valueObj) || '';
129
}
130
131
get hasChildren(): boolean {
132
return (Array.isArray(this.valueObj) && this.valueObj.length > 0) || (isObject(this.valueObj) && Object.getOwnPropertyNames(this.valueObj).length > 0);
133
}
134
135
evaluateLazy(): Promise<void> {
136
throw new Error('Method not implemented.');
137
}
138
139
getChildren(): Promise<IExpression[]> {
140
let result: IExpression[] = [];
141
if (Array.isArray(this.valueObj)) {
142
result = (<any[]>this.valueObj).slice(0, RawObjectReplElement.MAX_CHILDREN)
143
.map((v, index) => new RawObjectReplElement(`${this.id}:${index}`, String(index), v));
144
} else if (isObject(this.valueObj)) {
145
result = Object.getOwnPropertyNames(this.valueObj).slice(0, RawObjectReplElement.MAX_CHILDREN)
146
.map((key, index) => new RawObjectReplElement(`${this.id}:${index}`, key, this.valueObj[key]));
147
}
148
149
return Promise.resolve(result);
150
}
151
152
toString(): string {
153
return `${this.name}\n${this.value}`;
154
}
155
}
156
157
export class ReplEvaluationInput implements IReplElement {
158
private id: string;
159
160
constructor(public value: string) {
161
this.id = generateUuid();
162
}
163
164
toString(): string {
165
return this.value;
166
}
167
168
getId(): string {
169
return this.id;
170
}
171
}
172
173
export class ReplEvaluationResult extends ExpressionContainer implements IReplElement {
174
private _available = true;
175
176
get available(): boolean {
177
return this._available;
178
}
179
180
constructor(public readonly originalExpression: string) {
181
super(undefined, undefined, 0, generateUuid());
182
}
183
184
override async evaluateExpression(expression: string, session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string): Promise<boolean> {
185
const result = await super.evaluateExpression(expression, session, stackFrame, context);
186
this._available = result;
187
188
return result;
189
}
190
191
override toString(): string {
192
return `${this.value}`;
193
}
194
}
195
196
export class ReplGroup implements INestingReplElement {
197
198
private children: IReplElement[] = [];
199
private id: string;
200
private ended = false;
201
static COUNTER = 0;
202
203
constructor(
204
public readonly session: IDebugSession,
205
public name: string,
206
public autoExpand: boolean,
207
public sourceData?: IReplElementSource
208
) {
209
this.id = `replGroup:${ReplGroup.COUNTER++}`;
210
}
211
212
get hasChildren() {
213
return true;
214
}
215
216
getId(): string {
217
return this.id;
218
}
219
220
toString(includeSource = false): string {
221
const sourceStr = (includeSource && this.sourceData) ? ` ${this.sourceData.source.name}` : '';
222
return this.name + sourceStr;
223
}
224
225
addChild(child: IReplElement): void {
226
const lastElement = this.children.length ? this.children[this.children.length - 1] : undefined;
227
if (lastElement instanceof ReplGroup && !lastElement.hasEnded) {
228
lastElement.addChild(child);
229
} else {
230
this.children.push(child);
231
}
232
}
233
234
getChildren(): IReplElement[] {
235
return this.children;
236
}
237
238
end(): void {
239
const lastElement = this.children.length ? this.children[this.children.length - 1] : undefined;
240
if (lastElement instanceof ReplGroup && !lastElement.hasEnded) {
241
lastElement.end();
242
} else {
243
this.ended = true;
244
}
245
}
246
247
get hasEnded(): boolean {
248
return this.ended;
249
}
250
}
251
252
function areSourcesEqual(first: IReplElementSource | undefined, second: IReplElementSource | undefined): boolean {
253
if (!first && !second) {
254
return true;
255
}
256
if (first && second) {
257
return first.column === second.column && first.lineNumber === second.lineNumber && first.source.uri.toString() === second.source.uri.toString();
258
}
259
260
return false;
261
}
262
263
export interface INewReplElementData {
264
output: string;
265
expression?: IExpression;
266
sev: severity;
267
source?: IReplElementSource;
268
}
269
270
export class ReplModel {
271
private replElements: IReplElement[] = [];
272
private readonly _onDidChangeElements = new Emitter<IReplElement | undefined>();
273
readonly onDidChangeElements = this._onDidChangeElements.event;
274
275
constructor(private readonly configurationService: IConfigurationService) { }
276
277
getReplElements(): IReplElement[] {
278
return this.replElements;
279
}
280
281
async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, expression: string): Promise<void> {
282
this.addReplElement(new ReplEvaluationInput(expression));
283
const result = new ReplEvaluationResult(expression);
284
await result.evaluateExpression(expression, session, stackFrame, 'repl');
285
this.addReplElement(result);
286
}
287
288
appendToRepl(session: IDebugSession, { output, expression, sev, source }: INewReplElementData): void {
289
const clearAnsiSequence = '\u001b[2J';
290
const clearAnsiIndex = output.lastIndexOf(clearAnsiSequence);
291
if (clearAnsiIndex !== -1) {
292
// [2J is the ansi escape sequence for clearing the display http://ascii-table.com/ansi-escape-sequences.php
293
this.removeReplExpressions();
294
this.appendToRepl(session, { output: nls.localize('consoleCleared', "Console was cleared"), sev: severity.Ignore });
295
output = output.substring(clearAnsiIndex + clearAnsiSequence.length);
296
}
297
298
if (expression) {
299
// if there is an output string, prefer to show that, since the DA could
300
// have formatted it nicely e.g. with ANSI color codes.
301
this.addReplElement(output
302
? new ReplOutputElement(session, getUniqueId(), output, sev, source, expression)
303
: new ReplVariableElement(session, expression, sev, source));
304
return;
305
}
306
307
this.appendOutputToRepl(session, output, sev, source);
308
}
309
310
private appendOutputToRepl(session: IDebugSession, output: string, sev: severity, source?: IReplElementSource): void {
311
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
312
const previousElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined;
313
314
// Handle concatenation of incomplete lines first
315
if (previousElement instanceof ReplOutputElement && previousElement.severity === sev && areSourcesEqual(previousElement.sourceData, source)) {
316
if (!previousElement.value.endsWith('\n') && !previousElement.value.endsWith('\r\n') && previousElement.count === 1) {
317
// Concatenate with previous incomplete line
318
const combinedOutput = previousElement.value + output;
319
this.replElements[this.replElements.length - 1] = new ReplOutputElement(
320
session, getUniqueId(), combinedOutput, sev, source);
321
this._onDidChangeElements.fire(undefined);
322
323
// If the combined output now forms a complete line and collapsing is enabled,
324
// check if it can be collapsed with previous elements
325
if (config.console.collapseIdenticalLines && combinedOutput.endsWith('\n')) {
326
this.tryCollapseCompleteLine(sev, source);
327
}
328
329
// If the combined output contains multiple lines, apply line-level collapsing
330
if (config.console.collapseIdenticalLines && combinedOutput.includes('\n')) {
331
const lines = this.splitIntoLines(combinedOutput);
332
if (lines.length > 1) {
333
this.applyLineLevelCollapsing(session, sev, source);
334
}
335
}
336
return;
337
}
338
}
339
340
// If collapsing is enabled and the output contains line breaks, parse and collapse at line level
341
if (config.console.collapseIdenticalLines && output.includes('\n')) {
342
this.processMultiLineOutput(session, output, sev, source);
343
} else {
344
// For simple output without line breaks, use the original logic
345
if (previousElement instanceof ReplOutputElement && previousElement.severity === sev && areSourcesEqual(previousElement.sourceData, source)) {
346
if (previousElement.value === output && config.console.collapseIdenticalLines) {
347
previousElement.count++;
348
// No need to fire an event, just the count updates and badge will adjust automatically
349
return;
350
}
351
}
352
353
const element = new ReplOutputElement(session, getUniqueId(), output, sev, source);
354
this.addReplElement(element);
355
}
356
}
357
358
private tryCollapseCompleteLine(sev: severity, source?: IReplElementSource): void {
359
// Try to collapse the last element with the second-to-last if they are identical complete lines
360
if (this.replElements.length < 2) {
361
return;
362
}
363
364
const lastElement = this.replElements[this.replElements.length - 1];
365
const secondToLastElement = this.replElements[this.replElements.length - 2];
366
367
if (lastElement instanceof ReplOutputElement &&
368
secondToLastElement instanceof ReplOutputElement &&
369
lastElement.severity === sev &&
370
secondToLastElement.severity === sev &&
371
areSourcesEqual(lastElement.sourceData, source) &&
372
areSourcesEqual(secondToLastElement.sourceData, source) &&
373
lastElement.value === secondToLastElement.value &&
374
lastElement.count === 1 &&
375
lastElement.value.endsWith('\n')) {
376
377
// Collapse the last element into the second-to-last
378
secondToLastElement.count += lastElement.count;
379
this.replElements.pop();
380
this._onDidChangeElements.fire(undefined);
381
}
382
}
383
384
private processMultiLineOutput(session: IDebugSession, output: string, sev: severity, source?: IReplElementSource): void {
385
// Split output into lines, preserving line endings
386
const lines = this.splitIntoLines(output);
387
388
for (const line of lines) {
389
if (line.length === 0) { continue; }
390
391
const previousElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined;
392
393
// Check if this line can be collapsed with the previous one
394
if (previousElement instanceof ReplOutputElement &&
395
previousElement.severity === sev &&
396
areSourcesEqual(previousElement.sourceData, source) &&
397
previousElement.value === line) {
398
previousElement.count++;
399
// No need to fire an event, just the count updates and badge will adjust automatically
400
} else {
401
const element = new ReplOutputElement(session, getUniqueId(), line, sev, source);
402
this.addReplElement(element);
403
}
404
}
405
}
406
407
private splitIntoLines(text: string): string[] {
408
// Split text into lines while preserving line endings, using indexOf for efficiency
409
const lines: string[] = [];
410
let start = 0;
411
412
while (start < text.length) {
413
const nextLF = text.indexOf('\n', start);
414
if (nextLF === -1) {
415
lines.push(text.substring(start));
416
break;
417
}
418
lines.push(text.substring(start, nextLF + 1));
419
start = nextLF + 1;
420
}
421
422
return lines;
423
}
424
425
private applyLineLevelCollapsing(session: IDebugSession, sev: severity, source?: IReplElementSource): void {
426
// Apply line-level collapsing to the last element if it contains multiple lines
427
const lastElement = this.replElements[this.replElements.length - 1];
428
if (!(lastElement instanceof ReplOutputElement) || lastElement.severity !== sev || !areSourcesEqual(lastElement.sourceData, source)) {
429
return;
430
}
431
432
const lines = this.splitIntoLines(lastElement.value);
433
if (lines.length <= 1) {
434
return; // No multiple lines to collapse
435
}
436
437
// Remove the last element and reprocess it as multiple lines
438
this.replElements.pop();
439
440
// Process each line and try to collapse with existing elements
441
for (const line of lines) {
442
if (line.length === 0) { continue; }
443
444
const previousElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined;
445
446
// Check if this line can be collapsed with the previous one
447
if (previousElement instanceof ReplOutputElement &&
448
previousElement.severity === sev &&
449
areSourcesEqual(previousElement.sourceData, source) &&
450
previousElement.value === line) {
451
previousElement.count++;
452
} else {
453
const element = new ReplOutputElement(session, getUniqueId(), line, sev, source);
454
this.addReplElement(element);
455
}
456
}
457
458
this._onDidChangeElements.fire(undefined);
459
}
460
461
startGroup(session: IDebugSession, name: string, autoExpand: boolean, sourceData?: IReplElementSource): void {
462
const group = new ReplGroup(session, name, autoExpand, sourceData);
463
this.addReplElement(group);
464
}
465
466
endGroup(): void {
467
const lastElement = this.replElements[this.replElements.length - 1];
468
if (lastElement instanceof ReplGroup) {
469
lastElement.end();
470
}
471
}
472
473
private addReplElement(newElement: IReplElement): void {
474
const lastElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined;
475
if (lastElement instanceof ReplGroup && !lastElement.hasEnded) {
476
lastElement.addChild(newElement);
477
} else {
478
this.replElements.push(newElement);
479
const config = this.configurationService.getValue<IDebugConfiguration>('debug');
480
if (this.replElements.length > config.console.maximumLines) {
481
this.replElements.splice(0, this.replElements.length - config.console.maximumLines);
482
}
483
}
484
this._onDidChangeElements.fire(newElement);
485
}
486
487
removeReplExpressions(): void {
488
if (this.replElements.length > 0) {
489
this.replElements = [];
490
this._onDidChangeElements.fire(undefined);
491
}
492
}
493
494
/** Returns a new REPL model that's a copy of this one. */
495
clone() {
496
const newRepl = new ReplModel(this.configurationService);
497
newRepl.replElements = this.replElements.slice();
498
return newRepl;
499
}
500
}
501
502