提交 9e6c5684 编写于 作者: S Sandeep Somavarapu

#39638 Clean up Output services

上级 8c91f01f
......@@ -9,12 +9,12 @@ import strings = require('vs/base/common/strings');
import Event, { Emitter } from 'vs/base/common/event';
import { binarySearch } from 'vs/base/common/arrays';
import URI from 'vs/base/common/uri';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorOptions } from 'vs/workbench/common/editor';
import { IOutputChannelIdentifier, IOutputEvent, IOutputChannel, IOutputService, IOutputDelta, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, MAX_OUTPUT_LENGTH, OUTPUT_SCHEME, OUTPUT_MIME } from 'vs/workbench/parts/output/common/output';
import { IOutputChannelIdentifier, IOutputChannel, IOutputService, Extensions, OUTPUT_PANEL_ID, IOutputChannelRegistry, MAX_OUTPUT_LENGTH, OUTPUT_SCHEME, OUTPUT_MIME } from 'vs/workbench/parts/output/common/output';
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';
......@@ -33,6 +33,12 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel';
export interface IOutputDelta {
readonly value: string;
readonly id: number;
readonly append?: boolean;
}
export class BufferedContent {
private data: string[] = [];
......@@ -65,10 +71,10 @@ export class BufferedContent {
}
}
public getDelta(previousDelta?: IOutputDelta): IOutputDelta {
public getDelta(previousId?: number): IOutputDelta {
let idx = -1;
if (previousDelta) {
idx = binarySearch(this.dataIds, previousDelta.id, (a, b) => a - b);
if (previousId !== void 0) {
idx = binarySearch(this.dataIds, previousId, (a, b) => a - b);
}
const id = this.idPool;
......@@ -84,15 +90,18 @@ export class BufferedContent {
abstract class OutputChannel extends Disposable implements IOutputChannel {
protected _onDidChange: Emitter<boolean> = new Emitter<boolean>();
readonly onDidChange: Event<boolean> = this._onDidChange.event;
protected _onDidChange: Emitter<void> = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
protected _onDidClear: Emitter<void> = new Emitter<void>();
readonly onDidClear: Event<void> = this._onDidClear.event;
protected _onDispose: Emitter<void> = new Emitter<void>();
readonly onDispose: Event<void> = this._onDispose.event;
scrollLock: boolean = false;
constructor(private readonly oputChannelIdentifier: IOutputChannelIdentifier, ) {
constructor(private readonly oputChannelIdentifier: IOutputChannelIdentifier) {
super();
}
......@@ -107,7 +116,7 @@ abstract class OutputChannel extends Disposable implements IOutputChannel {
show(): TPromise<void> { return TPromise.as(null); }
hide(): void { }
append(output: string) { /** noop */ }
getOutputDelta(previousDelta?: IOutputDelta): TPromise<IOutputDelta> { return TPromise.as(null); }
getOutputDelta(id?: number): TPromise<IOutputDelta> { return TPromise.as(null); }
clear(): void { }
dispose(): void {
......@@ -123,16 +132,16 @@ class BufferredOutputChannel extends OutputChannel implements IOutputChannel {
append(output: string) {
this.bufferredContent.append(output);
this._onDidChange.fire(false);
this._onDidChange.fire();
}
getOutputDelta(previousDelta?: IOutputDelta): TPromise<IOutputDelta> {
return TPromise.as(this.bufferredContent.getDelta(previousDelta));
getOutputDelta(id?: number): TPromise<IOutputDelta> {
return TPromise.as(this.bufferredContent.getDelta(id));
}
clear(): void {
this.bufferredContent.clear();
this._onDidChange.fire(true);
this._onDidClear.fire();
}
}
......@@ -160,8 +169,14 @@ class FileOutputChannel extends OutputChannel implements IOutputChannel {
if (!this.shown) {
this.shown = true;
this.watch();
return this.resolve()
.then(content => {
if (this.endOffset !== content.length) {
this._onDidChange.fire();
}
});
}
return this.resolve() as TPromise;
return TPromise.as(null);
}
hide(): void {
......@@ -172,7 +187,7 @@ class FileOutputChannel extends OutputChannel implements IOutputChannel {
this.contentResolver = null;
}
getOutputDelta(previousDelta?: IOutputDelta): TPromise<IOutputDelta> {
getOutputDelta(previousId?: number): TPromise<IOutputDelta> {
if (!this.shown) {
// Do not return any content when not shown
return TPromise.as(null);
......@@ -180,23 +195,29 @@ class FileOutputChannel extends OutputChannel implements IOutputChannel {
return this.resolve()
.then(content => {
const startOffset = previousDelta ? previousDelta.id : this.startOffset;
const startOffset = previousId !== void 0 ? previousId : this.startOffset;
this.endOffset = content.length;
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
// Replace
return { append: false, value: content, id: this.endOffset };
});
}
clear(): void {
this.startOffset = this.endOffset;
this._onDidChange.fire(true);
this._onDidClear.fire();
}
private resolve(): TPromise<string> {
......@@ -212,7 +233,7 @@ class FileOutputChannel extends OutputChannel implements IOutputChannel {
this.disposables.push(this.fileService.onFileChanges(changes => {
if (changes.contains(this.file, FileChangeType.UPDATED)) {
this.contentResolver = null;
this._onDidChange.fire(false);
this._onDidChange.fire();
}
}));
}
......@@ -228,20 +249,19 @@ class FileOutputChannel extends OutputChannel implements IOutputChannel {
}
}
export class OutputService implements IOutputService {
export class OutputService implements IOutputService, ITextModelContentProvider {
public _serviceBrand: any;
private channels: Map<string, OutputChannel> = new Map<string, OutputChannel>();
private activeChannelId: string;
private _onOutput: Emitter<IOutputEvent> = new Emitter<IOutputEvent>();
readonly onOutput: Event<IOutputEvent> = this._onOutput.event;
private _onDidChannelContentChange: Emitter<string> = new Emitter<string>();
readonly onDidChannelContentChange: Event<string> = this._onDidChannelContentChange.event;
private _onActiveOutputChannel: Emitter<string> = new Emitter<string>();
readonly onActiveOutputChannel: Event<string> = this._onActiveOutputChannel.event;
private _outputContentProvider: OutputContentProvider;
private _outputPanel: OutputPanel;
constructor(
......@@ -249,7 +269,8 @@ export class OutputService implements IOutputService {
@IInstantiationService private instantiationService: IInstantiationService,
@IPanelService private panelService: IPanelService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IModelService modelService: IModelService,
@IModelService private modelService: IModelService,
@IModeService private modeService: IModeService,
@ITextModelService textModelResolverService: ITextModelService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
) {
......@@ -259,14 +280,19 @@ export class OutputService implements IOutputService {
instantiationService.createInstance(OutputLinkProvider);
// Register as text model content provider for output
this._outputContentProvider = instantiationService.createInstance(OutputContentProvider, this);
textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this._outputContentProvider);
textModelResolverService.registerTextModelContentProvider(OUTPUT_SCHEME, this);
this.onDidPanelOpen(this.panelService.getActivePanel());
panelService.onDidPanelOpen(this.onDidPanelOpen, this);
panelService.onDidPanelClose(this.onDidPanelClose, this);
}
provideTextContent(resource: URI): TPromise<IModel> {
const channel = <OutputChannel>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<void> {
if (this.isChannelShown(id)) {
return TPromise.as(null);
......@@ -287,25 +313,7 @@ export class OutputService implements IOutputService {
getChannel(id: string): IOutputChannel {
if (!this.channels.has(id)) {
const channelData = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);
const channelDisposables = channelData && channelData.file ? this.instantiationService.createInstance(FileOutputChannel, channelData) : this.instantiationService.createInstance(BufferredOutputChannel, { id: id, label: '' });
let disposables = [];
channelDisposables.onDidChange(isClear => this._onOutput.fire({ channelId: id, isClear }), disposables);
channelDisposables.onDispose(() => {
Registry.as<IOutputChannelRegistry>(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(disposables);
}, disposables);
this.channels.set(id, channelDisposables);
this.channels.set(id, this.createChannel(id));
}
return this.channels.get(id);
}
......@@ -318,6 +326,28 @@ export class OutputService implements IOutputService {
return this.getChannel(this.activeChannelId);
}
private createChannel(id: string): OutputChannel {
const channelDisposables = [];
const channelData = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).getChannel(id);
const channel = channelData && channelData.file ? this.instantiationService.createInstance(FileOutputChannel, channelData) : this.instantiationService.createInstance(BufferredOutputChannel, { id: id, label: '' });
channelDisposables.push(this.instantiationService.createInstance(ChannelModelUpdater, channel));
channel.onDidChange(() => this._onDidChannelContentChange.fire(id), channelDisposables);
channel.onDispose(() => {
Registry.as<IOutputChannelRegistry>(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;
......@@ -372,157 +402,61 @@ export class OutputService implements IOutputService {
}
}
class OutputContentProvider implements ITextModelContentProvider {
class ChannelModelUpdater extends Disposable {
private static readonly OUTPUT_DELAY = 300;
private bufferedOutput = new Map<string, IOutputDelta>();
private appendOutputScheduler: { [channel: string]: RunOnceScheduler; };
private channelIdsWithScrollLock: Set<string> = new Set<string>();
private toDispose: IDisposable[];
private updateInProgress: boolean = false;
private modelUpdater: RunOnceScheduler;
private lastReadId: number;
constructor(
private outputService: IOutputService,
private channel: OutputChannel,
@IModelService private modelService: IModelService,
@IModeService private modeService: IModeService,
@IPanelService private panelService: IPanelService
) {
this.appendOutputScheduler = Object.create(null);
this.toDispose = [];
this.registerListeners();
}
private registerListeners(): void {
this.toDispose.push(this.outputService.onOutput(e => this.onOutputReceived(e)));
this.toDispose.push(this.outputService.onActiveOutputChannel(channel => this.scheduleOutputAppend(channel)));
this.toDispose.push(this.panelService.onDidPanelOpen(panel => {
if (panel.getId() === OUTPUT_PANEL_ID) {
this.appendOutput();
}
}));
}
private onOutputReceived(e: IOutputEvent): void {
const model = this.getModel(e.channelId);
if (!model) {
return; // only react if we have a known model
}
// Append to model
if (e.isClear) {
model.setValue('');
} else {
this.scheduleOutputAppend(e.channelId);
}
}
private getModel(channel: string): IModel {
return this.modelService.getModel(URI.from({ scheme: OUTPUT_SCHEME, path: channel }));
}
private scheduleOutputAppend(channel: string): void {
if (!this.isVisible(channel)) {
return; // only if the output channel is visible
}
let scheduler = this.appendOutputScheduler[channel];
if (!scheduler) {
scheduler = new RunOnceScheduler(() => {
if (this.isVisible(channel)) {
this.appendOutput(channel);
}
}, OutputContentProvider.OUTPUT_DELAY);
this.appendOutputScheduler[channel] = scheduler;
this.toDispose.push(scheduler);
}
if (scheduler.isScheduled()) {
return; // only if not already scheduled
}
scheduler.schedule();
}
private appendOutput(channel?: string): void {
if (!channel) {
const activeChannel = this.outputService.getActiveChannel();
channel = activeChannel && activeChannel.id;
}
if (!channel) {
return; // return if we do not have a valid channel to append to
}
const model = this.getModel(channel);
if (!model) {
return; // only react if we have a known model
}
const bufferedOutput = this.bufferedOutput.get(channel);
const outputChannel = <OutputChannel>this.outputService.getChannel(channel);
outputChannel.getOutputDelta(bufferedOutput)
.then(newOutput => {
if (!newOutput) {
model.setValue('');
return;
}
this.bufferedOutput.set(channel, newOutput);
// just fill in the full (trimmed) output if we exceed max length
if (!newOutput.append) {
model.setValue(newOutput.value);
}
// otherwise append
else {
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), newOutput.value)]);
}
if (!this.channelIdsWithScrollLock.has(channel)) {
// reveal last line
const panel = this.panelService.getActivePanel();
(<OutputPanel>panel).revealLastLine();
}
});
}
private isVisible(channel: string): boolean {
const panel = this.panelService.getActivePanel();
return panel && panel.getId() === OUTPUT_PANEL_ID && this.outputService.getActiveChannel().id === channel;
}
public scrollLock(channelId: string): boolean {
return this.channelIdsWithScrollLock.has(channelId);
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()));
}
public setScrollLock(channelId: string, value: boolean): void {
if (value) {
this.channelIdsWithScrollLock.add(channelId);
} else {
this.channelIdsWithScrollLock.delete(channelId);
private onDidChange(): void {
if (!this.updateInProgress) {
this.updateInProgress = true;
this.modelUpdater.schedule();
}
}
public provideTextContent(resource: URI): TPromise<IModel> {
const channel = <OutputChannel>this.outputService.getChannel(resource.fsPath);
return channel.getOutputDelta()
.then(output => {
const content = output ? output.value : '';
let codeEditorModel = this.modelService.getModel(resource);
if (!codeEditorModel) {
codeEditorModel = this.modelService.createModel(content, this.modeService.getOrCreateMode(OUTPUT_MIME), resource);
private onDidClear(): void {
this.modelUpdater.cancel();
this.updateInProgress = true;
this.doUpdate();
}
private doUpdate(): void {
this.channel.getOutputDelta(this.lastReadId)
.then(delta => {
const model = this.getModel(this.channel.id);
if (model && !model.isDisposed()) {
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) {
(<OutputPanel>this.panelService.getActivePanel()).revealLastLine();
}
}
}
return codeEditorModel;
});
this.updateInProgress = false;
}, () => this.updateInProgress = false);
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
private getModel(channel: string): IModel {
return this.modelService.getModel(URI.from({ scheme: OUTPUT_SCHEME, path: channel }));
}
}
......@@ -41,14 +41,6 @@ export const MAX_OUTPUT_LENGTH = 10000 /* Max. number of output lines to show in
export const CONTEXT_IN_OUTPUT = new RawContextKey<boolean>('inOutput', false);
/**
* The output event informs when new output got received.
*/
export interface IOutputEvent {
channelId: string;
isClear: boolean;
}
export const IOutputService = createDecorator<IOutputService>(OUTPUT_SERVICE_ID);
/**
......@@ -84,23 +76,12 @@ export interface IOutputService {
*/
showChannelInEditor(id: string): TPromise<void>;
/**
* Allows to register on Output events.
*/
onOutput: Event<IOutputEvent>;
/**
* Allows to register on active output channel change.
*/
onActiveOutputChannel: Event<string>;
}
export interface IOutputDelta {
readonly value: string;
readonly id: number;
readonly append?: boolean;
}
export interface IOutputChannel {
/**
......
......@@ -17,7 +17,7 @@ suite('Workbench - Output Buffered Content', () => {
assert.equal(bufferedContent.getDelta().value, 'firstsecondthird');
bufferedContent.clear();
assert.equal(bufferedContent.getDelta().value, '');
assert.equal(bufferedContent.getDelta(delta).value, '');
assert.equal(bufferedContent.getDelta(delta.id).value, '');
});
test('Buffered Content - Appending Output', () => {
......@@ -26,13 +26,13 @@ suite('Workbench - Output Buffered Content', () => {
const firstDelta = bufferedContent.getDelta();
bufferedContent.append('second');
bufferedContent.append('third');
const secondDelta = bufferedContent.getDelta(firstDelta);
const secondDelta = bufferedContent.getDelta(firstDelta.id);
assert.equal(secondDelta.append, true);
assert.equal(secondDelta.value, 'secondthird');
bufferedContent.append('fourth');
bufferedContent.append('fifth');
assert.equal(bufferedContent.getDelta(firstDelta).value, 'secondthirdfourthfifth');
assert.equal(bufferedContent.getDelta(secondDelta).value, 'fourthfifth');
assert.equal(bufferedContent.getDelta(firstDelta.id).value, 'secondthirdfourthfifth');
assert.equal(bufferedContent.getDelta(secondDelta.id).value, 'fourthfifth');
});
test('Buffered Content - Lots of Output', function () {
......@@ -45,13 +45,13 @@ suite('Workbench - Output Buffered Content', () => {
bufferedContent.append(i.toString());
longString += i.toString();
}
const secondDelta = bufferedContent.getDelta(firstDelta);
const secondDelta = bufferedContent.getDelta(firstDelta.id);
assert.equal(secondDelta.append, true);
assert.equal(secondDelta.value.substr(secondDelta.value.length - 4), '4999');
longString = longString + longString + longString + longString;
bufferedContent.append(longString);
bufferedContent.append(longString);
const thirdDelta = bufferedContent.getDelta(firstDelta);
const thirdDelta = bufferedContent.getDelta(firstDelta.id);
assert.equal(!!thirdDelta.append, true);
assert.equal(thirdDelta.value.substr(thirdDelta.value.length - 4), '4999');
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册