diff --git a/src/vs/workbench/parts/output/electron-browser/outputServices.ts b/src/vs/workbench/parts/output/electron-browser/outputServices.ts index e56069a309877ee9a9f6a9b03c1bcc1d3315bc12..29aac683a9d2f9c81f73290d7f9b45d6580045bf 100644 --- a/src/vs/workbench/parts/output/electron-browser/outputServices.ts +++ b/src/vs/workbench/parts/output/electron-browser/outputServices.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as fs from 'fs'; import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import Event, { Emitter } from 'vs/base/common/event'; @@ -36,173 +35,10 @@ import { toLocalISOString } from 'vs/base/common/date'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; -export class OutputService implements IOutputService, ITextModelContentProvider { - - public _serviceBrand: any; - - private channels: Map = new Map(); - private activeChannelId: string; - - private _onDidChannelContentChange: Emitter = new Emitter(); - readonly onDidChannelContentChange: Event = this._onDidChannelContentChange.event; - - private _onActiveOutputChannel: Emitter = new Emitter(); - readonly onActiveOutputChannel: Event = this._onActiveOutputChannel.event; - - private _outputPanel: OutputPanel; - - constructor( - @IStorageService private storageService: IStorageService, - @IInstantiationService private instantiationService: IInstantiationService, - @IPanelService private panelService: IPanelService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IModelService private modelService: IModelService, - @IModeService private modeService: IModeService, - @ITextModelService textModelResolverService: ITextModelService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IEnvironmentService private environmentService: IEnvironmentService - ) { - const channels = this.getChannels(); - this.activeChannelId = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, channels && channels.length > 0 ? channels[0].id : null); - - instantiationService.createInstance(OutputLinkProvider); - - // Register as text model content provider for output - textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); - - this.onDidPanelOpen(this.panelService.getActivePanel()); - panelService.onDidPanelOpen(this.onDidPanelOpen, this); - panelService.onDidPanelClose(this.onDidPanelClose, this); - } - - provideTextContent(resource: URI): TPromise { - const channel = this.getChannel(resource.fsPath); - return channel.getOutputDelta() - .then(outputDelta => this.modelService.createModel(outputDelta.value, this.modeService.getOrCreateMode(OUTPUT_MIME), resource)); - } - - showChannel(id: string, preserveFocus?: boolean): TPromise { - if (this.isChannelShown(id)) { - return TPromise.as(null); - } - - if (this.activeChannelId) { - this.doHideChannel(this.activeChannelId); - } - - this.activeChannelId = id; - const promise: TPromise = this._outputPanel ? this.doShowChannel(id, preserveFocus) : this.panelService.openPanel(OUTPUT_PANEL_ID); - return promise.then(() => this._onActiveOutputChannel.fire(id)); - } - - showChannelInEditor(channelId: string): TPromise { - return this.editorService.openEditor(this.createInput(channelId)) as TPromise; - } - - getChannel(id: string): IOutputChannel { - if (!this.channels.has(id)) { - this.channels.set(id, this.createChannel(id)); - } - return this.channels.get(id); - } - - getChannels(): IOutputChannelIdentifier[] { - return Registry.as(Extensions.OutputChannels).getChannels(); - } - - getActiveChannel(): IOutputChannel { - return this.getChannel(this.activeChannelId); - } - - private createChannel(id: string): OutputChannel { - const channelDisposables = []; - const channelData = Registry.as(Extensions.OutputChannels).getChannel(id); - const file = channelData && channelData.file ? channelData.file : URI.file(paths.join(this.environmentService.userDataPath, 'outputs', toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''), `${id}.output.log`)); - const channel = channelData && channelData.file ? this.instantiationService.createInstance(OutputChannel, channelData) : - this.instantiationService.createInstance(WritableOutputChannel, { id, label: channelData ? channelData.label : '', file }); - channelDisposables.push(this.instantiationService.createInstance(ChannelModelUpdater, channel)); - channel.onDidChange(() => this._onDidChannelContentChange.fire(id), channelDisposables); - channel.onDispose(() => { - Registry.as(Extensions.OutputChannels).removeChannel(id); - if (this.activeChannelId === id) { - const channels = this.getChannels(); - if (this._outputPanel && channels.length) { - this.showChannel(channels[0].id); - } else { - this._onActiveOutputChannel.fire(void 0); - } - } - dispose(channelDisposables); - }, channelDisposables); - - return channel; - } - - - private isChannelShown(channelId: string): boolean { - const panel = this.panelService.getActivePanel(); - return panel && panel.getId() === OUTPUT_PANEL_ID && this.activeChannelId === channelId; - } - - private onDidPanelClose(panel: IPanel): void { - if (this._outputPanel && panel.getId() === OUTPUT_PANEL_ID) { - if (this.activeChannelId) { - this.doHideChannel(this.activeChannelId); - } - this._outputPanel.clearInput(); - } - } - - private onDidPanelOpen(panel: IPanel): void { - if (panel && panel.getId() === OUTPUT_PANEL_ID) { - this._outputPanel = this.panelService.getActivePanel(); - if (this.activeChannelId) { - this.doShowChannel(this.activeChannelId, true); - } - } - } - - private doShowChannel(channelId: string, preserveFocus: boolean): TPromise { - if (this._outputPanel) { - const channel = this.getChannel(channelId); - return channel.show() - .then(() => { - this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, channelId, StorageScope.WORKSPACE); - this._outputPanel.setInput(this.createInput(channelId), EditorOptions.create({ preserveFocus: preserveFocus })); - if (!preserveFocus) { - this._outputPanel.focus(); - } - }); - } else { - return TPromise.as(null); - } - } - - private doHideChannel(channelId): void { - const channel = this.getChannel(channelId); - if (channel) { - channel.hide(); - } - } - - private createInput(channelId: string): ResourceEditorInput { - const resource = URI.from({ scheme: OUTPUT_SCHEME, path: channelId }); - const channelData = Registry.as(Extensions.OutputChannels).getChannel(channelId); - const label = channelData ? channelData.label : channelId; - return this.instantiationService.createInstance(ResourceEditorInput, nls.localize('output', "{0} - Output", label), nls.localize('channel', "Output channel for '{0}'", label), resource); - } -} - -export interface IOutputDelta { - readonly value: string; - readonly id: number; - readonly append?: boolean; -} - -class OutputFileListener extends Disposable { +class OutputFileHandler extends Disposable { private _onDidChange: Emitter = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; + readonly onDidContentChange: Event = this._onDidChange.event; private disposables: IDisposable[] = []; @@ -238,13 +74,12 @@ class OutputFileListener extends Disposable { } } -class OutputChannel extends Disposable implements IOutputChannel { - - protected _onDidChange: Emitter = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; +interface OutputChannel extends IOutputChannel { + readonly onDispose: Event; + resolve(): TPromise; +} - protected _onDidClear: Emitter = new Emitter(); - readonly onDidClear: Event = this._onDidClear.event; +class FileOutputChannel extends Disposable implements OutputChannel { protected _onDispose: Emitter = new Emitter(); readonly onDispose: Event = this._onDispose.event; @@ -252,21 +87,33 @@ class OutputChannel extends Disposable implements IOutputChannel { scrollLock: boolean = false; protected readonly file: URI; - private disposables: IDisposable[] = []; - protected shown: boolean = false; + private readonly fileHandler: OutputFileHandler; - private contentResolver: TPromise; + private updateInProgress: boolean = false; + private modelUpdater: RunOnceScheduler; private startOffset: number; private endOffset: number; constructor( private readonly outputChannelIdentifier: IOutputChannelIdentifier, - @IFileService protected fileService: IFileService + @IFileService protected fileService: IFileService, + @IModelService private modelService: IModelService, + @IPanelService private panelService: IPanelService ) { super(); this.file = outputChannelIdentifier.file; this.startOffset = 0; this.endOffset = 0; + + this.modelUpdater = new RunOnceScheduler(() => this.doUpdate(), 300); + this._register(toDisposable(() => this.modelUpdater.cancel())); + + this.fileHandler = this._register(new OutputFileHandler(this.file, this.fileService)); + this._register(this.fileHandler.onDidContentChange(() => this.onDidContentChange())); + this._register(toDisposable(() => this.fileHandler.unwatch())); + + this._register(this.modelService.onModelAdded(this.onModelAdded, this)); + this._register(this.modelService.onModelRemoved(this.onModelRemoved, this)); } get id(): string { @@ -277,185 +124,248 @@ class OutputChannel extends Disposable implements IOutputChannel { return this.outputChannelIdentifier.label; } - show(): TPromise { - if (!this.shown) { - this.shown = true; - this.watch(); - return this.resolve() as TPromise; - } - return TPromise.as(null); + append(message: string): void { + throw new Error('Not supported'); } - hide(): void { - if (this.shown) { - this.shown = false; - this.unwatch(); - this.contentResolver = null; + clear(): void { + this.startOffset = this.endOffset; + const model = this.getModel(); + if (model) { + model.setValue(''); } } - append(message: string): void { - throw new Error(nls.localize('appendNotSupported', "Append is not supported on File output channel")); + resolve(): TPromise { + return this.fileHandler.loadContent(this.startOffset); } - getOutputDelta(previousId?: number): TPromise { - return this.resolve() - .then(content => { - const startOffset = previousId !== void 0 ? previousId : this.startOffset; - if (this.startOffset === this.endOffset) { - // Content cleared - return { append: false, id: this.endOffset, value: '' }; - } - if (startOffset === this.endOffset) { - // Content not changed - return { append: true, id: this.endOffset, value: '' }; - } - if (startOffset > 0 && startOffset < this.endOffset) { - // Delta - const value = content.substring(startOffset, this.endOffset); - return { append: true, value, id: this.endOffset }; - } - // Replace - return { append: false, value: content, id: this.endOffset }; - }); + private onModelAdded(model: IModel): void { + if (model.uri.fsPath === this.id) { + this.endOffset = this.startOffset + model.getValueLength(); + this.fileHandler.watch(); + } } - clear(): void { - this.startOffset = this.endOffset; - this._onDidClear.fire(); + private onModelRemoved(model: IModel): void { + if (model.uri.fsPath === this.id) { + this.fileHandler.unwatch(); + } } - private resolve(): TPromise { - if (!this.contentResolver) { - this.contentResolver = this.fileService.resolveContent(this.file) - .then(result => { - const content = result.value; - if (this.endOffset !== content.length) { - this.endOffset = content.length; - this._onDidChange.fire(); - } - return content; - }); + private onDidContentChange(): void { + if (!this.updateInProgress) { + this.updateInProgress = true; + this.modelUpdater.schedule(); } - return this.contentResolver; } - private watch(): void { - this.fileService.watchFileChanges(this.file); - this.disposables.push(this.fileService.onFileChanges(changes => { - if (changes.contains(this.file, FileChangeType.UPDATED)) { - this.contentResolver = null; - this._onDidChange.fire(); - } - })); + private doUpdate(): void { + let model = this.getModel(); + if (model) { + this.fileHandler.loadContent(this.endOffset) + .then(delta => { + model = this.getModel(); + if (model && delta) { + const lastLine = model.getLineCount(); + const lastLineMaxColumn = model.getLineMaxColumn(lastLine); + model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), delta)]); + this.endOffset = this.endOffset + delta.length; + if (!this.scrollLock) { + (this.panelService.getActivePanel()).revealLastLine(); + } + } + this.updateInProgress = false; + }, () => this.updateInProgress = false); + } else { + this.updateInProgress = false; + } } - private unwatch(): void { - this.fileService.unwatchFileChanges(this.file); - this.disposables = dispose(this.disposables); + protected getModel(): IModel { + const model = this.modelService.getModel(URI.from({ scheme: OUTPUT_SCHEME, path: this.id })); + return model && !model.isDisposed() ? model : null; } dispose(): void { - this.hide(); this._onDispose.fire(); super.dispose(); } } -class WritableOutputChannel extends OutputChannel implements IOutputChannel { +class AppendableFileOutoutChannel extends FileOutputChannel implements OutputChannel { private outputWriter: RotatingLogger; private flushScheduler: RunOnceScheduler; constructor( outputChannelIdentifier: IOutputChannelIdentifier, - @IFileService fileService: IFileService + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + @IPanelService panelService: IPanelService, ) { - super(outputChannelIdentifier, fileService); + super(outputChannelIdentifier, fileService, modelService, panelService); this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, 1024 * 1024 * 5, 1); this.outputWriter.clearFormatters(); + this.flushScheduler = new RunOnceScheduler(() => this.outputWriter.flush(), 300); + this._register(toDisposable(() => this.flushScheduler.cancel())); + + this._register(modelService.onModelAdded(model => { + if (model.uri.fsPath === this.id && !this.flushScheduler.isScheduled()) { + this.flushScheduler.schedule(); + } + })); } append(message: string): void { this.outputWriter.critical(message); - if (this.shown && !this.flushScheduler.isScheduled()) { - this.flushScheduler.schedule(); - } - } - - show(): TPromise { - if (!this.flushScheduler.isScheduled()) { + if (this.getModel() && !this.flushScheduler.isScheduled()) { this.flushScheduler.schedule(); } - return super.show(); } } -class ChannelModelUpdater extends Disposable { +export class OutputService implements IOutputService, ITextModelContentProvider { - private updateInProgress: boolean = false; - private modelUpdater: RunOnceScheduler; - private lastReadId: number; + public _serviceBrand: any; + + private channels: Map = new Map(); + private activeChannelId: string; + + private _onActiveOutputChannel: Emitter = new Emitter(); + readonly onActiveOutputChannel: Event = this._onActiveOutputChannel.event; + + private _outputPanel: OutputPanel; constructor( - private channel: OutputChannel, + @IStorageService private storageService: IStorageService, + @IInstantiationService private instantiationService: IInstantiationService, + @IPanelService private panelService: IPanelService, + @IWorkspaceContextService contextService: IWorkspaceContextService, @IModelService private modelService: IModelService, - @IPanelService private panelService: IPanelService + @IModeService private modeService: IModeService, + @ITextModelService textModelResolverService: ITextModelService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEnvironmentService private environmentService: IEnvironmentService ) { - super(); - this.modelUpdater = new RunOnceScheduler(() => this.doUpdate(), 300); - this._register(channel.onDidChange(() => this.onDidChange())); - this._register(channel.onDidClear(() => this.onDidClear())); - this._register(toDisposable(() => this.modelUpdater.cancel())); - this._register(this.modelService.onModelRemoved(this.onModelRemoved, this)); + const channels = this.getChannels(); + this.activeChannelId = this.storageService.get(OUTPUT_ACTIVE_CHANNEL_KEY, StorageScope.WORKSPACE, channels && channels.length > 0 ? channels[0].id : null); + + instantiationService.createInstance(OutputLinkProvider); + + // Register as text model content provider for output + textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this); + + this.onDidPanelOpen(this.panelService.getActivePanel()); + panelService.onDidPanelOpen(this.onDidPanelOpen, this); + panelService.onDidPanelClose(this.onDidPanelClose, this); } - private onDidChange(): void { - if (!this.updateInProgress) { - this.updateInProgress = true; - this.modelUpdater.schedule(); + provideTextContent(resource: URI): TPromise { + const channel = this.getChannel(resource.fsPath); + return channel.resolve() + .then(content => this.modelService.createModel(content, this.modeService.getOrCreateMode(OUTPUT_MIME), resource)); + } + + showChannel(id: string, preserveFocus?: boolean): TPromise { + if (this.isChannelShown(id)) { + return TPromise.as(null); + } + + this.activeChannelId = id; + let promise = TPromise.as(null); + if (this._outputPanel) { + this.doShowChannel(id, preserveFocus); + } else { + promise = this.panelService.openPanel(OUTPUT_PANEL_ID) as TPromise; } + return promise.then(() => this._onActiveOutputChannel.fire(id)); } - private onDidClear(): void { - this.modelUpdater.cancel(); - this.updateInProgress = true; - this.doUpdate(); + showChannelInEditor(channelId: string): TPromise { + return this.editorService.openEditor(this.createInput(channelId)) as TPromise; } - private doUpdate(): void { - const model = this.getModel(this.channel.id); - if (model && !model.isDisposed()) { - this.channel.getOutputDelta(this.lastReadId) - .then(delta => { - if (delta) { - if (delta.append) { - const lastLine = model.getLineCount(); - const lastLineMaxColumn = model.getLineMaxColumn(lastLine); - model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), delta.value)]); - } else { - model.setValue(delta.value); - } - this.lastReadId = delta.id; - if (!this.channel.scrollLock) { - (this.panelService.getActivePanel()).revealLastLine(); - } - } - this.updateInProgress = false; - }, () => this.updateInProgress = false); - } else { - this.updateInProgress = false; + getChannel(id: string): IOutputChannel { + if (!this.channels.has(id)) { + this.channels.set(id, this.createChannel(id)); } + return this.channels.get(id); + } + + getChannels(): IOutputChannelIdentifier[] { + return Registry.as(Extensions.OutputChannels).getChannels(); } - private getModel(channel: string): IModel { - return this.modelService.getModel(URI.from({ scheme: OUTPUT_SCHEME, path: channel })); + getActiveChannel(): IOutputChannel { + return this.getChannel(this.activeChannelId); } - private onModelRemoved(model: IModel): void { - if (model.uri.fsPath === this.channel.id) { - this.lastReadId = void 0; + private createChannel(id: string): OutputChannel { + const channelDisposables = []; + const channel = this.instantiateChannel(id); + channel.onDispose(() => { + Registry.as(Extensions.OutputChannels).removeChannel(id); + if (this.activeChannelId === id) { + const channels = this.getChannels(); + if (this._outputPanel && channels.length) { + this.showChannel(channels[0].id); + } else { + this._onActiveOutputChannel.fire(void 0); + } + } + dispose(channelDisposables); + }, channelDisposables); + + return channel; + } + + private instantiateChannel(id: string): OutputChannel { + const channelData = Registry.as(Extensions.OutputChannels).getChannel(id); + if (channelData && channelData.file) { + return this.instantiationService.createInstance(FileOutputChannel, channelData); } + const sessionId = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, ''); + const file = URI.file(paths.join(this.environmentService.logsPath, 'outputs', `${id}.${sessionId}.log`)); + return this.instantiationService.createInstance(AppendableFileOutoutChannel, { id, label: channelData ? channelData.label : '', file }); } -} + + + private isChannelShown(channelId: string): boolean { + const panel = this.panelService.getActivePanel(); + return panel && panel.getId() === OUTPUT_PANEL_ID && this.activeChannelId === channelId; + } + + private onDidPanelClose(panel: IPanel): void { + if (this._outputPanel && panel.getId() === OUTPUT_PANEL_ID) { + this._outputPanel.clearInput(); + } + } + + private onDidPanelOpen(panel: IPanel): void { + if (panel && panel.getId() === OUTPUT_PANEL_ID) { + this._outputPanel = this.panelService.getActivePanel(); + if (this.activeChannelId) { + this.doShowChannel(this.activeChannelId, true); + } + } + } + + private doShowChannel(channelId: string, preserveFocus: boolean): void { + if (this._outputPanel) { + this.storageService.store(OUTPUT_ACTIVE_CHANNEL_KEY, channelId, StorageScope.WORKSPACE); + this._outputPanel.setInput(this.createInput(channelId), EditorOptions.create({ preserveFocus: preserveFocus })); + if (!preserveFocus) { + this._outputPanel.focus(); + } + } + } + + private createInput(channelId: string): ResourceEditorInput { + const resource = URI.from({ scheme: OUTPUT_SCHEME, path: channelId }); + const channelData = Registry.as(Extensions.OutputChannels).getChannel(channelId); + const label = channelData ? channelData.label : channelId; + return this.instantiationService.createInstance(ResourceEditorInput, nls.localize('output', "{0} - Output", label), nls.localize('channel', "Output channel for '{0}'", label), resource); + } +} \ No newline at end of file