outputServices.ts 24.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';
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 44

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

46
let watchingOutputDir = false;
47
let callbacks: ((eventType: string, fileName: string) => void)[] = [];
48 49 50 51 52 53 54 55
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);
				}
56 57
			}, (code: number, signal: string) => {
				logService.error(`Error watching ${outputDir}: (${code}, ${signal})`);
58 59 60 61 62 63 64 65 66 67
			});
			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

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,
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
	}

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

	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)]);
188
			this._onDidAppendedContent.fire();
189
		}
190 191
	}

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

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

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

207 208 209
	private outputWriter: RotatingLogger;
	private appendedMessage = '';
	private loadingFromFileInProgress: boolean = false;
210
	private resettingDelayer: ThrottledDelayer<void>;
211
	private readonly rotatingFilePath: string;
212 213 214

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

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

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

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

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

A
Alex Dima 已提交
254
	loadModel(): TPromise<ITextModel> {
S
Sandeep Somavarapu 已提交
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
		this.loadingFromFileInProgress = true;
		if (this.modelUpdater.isScheduled()) {
			this.modelUpdater.cancel();
		}
		this.appendedMessage = '';
		return this.loadFile()
			.then(content => {
				if (this.endOffset !== this.startOffset + new Buffer(content).byteLength) {
					// 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);
277
			});
278 279
	}

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

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

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

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

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

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

317 318 319 320
class OutputFileListener extends Disposable {

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

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

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

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

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

	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;
359 360 361

	constructor(
		outputChannelIdentifier: IOutputChannelIdentifier,
362
		modelUri: URI,
363 364
		@IFileService fileService: IFileService,
		@IModelService modelService: IModelService,
365 366
		@IModeService modeService: IModeService,
		@ILogService logService: ILogService,
367
	) {
368
		super(outputChannelIdentifier, modelUri, fileService, modelService, modeService);
369

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

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

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

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

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

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

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

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

420 421 422
	public _serviceBrand: any;

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

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

	private _outputPanel: OutputPanel;
431 432

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

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

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

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

		// 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());
471 472
	}

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

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

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

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

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

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

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

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

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

S
Sandeep Somavarapu 已提交
590
	private isChannelShown(channel: IOutputChannel): boolean {
S
Sandeep Somavarapu 已提交
591 592 593 594
		return this.isPanelShown() && this.activeChannel === channel;
	}

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

	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();
609
	}
610 611
}

612 613 614 615 616
export class LogContentProvider {

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

	constructor(
617
		@IInstantiationService private instantiationService: IInstantiationService
618 619 620
	) {
	}

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

644 645 646 647 648
// 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 已提交
649
	readonly file: URI = null;
650 651
	scrollLock: boolean = false;

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

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

	private modelUpdater: RunOnceScheduler;
A
Alex Dima 已提交
659
	private model: ITextModel;
660 661 662 663 664 665
	private readonly bufferredContent: BufferedContent;
	private lastReadId: number = void 0;

	constructor(
		protected readonly outputChannelIdentifier: IOutputChannelIdentifier,
		@IModelService private modelService: IModelService,
666
		@IModeService private modeService: IModeService
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
	) {
		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 已提交
698
	loadModel(): TPromise<ITextModel> {
699 700 701 702 703 704 705 706 707 708
		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 已提交
709
	private createModel(content: string): ITextModel {
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725
		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)]);
726
			this._onDidAppendedContent.fire();
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
		}
	}

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