Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/output/common/outputChannelModel.ts
4780 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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
7
import * as resources from '../../../../base/common/resources.js';
8
import { ITextModel } from '../../../../editor/common/model.js';
9
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { Promises, ThrottledDelayer } from '../../../../base/common/async.js';
13
import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js';
14
import { IModelService } from '../../../../editor/common/services/model.js';
15
import { ILanguageSelection } from '../../../../editor/common/languages/language.js';
16
import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js';
17
import { isNumber } from '../../../../base/common/types.js';
18
import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js';
19
import { Position } from '../../../../editor/common/core/position.js';
20
import { Range } from '../../../../editor/common/core/range.js';
21
import { VSBuffer } from '../../../../base/common/buffer.js';
22
import { ILogger, ILoggerService, ILogService, LogLevel } from '../../../../platform/log/common/log.js';
23
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
24
import { ILogEntry, IOutputContentSource, LOG_MIME, OutputChannelUpdateMode } from '../../../services/output/common/output.js';
25
import { isCancellationError } from '../../../../base/common/errors.js';
26
import { TextModel } from '../../../../editor/common/model/textModel.js';
27
import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js';
28
29
const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\])?/;
30
31
export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null {
32
const lineContent = model.getLineContent(lineNumber);
33
const match = LOG_ENTRY_REGEX.exec(lineContent);
34
if (match) {
35
const timestamp = new Date(match[1]).getTime();
36
const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length);
37
const logLevel = parseLogLevel(match[3]);
38
const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length);
39
const category = match[5];
40
const startLine = lineNumber;
41
let endLine = lineNumber;
42
43
const lineCount = model.getLineCount();
44
while (endLine < lineCount) {
45
const nextLineContent = model.getLineContent(endLine + 1);
46
const isLastLine = endLine + 1 === lineCount && nextLineContent === ''; // Last line will be always empty
47
if (LOG_ENTRY_REGEX.test(nextLineContent) || isLastLine) {
48
break;
49
}
50
endLine++;
51
}
52
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
53
return { range, timestamp, timestampRange, logLevel, logLevelRange, category };
54
}
55
return null;
56
}
57
58
function* logEntryIterator<T>(model: ITextModel, process: (logEntry: ILogEntry) => T): IterableIterator<T> {
59
for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) {
60
const logEntry = parseLogEntryAt(model, lineNumber);
61
if (logEntry) {
62
yield process(logEntry);
63
lineNumber = logEntry.range.endLineNumber;
64
}
65
}
66
}
67
68
function changeStartLineNumber(logEntry: ILogEntry, lineNumber: number): ILogEntry {
69
return {
70
...logEntry,
71
range: new Range(lineNumber, logEntry.range.startColumn, lineNumber + logEntry.range.endLineNumber - logEntry.range.startLineNumber, logEntry.range.endColumn),
72
timestampRange: new Range(lineNumber, logEntry.timestampRange.startColumn, lineNumber, logEntry.timestampRange.endColumn),
73
logLevelRange: new Range(lineNumber, logEntry.logLevelRange.startColumn, lineNumber, logEntry.logLevelRange.endColumn),
74
};
75
}
76
77
function parseLogLevel(level: string): LogLevel {
78
switch (level.toLowerCase()) {
79
case 'trace':
80
return LogLevel.Trace;
81
case 'debug':
82
return LogLevel.Debug;
83
case 'info':
84
return LogLevel.Info;
85
case 'warning':
86
return LogLevel.Warning;
87
case 'error':
88
return LogLevel.Error;
89
default:
90
throw new Error(`Unknown log level: ${level}`);
91
}
92
}
93
94
export interface IOutputChannelModel extends IDisposable {
95
readonly onDispose: Event<void>;
96
readonly source: IOutputContentSource | ReadonlyArray<IOutputContentSource>;
97
getLogEntries(): ReadonlyArray<ILogEntry>;
98
append(output: string): void;
99
update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void;
100
updateChannelSources(sources: ReadonlyArray<IOutputContentSource>): void;
101
loadModel(): Promise<ITextModel>;
102
clear(): void;
103
replace(value: string): void;
104
}
105
106
interface IContentProvider {
107
readonly onDidAppend: Event<void>;
108
readonly onDidReset: Event<void>;
109
reset(): void;
110
watch(): void;
111
unwatch(): void;
112
getContent(): Promise<{ readonly content: string; readonly consume: () => void }>;
113
getLogEntries(): ReadonlyArray<ILogEntry>;
114
}
115
116
class FileContentProvider extends Disposable implements IContentProvider {
117
118
private readonly _onDidAppend = new Emitter<void>();
119
get onDidAppend() { return this._onDidAppend.event; }
120
121
private readonly _onDidReset = new Emitter<void>();
122
get onDidReset() { return this._onDidReset.event; }
123
124
private watching: boolean = false;
125
private syncDelayer: ThrottledDelayer<void>;
126
private etag: string | undefined = '';
127
128
private logEntries: ILogEntry[] = [];
129
private startOffset: number = 0;
130
private endOffset: number = 0;
131
132
readonly resource: URI;
133
readonly name: string;
134
135
constructor(
136
{ name, resource }: IOutputContentSource,
137
@IFileService private readonly fileService: IFileService,
138
@IInstantiationService private readonly instantiationService: IInstantiationService,
139
@ILogService private readonly logService: ILogService,
140
) {
141
super();
142
143
this.name = name ?? '';
144
this.resource = resource;
145
this.syncDelayer = new ThrottledDelayer<void>(500);
146
this._register(toDisposable(() => this.unwatch()));
147
}
148
149
reset(offset?: number): void {
150
this.endOffset = this.startOffset = offset ?? this.startOffset;
151
this.logEntries = [];
152
}
153
154
resetToEnd(): void {
155
this.startOffset = this.endOffset;
156
this.logEntries = [];
157
}
158
159
watch(): void {
160
if (!this.watching) {
161
this.logService.trace('Started polling', this.resource.toString());
162
this.poll();
163
this.watching = true;
164
}
165
}
166
167
unwatch(): void {
168
if (this.watching) {
169
this.syncDelayer.cancel();
170
this.watching = false;
171
this.logService.trace('Stopped polling', this.resource.toString());
172
}
173
}
174
175
private poll(): void {
176
const loop = () => this.doWatch().then(() => this.poll());
177
this.syncDelayer.trigger(loop).catch(error => {
178
if (!isCancellationError(error)) {
179
throw error;
180
}
181
});
182
}
183
184
private async doWatch(): Promise<void> {
185
try {
186
if (!this.fileService.hasProvider(this.resource)) {
187
return;
188
}
189
const stat = await this.fileService.stat(this.resource);
190
if (stat.etag !== this.etag) {
191
this.etag = stat.etag;
192
if (isNumber(stat.size) && this.endOffset > stat.size) {
193
this.reset(0);
194
this._onDidReset.fire();
195
} else {
196
this._onDidAppend.fire();
197
}
198
}
199
} catch (error) {
200
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
201
throw error;
202
}
203
}
204
}
205
206
getLogEntries(): ReadonlyArray<ILogEntry> {
207
return this.logEntries;
208
}
209
210
async getContent(donotConsumeLogEntries?: boolean): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> {
211
try {
212
if (!this.fileService.hasProvider(this.resource)) {
213
return {
214
name: this.name,
215
content: '',
216
consume: () => { /* No Op */ }
217
};
218
}
219
const fileContent = await this.fileService.readFile(this.resource, { position: this.endOffset });
220
const content = fileContent.value.toString();
221
const logEntries = donotConsumeLogEntries ? [] : this.parseLogEntries(content, this.logEntries[this.logEntries.length - 1]);
222
let consumed = false;
223
return {
224
name: this.name,
225
content,
226
consume: () => {
227
if (!consumed) {
228
consumed = true;
229
this.endOffset += fileContent.value.byteLength;
230
this.etag = fileContent.etag;
231
this.logEntries.push(...logEntries);
232
}
233
}
234
};
235
} catch (error) {
236
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
237
throw error;
238
}
239
return {
240
name: this.name,
241
content: '',
242
consume: () => { /* No Op */ }
243
};
244
}
245
}
246
247
private parseLogEntries(content: string, lastLogEntry: ILogEntry | undefined): ILogEntry[] {
248
const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);
249
try {
250
if (!parseLogEntryAt(model, 1)) {
251
return [];
252
}
253
const logEntries: ILogEntry[] = [];
254
let logEntryStartLineNumber = lastLogEntry ? lastLogEntry.range.endLineNumber + 1 : 1;
255
for (const entry of logEntryIterator(model, (e) => changeStartLineNumber(e, logEntryStartLineNumber))) {
256
logEntries.push(entry);
257
logEntryStartLineNumber = entry.range.endLineNumber + 1;
258
}
259
return logEntries;
260
} finally {
261
model.dispose();
262
}
263
}
264
}
265
266
class MultiFileContentProvider extends Disposable implements IContentProvider {
267
268
private readonly _onDidAppend = this._register(new Emitter<void>());
269
readonly onDidAppend = this._onDidAppend.event;
270
readonly onDidReset = Event.None;
271
272
private logEntries: ILogEntry[] = [];
273
private readonly fileContentProviderItems: [FileContentProvider, DisposableStore][] = [];
274
275
private watching: boolean = false;
276
277
constructor(
278
filesInfos: IOutputContentSource[],
279
@IInstantiationService private readonly instantiationService: IInstantiationService,
280
@IFileService private readonly fileService: IFileService,
281
@ILogService private readonly logService: ILogService,
282
) {
283
super();
284
for (const file of filesInfos) {
285
this.fileContentProviderItems.push(this.createFileContentProvider(file));
286
}
287
this._register(toDisposable(() => {
288
for (const [, disposables] of this.fileContentProviderItems) {
289
disposables.dispose();
290
}
291
}));
292
}
293
294
private createFileContentProvider(file: IOutputContentSource): [FileContentProvider, DisposableStore] {
295
const disposables = new DisposableStore();
296
const fileOutput = disposables.add(new FileContentProvider(file, this.fileService, this.instantiationService, this.logService));
297
disposables.add(fileOutput.onDidAppend(() => this._onDidAppend.fire()));
298
return [fileOutput, disposables];
299
}
300
301
watch(): void {
302
if (!this.watching) {
303
this.watching = true;
304
for (const [output] of this.fileContentProviderItems) {
305
output.watch();
306
}
307
}
308
}
309
310
unwatch(): void {
311
if (this.watching) {
312
this.watching = false;
313
for (const [output] of this.fileContentProviderItems) {
314
output.unwatch();
315
}
316
}
317
}
318
319
updateFiles(files: IOutputContentSource[]): void {
320
const wasWatching = this.watching;
321
if (wasWatching) {
322
this.unwatch();
323
}
324
325
const result = sortedDiff(this.fileContentProviderItems.map(([output]) => output), files, (a, b) => resources.extUri.compare(a.resource, b.resource));
326
for (const { start, deleteCount, toInsert } of result) {
327
const outputs = toInsert.map(file => this.createFileContentProvider(file));
328
const outputsToRemove = this.fileContentProviderItems.splice(start, deleteCount, ...outputs);
329
for (const [, disposables] of outputsToRemove) {
330
disposables.dispose();
331
}
332
}
333
334
if (wasWatching) {
335
this.watch();
336
}
337
}
338
339
reset(): void {
340
for (const [output] of this.fileContentProviderItems) {
341
output.reset();
342
}
343
this.logEntries = [];
344
}
345
346
resetToEnd(): void {
347
for (const [output] of this.fileContentProviderItems) {
348
output.resetToEnd();
349
}
350
this.logEntries = [];
351
}
352
353
getLogEntries(): ReadonlyArray<ILogEntry> {
354
return this.logEntries;
355
}
356
357
async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> {
358
const outputs = await Promise.all(this.fileContentProviderItems.map(([output]) => output.getContent(true)));
359
const { content, logEntries } = this.combineLogEntries(outputs, this.logEntries[this.logEntries.length - 1]);
360
let consumed = false;
361
return {
362
content,
363
consume: () => {
364
if (!consumed) {
365
consumed = true;
366
outputs.forEach(({ consume }) => consume());
367
this.logEntries.push(...logEntries);
368
}
369
}
370
};
371
}
372
373
private combineLogEntries(outputs: { content: string; name: string }[], lastEntry: ILogEntry | undefined): { logEntries: ILogEntry[]; content: string } {
374
375
outputs = outputs.filter(output => !!output.content);
376
377
if (outputs.length === 0) {
378
return { logEntries: [], content: '' };
379
}
380
381
const logEntries: ILogEntry[] = [];
382
const contents: string[] = [];
383
const process = (model: ITextModel, logEntry: ILogEntry, name: string): [ILogEntry, string] => {
384
const lineContent = model.getValueInRange(logEntry.range);
385
const content = name ? `${lineContent.substring(0, logEntry.logLevelRange.endColumn)} [${name}]${lineContent.substring(logEntry.logLevelRange.endColumn)}` : lineContent;
386
return [{
387
...logEntry,
388
category: name,
389
range: new Range(logEntry.range.startLineNumber, logEntry.logLevelRange.startColumn, logEntry.range.endLineNumber, name ? logEntry.range.endColumn + name.length + 3 : logEntry.range.endColumn),
390
}, content];
391
};
392
393
const model = this.instantiationService.createInstance(TextModel, outputs[0].content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);
394
try {
395
for (const [logEntry, content] of logEntryIterator(model, (e) => process(model, e, outputs[0].name))) {
396
logEntries.push(logEntry);
397
contents.push(content);
398
}
399
} finally {
400
model.dispose();
401
}
402
403
for (let index = 1; index < outputs.length; index++) {
404
const { content, name } = outputs[index];
405
const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);
406
try {
407
const iterator = logEntryIterator(model, (e) => process(model, e, name));
408
let next = iterator.next();
409
while (!next.done) {
410
const [logEntry, content] = next.value;
411
const logEntriesToAdd = [logEntry];
412
const contentsToAdd = [content];
413
414
let insertionIndex;
415
416
// If the timestamp is greater than or equal to the last timestamp,
417
// we can just append all the entries at the end
418
if (logEntry.timestamp >= logEntries[logEntries.length - 1].timestamp) {
419
insertionIndex = logEntries.length;
420
for (next = iterator.next(); !next.done; next = iterator.next()) {
421
logEntriesToAdd.push(next.value[0]);
422
contentsToAdd.push(next.value[1]);
423
}
424
}
425
else {
426
if (logEntry.timestamp <= logEntries[0].timestamp) {
427
// If the timestamp is less than or equal to the first timestamp
428
// then insert at the beginning
429
insertionIndex = 0;
430
} else {
431
// Otherwise, find the insertion index
432
const idx = binarySearch(logEntries, logEntry, (a, b) => a.timestamp - b.timestamp);
433
insertionIndex = idx < 0 ? ~idx : idx;
434
}
435
436
// Collect all entries that have a timestamp less than or equal to the timestamp at the insertion index
437
for (next = iterator.next(); !next.done && next.value[0].timestamp <= logEntries[insertionIndex].timestamp; next = iterator.next()) {
438
logEntriesToAdd.push(next.value[0]);
439
contentsToAdd.push(next.value[1]);
440
}
441
}
442
443
contents.splice(insertionIndex, 0, ...contentsToAdd);
444
logEntries.splice(insertionIndex, 0, ...logEntriesToAdd);
445
}
446
} finally {
447
model.dispose();
448
}
449
}
450
451
let content = '';
452
const updatedLogEntries: ILogEntry[] = [];
453
let logEntryStartLineNumber = lastEntry ? lastEntry.range.endLineNumber + 1 : 1;
454
for (let i = 0; i < logEntries.length; i++) {
455
content += contents[i] + '\n';
456
const updatedLogEntry = changeStartLineNumber(logEntries[i], logEntryStartLineNumber);
457
updatedLogEntries.push(updatedLogEntry);
458
logEntryStartLineNumber = updatedLogEntry.range.endLineNumber + 1;
459
}
460
461
return { logEntries: updatedLogEntries, content };
462
}
463
464
}
465
466
export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel {
467
468
private readonly _onDispose = this._register(new Emitter<void>());
469
readonly onDispose: Event<void> = this._onDispose.event;
470
471
protected loadModelPromise: Promise<ITextModel> | null = null;
472
473
private readonly modelDisposable = this._register(new MutableDisposable<DisposableStore>());
474
protected model: ITextModel | null = null;
475
private modelUpdateInProgress: boolean = false;
476
private readonly modelUpdateCancellationSource = this._register(new MutableDisposable<CancellationTokenSource>());
477
private readonly appendThrottler = this._register(new ThrottledDelayer(300));
478
private replacePromise: Promise<void> | undefined;
479
480
abstract readonly source: IOutputContentSource | ReadonlyArray<IOutputContentSource>;
481
482
constructor(
483
private readonly modelUri: URI,
484
private readonly language: ILanguageSelection,
485
private readonly outputContentProvider: IContentProvider,
486
@IModelService protected readonly modelService: IModelService,
487
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
488
) {
489
super();
490
}
491
492
async loadModel(): Promise<ITextModel> {
493
this.loadModelPromise = Promises.withAsyncBody<ITextModel>(async (c, e) => {
494
try {
495
this.modelDisposable.value = new DisposableStore();
496
this.model = this.modelService.createModel('', this.language, this.modelUri);
497
const { content, consume } = await this.outputContentProvider.getContent();
498
consume();
499
this.doAppendContent(this.model, content);
500
this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true)));
501
this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false)));
502
this.outputContentProvider.watch();
503
this.modelDisposable.value.add(toDisposable(() => this.outputContentProvider.unwatch()));
504
this.modelDisposable.value.add(this.model.onWillDispose(() => {
505
this.outputContentProvider.reset();
506
this.modelDisposable.value = undefined;
507
this.cancelModelUpdate();
508
this.model = null;
509
}));
510
c(this.model);
511
} catch (error) {
512
e(error);
513
}
514
});
515
return this.loadModelPromise;
516
}
517
518
getLogEntries(): readonly ILogEntry[] {
519
return this.outputContentProvider.getLogEntries();
520
}
521
522
private onDidContentChange(reset: boolean, appendImmediately: boolean): void {
523
if (reset && !this.modelUpdateInProgress) {
524
this.doUpdate(OutputChannelUpdateMode.Clear, true);
525
}
526
this.doUpdate(OutputChannelUpdateMode.Append, appendImmediately);
527
}
528
529
protected doUpdate(mode: OutputChannelUpdateMode, immediate: boolean): void {
530
if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) {
531
this.cancelModelUpdate();
532
}
533
if (!this.model) {
534
return;
535
}
536
537
this.modelUpdateInProgress = true;
538
if (!this.modelUpdateCancellationSource.value) {
539
this.modelUpdateCancellationSource.value = new CancellationTokenSource();
540
}
541
const token = this.modelUpdateCancellationSource.value.token;
542
543
if (mode === OutputChannelUpdateMode.Clear) {
544
this.clearContent(this.model);
545
}
546
547
else if (mode === OutputChannelUpdateMode.Replace) {
548
this.replacePromise = this.replaceContent(this.model, token).finally(() => this.replacePromise = undefined);
549
}
550
551
else {
552
this.appendContent(this.model, immediate, token);
553
}
554
}
555
556
private clearContent(model: ITextModel): void {
557
model.applyEdits([EditOperation.delete(model.getFullModelRange())]);
558
this.modelUpdateInProgress = false;
559
}
560
561
private appendContent(model: ITextModel, immediate: boolean, token: CancellationToken): void {
562
this.appendThrottler.trigger(async () => {
563
/* Abort if operation is cancelled */
564
if (token.isCancellationRequested) {
565
return;
566
}
567
568
/* Wait for replace to finish */
569
if (this.replacePromise) {
570
try { await this.replacePromise; } catch (e) { /* Ignore */ }
571
/* Abort if operation is cancelled */
572
if (token.isCancellationRequested) {
573
return;
574
}
575
}
576
577
/* Get content to append */
578
const { content, consume } = await this.outputContentProvider.getContent();
579
/* Abort if operation is cancelled */
580
if (token.isCancellationRequested) {
581
return;
582
}
583
584
/* Appned Content */
585
consume();
586
this.doAppendContent(model, content);
587
this.modelUpdateInProgress = false;
588
}, immediate ? 0 : undefined).catch(error => {
589
if (!isCancellationError(error)) {
590
throw error;
591
}
592
});
593
}
594
595
private doAppendContent(model: ITextModel, content: string): void {
596
const lastLine = model.getLineCount();
597
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
598
model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), content)]);
599
}
600
601
private async replaceContent(model: ITextModel, token: CancellationToken): Promise<void> {
602
/* Get content to replace */
603
const { content, consume } = await this.outputContentProvider.getContent();
604
/* Abort if operation is cancelled */
605
if (token.isCancellationRequested) {
606
return;
607
}
608
609
/* Compute Edits */
610
const edits = await this.getReplaceEdits(model, content.toString());
611
/* Abort if operation is cancelled */
612
if (token.isCancellationRequested) {
613
return;
614
}
615
616
consume();
617
if (edits.length) {
618
/* Apply Edits */
619
model.applyEdits(edits);
620
}
621
this.modelUpdateInProgress = false;
622
}
623
624
private async getReplaceEdits(model: ITextModel, contentToReplace: string): Promise<ISingleEditOperation[]> {
625
if (!contentToReplace) {
626
return [EditOperation.delete(model.getFullModelRange())];
627
}
628
if (contentToReplace !== model.getValue()) {
629
const edits = await this.editorWorkerService.computeMoreMinimalEdits(model.uri, [{ text: contentToReplace.toString(), range: model.getFullModelRange() }]);
630
if (edits?.length) {
631
return edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
632
}
633
}
634
return [];
635
}
636
637
protected cancelModelUpdate(): void {
638
this.modelUpdateCancellationSource.value?.cancel();
639
this.modelUpdateCancellationSource.value = undefined;
640
this.appendThrottler.cancel();
641
this.replacePromise = undefined;
642
this.modelUpdateInProgress = false;
643
}
644
645
protected isVisible(): boolean {
646
return !!this.model;
647
}
648
649
override dispose(): void {
650
this._onDispose.fire();
651
super.dispose();
652
}
653
654
append(message: string): void { throw new Error('Not supported'); }
655
replace(message: string): void { throw new Error('Not supported'); }
656
657
abstract clear(): void;
658
abstract update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void;
659
abstract updateChannelSources(files: IOutputContentSource[]): void;
660
}
661
662
export class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel {
663
664
private readonly fileOutput: FileContentProvider;
665
666
constructor(
667
modelUri: URI,
668
language: ILanguageSelection,
669
readonly source: IOutputContentSource,
670
@IFileService fileService: IFileService,
671
@IModelService modelService: IModelService,
672
@IInstantiationService instantiationService: IInstantiationService,
673
@ILogService logService: ILogService,
674
@IEditorWorkerService editorWorkerService: IEditorWorkerService,
675
) {
676
const fileOutput = new FileContentProvider(source, fileService, instantiationService, logService);
677
super(modelUri, language, fileOutput, modelService, editorWorkerService);
678
this.fileOutput = this._register(fileOutput);
679
}
680
681
override clear(): void {
682
this.update(OutputChannelUpdateMode.Clear, undefined, true);
683
}
684
685
override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void {
686
const loadModelPromise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve();
687
loadModelPromise.then(() => {
688
if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) {
689
if (isNumber(till)) {
690
this.fileOutput.reset(till);
691
} else {
692
this.fileOutput.resetToEnd();
693
}
694
}
695
this.doUpdate(mode, immediate);
696
});
697
}
698
699
override updateChannelSources(files: IOutputContentSource[]): void { throw new Error('Not supported'); }
700
}
701
702
export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel {
703
704
private readonly multifileOutput: MultiFileContentProvider;
705
706
constructor(
707
modelUri: URI,
708
language: ILanguageSelection,
709
readonly source: IOutputContentSource[],
710
@IFileService fileService: IFileService,
711
@IModelService modelService: IModelService,
712
@ILogService logService: ILogService,
713
@IEditorWorkerService editorWorkerService: IEditorWorkerService,
714
@IInstantiationService instantiationService: IInstantiationService,
715
) {
716
const multifileOutput = new MultiFileContentProvider(source, instantiationService, fileService, logService);
717
super(modelUri, language, multifileOutput, modelService, editorWorkerService);
718
this.multifileOutput = this._register(multifileOutput);
719
}
720
721
override updateChannelSources(files: IOutputContentSource[]): void {
722
this.multifileOutput.unwatch();
723
this.multifileOutput.updateFiles(files);
724
this.multifileOutput.reset();
725
this.doUpdate(OutputChannelUpdateMode.Replace, true);
726
if (this.isVisible()) {
727
this.multifileOutput.watch();
728
}
729
}
730
731
override clear(): void {
732
const loadModelPromise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve();
733
loadModelPromise.then(() => {
734
this.multifileOutput.resetToEnd();
735
this.doUpdate(OutputChannelUpdateMode.Clear, true);
736
});
737
}
738
739
override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { throw new Error('Not supported'); }
740
}
741
742
class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutputChannelModel {
743
744
private logger: ILogger;
745
private _offset: number;
746
747
constructor(
748
id: string,
749
modelUri: URI,
750
language: ILanguageSelection,
751
file: URI,
752
@IFileService fileService: IFileService,
753
@IModelService modelService: IModelService,
754
@ILoggerService loggerService: ILoggerService,
755
@IInstantiationService instantiationService: IInstantiationService,
756
@ILogService logService: ILogService,
757
@IEditorWorkerService editorWorkerService: IEditorWorkerService
758
) {
759
super(modelUri, language, { resource: file, name: '' }, fileService, modelService, instantiationService, logService, editorWorkerService);
760
761
// Donot rotate to check for the file reset
762
this.logger = loggerService.createLogger(file, { logLevel: 'always', donotRotate: true, donotUseFormatters: true, hidden: true });
763
this._offset = 0;
764
}
765
766
override append(message: string): void {
767
this.write(message);
768
this.update(OutputChannelUpdateMode.Append, undefined, this.isVisible());
769
}
770
771
override replace(message: string): void {
772
const till = this._offset;
773
this.write(message);
774
this.update(OutputChannelUpdateMode.Replace, till, true);
775
}
776
777
private write(content: string): void {
778
this._offset += VSBuffer.fromString(content).byteLength;
779
this.logger.info(content);
780
if (this.isVisible()) {
781
this.logger.flush();
782
}
783
}
784
785
}
786
787
export class DelegatedOutputChannelModel extends Disposable implements IOutputChannelModel {
788
789
private readonly _onDispose: Emitter<void> = this._register(new Emitter<void>());
790
readonly onDispose: Event<void> = this._onDispose.event;
791
792
private readonly outputChannelModel: Promise<IOutputChannelModel>;
793
readonly source: IOutputContentSource;
794
795
constructor(
796
id: string,
797
modelUri: URI,
798
language: ILanguageSelection,
799
outputDir: URI,
800
outputDirCreationPromise: Promise<void>,
801
@IInstantiationService private readonly instantiationService: IInstantiationService,
802
@IFileService private readonly fileService: IFileService,
803
) {
804
super();
805
this.outputChannelModel = this.createOutputChannelModel(id, modelUri, language, outputDir, outputDirCreationPromise);
806
const resource = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`);
807
this.source = { resource };
808
}
809
810
private async createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, outputDir: URI, outputDirPromise: Promise<void>): Promise<IOutputChannelModel> {
811
await outputDirPromise;
812
const file = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`);
813
await this.fileService.createFile(file);
814
const outputChannelModel = this._register(this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, language, file));
815
this._register(outputChannelModel.onDispose(() => this._onDispose.fire()));
816
return outputChannelModel;
817
}
818
819
getLogEntries(): readonly ILogEntry[] {
820
return [];
821
}
822
823
append(output: string): void {
824
this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output));
825
}
826
827
update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void {
828
this.outputChannelModel.then(outputChannelModel => outputChannelModel.update(mode, till, immediate));
829
}
830
831
loadModel(): Promise<ITextModel> {
832
return this.outputChannelModel.then(outputChannelModel => outputChannelModel.loadModel());
833
}
834
835
clear(): void {
836
this.outputChannelModel.then(outputChannelModel => outputChannelModel.clear());
837
}
838
839
replace(value: string): void {
840
this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value));
841
}
842
843
updateChannelSources(files: IOutputContentSource[]): void {
844
this.outputChannelModel.then(outputChannelModel => outputChannelModel.updateChannelSources(files));
845
}
846
}
847
848