outputServices.ts 24.9 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';
8 9
import * as strings from 'vs/base/common/strings';
import * as extfs from 'vs/base/node/extfs';
10
import * as fs from 'fs';
J
Johannes Rieken 已提交
11 12
import { TPromise } from 'vs/base/common/winjs.base';
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';
J
Johannes Rieken 已提交
15 16
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
17
import { Registry } from 'vs/platform/registry/common/platform';
J
Johannes Rieken 已提交
18
import { EditorOptions } from 'vs/workbench/common/editor';
19
import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, OUTPUT_SCHEME, OUTPUT_MIME, MAX_OUTPUT_LENGTH, LOG_SCHEME } from 'vs/workbench/parts/output/common/output';
J
Johannes Rieken 已提交
20 21 22 23 24
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';
25
import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService';
A
Alex Dima 已提交
26
import { ITextModel } from 'vs/editor/common/model';
27
import { IModeService } from 'vs/editor/common/services/modeService';
28
import { RunOnceScheduler, ThrottledDelayer } from 'vs/base/common/async';
29 30
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
31
import { IFileService } from 'vs/platform/files/common/files';
32 33
import { IPanel } from 'vs/workbench/common/panel';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
34 35 36
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { RotatingLogger } from 'spdlog';
import { toLocalISOString } from 'vs/base/common/date';
37
import { IWindowService } from 'vs/platform/windows/common/windows';
38 39 40
import { ILogService } from 'vs/platform/log/common/log';
import { binarySearch } from 'vs/base/common/arrays';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
41
import { Schemas } from 'vs/base/common/network';
S
Sandeep Somavarapu 已提交
42
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
43
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
44 45

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

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
let watchingOutputDir = false;
let callbacks = [];
function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable {
	callbacks.push(onChange);
	if (!watchingOutputDir) {
		try {
			const watcher = extfs.watch(outputDir, (eventType, fileName) => {
				for (const callback of callbacks) {
					callback(eventType, fileName);
				}
			});
			watcher.on('error', (code: number, signal: string) => logService.error(`Error watching ${outputDir}: (${code}, ${signal})`));
			watchingOutputDir = true;
			return toDisposable(() => {
				callbacks = [];
				watcher.removeAllListeners();
				watcher.close();
			});
		} catch (error) {
			logService.error(`Error watching ${outputDir}:  (${error.toString()})`);
		}
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 133

	private startOffset: number = 0;
134
	protected endOffset: number = 0;
135 136

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

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

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

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

A
Alex Dima 已提交
167
	loadModel(): TPromise<ITextModel> {
168 169
		return this.fileService.resolveContent(this.file, { position: this.startOffset })
			.then(content => {
170 171 172 173 174 175 176
				if (this.model) {
					this.model.setValue(content.value);
				} else {
					this.model = this.createModel(content.value);
				}
				this.endOffset = this.startOffset + new Buffer(this.model.getValueLength()).byteLength;
				return this.model;
177
			});
178 179
	}

180 181 182 183 184 185 186 187 188
	resetModel(): TPromise<void> {
		this.startOffset = 0;
		this.endOffset = 0;
		if (this.model) {
			return this.loadModel() as TPromise;
		}
		return TPromise.as(null);
	}

A
Alex Dima 已提交
189
	private createModel(content: string): ITextModel {
190
		const model = this.modelService.createModel(content, this.modeService.getOrCreateMode(OUTPUT_MIME), this.modelUri);
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
		this.onModelCreated(model);
		const disposables: IDisposable[] = [];
		disposables.push(model.onWillDispose(() => {
			this.onModelWillDispose(model);
			this.model = null;
			dispose(disposables);
		}));
		return model;
	}

	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)]);
206
			this._onDidAppendedContent.fire();
207
		}
208 209
	}

A
Alex Dima 已提交
210 211
	protected onModelCreated(model: ITextModel) { }
	protected onModelWillDispose(model: ITextModel) { }
212 213 214 215 216
	protected updateModel() { }

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

220 221 222 223
/**
 * An output channel that stores appended messages in a backup file.
 */
class OutputChannelBackedByFile extends AbstractFileOutputChannel implements OutputChannel {
224

225 226 227
	private outputWriter: RotatingLogger;
	private appendedMessage = '';
	private loadingFromFileInProgress: boolean = false;
228
	private resettingDelayer: ThrottledDelayer<void>;
229
	private readonly rotatingFilePath: string;
230
	private hasContentsToFlush: boolean = false;
231 232 233

	constructor(
		outputChannelIdentifier: IOutputChannelIdentifier,
234
		outputDir: string,
235
		modelUri: URI,
236 237 238
		@IFileService fileService: IFileService,
		@IModelService modelService: IModelService,
		@IModeService modeService: IModeService,
239 240
		@ILogService logService: ILogService,
		@IConfigurationService configurationService: IConfigurationService
241
	) {
242
		super({ ...outputChannelIdentifier, file: URI.file(paths.join(outputDir, `${outputChannelIdentifier.id}.log`)) }, modelUri, fileService, modelService, modeService);
243

244
		// Use one rotating file to check for main file reset
245 246
		const threshold = configurationService.getValue('output.threshold');
		this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, threshold && typeof threshold === 'number' ? threshold : 1024 * 1024 * 30, 1);
247
		this.outputWriter.clearFormatters();
248
		this.rotatingFilePath = `${outputChannelIdentifier.id}.1.log`;
249
		this._register(watchOutputDirectory(paths.dirname(this.file.fsPath), logService, (eventType, file) => this.onFileChangedInOutputDirector(eventType, file)));
250 251

		this.resettingDelayer = new ThrottledDelayer<void>(50);
252 253 254
	}

	append(message: string): void {
S
Sandeep Somavarapu 已提交
255 256
		// update end offset always as message is read
		this.endOffset = this.endOffset + new Buffer(message).byteLength;
257 258 259
		if (this.loadingFromFileInProgress) {
			this.appendedMessage += message;
		} else {
260
			this.write(message);
261 262 263 264 265 266 267 268 269 270 271 272 273 274
			if (this.model) {
				this.appendedMessage += message;
				if (!this.modelUpdater.isScheduled()) {
					this.modelUpdater.schedule();
				}
			}
		}
	}

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

A
Alex Dima 已提交
275
	loadModel(): TPromise<ITextModel> {
276 277 278 279 280 281
		this.startLoadingFromFile();
		return super.loadModel()
			.then(model => {
				this.finishedLoadingFromFile();
				return model;
			});
282 283 284
	}

	protected updateModel(): void {
285 286 287
		if (this.model && this.appendedMessage) {
			this.appendToModel(this.appendedMessage);
			this.appendedMessage = '';
288
		}
289 290
	}

291 292
	private startLoadingFromFile(): void {
		this.loadingFromFileInProgress = true;
293
		this.flush();
294 295 296 297
		if (this.modelUpdater.isScheduled()) {
			this.modelUpdater.cancel();
		}
		this.appendedMessage = '';
298 299
	}

300 301
	private finishedLoadingFromFile(): void {
		if (this.appendedMessage) {
302
			this.write(this.appendedMessage);
303 304 305 306
			this.appendToModel(this.appendedMessage);
			this.appendedMessage = '';
		}
		this.loadingFromFileInProgress = false;
307 308
	}

309
	private onFileChangedInOutputDirector(eventType: string, fileName: string): void {
310
		// Check if rotating file has changed. It changes only when the main file exceeds its limit.
311
		if (this.rotatingFilePath === fileName) {
312
			this.resettingDelayer.trigger(() => this.resetModel());
313
		}
314
	}
315 316 317 318 319 320 321 322 323 324 325 326

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

	private flush(): void {
		if (this.hasContentsToFlush) {
			this.outputWriter.flush();
			this.hasContentsToFlush = false;
		}
	}
327 328
}

329 330 331 332
class OutputFileListener extends Disposable {

	private _onDidChange: Emitter<void> = new Emitter<void>();
	readonly onDidContentChange: Event<void> = this._onDidChange.event;
333

334
	private watching: boolean = false;
335 336 337 338 339 340 341 342 343
	private disposables: IDisposable[] = [];

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

	watch(): void {
344 345 346 347
		if (!this.watching) {
			this.disposables.push(watchFile(this.file.fsPath, () => this._onDidChange.fire()));
			this.watching = true;
		}
348 349 350
	}

	unwatch(): void {
351 352 353 354
		if (this.watching) {
			this.disposables = dispose(this.disposables);
			this.watching = false;
		}
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
	}

	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;
371 372 373

	constructor(
		outputChannelIdentifier: IOutputChannelIdentifier,
374
		modelUri: URI,
375 376
		@IFileService fileService: IFileService,
		@IModelService modelService: IModelService,
377 378
		@IModeService modeService: IModeService,
		@ILogService logService: ILogService,
379
	) {
380
		super(outputChannelIdentifier, modelUri, fileService, modelService, modeService);
381

382
		this.fileHandler = this._register(new OutputFileListener(this.file));
383 384
		this._register(this.fileHandler.onDidContentChange(() => this.onDidContentChange()));
		this._register(toDisposable(() => this.fileHandler.unwatch()));
385 386 387
	}

	append(message: string): void {
388 389 390 391 392 393 394
		throw new Error('Not supported');
	}

	protected updateModel(): void {
		if (this.model) {
			this.fileService.resolveContent(this.file, { position: this.endOffset })
				.then(content => {
S
Sandeep Somavarapu 已提交
395 396 397 398
					if (content.value) {
						this.endOffset = this.endOffset + new Buffer(content.value).byteLength;
						this.appendToModel(content.value);
					}
399 400 401 402
					this.updateInProgress = false;
				}, () => this.updateInProgress = false);
		} else {
			this.updateInProgress = false;
403 404 405
		}
	}

A
Alex Dima 已提交
406
	protected onModelCreated(model: ITextModel): void {
407
		this.fileHandler.watch();
408 409
	}

A
Alex Dima 已提交
410
	protected onModelWillDispose(model: ITextModel): void {
411
		this.fileHandler.unwatch();
412 413
	}

414 415 416 417
	private onDidContentChange(): void {
		if (!this.updateInProgress) {
			this.updateInProgress = true;
			this.modelUpdater.schedule();
418 419 420 421
		}
	}
}

422
export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider {
423

424 425 426
	public _serviceBrand: any;

	private channels: Map<string, OutputChannel> = new Map<string, OutputChannel>();
S
Sandeep Somavarapu 已提交
427 428
	private activeChannelIdInStorage: string;
	private activeChannel: IOutputChannel;
429
	private readonly outputDir: string;
430 431 432 433 434

	private _onActiveOutputChannel: Emitter<string> = new Emitter<string>();
	readonly onActiveOutputChannel: Event<string> = this._onActiveOutputChannel.event;

	private _outputPanel: OutputPanel;
435 436

	constructor(
437 438 439 440 441
		@IStorageService private storageService: IStorageService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@IPanelService private panelService: IPanelService,
		@IWorkspaceContextService contextService: IWorkspaceContextService,
		@ITextModelService textModelResolverService: ITextModelService,
442 443 444
		@IEnvironmentService environmentService: IEnvironmentService,
		@IWindowService windowService: IWindowService,
		@ITelemetryService private telemetryService: ITelemetryService,
S
Sandeep Somavarapu 已提交
445 446
		@ILogService private logService: ILogService,
		@ILifecycleService private lifecycleService: ILifecycleService,
447
	) {
448
		super();
S
Sandeep Somavarapu 已提交
449 450
		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, '')}`);
451 452 453

		// Register as text model content provider for output
		textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this);
S
Sandeep Somavarapu 已提交
454 455 456 457 458 459 460 461
		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));
462 463 464

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

466
		this._register(toDisposable(() => unWatchAllFiles()));
S
Sandeep Somavarapu 已提交
467 468 469 470 471 472 473 474

		// 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());
475 476
	}

A
Alex Dima 已提交
477
	provideTextContent(resource: URI): TPromise<ITextModel> {
478
		const channel = <OutputChannel>this.getChannel(resource.fsPath);
479 480 481 482
		if (channel) {
			return channel.loadModel();
		}
		return TPromise.as(null);
483 484 485
	}

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

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

501 502 503 504 505 506
	getChannel(id: string): IOutputChannel {
		return this.channels.get(id);
	}

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

509
	getActiveChannel(): IOutputChannel {
S
Sandeep Somavarapu 已提交
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
		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;
			this.onDidPanelOpen(this.panelService.getActivePanel());
		}
	}

	private onDidPanelOpen(panel: IPanel): void {
		if (panel && panel.getId() === OUTPUT_PANEL_ID) {
			this._outputPanel = <OutputPanel>this.panelService.getActivePanel();
			if (this.activeChannel) {
				this.doShowChannel(this.activeChannel, true);
			}
		}
	}

	private onDidPanelClose(panel: IPanel): void {
		if (this._outputPanel && panel.getId() === OUTPUT_PANEL_ID) {
			this._outputPanel.clearInput();
		}
535
	}
536

537 538 539
	private createChannel(id: string): OutputChannel {
		const channelDisposables = [];
		const channel = this.instantiateChannel(id);
540 541 542
		channel.onDidAppendedContent(() => {
			if (!channel.scrollLock) {
				const panel = this.panelService.getActivePanel();
S
Sandeep Somavarapu 已提交
543
				if (panel && panel.getId() === OUTPUT_PANEL_ID && this.isChannelShown(channel)) {
544 545 546 547
					(<OutputPanel>panel).revealLastLine();
				}
			}
		}, channelDisposables);
548 549
		channel.onDispose(() => {
			Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(id);
S
Sandeep Somavarapu 已提交
550
			if (this.activeChannel === channel) {
551
				const channels = this.getChannels();
S
Sandeep Somavarapu 已提交
552 553 554
				if (this.isPanelShown() && channels.length) {
					this.doShowChannel(this.getChannel(channels[0].id), true);
					this._onActiveOutputChannel.fire(channels[0].id);
555 556 557 558 559 560 561 562 563 564 565 566
				} 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 已提交
567 568 569 570 571
		if (!channelData) {
			this.logService.error(`Channel '${id}' is not registered yet`);
			throw new Error(`Channel '${id}' is not registered yet`);
		}

572
		const uri = URI.from({ scheme: OUTPUT_SCHEME, path: id });
573
		if (channelData && channelData.file) {
574
			return this.instantiationService.createInstance(FileOutputChannel, channelData, uri);
575
		}
576
		try {
577
			return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri);
578 579 580 581 582
		} catch (e) {
			this.logService.error(e);
			this.telemetryService.publicLog('output.used.bufferedChannel');
			return this.instantiationService.createInstance(BufferredOutputChannel, { id, label: channelData ? channelData.label : '' });
		}
583
	}
584

S
Sandeep Somavarapu 已提交
585
	private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): void {
586
		if (this._outputPanel) {
S
Sandeep Somavarapu 已提交
587
			this._outputPanel.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: preserveFocus }));
588 589 590 591 592 593
			if (!preserveFocus) {
				this._outputPanel.focus();
			}
		}
	}

S
Sandeep Somavarapu 已提交
594
	private isChannelShown(channel: IOutputChannel): boolean {
S
Sandeep Somavarapu 已提交
595 596 597 598
		return this.isPanelShown() && this.activeChannel === channel;
	}

	private isPanelShown(): boolean {
S
Sandeep Somavarapu 已提交
599
		const panel = this.panelService.getActivePanel();
S
Sandeep Somavarapu 已提交
600
		return panel && panel.getId() === OUTPUT_PANEL_ID;
S
Sandeep Somavarapu 已提交
601 602 603 604 605 606 607 608 609 610 611 612
	}

	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();
613
	}
614 615
}

616 617 618 619 620
export class LogContentProvider {

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

	constructor(
621
		@IInstantiationService private instantiationService: IInstantiationService
622 623 624
	) {
	}

A
Alex Dima 已提交
625
	provideTextContent(resource: URI): TPromise<ITextModel> {
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
		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) {
			const channelDisposables = [];
			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;
	}
}

648 649 650 651 652
// Remove this channel when there are no issues using Output channel backed by file
class BufferredOutputChannel extends Disposable implements OutputChannel {

	readonly id: string;
	readonly label: string;
S
Sandeep Somavarapu 已提交
653
	readonly file: URI = null;
654 655
	scrollLock: boolean = false;

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

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

	private modelUpdater: RunOnceScheduler;
A
Alex Dima 已提交
663
	private model: ITextModel;
664 665 666 667 668 669
	private readonly bufferredContent: BufferedContent;
	private lastReadId: number = void 0;

	constructor(
		protected readonly outputChannelIdentifier: IOutputChannelIdentifier,
		@IModelService private modelService: IModelService,
670
		@IModeService private modeService: IModeService
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
	) {
		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;
	}

A
Alex Dima 已提交
702
	loadModel(): TPromise<ITextModel> {
703 704 705 706 707 708 709 710 711 712
		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);
	}

A
Alex Dima 已提交
713
	private createModel(content: string): ITextModel {
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
		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)]);
730
			this._onDidAppendedContent.fire();
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
		}
	}

	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 };
		}
	}
787
}