outputServices.ts 25.2 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';
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';
S
Sandeep Somavarapu 已提交
19
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 已提交
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
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
S
Sandeep Somavarapu 已提交
35
import { RotatingLogger } from 'spdlog';
36
import { toLocalISOString } from 'vs/base/common/date';
37
import { IWindowService } from 'vs/platform/windows/common/windows';
S
Sandeep Somavarapu 已提交
38 39
import { ILogService } from 'vs/platform/log/common/log';
import { binarySearch } from 'vs/base/common/arrays';
40
import { Schemas } from 'vs/base/common/network';
S
Sandeep Somavarapu 已提交
41
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
S
Sandeep Somavarapu 已提交
42
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
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
function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable {
	callbacks.push(onChange);
	if (!watchingOutputDir) {
51 52 53 54 55 56 57 58 59 60 61
		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) {
62 63
				watcher.removeAllListeners();
				watcher.close();
64 65
			}
		});
66
	}
67
	return toDisposable(() => { });
68 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
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();
}
109

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

117
abstract class AbstractFileOutputChannel extends Disposable {
118 119

	scrollLock: boolean = false;
120

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

316 317
class OutputFileListener extends Disposable {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

419 420 421
	public _serviceBrand: any;

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

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

	private _outputPanel: OutputPanel;
430 431

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

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

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

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

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

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

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

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

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

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

504
	getActiveChannel(): IOutputChannel {
S
Sandeep Somavarapu 已提交
505 506 507 508 509 510 511 512
		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 已提交
513 514
			this.onDidPanelOpen(this.panelService.getActivePanel())
				.then(() => this._onActiveOutputChannel.fire(channelId));
S
Sandeep Somavarapu 已提交
515 516 517
		}
	}

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

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

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

570
		const uri = URI.from({ scheme: OUTPUT_SCHEME, path: id });
S
Sandeep Somavarapu 已提交
571 572 573 574 575 576 577 578 579 580
		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 : '' });
		}
581
	}
582

S
Sandeep Somavarapu 已提交
583
	private doShowChannel(channel: IOutputChannel, preserveFocus: boolean): TPromise<void> {
584
		if (this._outputPanel) {
S
Sandeep Somavarapu 已提交
585 586 587 588 589 590 591
			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();
					}
				});
592
		}
S
Sandeep Somavarapu 已提交
593
		return TPromise.as(null);
594 595
	}

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

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

	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();
615
	}
616 617
}

618 619 620 621 622
export class LogContentProvider {

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

	constructor(
623
		@IInstantiationService private instantiationService: IInstantiationService
624 625 626
	) {
	}

A
Alex Dima 已提交
627
	provideTextContent(resource: URI): TPromise<ITextModel> {
628 629 630 631 632 633 634 635 636 637 638 639 640
		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) {
641
			const channelDisposables: IDisposable[] = [];
642 643 644 645 646 647
			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 已提交
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 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
}
// 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 };
		}
	}
788
}