outputServices.ts 26.3 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 45

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

47
let watchingOutputDir = false;
48
let callbacks: ((eventType: string, fileName: string) => void)[] = [];
49 50 51
function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable {
	callbacks.push(onChange);
	if (!watchingOutputDir) {
52 53 54 55 56 57 58 59 60 61 62
		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) {
63 64
				watcher.removeAllListeners();
				watcher.close();
65 66
			}
		});
67
	}
68
	return toDisposable(() => { });
69 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
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();
}
110

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

118
abstract class AbstractFileOutputChannel extends Disposable {
119 120

	scrollLock: boolean = false;
121

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

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

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

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

	constructor(
136
		protected readonly outputChannelIdentifier: IOutputChannelIdentifier,
137
		private readonly modelUri: URI,
S
Sandeep Somavarapu 已提交
138
		private mimeType: string,
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
			this.model.setValue(content);
		} else {
S
Sandeep Somavarapu 已提交
171
			this.model = this.modelService.createModel(content, this.modeService.getOrCreateMode(this.mimeType), this.modelUri);
S
Sandeep Somavarapu 已提交
172 173 174 175 176 177 178
			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

S
Sandeep Somavarapu 已提交
207
	private outputWriter: RotatingLogger;
208 209
	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
	) {
S
Sandeep Somavarapu 已提交
222
		super({ ...outputChannelIdentifier, file: URI.file(paths.join(outputDir, `${outputChannelIdentifier.id}.log`)) }, modelUri, OUTPUT_MIME, fileService, modelService, modeService);
223

224
		// Use one rotating file to check for main file reset
S
Sandeep Somavarapu 已提交
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
		// update end offset always as message is read
235
		this.endOffset = this.endOffset + Buffer.from(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
		this.loadingFromFileInProgress = true;
		if (this.modelUpdater.isScheduled()) {
			this.modelUpdater.cancel();
		}
		this.appendedMessage = '';
		return this.loadFile()
			.then(content => {
262
				if (this.endOffset !== this.startOffset + Buffer.from(content).byteLength) {
S
Sandeep Somavarapu 已提交
263 264 265 266 267 268 269 270 271 272 273 274 275 276
					// 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
class OutputFileListener extends Disposable {

M
Matt Bierner 已提交
319
	private readonly _onDidChange: Emitter<void> = new Emitter<void>();
320
	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
	) {
S
Sandeep Somavarapu 已提交
368
		super(outputChannelIdentifier, modelUri, LOG_MIME, 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
	loadModel(): TPromise<ITextModel> {
		return this.fileService.resolveContent(this.file, { position: this.startOffset })
			.then(content => {
378
				this.endOffset = this.startOffset + Buffer.from(content.value).byteLength;
S
Sandeep Somavarapu 已提交
379 380 381 382
				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
					if (content.value) {
392
						this.endOffset = this.endOffset + Buffer.from(content.value).byteLength;
S
Sandeep Somavarapu 已提交
393 394
						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

M
Matt Bierner 已提交
427
	private readonly _onActiveOutputChannel: Emitter<string> = new Emitter<string>();
428 429 430
	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
		@IEnvironmentService environmentService: IEnvironmentService,
		@IWindowService windowService: IWindowService,
S
Sandeep Somavarapu 已提交
440 441
		@ILogService private logService: ILogService,
		@ILifecycleService private lifecycleService: ILifecycleService,
S
Sandeep Somavarapu 已提交
442
		@IContextKeyService private contextKeyService: IContextKeyService,
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> {
S
Sandeep Somavarapu 已提交
474
		const channel = <OutputChannel>this.getChannel(resource.path);
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
		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 已提交
514 515
			this.onDidPanelOpen(this.panelService.getActivePanel())
				.then(() => this._onActiveOutputChannel.fire(channelId));
S
Sandeep Somavarapu 已提交
516 517 518
		}
	}

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

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

536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
	private smartRevealLastLine(outputPanel: OutputPanel) {
		const codeEditor = <ICodeEditor>outputPanel.getControl();
		const model = codeEditor.getModel();

		if (model) {
			// Only scroll if the cursor is currently on the last line of the output panel. This allows
			// users to click on the output panel to stop scrolling when they see something of interest.
			// To resume, they should scroll to the end of the output panel again.
			const lastLine = model.getLineCount();
			if (codeEditor.getPosition().lineNumber === lastLine) {
				codeEditor.revealPosition({ lineNumber: lastLine, column: model.getLineMaxColumn(lastLine) });
			}
		}
	}

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

561
	private createChannel(id: string): OutputChannel {
562
		const channelDisposables: IDisposable[] = [];
563
		const channel = this.instantiateChannel(id);
564 565 566
		channel.onDidAppendedContent(() => {
			if (!channel.scrollLock) {
				const panel = this.panelService.getActivePanel();
S
Sandeep Somavarapu 已提交
567
				if (panel && panel.getId() === OUTPUT_PANEL_ID && this.isChannelShown(channel)) {
568 569
					let outputPanel = <OutputPanel>panel;
					this.smartRevealLastLine(outputPanel);
570 571 572
				}
			}
		}, channelDisposables);
573 574
		channel.onDispose(() => {
			Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).removeChannel(id);
S
Sandeep Somavarapu 已提交
575
			if (this.activeChannel === channel) {
576
				const channels = this.getChannels();
S
Sandeep Somavarapu 已提交
577 578 579
				if (this.isPanelShown() && channels.length) {
					this.doShowChannel(this.getChannel(channels[0].id), true);
					this._onActiveOutputChannel.fire(channels[0].id);
580 581 582 583 584 585 586 587 588 589 590 591
				} 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 已提交
592 593 594 595 596
		if (!channelData) {
			this.logService.error(`Channel '${id}' is not registered yet`);
			throw new Error(`Channel '${id}' is not registered yet`);
		}

597
		const uri = URI.from({ scheme: OUTPUT_SCHEME, path: id });
S
Sandeep Somavarapu 已提交
598 599 600 601 602 603 604 605 606 607
		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 : '' });
		}
608
	}
609

S
Sandeep Somavarapu 已提交
610
	private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): TPromise<void> {
611
		if (this._outputPanel) {
S
Sandeep Somavarapu 已提交
612 613 614 615 616 617
			CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(channel instanceof FileOutputChannel);
			return this._outputPanel.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: preserveFocus }))
				.then(() => {
					if (!preserveFocus) {
						this._outputPanel.focus();
					}
618 619 620
				})
				// Activate smart scroll when switching back to the output panel
				.then(() => this.setPrimaryCursorToLastLine());
621
		}
S
Sandeep Somavarapu 已提交
622
		return TPromise.as(null);
623 624
	}

S
Sandeep Somavarapu 已提交
625
	private isChannelShown(channel: IOutputChannel): boolean {
S
Sandeep Somavarapu 已提交
626 627 628 629
		return this.isPanelShown() && this.activeChannel === channel;
	}

	private isPanelShown(): boolean {
S
Sandeep Somavarapu 已提交
630
		const panel = this.panelService.getActivePanel();
S
Sandeep Somavarapu 已提交
631
		return panel && panel.getId() === OUTPUT_PANEL_ID;
S
Sandeep Somavarapu 已提交
632 633 634 635 636 637 638 639 640 641 642 643
	}

	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();
644
	}
645 646
}

647 648 649 650 651
export class LogContentProvider {

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

	constructor(
652
		@IInstantiationService private instantiationService: IInstantiationService
653 654 655
	) {
	}

A
Alex Dima 已提交
656
	provideTextContent(resource: URI): TPromise<ITextModel> {
657 658 659 660 661 662 663 664 665 666 667 668 669
		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) {
670
			const channelDisposables: IDisposable[] = [];
671 672 673 674 675 676
			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 已提交
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 803 804 805 806 807 808 809 810 811 812 813 814 815 816
}
// 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 };
		}
	}
817
}