Path: blob/main/src/vs/workbench/contrib/output/common/outputChannelModel.ts
4780 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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';6import * as resources from '../../../../base/common/resources.js';7import { ITextModel } from '../../../../editor/common/model.js';8import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';9import { Emitter, Event } from '../../../../base/common/event.js';10import { URI } from '../../../../base/common/uri.js';11import { Promises, ThrottledDelayer } from '../../../../base/common/async.js';12import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js';13import { IModelService } from '../../../../editor/common/services/model.js';14import { ILanguageSelection } from '../../../../editor/common/languages/language.js';15import { Disposable, toDisposable, IDisposable, MutableDisposable, DisposableStore } from '../../../../base/common/lifecycle.js';16import { isNumber } from '../../../../base/common/types.js';17import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js';18import { Position } from '../../../../editor/common/core/position.js';19import { Range } from '../../../../editor/common/core/range.js';20import { VSBuffer } from '../../../../base/common/buffer.js';21import { ILogger, ILoggerService, ILogService, LogLevel } from '../../../../platform/log/common/log.js';22import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';23import { ILogEntry, IOutputContentSource, LOG_MIME, OutputChannelUpdateMode } from '../../../services/output/common/output.js';24import { isCancellationError } from '../../../../base/common/errors.js';25import { TextModel } from '../../../../editor/common/model/textModel.js';26import { binarySearch, sortedDiff } from '../../../../base/common/arrays.js';2728const LOG_ENTRY_REGEX = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s(\[(info|trace|debug|error|warning)\])\s(\[(.*?)\])?/;2930export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null {31const lineContent = model.getLineContent(lineNumber);32const match = LOG_ENTRY_REGEX.exec(lineContent);33if (match) {34const timestamp = new Date(match[1]).getTime();35const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length);36const logLevel = parseLogLevel(match[3]);37const logLevelRange = new Range(lineNumber, timestampRange.endColumn + 1, lineNumber, timestampRange.endColumn + 1 + match[2].length);38const category = match[5];39const startLine = lineNumber;40let endLine = lineNumber;4142const lineCount = model.getLineCount();43while (endLine < lineCount) {44const nextLineContent = model.getLineContent(endLine + 1);45const isLastLine = endLine + 1 === lineCount && nextLineContent === ''; // Last line will be always empty46if (LOG_ENTRY_REGEX.test(nextLineContent) || isLastLine) {47break;48}49endLine++;50}51const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));52return { range, timestamp, timestampRange, logLevel, logLevelRange, category };53}54return null;55}5657function* logEntryIterator<T>(model: ITextModel, process: (logEntry: ILogEntry) => T): IterableIterator<T> {58for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) {59const logEntry = parseLogEntryAt(model, lineNumber);60if (logEntry) {61yield process(logEntry);62lineNumber = logEntry.range.endLineNumber;63}64}65}6667function changeStartLineNumber(logEntry: ILogEntry, lineNumber: number): ILogEntry {68return {69...logEntry,70range: new Range(lineNumber, logEntry.range.startColumn, lineNumber + logEntry.range.endLineNumber - logEntry.range.startLineNumber, logEntry.range.endColumn),71timestampRange: new Range(lineNumber, logEntry.timestampRange.startColumn, lineNumber, logEntry.timestampRange.endColumn),72logLevelRange: new Range(lineNumber, logEntry.logLevelRange.startColumn, lineNumber, logEntry.logLevelRange.endColumn),73};74}7576function parseLogLevel(level: string): LogLevel {77switch (level.toLowerCase()) {78case 'trace':79return LogLevel.Trace;80case 'debug':81return LogLevel.Debug;82case 'info':83return LogLevel.Info;84case 'warning':85return LogLevel.Warning;86case 'error':87return LogLevel.Error;88default:89throw new Error(`Unknown log level: ${level}`);90}91}9293export interface IOutputChannelModel extends IDisposable {94readonly onDispose: Event<void>;95readonly source: IOutputContentSource | ReadonlyArray<IOutputContentSource>;96getLogEntries(): ReadonlyArray<ILogEntry>;97append(output: string): void;98update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void;99updateChannelSources(sources: ReadonlyArray<IOutputContentSource>): void;100loadModel(): Promise<ITextModel>;101clear(): void;102replace(value: string): void;103}104105interface IContentProvider {106readonly onDidAppend: Event<void>;107readonly onDidReset: Event<void>;108reset(): void;109watch(): void;110unwatch(): void;111getContent(): Promise<{ readonly content: string; readonly consume: () => void }>;112getLogEntries(): ReadonlyArray<ILogEntry>;113}114115class FileContentProvider extends Disposable implements IContentProvider {116117private readonly _onDidAppend = new Emitter<void>();118get onDidAppend() { return this._onDidAppend.event; }119120private readonly _onDidReset = new Emitter<void>();121get onDidReset() { return this._onDidReset.event; }122123private watching: boolean = false;124private syncDelayer: ThrottledDelayer<void>;125private etag: string | undefined = '';126127private logEntries: ILogEntry[] = [];128private startOffset: number = 0;129private endOffset: number = 0;130131readonly resource: URI;132readonly name: string;133134constructor(135{ name, resource }: IOutputContentSource,136@IFileService private readonly fileService: IFileService,137@IInstantiationService private readonly instantiationService: IInstantiationService,138@ILogService private readonly logService: ILogService,139) {140super();141142this.name = name ?? '';143this.resource = resource;144this.syncDelayer = new ThrottledDelayer<void>(500);145this._register(toDisposable(() => this.unwatch()));146}147148reset(offset?: number): void {149this.endOffset = this.startOffset = offset ?? this.startOffset;150this.logEntries = [];151}152153resetToEnd(): void {154this.startOffset = this.endOffset;155this.logEntries = [];156}157158watch(): void {159if (!this.watching) {160this.logService.trace('Started polling', this.resource.toString());161this.poll();162this.watching = true;163}164}165166unwatch(): void {167if (this.watching) {168this.syncDelayer.cancel();169this.watching = false;170this.logService.trace('Stopped polling', this.resource.toString());171}172}173174private poll(): void {175const loop = () => this.doWatch().then(() => this.poll());176this.syncDelayer.trigger(loop).catch(error => {177if (!isCancellationError(error)) {178throw error;179}180});181}182183private async doWatch(): Promise<void> {184try {185if (!this.fileService.hasProvider(this.resource)) {186return;187}188const stat = await this.fileService.stat(this.resource);189if (stat.etag !== this.etag) {190this.etag = stat.etag;191if (isNumber(stat.size) && this.endOffset > stat.size) {192this.reset(0);193this._onDidReset.fire();194} else {195this._onDidAppend.fire();196}197}198} catch (error) {199if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {200throw error;201}202}203}204205getLogEntries(): ReadonlyArray<ILogEntry> {206return this.logEntries;207}208209async getContent(donotConsumeLogEntries?: boolean): Promise<{ readonly name: string; readonly content: string; readonly consume: () => void }> {210try {211if (!this.fileService.hasProvider(this.resource)) {212return {213name: this.name,214content: '',215consume: () => { /* No Op */ }216};217}218const fileContent = await this.fileService.readFile(this.resource, { position: this.endOffset });219const content = fileContent.value.toString();220const logEntries = donotConsumeLogEntries ? [] : this.parseLogEntries(content, this.logEntries[this.logEntries.length - 1]);221let consumed = false;222return {223name: this.name,224content,225consume: () => {226if (!consumed) {227consumed = true;228this.endOffset += fileContent.value.byteLength;229this.etag = fileContent.etag;230this.logEntries.push(...logEntries);231}232}233};234} catch (error) {235if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {236throw error;237}238return {239name: this.name,240content: '',241consume: () => { /* No Op */ }242};243}244}245246private parseLogEntries(content: string, lastLogEntry: ILogEntry | undefined): ILogEntry[] {247const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);248try {249if (!parseLogEntryAt(model, 1)) {250return [];251}252const logEntries: ILogEntry[] = [];253let logEntryStartLineNumber = lastLogEntry ? lastLogEntry.range.endLineNumber + 1 : 1;254for (const entry of logEntryIterator(model, (e) => changeStartLineNumber(e, logEntryStartLineNumber))) {255logEntries.push(entry);256logEntryStartLineNumber = entry.range.endLineNumber + 1;257}258return logEntries;259} finally {260model.dispose();261}262}263}264265class MultiFileContentProvider extends Disposable implements IContentProvider {266267private readonly _onDidAppend = this._register(new Emitter<void>());268readonly onDidAppend = this._onDidAppend.event;269readonly onDidReset = Event.None;270271private logEntries: ILogEntry[] = [];272private readonly fileContentProviderItems: [FileContentProvider, DisposableStore][] = [];273274private watching: boolean = false;275276constructor(277filesInfos: IOutputContentSource[],278@IInstantiationService private readonly instantiationService: IInstantiationService,279@IFileService private readonly fileService: IFileService,280@ILogService private readonly logService: ILogService,281) {282super();283for (const file of filesInfos) {284this.fileContentProviderItems.push(this.createFileContentProvider(file));285}286this._register(toDisposable(() => {287for (const [, disposables] of this.fileContentProviderItems) {288disposables.dispose();289}290}));291}292293private createFileContentProvider(file: IOutputContentSource): [FileContentProvider, DisposableStore] {294const disposables = new DisposableStore();295const fileOutput = disposables.add(new FileContentProvider(file, this.fileService, this.instantiationService, this.logService));296disposables.add(fileOutput.onDidAppend(() => this._onDidAppend.fire()));297return [fileOutput, disposables];298}299300watch(): void {301if (!this.watching) {302this.watching = true;303for (const [output] of this.fileContentProviderItems) {304output.watch();305}306}307}308309unwatch(): void {310if (this.watching) {311this.watching = false;312for (const [output] of this.fileContentProviderItems) {313output.unwatch();314}315}316}317318updateFiles(files: IOutputContentSource[]): void {319const wasWatching = this.watching;320if (wasWatching) {321this.unwatch();322}323324const result = sortedDiff(this.fileContentProviderItems.map(([output]) => output), files, (a, b) => resources.extUri.compare(a.resource, b.resource));325for (const { start, deleteCount, toInsert } of result) {326const outputs = toInsert.map(file => this.createFileContentProvider(file));327const outputsToRemove = this.fileContentProviderItems.splice(start, deleteCount, ...outputs);328for (const [, disposables] of outputsToRemove) {329disposables.dispose();330}331}332333if (wasWatching) {334this.watch();335}336}337338reset(): void {339for (const [output] of this.fileContentProviderItems) {340output.reset();341}342this.logEntries = [];343}344345resetToEnd(): void {346for (const [output] of this.fileContentProviderItems) {347output.resetToEnd();348}349this.logEntries = [];350}351352getLogEntries(): ReadonlyArray<ILogEntry> {353return this.logEntries;354}355356async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> {357const outputs = await Promise.all(this.fileContentProviderItems.map(([output]) => output.getContent(true)));358const { content, logEntries } = this.combineLogEntries(outputs, this.logEntries[this.logEntries.length - 1]);359let consumed = false;360return {361content,362consume: () => {363if (!consumed) {364consumed = true;365outputs.forEach(({ consume }) => consume());366this.logEntries.push(...logEntries);367}368}369};370}371372private combineLogEntries(outputs: { content: string; name: string }[], lastEntry: ILogEntry | undefined): { logEntries: ILogEntry[]; content: string } {373374outputs = outputs.filter(output => !!output.content);375376if (outputs.length === 0) {377return { logEntries: [], content: '' };378}379380const logEntries: ILogEntry[] = [];381const contents: string[] = [];382const process = (model: ITextModel, logEntry: ILogEntry, name: string): [ILogEntry, string] => {383const lineContent = model.getValueInRange(logEntry.range);384const content = name ? `${lineContent.substring(0, logEntry.logLevelRange.endColumn)} [${name}]${lineContent.substring(logEntry.logLevelRange.endColumn)}` : lineContent;385return [{386...logEntry,387category: name,388range: new Range(logEntry.range.startLineNumber, logEntry.logLevelRange.startColumn, logEntry.range.endLineNumber, name ? logEntry.range.endColumn + name.length + 3 : logEntry.range.endColumn),389}, content];390};391392const model = this.instantiationService.createInstance(TextModel, outputs[0].content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);393try {394for (const [logEntry, content] of logEntryIterator(model, (e) => process(model, e, outputs[0].name))) {395logEntries.push(logEntry);396contents.push(content);397}398} finally {399model.dispose();400}401402for (let index = 1; index < outputs.length; index++) {403const { content, name } = outputs[index];404const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);405try {406const iterator = logEntryIterator(model, (e) => process(model, e, name));407let next = iterator.next();408while (!next.done) {409const [logEntry, content] = next.value;410const logEntriesToAdd = [logEntry];411const contentsToAdd = [content];412413let insertionIndex;414415// If the timestamp is greater than or equal to the last timestamp,416// we can just append all the entries at the end417if (logEntry.timestamp >= logEntries[logEntries.length - 1].timestamp) {418insertionIndex = logEntries.length;419for (next = iterator.next(); !next.done; next = iterator.next()) {420logEntriesToAdd.push(next.value[0]);421contentsToAdd.push(next.value[1]);422}423}424else {425if (logEntry.timestamp <= logEntries[0].timestamp) {426// If the timestamp is less than or equal to the first timestamp427// then insert at the beginning428insertionIndex = 0;429} else {430// Otherwise, find the insertion index431const idx = binarySearch(logEntries, logEntry, (a, b) => a.timestamp - b.timestamp);432insertionIndex = idx < 0 ? ~idx : idx;433}434435// Collect all entries that have a timestamp less than or equal to the timestamp at the insertion index436for (next = iterator.next(); !next.done && next.value[0].timestamp <= logEntries[insertionIndex].timestamp; next = iterator.next()) {437logEntriesToAdd.push(next.value[0]);438contentsToAdd.push(next.value[1]);439}440}441442contents.splice(insertionIndex, 0, ...contentsToAdd);443logEntries.splice(insertionIndex, 0, ...logEntriesToAdd);444}445} finally {446model.dispose();447}448}449450let content = '';451const updatedLogEntries: ILogEntry[] = [];452let logEntryStartLineNumber = lastEntry ? lastEntry.range.endLineNumber + 1 : 1;453for (let i = 0; i < logEntries.length; i++) {454content += contents[i] + '\n';455const updatedLogEntry = changeStartLineNumber(logEntries[i], logEntryStartLineNumber);456updatedLogEntries.push(updatedLogEntry);457logEntryStartLineNumber = updatedLogEntry.range.endLineNumber + 1;458}459460return { logEntries: updatedLogEntries, content };461}462463}464465export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel {466467private readonly _onDispose = this._register(new Emitter<void>());468readonly onDispose: Event<void> = this._onDispose.event;469470protected loadModelPromise: Promise<ITextModel> | null = null;471472private readonly modelDisposable = this._register(new MutableDisposable<DisposableStore>());473protected model: ITextModel | null = null;474private modelUpdateInProgress: boolean = false;475private readonly modelUpdateCancellationSource = this._register(new MutableDisposable<CancellationTokenSource>());476private readonly appendThrottler = this._register(new ThrottledDelayer(300));477private replacePromise: Promise<void> | undefined;478479abstract readonly source: IOutputContentSource | ReadonlyArray<IOutputContentSource>;480481constructor(482private readonly modelUri: URI,483private readonly language: ILanguageSelection,484private readonly outputContentProvider: IContentProvider,485@IModelService protected readonly modelService: IModelService,486@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,487) {488super();489}490491async loadModel(): Promise<ITextModel> {492this.loadModelPromise = Promises.withAsyncBody<ITextModel>(async (c, e) => {493try {494this.modelDisposable.value = new DisposableStore();495this.model = this.modelService.createModel('', this.language, this.modelUri);496const { content, consume } = await this.outputContentProvider.getContent();497consume();498this.doAppendContent(this.model, content);499this.modelDisposable.value.add(this.outputContentProvider.onDidReset(() => this.onDidContentChange(true, true)));500this.modelDisposable.value.add(this.outputContentProvider.onDidAppend(() => this.onDidContentChange(false, false)));501this.outputContentProvider.watch();502this.modelDisposable.value.add(toDisposable(() => this.outputContentProvider.unwatch()));503this.modelDisposable.value.add(this.model.onWillDispose(() => {504this.outputContentProvider.reset();505this.modelDisposable.value = undefined;506this.cancelModelUpdate();507this.model = null;508}));509c(this.model);510} catch (error) {511e(error);512}513});514return this.loadModelPromise;515}516517getLogEntries(): readonly ILogEntry[] {518return this.outputContentProvider.getLogEntries();519}520521private onDidContentChange(reset: boolean, appendImmediately: boolean): void {522if (reset && !this.modelUpdateInProgress) {523this.doUpdate(OutputChannelUpdateMode.Clear, true);524}525this.doUpdate(OutputChannelUpdateMode.Append, appendImmediately);526}527528protected doUpdate(mode: OutputChannelUpdateMode, immediate: boolean): void {529if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) {530this.cancelModelUpdate();531}532if (!this.model) {533return;534}535536this.modelUpdateInProgress = true;537if (!this.modelUpdateCancellationSource.value) {538this.modelUpdateCancellationSource.value = new CancellationTokenSource();539}540const token = this.modelUpdateCancellationSource.value.token;541542if (mode === OutputChannelUpdateMode.Clear) {543this.clearContent(this.model);544}545546else if (mode === OutputChannelUpdateMode.Replace) {547this.replacePromise = this.replaceContent(this.model, token).finally(() => this.replacePromise = undefined);548}549550else {551this.appendContent(this.model, immediate, token);552}553}554555private clearContent(model: ITextModel): void {556model.applyEdits([EditOperation.delete(model.getFullModelRange())]);557this.modelUpdateInProgress = false;558}559560private appendContent(model: ITextModel, immediate: boolean, token: CancellationToken): void {561this.appendThrottler.trigger(async () => {562/* Abort if operation is cancelled */563if (token.isCancellationRequested) {564return;565}566567/* Wait for replace to finish */568if (this.replacePromise) {569try { await this.replacePromise; } catch (e) { /* Ignore */ }570/* Abort if operation is cancelled */571if (token.isCancellationRequested) {572return;573}574}575576/* Get content to append */577const { content, consume } = await this.outputContentProvider.getContent();578/* Abort if operation is cancelled */579if (token.isCancellationRequested) {580return;581}582583/* Appned Content */584consume();585this.doAppendContent(model, content);586this.modelUpdateInProgress = false;587}, immediate ? 0 : undefined).catch(error => {588if (!isCancellationError(error)) {589throw error;590}591});592}593594private doAppendContent(model: ITextModel, content: string): void {595const lastLine = model.getLineCount();596const lastLineMaxColumn = model.getLineMaxColumn(lastLine);597model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), content)]);598}599600private async replaceContent(model: ITextModel, token: CancellationToken): Promise<void> {601/* Get content to replace */602const { content, consume } = await this.outputContentProvider.getContent();603/* Abort if operation is cancelled */604if (token.isCancellationRequested) {605return;606}607608/* Compute Edits */609const edits = await this.getReplaceEdits(model, content.toString());610/* Abort if operation is cancelled */611if (token.isCancellationRequested) {612return;613}614615consume();616if (edits.length) {617/* Apply Edits */618model.applyEdits(edits);619}620this.modelUpdateInProgress = false;621}622623private async getReplaceEdits(model: ITextModel, contentToReplace: string): Promise<ISingleEditOperation[]> {624if (!contentToReplace) {625return [EditOperation.delete(model.getFullModelRange())];626}627if (contentToReplace !== model.getValue()) {628const edits = await this.editorWorkerService.computeMoreMinimalEdits(model.uri, [{ text: contentToReplace.toString(), range: model.getFullModelRange() }]);629if (edits?.length) {630return edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));631}632}633return [];634}635636protected cancelModelUpdate(): void {637this.modelUpdateCancellationSource.value?.cancel();638this.modelUpdateCancellationSource.value = undefined;639this.appendThrottler.cancel();640this.replacePromise = undefined;641this.modelUpdateInProgress = false;642}643644protected isVisible(): boolean {645return !!this.model;646}647648override dispose(): void {649this._onDispose.fire();650super.dispose();651}652653append(message: string): void { throw new Error('Not supported'); }654replace(message: string): void { throw new Error('Not supported'); }655656abstract clear(): void;657abstract update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void;658abstract updateChannelSources(files: IOutputContentSource[]): void;659}660661export class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel {662663private readonly fileOutput: FileContentProvider;664665constructor(666modelUri: URI,667language: ILanguageSelection,668readonly source: IOutputContentSource,669@IFileService fileService: IFileService,670@IModelService modelService: IModelService,671@IInstantiationService instantiationService: IInstantiationService,672@ILogService logService: ILogService,673@IEditorWorkerService editorWorkerService: IEditorWorkerService,674) {675const fileOutput = new FileContentProvider(source, fileService, instantiationService, logService);676super(modelUri, language, fileOutput, modelService, editorWorkerService);677this.fileOutput = this._register(fileOutput);678}679680override clear(): void {681this.update(OutputChannelUpdateMode.Clear, undefined, true);682}683684override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void {685const loadModelPromise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve();686loadModelPromise.then(() => {687if (mode === OutputChannelUpdateMode.Clear || mode === OutputChannelUpdateMode.Replace) {688if (isNumber(till)) {689this.fileOutput.reset(till);690} else {691this.fileOutput.resetToEnd();692}693}694this.doUpdate(mode, immediate);695});696}697698override updateChannelSources(files: IOutputContentSource[]): void { throw new Error('Not supported'); }699}700701export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel {702703private readonly multifileOutput: MultiFileContentProvider;704705constructor(706modelUri: URI,707language: ILanguageSelection,708readonly source: IOutputContentSource[],709@IFileService fileService: IFileService,710@IModelService modelService: IModelService,711@ILogService logService: ILogService,712@IEditorWorkerService editorWorkerService: IEditorWorkerService,713@IInstantiationService instantiationService: IInstantiationService,714) {715const multifileOutput = new MultiFileContentProvider(source, instantiationService, fileService, logService);716super(modelUri, language, multifileOutput, modelService, editorWorkerService);717this.multifileOutput = this._register(multifileOutput);718}719720override updateChannelSources(files: IOutputContentSource[]): void {721this.multifileOutput.unwatch();722this.multifileOutput.updateFiles(files);723this.multifileOutput.reset();724this.doUpdate(OutputChannelUpdateMode.Replace, true);725if (this.isVisible()) {726this.multifileOutput.watch();727}728}729730override clear(): void {731const loadModelPromise = this.loadModelPromise ? this.loadModelPromise : Promise.resolve();732loadModelPromise.then(() => {733this.multifileOutput.resetToEnd();734this.doUpdate(OutputChannelUpdateMode.Clear, true);735});736}737738override update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void { throw new Error('Not supported'); }739}740741class OutputChannelBackedByFile extends FileOutputChannelModel implements IOutputChannelModel {742743private logger: ILogger;744private _offset: number;745746constructor(747id: string,748modelUri: URI,749language: ILanguageSelection,750file: URI,751@IFileService fileService: IFileService,752@IModelService modelService: IModelService,753@ILoggerService loggerService: ILoggerService,754@IInstantiationService instantiationService: IInstantiationService,755@ILogService logService: ILogService,756@IEditorWorkerService editorWorkerService: IEditorWorkerService757) {758super(modelUri, language, { resource: file, name: '' }, fileService, modelService, instantiationService, logService, editorWorkerService);759760// Donot rotate to check for the file reset761this.logger = loggerService.createLogger(file, { logLevel: 'always', donotRotate: true, donotUseFormatters: true, hidden: true });762this._offset = 0;763}764765override append(message: string): void {766this.write(message);767this.update(OutputChannelUpdateMode.Append, undefined, this.isVisible());768}769770override replace(message: string): void {771const till = this._offset;772this.write(message);773this.update(OutputChannelUpdateMode.Replace, till, true);774}775776private write(content: string): void {777this._offset += VSBuffer.fromString(content).byteLength;778this.logger.info(content);779if (this.isVisible()) {780this.logger.flush();781}782}783784}785786export class DelegatedOutputChannelModel extends Disposable implements IOutputChannelModel {787788private readonly _onDispose: Emitter<void> = this._register(new Emitter<void>());789readonly onDispose: Event<void> = this._onDispose.event;790791private readonly outputChannelModel: Promise<IOutputChannelModel>;792readonly source: IOutputContentSource;793794constructor(795id: string,796modelUri: URI,797language: ILanguageSelection,798outputDir: URI,799outputDirCreationPromise: Promise<void>,800@IInstantiationService private readonly instantiationService: IInstantiationService,801@IFileService private readonly fileService: IFileService,802) {803super();804this.outputChannelModel = this.createOutputChannelModel(id, modelUri, language, outputDir, outputDirCreationPromise);805const resource = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`);806this.source = { resource };807}808809private async createOutputChannelModel(id: string, modelUri: URI, language: ILanguageSelection, outputDir: URI, outputDirPromise: Promise<void>): Promise<IOutputChannelModel> {810await outputDirPromise;811const file = resources.joinPath(outputDir, `${id.replace(/[\\/:\*\?"<>\|]/g, '')}.log`);812await this.fileService.createFile(file);813const outputChannelModel = this._register(this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, language, file));814this._register(outputChannelModel.onDispose(() => this._onDispose.fire()));815return outputChannelModel;816}817818getLogEntries(): readonly ILogEntry[] {819return [];820}821822append(output: string): void {823this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output));824}825826update(mode: OutputChannelUpdateMode, till: number | undefined, immediate: boolean): void {827this.outputChannelModel.then(outputChannelModel => outputChannelModel.update(mode, till, immediate));828}829830loadModel(): Promise<ITextModel> {831return this.outputChannelModel.then(outputChannelModel => outputChannelModel.loadModel());832}833834clear(): void {835this.outputChannelModel.then(outputChannelModel => outputChannelModel.clear());836}837838replace(value: string): void {839this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value));840}841842updateChannelSources(files: IOutputContentSource[]): void {843this.outputChannelModel.then(outputChannelModel => outputChannelModel.updateChannelSources(files));844}845}846847848