outputServices.ts 25.8 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6 7
import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths';
S
Sandeep Somavarapu 已提交
8
import * as strings from 'vs/base/common/strings';
9
import * as extfs from 'vs/base/node/extfs';
10
import * as fs from 'fs';
J
Johannes Rieken 已提交
11
import { TPromise } from 'vs/base/common/winjs.base';
M
Matt Bierner 已提交
12
import { Event, Emitter } from 'vs/base/common/event';
13
import URI from 'vs/base/common/uri';
14
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
15
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
J
Johannes Rieken 已提交
16 17
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
18
import { Registry } from 'vs/platform/registry/common/platform';
J
Johannes Rieken 已提交
19
import { EditorOptions } from 'vs/workbench/common/editor';
S
Sandeep Somavarapu 已提交
20
import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, MAX_OUTPUT_LENGTH, LOG_SCHEME, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/parts/output/common/output';
J
Johannes Rieken 已提交
21 22 23 24 25
import { OutputPanel } from 'vs/workbench/parts/output/browser/outputPanel';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { OutputLinkProvider } from 'vs/workbench/parts/output/common/outputLinkProvider';
26
import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
A
Alex Dima 已提交
27
import { ITextModel } from 'vs/editor/common/model';
28
import { IModeService } from 'vs/editor/common/services/modeService';
29
import { RunOnceScheduler, ThrottledDelayer } from 'vs/base/common/async';
30 31
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
32
import { IFileService } from 'vs/platform/files/common/files';
33 34
import { IPanel } from 'vs/workbench/common/panel';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
35
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
S
Sandeep Somavarapu 已提交
36
import { RotatingLogger } from 'spdlog';
37
import { toLocalISOString } from 'vs/base/common/date';
38
import { IWindowService } from 'vs/platform/windows/common/windows';
S
Sandeep Somavarapu 已提交
39 40
import { ILogService } from 'vs/platform/log/common/log';
import { binarySearch } from 'vs/base/common/arrays';
41
import { Schemas } from 'vs/base/common/network';
S
Sandeep Somavarapu 已提交
42
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
S
Sandeep Somavarapu 已提交
43
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
44
import { CancellationToken } from 'vs/base/common/cancellation';
45 46

const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel';
E
Erich Gamma 已提交
47

48
let watchingOutputDir = false;
49
let callbacks: ((eventType: string, fileName: string) => void)[] = [];
50 51 52
function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable {
	callbacks.push(onChange);
	if (!watchingOutputDir) {
53 54 55 56 57 58 59 60 61 62 63
		const watcher = extfs.watch(outputDir, (eventType, fileName) => {
			for (const callback of callbacks) {
				callback(eventType, fileName);
			}
		}, (error: string) => {
			logService.error(error);
		});
		watchingOutputDir = true;
		return toDisposable(() => {
			callbacks = [];
			if (watcher) {
64 65
				watcher.removeAllListeners();
				watcher.close();
66 67
			}
		});
68
	}
69
	return toDisposable(() => { });
70 71
}

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
const fileWatchers: Map<string, any[]> = new Map<string, any[]>();
function watchFile(file: string, callback: () => void): IDisposable {

	const onFileChange = (file: string) => {
		for (const callback of fileWatchers.get(file)) {
			callback();
		}
	};

	let callbacks = fileWatchers.get(file);
	if (!callbacks) {
		callbacks = [];
		fileWatchers.set(file, callbacks);
		fs.watchFile(file, { interval: 1000 }, (current, previous) => {
			if ((previous && !current) || (!previous && !current)) {
				onFileChange(file);
				return;
			}
			if (previous && current && previous.mtime !== current.mtime) {
				onFileChange(file);
				return;
			}
		});
	}
	callbacks.push(callback);
	return toDisposable(() => {
		let allCallbacks = fileWatchers.get(file);
		allCallbacks.splice(allCallbacks.indexOf(callback), 1);
		if (!allCallbacks.length) {
			fs.unwatchFile(file);
			fileWatchers.delete(file);
		}
	});
}

function unWatchAllFiles(): void {
	fileWatchers.forEach((value, file) => fs.unwatchFile(file));
	fileWatchers.clear();
}
111

112
interface OutputChannel extends IOutputChannel {
S
Sandeep Somavarapu 已提交
113
	readonly file: URI;
114
	readonly onDidAppendedContent: Event<void>;
S
Sandeep Somavarapu 已提交
115
	readonly onDispose: Event<void>;
A
Alex Dima 已提交
116
	loadModel(): TPromise<ITextModel>;
117
}
118

119
abstract class AbstractFileOutputChannel extends Disposable {
120 121

	scrollLock: boolean = false;
122

123 124 125
	protected _onDidAppendedContent: Emitter<void> = new Emitter<void>();
	readonly onDidAppendedContent: Event<void> = this._onDidAppendedContent.event;

126 127 128
	protected _onDispose: Emitter<void> = new Emitter<void>();
	readonly onDispose: Event<void> = this._onDispose.event;

129
	protected modelUpdater: RunOnceScheduler;
A
Alex Dima 已提交
130
	protected model: ITextModel;
S
Sandeep Somavarapu 已提交
131
	readonly file: URI;
S
Sandeep Somavarapu 已提交
132

S
Sandeep Somavarapu 已提交
133
	protected startOffset: number = 0;
134
	protected endOffset: number = 0;
135 136

	constructor(
137
		protected readonly outputChannelIdentifier: IOutputChannelIdentifier,
138
		private readonly modelUri: URI,
S
Sandeep Somavarapu 已提交
139
		private mimeType: string,
140
		protected fileService: IFileService,
141 142
		protected modelService: IModelService,
		protected modeService: IModeService,
143 144
	) {
		super();
145
		this.file = this.outputChannelIdentifier.file;
146
		this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300);
147
		this._register(toDisposable(() => this.modelUpdater.cancel()));
148 149 150 151 152 153 154 155 156 157
	}

	get id(): string {
		return this.outputChannelIdentifier.id;
	}

	get label(): string {
		return this.outputChannelIdentifier.label;
	}

158
	clear(): void {
159 160 161
		if (this.modelUpdater.isScheduled()) {
			this.modelUpdater.cancel();
		}
162 163
		if (this.model) {
			this.model.setValue('');
164
		}
165
		this.startOffset = this.endOffset;
166 167
	}

S
Sandeep Somavarapu 已提交
168
	protected createModel(content: string): ITextModel {
169
		if (this.model) {
S
Sandeep Somavarapu 已提交
170 171
			this.model.setValue(content);
		} else {
S
Sandeep Somavarapu 已提交
172
			this.model = this.modelService.createModel(content, this.modeService.getOrCreateMode(this.mimeType), this.modelUri);
S
Sandeep Somavarapu 已提交
173 174 175 176 177 178 179
			this.onModelCreated(this.model);
			const disposables: IDisposable[] = [];
			disposables.push(this.model.onWillDispose(() => {
				this.onModelWillDispose(this.model);
				this.model = null;
				dispose(disposables);
			}));
180
		}
S
Sandeep Somavarapu 已提交
181
		return this.model;
182 183 184 185 186 187 188
	}

	appendToModel(content: string): void {
		if (this.model && content) {
			const lastLine = this.model.getLineCount();
			const lastLineMaxColumn = this.model.getLineMaxColumn(lastLine);
			this.model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), content)]);
189
			this._onDidAppendedContent.fire();
190
		}
191 192
	}

A
Alex Dima 已提交
193 194
	protected onModelCreated(model: ITextModel) { }
	protected onModelWillDispose(model: ITextModel) { }
195 196 197 198 199
	protected updateModel() { }

	dispose(): void {
		this._onDispose.fire();
		super.dispose();
200
	}
201 202
}

203 204 205 206
/**
 * An output channel that stores appended messages in a backup file.
 */
class OutputChannelBackedByFile extends AbstractFileOutputChannel implements OutputChannel {
207

S
Sandeep Somavarapu 已提交
208
	private outputWriter: RotatingLogger;
209 210
	private appendedMessage = '';
	private loadingFromFileInProgress: boolean = false;
211
	private resettingDelayer: ThrottledDelayer<void>;
212
	private readonly rotatingFilePath: string;
213 214 215

	constructor(
		outputChannelIdentifier: IOutputChannelIdentifier,
216
		outputDir: string,
217
		modelUri: URI,
218 219 220
		@IFileService fileService: IFileService,
		@IModelService modelService: IModelService,
		@IModeService modeService: IModeService,
221
		@ILogService logService: ILogService
222
	) {
S
Sandeep Somavarapu 已提交
223
		super({ ...outputChannelIdentifier, file: URI.file(paths.join(outputDir, `${outputChannelIdentifier.id}.log`)) }, modelUri, OUTPUT_MIME, fileService, modelService, modeService);
224

225
		// Use one rotating file to check for main file reset
S
Sandeep Somavarapu 已提交
226
		this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, 1024 * 1024 * 30, 1);
227
		this.outputWriter.clearFormatters();
228
		this.rotatingFilePath = `${outputChannelIdentifier.id}.1.log`;
229
		this._register(watchOutputDirectory(paths.dirname(this.file.fsPath), logService, (eventType, file) => this.onFileChangedInOutputDirector(eventType, file)));
230 231

		this.resettingDelayer = new ThrottledDelayer<void>(50);
232 233 234
	}

	append(message: string): void {
S
Sandeep Somavarapu 已提交
235
		// update end offset always as message is read
236
		this.endOffset = this.endOffset + Buffer.from(message).byteLength;
237 238 239
		if (this.loadingFromFileInProgress) {
			this.appendedMessage += message;
		} else {
240
			this.write(message);
241 242 243 244 245 246 247 248 249 250 251 252 253 254
			if (this.model) {
				this.appendedMessage += message;
				if (!this.modelUpdater.isScheduled()) {
					this.modelUpdater.schedule();
				}
			}
		}
	}

	clear(): void {
		super.clear();
		this.appendedMessage = '';
	}

A
Alex Dima 已提交
255
	loadModel(): TPromise<ITextModel> {
S
Sandeep Somavarapu 已提交
256 257 258 259 260 261 262
		this.loadingFromFileInProgress = true;
		if (this.modelUpdater.isScheduled()) {
			this.modelUpdater.cancel();
		}
		this.appendedMessage = '';
		return this.loadFile()
			.then(content => {
263
				if (this.endOffset !== this.startOffset + Buffer.from(content).byteLength) {
S
Sandeep Somavarapu 已提交
264 265 266 267 268 269 270 271 272 273 274 275 276 277
					// Queue content is not written into the file
					// Flush it and load file again
					this.flush();
					return this.loadFile();
				}
				return content;
			})
			.then(content => {
				if (this.appendedMessage) {
					this.write(this.appendedMessage);
					this.appendedMessage = '';
				}
				this.loadingFromFileInProgress = false;
				return this.createModel(content);
278
			});
279 280
	}

S
Sandeep Somavarapu 已提交
281 282 283 284 285
	private resetModel(): TPromise<void> {
		this.startOffset = 0;
		this.endOffset = 0;
		if (this.model) {
			return this.loadModel() as TPromise;
286
		}
S
Sandeep Somavarapu 已提交
287
		return TPromise.as(null);
288 289
	}

S
Sandeep Somavarapu 已提交
290
	private loadFile(): TPromise<string> {
S
Sandeep Somavarapu 已提交
291
		return this.fileService.resolveContent(this.file, { position: this.startOffset, encoding: 'utf8' })
S
Sandeep Somavarapu 已提交
292
			.then(content => this.appendedMessage ? content.value + this.appendedMessage : content.value);
293 294
	}

S
Sandeep Somavarapu 已提交
295 296
	protected updateModel(): void {
		if (this.model && this.appendedMessage) {
297 298 299
			this.appendToModel(this.appendedMessage);
			this.appendedMessage = '';
		}
300 301
	}

302
	private onFileChangedInOutputDirector(eventType: string, fileName: string): void {
303
		// Check if rotating file has changed. It changes only when the main file exceeds its limit.
304
		if (this.rotatingFilePath === fileName) {
305
			this.resettingDelayer.trigger(() => this.resetModel());
306
		}
307
	}
308 309 310 311 312 313

	private write(content: string): void {
		this.outputWriter.critical(content);
	}

	private flush(): void {
S
Sandeep Somavarapu 已提交
314
		this.outputWriter.flush();
315
	}
316 317
}

318 319
class OutputFileListener extends Disposable {

M
Matt Bierner 已提交
320
	private readonly _onDidChange: Emitter<void> = new Emitter<void>();
321
	readonly onDidContentChange: Event<void> = this._onDidChange.event;
322

323
	private watching: boolean = false;
324 325 326 327 328 329 330 331 332
	private disposables: IDisposable[] = [];

	constructor(
		private readonly file: URI,
	) {
		super();
	}

	watch(): void {
333 334 335 336
		if (!this.watching) {
			this.disposables.push(watchFile(this.file.fsPath, () => this._onDidChange.fire()));
			this.watching = true;
		}
337 338 339
	}

	unwatch(): void {
340 341 342 343
		if (this.watching) {
			this.disposables = dispose(this.disposables);
			this.watching = false;
		}
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
	}

	dispose(): void {
		this.unwatch();
		super.dispose();
	}
}

/**
 * An output channel driven by a file and does not support appending messages.
 */
class FileOutputChannel extends AbstractFileOutputChannel implements OutputChannel {

	private readonly fileHandler: OutputFileListener;

	private updateInProgress: boolean = false;
360 361 362

	constructor(
		outputChannelIdentifier: IOutputChannelIdentifier,
363
		modelUri: URI,
364 365
		@IFileService fileService: IFileService,
		@IModelService modelService: IModelService,
366 367
		@IModeService modeService: IModeService,
		@ILogService logService: ILogService,
368
	) {
S
Sandeep Somavarapu 已提交
369
		super(outputChannelIdentifier, modelUri, LOG_MIME, fileService, modelService, modeService);
370

371
		this.fileHandler = this._register(new OutputFileListener(this.file));
372 373
		this._register(this.fileHandler.onDidContentChange(() => this.onDidContentChange()));
		this._register(toDisposable(() => this.fileHandler.unwatch()));
374 375
	}

S
Sandeep Somavarapu 已提交
376
	loadModel(): TPromise<ITextModel> {
S
Sandeep Somavarapu 已提交
377
		return this.fileService.resolveContent(this.file, { position: this.startOffset, encoding: 'utf8' })
S
Sandeep Somavarapu 已提交
378
			.then(content => {
379
				this.endOffset = this.startOffset + Buffer.from(content.value).byteLength;
S
Sandeep Somavarapu 已提交
380 381 382 383
				return this.createModel(content.value);
			});
	}

384
	append(message: string): void {
385 386 387 388 389
		throw new Error('Not supported');
	}

	protected updateModel(): void {
		if (this.model) {
S
Sandeep Somavarapu 已提交
390
			this.fileService.resolveContent(this.file, { position: this.endOffset, encoding: 'utf8' })
391
				.then(content => {
S
Sandeep Somavarapu 已提交
392
					if (content.value) {
393
						this.endOffset = this.endOffset + Buffer.from(content.value).byteLength;
S
Sandeep Somavarapu 已提交
394 395
						this.appendToModel(content.value);
					}
396 397 398 399
					this.updateInProgress = false;
				}, () => this.updateInProgress = false);
		} else {
			this.updateInProgress = false;
400 401 402
		}
	}

A
Alex Dima 已提交
403
	protected onModelCreated(model: ITextModel): void {
404
		this.fileHandler.watch();
405 406
	}

A
Alex Dima 已提交
407
	protected onModelWillDispose(model: ITextModel): void {
408
		this.fileHandler.unwatch();
409 410
	}

411 412 413 414
	private onDidContentChange(): void {
		if (!this.updateInProgress) {
			this.updateInProgress = true;
			this.modelUpdater.schedule();
415 416 417 418
		}
	}
}

419
export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider {
420

421 422 423
	public _serviceBrand: any;

	private channels: Map<string, OutputChannel> = new Map<string, OutputChannel>();
S
Sandeep Somavarapu 已提交
424 425
	private activeChannelIdInStorage: string;
	private activeChannel: IOutputChannel;
426
	private readonly outputDir: string;
427

M
Matt Bierner 已提交
428
	private readonly _onActiveOutputChannel: Emitter<string> = new Emitter<string>();
429 430 431
	readonly onActiveOutputChannel: Event<string> = this._onActiveOutputChannel.event;

	private _outputPanel: OutputPanel;
432 433

	constructor(
434 435 436 437 438
		@IStorageService private storageService: IStorageService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IPanelService private panelService: IPanelService,
		@IWorkspaceContextService contextService: IWorkspaceContextService,
		@ITextModelService textModelResolverService: ITextModelService,
439 440
		@IEnvironmentService environmentService: IEnvironmentService,
		@IWindowService windowService: IWindowService,
S
Sandeep Somavarapu 已提交
441 442
		@ILogService private logService: ILogService,
		@ILifecycleService private lifecycleService: ILifecycleService,
S
Sandeep Somavarapu 已提交
443
		@IContextKeyService private contextKeyService: IContextKeyService,
444
	) {
445
		super();
S
Sandeep Somavarapu 已提交
446 447
		this.activeChannelIdInStorage = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, null);
		this.outputDir = paths.join(environmentService.logsPath, `output_${windowService.getCurrentWindowId()}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`);
448 449 450

		// Register as text model content provider for output
		textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this);
S
Sandeep Somavarapu 已提交
451 452 453 454 455 456 457 458
		instantiationService.createInstance(OutputLinkProvider);

		// Create output channels for already registered channels
		const registry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels);
		for (const channelIdentifier of registry.getChannels()) {
			this.onDidRegisterChannel(channelIdentifier.id);
		}
		this._register(registry.onDidRegisterChannel(this.onDidRegisterChannel, this));
459 460 461

		panelService.onDidPanelOpen(this.onDidPanelOpen, this);
		panelService.onDidPanelClose(this.onDidPanelClose, this);
462

463
		this._register(toDisposable(() => unWatchAllFiles()));
S
Sandeep Somavarapu 已提交
464 465 466 467 468 469 470 471

		// Set active channel to first channel if not set
		if (!this.activeChannel) {
			const channels = this.getChannels();
			this.activeChannel = channels && channels.length > 0 ? this.getChannel(channels[0].id) : null;
		}

		this.lifecycleService.onShutdown(() => this.onShutdown());
472 473
	}

A
Alex Dima 已提交
474
	provideTextContent(resource: URI): TPromise<ITextModel> {
S
Sandeep Somavarapu 已提交
475
		const channel = <OutputChannel>this.getChannel(resource.path);
476 477 478 479
		if (channel) {
			return channel.loadModel();
		}
		return TPromise.as(null);
480 481 482
	}

	showChannel(id: string, preserveFocus?: boolean): TPromise<void> {
S
Sandeep Somavarapu 已提交
483 484
		const channel = this.getChannel(id);
		if (!channel || this.isChannelShown(channel)) {
485 486 487
			return TPromise.as(null);
		}

S
Sandeep Somavarapu 已提交
488
		this.activeChannel = channel;
489
		let promise = TPromise.as(null);
S
Sandeep Somavarapu 已提交
490
		if (this.isPanelShown()) {
S
Sandeep Somavarapu 已提交
491
			this.doShowChannel(channel, preserveFocus);
492 493
		} else {
			promise = this.panelService.openPanel(OUTPUT_PANEL_ID) as TPromise;
494
		}
495
		return promise.then(() => this._onActiveOutputChannel.fire(id));
496 497
	}

498 499 500 501 502 503
	getChannel(id: string): IOutputChannel {
		return this.channels.get(id);
	}

	getChannels(): IOutputChannelIdentifier[] {
		return Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannels();
504 505
	}

506
	getActiveChannel(): IOutputChannel {
S
Sandeep Somavarapu 已提交
507 508 509 510 511 512 513 514
		return this.activeChannel;
	}

	private onDidRegisterChannel(channelId: string): void {
		const channel = this.createChannel(channelId);
		this.channels.set(channelId, channel);
		if (this.activeChannelIdInStorage === channelId) {
			this.activeChannel = channel;
S
Sandeep Somavarapu 已提交
515 516
			this.onDidPanelOpen(this.panelService.getActivePanel())
				.then(() => this._onActiveOutputChannel.fire(channelId));
S
Sandeep Somavarapu 已提交
517 518 519
		}
	}

520
	private onDidPanelOpen(panel: IPanel): Thenable<void> {
S
Sandeep Somavarapu 已提交
521 522 523
		if (panel && panel.getId() === OUTPUT_PANEL_ID) {
			this._outputPanel = <OutputPanel>this.panelService.getActivePanel();
			if (this.activeChannel) {
S
Sandeep Somavarapu 已提交
524
				return this.doShowChannel(this.activeChannel, true);
S
Sandeep Somavarapu 已提交
525 526
			}
		}
S
Sandeep Somavarapu 已提交
527
		return TPromise.as(null);
S
Sandeep Somavarapu 已提交
528 529 530 531
	}

	private onDidPanelClose(panel: IPanel): void {
		if (this._outputPanel && panel.getId() === OUTPUT_PANEL_ID) {
S
Sandeep Somavarapu 已提交
532
			CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(false);
S
Sandeep Somavarapu 已提交
533 534
			this._outputPanel.clearInput();
		}
535
	}
536

537 538 539 540 541 542 543 544 545 546
	private setPrimaryCursorToLastLine(): void {
		const codeEditor = <ICodeEditor>this._outputPanel.getControl();
		const model = codeEditor.getModel();

		if (model) {
			const lastLine = model.getLineCount();
			codeEditor.setPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) });
		}
	}

547
	private createChannel(id: string): OutputChannel {
548
		const channelDisposables: IDisposable[] = [];
549
		const channel = this.instantiateChannel(id);
550 551 552
		channel.onDidAppendedContent(() => {
			if (!channel.scrollLock) {
				const panel = this.panelService.getActivePanel();
S
Sandeep Somavarapu 已提交
553
				if (panel && panel.getId() === OUTPUT_PANEL_ID && this.isChannelShown(channel)) {
554
					let outputPanel = <OutputPanel>panel;
I
isidor 已提交
555
					outputPanel.revealLastLine(true);
556 557 558
				}
			}
		}, channelDisposables);
559 560
		channel.onDispose(() => {
			Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(id);
S
Sandeep Somavarapu 已提交
561
			if (this.activeChannel === channel) {
562
				const channels = this.getChannels();
S
Sandeep Somavarapu 已提交
563 564 565
				if (this.isPanelShown() && channels.length) {
					this.doShowChannel(this.getChannel(channels[0].id), true);
					this._onActiveOutputChannel.fire(channels[0].id);
566 567 568 569 570 571 572 573 574 575 576 577
				} else {
					this._onActiveOutputChannel.fire(void 0);
				}
			}
			dispose(channelDisposables);
		}, channelDisposables);

		return channel;
	}

	private instantiateChannel(id: string): OutputChannel {
		const channelData = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);
S
Sandeep Somavarapu 已提交
578 579 580 581 582
		if (!channelData) {
			this.logService.error(`Channel '${id}' is not registered yet`);
			throw new Error(`Channel '${id}' is not registered yet`);
		}

583
		const uri = URI.from({ scheme: OUTPUT_SCHEME, path: id });
S
Sandeep Somavarapu 已提交
584 585 586 587 588 589 590 591 592 593
		if (channelData && channelData.file) {
			return this.instantiationService.createInstance(FileOutputChannel, channelData, uri);
		}
		try {
			return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri);
		} catch (e) {
			// Do not crash if spdlog rotating logger cannot be loaded (workaround for https://github.com/Microsoft/vscode/issues/47883)
			this.logService.error(e);
			return this.instantiationService.createInstance(BufferredOutputChannel, { id, label: channelData ? channelData.label : '' });
		}
594
	}
595

596
	private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): Thenable<void> {
597
		if (this._outputPanel) {
S
Sandeep Somavarapu 已提交
598
			CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(channel instanceof FileOutputChannel);
599
			return this._outputPanel.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: preserveFocus }), CancellationToken.None)
S
Sandeep Somavarapu 已提交
600 601 602 603
				.then(() => {
					if (!preserveFocus) {
						this._outputPanel.focus();
					}
604 605 606
				})
				// Activate smart scroll when switching back to the output panel
				.then(() => this.setPrimaryCursorToLastLine());
607
		}
S
Sandeep Somavarapu 已提交
608
		return TPromise.as(null);
609 610
	}

S
Sandeep Somavarapu 已提交
611
	private isChannelShown(channel: IOutputChannel): boolean {
S
Sandeep Somavarapu 已提交
612 613 614 615
		return this.isPanelShown() && this.activeChannel === channel;
	}

	private isPanelShown(): boolean {
S
Sandeep Somavarapu 已提交
616
		const panel = this.panelService.getActivePanel();
S
Sandeep Somavarapu 已提交
617
		return panel && panel.getId() === OUTPUT_PANEL_ID;
S
Sandeep Somavarapu 已提交
618 619 620 621 622 623 624 625 626 627 628 629
	}

	private createInput(channel: IOutputChannel): ResourceEditorInput {
		const resource = URI.from({ scheme: OUTPUT_SCHEME, path: channel.id });
		return this.instantiationService.createInstance(ResourceEditorInput, nls.localize('output', "{0} - Output", channel.label), nls.localize('channel', "Output channel for '{0}'", channel.label), resource);
	}

	onShutdown(): void {
		if (this.activeChannel) {
			this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, this.activeChannel.id, StorageScope.WORKSPACE);
		}
		this.dispose();
630
	}
631 632
}

633 634 635 636 637
export class LogContentProvider {

	private channels: Map<string, OutputChannel> = new Map<string, OutputChannel>();

	constructor(
638
		@IInstantiationService private instantiationService: IInstantiationService
639 640 641
	) {
	}

A
Alex Dima 已提交
642
	provideTextContent(resource: URI): TPromise<ITextModel> {
643 644 645 646 647 648 649 650 651 652 653 654 655
		if (resource.scheme === LOG_SCHEME) {
			let channel = this.getChannel(resource);
			if (channel) {
				return channel.loadModel();
			}
		}
		return TPromise.as(null);
	}

	private getChannel(resource: URI): OutputChannel {
		const id = resource.path;
		let channel = this.channels.get(id);
		if (!channel) {
656
			const channelDisposables: IDisposable[] = [];
657 658 659 660 661 662
			channel = this.instantiationService.createInstance(FileOutputChannel, { id, label: '', file: resource.with({ scheme: Schemas.file }) }, resource);
			channel.onDispose(() => dispose(channelDisposables), channelDisposables);
			this.channels.set(id, channel);
		}
		return channel;
	}
S
Sandeep Somavarapu 已提交
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
}
// Remove this channel when https://github.com/Microsoft/vscode/issues/47883 is fixed
class BufferredOutputChannel extends Disposable implements OutputChannel {

	readonly id: string;
	readonly label: string;
	readonly file: URI = null;
	scrollLock: boolean = false;

	protected _onDidAppendedContent: Emitter<void> = new Emitter<void>();
	readonly onDidAppendedContent: Event<void> = this._onDidAppendedContent.event;

	private readonly _onDispose: Emitter<void> = new Emitter<void>();
	readonly onDispose: Event<void> = this._onDispose.event;

	private modelUpdater: RunOnceScheduler;
	private model: ITextModel;
	private readonly bufferredContent: BufferedContent;
	private lastReadId: number = void 0;

	constructor(
		protected readonly outputChannelIdentifier: IOutputChannelIdentifier,
		@IModelService private modelService: IModelService,
		@IModeService private modeService: IModeService
	) {
		super();

		this.id = outputChannelIdentifier.id;
		this.label = outputChannelIdentifier.label;

		this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300);
		this._register(toDisposable(() => this.modelUpdater.cancel()));

		this.bufferredContent = new BufferedContent();
		this._register(toDisposable(() => this.bufferredContent.clear()));
	}

	append(output: string) {
		this.bufferredContent.append(output);
		if (!this.modelUpdater.isScheduled()) {
			this.modelUpdater.schedule();
		}
	}

	clear(): void {
		if (this.modelUpdater.isScheduled()) {
			this.modelUpdater.cancel();
		}
		if (this.model) {
			this.model.setValue('');
		}
		this.bufferredContent.clear();
		this.lastReadId = void 0;
	}

	loadModel(): TPromise<ITextModel> {
		const { value, id } = this.bufferredContent.getDelta(this.lastReadId);
		if (this.model) {
			this.model.setValue(value);
		} else {
			this.model = this.createModel(value);
		}
		this.lastReadId = id;
		return TPromise.as(this.model);
	}

	private createModel(content: string): ITextModel {
		const model = this.modelService.createModel(content, this.modeService.getOrCreateMode(OUTPUT_MIME), URI.from({ scheme: OUTPUT_SCHEME, path: this.id }));
		const disposables: IDisposable[] = [];
		disposables.push(model.onWillDispose(() => {
			this.model = null;
			dispose(disposables);
		}));
		return model;
	}

	private updateModel(): void {
		if (this.model) {
			const { value, id } = this.bufferredContent.getDelta(this.lastReadId);
			this.lastReadId = id;
			const lastLine = this.model.getLineCount();
			const lastLineMaxColumn = this.model.getLineMaxColumn(lastLine);
			this.model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), value)]);
			this._onDidAppendedContent.fire();
		}
	}

	dispose(): void {
		this._onDispose.fire();
		super.dispose();
	}
}

class BufferedContent {

	private data: string[] = [];
	private dataIds: number[] = [];
	private idPool = 0;
	private length = 0;

	public append(content: string): void {
		this.data.push(content);
		this.dataIds.push(++this.idPool);
		this.length += content.length;
		this.trim();
	}

	public clear(): void {
		this.data.length = 0;
		this.dataIds.length = 0;
		this.length = 0;
	}

	private trim(): void {
		if (this.length < MAX_OUTPUT_LENGTH * 1.2) {
			return;
		}

		while (this.length > MAX_OUTPUT_LENGTH) {
			this.dataIds.shift();
			const removed = this.data.shift();
			this.length -= removed.length;
		}
	}

	public getDelta(previousId?: number): { value: string, id: number } {
		let idx = -1;
		if (previousId !== void 0) {
			idx = binarySearch(this.dataIds, previousId, (a, b) => a - b);
		}

		const id = this.idPool;
		if (idx >= 0) {
			const value = strings.removeAnsiEscapeCodes(this.data.slice(idx + 1).join(''));
			return { value, id };
		} else {
			const value = strings.removeAnsiEscapeCodes(this.data.join(''));
			return { value, id };
		}
	}
I
isidor 已提交
803
}