From d7b5fe4cc7f07ca3550ec4deff29071ab79e7040 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 22 Jan 2020 15:42:43 -0500 Subject: [PATCH] Adds refresh for when timeline changes --- extensions/git/src/timelineProvider.ts | 42 +++++++++++++++++- src/vs/vscode.proposed.d.ts | 2 +- .../api/browser/mainThreadTimeline.ts | 23 +++++++++- .../workbench/api/common/extHost.protocol.ts | 3 +- .../workbench/api/common/extHostTimeline.ts | 23 ++++++++-- .../contrib/timeline/browser/timelinePane.ts | 25 ++++++++++- .../contrib/timeline/common/timeline.ts | 4 ++ .../timeline/common/timelineService.ts | 43 ++++++++++++++----- 8 files changed, 144 insertions(+), 21 deletions(-) diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 2d0f40e4879..ebb6b2efe0b 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -3,15 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Disposable, TimelineItem, TimelineProvider, Uri, workspace, ThemeIcon } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode'; import { Model } from './model'; +import { Repository } from './repository'; +import { debounce } from './decorators'; export class GitTimelineProvider implements TimelineProvider { + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + readonly source = 'git-history'; readonly sourceDescription = 'Git History'; private _disposable: Disposable; + private _repo: Repository | undefined; + private _repoDisposable: Disposable | undefined; + constructor(private readonly _model: Model) { this._disposable = workspace.registerTimelineProvider('*', this); } @@ -21,12 +31,35 @@ export class GitTimelineProvider implements TimelineProvider { } async provideTimeline(uri: Uri, _since: number, _token: CancellationToken): Promise { + console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`); + const repo = this._model.getRepository(uri); if (!repo) { + console.log(`GitTimelineProvider.provideTimeline: repo NOT found`); + + this._repoDisposable?.dispose(); + this._repo = undefined; + return []; } + console.log(`GitTimelineProvider.provideTimeline: repo found`); + + if (this._repo?.root !== repo.root) { + this._repoDisposable?.dispose(); + + this._repo = repo; + this._repoDisposable = Disposable.from( + repo.onDidChangeRepository(() => this.onRepositoryChanged(), this) + ); + } + + // TODO: Ensure that the uri is a file -- if not we could get the history of the repo? + const commits = await repo.logFile(uri, { maxEntries: 10 }); + + console.log(`GitTimelineProvider.provideTimeline: commits=${commits.length}`); + return commits.map(c => { let message = c.message; @@ -50,4 +83,11 @@ export class GitTimelineProvider implements TimelineProvider { }; }); } + + @debounce(500) + private onRepositoryChanged() { + console.log(`GitTimelineProvider.onRepositoryChanged`); + + this._onDidChange.fire(); + } } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index cea83f1dfb8..5afbad2ba1d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1549,7 +1549,7 @@ declare module 'vscode' { // onDidAdd?: Event; // onDidChange?: Event; - onDidChange?: Event; + onDidChange?: Event; /** * An identifier of the source of the timeline items. This can be used for filtering and/or overriding existing sources. diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 8448adca0da..d7e7e9edabc 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -8,10 +8,12 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITimelineService, TimelineItem, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; @extHostNamedCustomer(MainContext.MainThreadTimeline) export class MainThreadTimeline implements MainThreadTimelineShape { private readonly _proxy: ExtHostTimelineShape; + private readonly _providerEmitters = new Map>(); constructor( context: IExtHostContext, @@ -29,12 +31,24 @@ export class MainThreadTimeline implements MainThreadTimelineShape { const proxy = this._proxy; + const emitters = this._providerEmitters; + let onDidChange = emitters.get(provider.source); + // eslint-disable-next-line eqeqeq + if (onDidChange == null) { + onDidChange = new Emitter(); + emitters.set(provider.source, onDidChange); + } + this._timelineService.registerTimelineProvider({ ...provider, + onDidChange: onDidChange.event, provideTimeline(uri: URI, since: number, token: CancellationToken) { return proxy.$getTimeline(provider.source, uri, since, token); }, - dispose() { } + dispose() { + emitters.delete(provider.source); + onDidChange?.dispose(); + } }); } @@ -43,6 +57,13 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.unregisterTimelineProvider(source); } + $emitTimelineChangeEvent(source: string, uri: URI | undefined): void { + console.log(`MainThreadTimeline#emitChangeEvent: source=${source} uri=${uri?.toString(true)}`); + + const emitter = this._providerEmitters.get(source); + emitter?.fire(uri); + } + dispose(): void { // noop } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a77eede3689..0145d42ee5e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -801,8 +801,9 @@ export interface MainThreadTunnelServiceShape extends IDisposable { export interface MainThreadTimelineShape extends IDisposable { $registerTimelineProvider(provider: TimelineProviderDescriptor): void; $unregisterTimelineProvider(source: string): void; + $emitTimelineChangeEvent(source: string, uri: UriComponents | undefined): void; - $getTimeline(resource: UriComponents, since: number, token: CancellationToken): Promise; + $getTimeline(uri: UriComponents, since: number, token: CancellationToken): Promise; } // -- extension host diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 802803cf5cd..65149591857 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -39,13 +39,20 @@ export class ExtHostTimeline implements IExtHostTimeline { } registerTimelineProvider(provider: vscode.TimelineProvider, commandConverter: CommandsConverter): IDisposable { - const disposables = new DisposableStore(); + const timelineDisposables = new DisposableStore(); + + const convertTimelineItem = this.convertTimelineItem(provider.source, commandConverter, timelineDisposables); + + let disposable: IDisposable | undefined; + if (provider.onDidChange) { + console.log(`ExtHostTimeline#registerTimelineProvider: provider=${provider.source} hooking up onDidChange`); + disposable = provider.onDidChange(this.emitTimelineChangeEvent(provider.source), this); + } - const convertTimelineItem = this.convertTimelineItem(provider.source, commandConverter, disposables); return this.registerTimelineProviderCore({ ...provider, async provideTimeline(uri: URI, since: number, token: CancellationToken) { - disposables.clear(); + timelineDisposables.clear(); const results = await provider.provideTimeline(uri, since, token); // eslint-disable-next-line eqeqeq @@ -54,7 +61,8 @@ export class ExtHostTimeline implements IExtHostTimeline { : []; }, dispose() { - disposables.dispose(); + disposable?.dispose(); + timelineDisposables.dispose(); } }); } @@ -90,6 +98,13 @@ export class ExtHostTimeline implements IExtHostTimeline { }; } + private emitTimelineChangeEvent(source: string) { + return (uri: vscode.Uri | undefined) => { + console.log(`ExtHostTimeline#registerTimelineProvider: provider=${source} onDidChange fired; uri=${uri?.toString(true)}`); + this._proxy.$emitTimelineChangeEvent(source, uri); + }; + } + private registerTimelineProviderCore(provider: TimelineProvider): IDisposable { console.log(`ExtHostTimeline#registerTimelineProvider: provider=${provider.source}`); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 28b901a1d8a..26951a502a9 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -14,7 +14,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IListVirtualDelegate, IIdentityProvider, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { WorkbenchObjectTree, TreeResourceNavigator } from 'vs/platform/list/browser/listService'; +import { TreeResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -62,17 +62,32 @@ export class TimelinePane extends ViewPane { uri = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); } + console.log(`TimelinePane.onActiveEditorChanged: uri=${uri?.toString(true)}`); + this.updateUri(uri); } private onProvidersChanged() { + console.log(`TimelinePane.onProvidersChanged`); + this.refresh(); } + private onTimelineChanged(uri: URI | undefined) { + console.log(`TimelinePane.onTimelineChanged: uri=${uri?.toString(true)} this._uri=${this._uri?.toString(true)}`); + + // eslint-disable-next-line eqeqeq + if (uri == null || uri.toString(true) !== this._uri?.toString(true)) { + this.refresh(); + } + } + private async refresh() { this._tokenSource?.cancel(); this._tokenSource = new CancellationTokenSource(); + console.log(`TimelinePane.refresh: uri=${this._uri?.toString(true)}`); + // TODO: Deal with no uri -- use a view title? or keep the last one cached? // TODO: Deal with no items -- use a view title? @@ -89,9 +104,12 @@ export class TimelinePane extends ViewPane { private updateUri(uri: URI | undefined) { if (uri?.toString(true) === this._uri?.toString(true)) { + console.log(`TimelinePane.updateUri(same): uri=${uri?.toString(true)}`); + return; } + console.log(`TimelinePane.updateUri: uri=${uri?.toString(true)}`); this._uri = uri; this.refresh(); } @@ -102,12 +120,15 @@ export class TimelinePane extends ViewPane { } setVisible(visible: boolean): void { + console.log(`TimelinePane.setVisible: visible=${visible}`); + if (visible) { this._visibilityDisposables = new DisposableStore(); this.timelineService.onDidChangeProviders(this.onProvidersChanged, this, this._visibilityDisposables); - + this.timelineService.onDidChangeTimeline(this.onTimelineChanged, this, this._visibilityDisposables); this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._visibilityDisposables); + this.onActiveEditorChanged(); } else { this._visibilityDisposables?.dispose(); diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index aae6dd4d3dd..8dfd5ae8e8d 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -33,6 +33,8 @@ export interface TimelineItemWithSource extends TimelineItem { } export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { + onDidChange?: Event; + provideTimeline(uri: URI, since: number, token: CancellationToken): Promise; } @@ -48,6 +50,8 @@ export interface ITimelineService { readonly _serviceBrand: undefined; onDidChangeProviders: Event; + onDidChangeTimeline: Event; + registerTimelineProvider(provider: TimelineProvider): IDisposable; unregisterTimelineProvider(source: string): void; diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 210c5ee6e77..a726e0fa163 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineProvider } from './timeline'; +import { ITimelineService, TimelineProvider, TimelineItem } from './timeline'; export class TimelineService implements ITimelineService { _serviceBrand: undefined; @@ -16,7 +16,11 @@ export class TimelineService implements ITimelineService { private readonly _onDidChangeProviders = new Emitter(); readonly onDidChangeProviders: Event = this._onDidChangeProviders.event; + private readonly _onDidChangeTimeline = new Emitter(); + readonly onDidChangeTimeline: Event = this._onDidChangeTimeline.event; + private readonly _providers = new Map(); + private readonly _providerSubscriptions = new Map(); constructor(@ILogService private readonly logService: ILogService) { this.registerTimelineProvider({ @@ -42,27 +46,34 @@ export class TimelineService implements ITimelineService { }); } - // TODO: Add filtering - async getTimeline(uri: URI, since: number, token: CancellationToken) { + async getTimeline(uri: URI, since: number, token: CancellationToken, sources?: Set) { this.logService.trace(`TimelineService#getTimeline: uri=${uri.toString(true)}`); - const requests = []; + + console.log(`TimelineService.getTimeline providers=${this._providers.size}`); + + const requests = new Map>>(); for (const provider of this._providers.values()) { - requests.push( - provider.provideTimeline(uri, since, token).then(items => ({ source: provider.source, items: items })) - ); + if (sources && !sources.has(provider.source)) { + continue; + } + + requests.set(provider.source, provider.provideTimeline(uri, since, token)); } const timelines = await raceAll(requests /*, 5000*/); + console.log(`TimelineService.getTimeline timelines=${timelines.size}`); + const timeline = []; - for (const result of timelines) { - // eslint-disable-next-line eqeqeq - if (result == null || result instanceof CancellationError) { + for (const [source, items] of timelines) { + if (items instanceof CancellationError) { + console.log(`TimelineService.getTimeline source=${source} cancelled`); continue; } - const { source, items } = result; + console.log(`TimelineService.getTimeline source=${source} items=${items.length}`); + if (items.length === 0) { continue; } @@ -85,6 +96,9 @@ export class TimelineService implements ITimelineService { } this._providers.set(source, provider); + if (provider.onDidChange) { + this._providerSubscriptions.set(source, provider.onDidChange(uri => this.onProviderTimelineChanged(provider.source, uri))); + } this._onDidChangeProviders.fire(); return { @@ -103,8 +117,15 @@ export class TimelineService implements ITimelineService { } this._providers.delete(source); + this._providerSubscriptions.delete(source); this._onDidChangeProviders.fire(); } + + private onProviderTimelineChanged(source: string, uri: URI | undefined) { + console.log(`TimelineService.onProviderTimelineChanged: source=${source} uri=${uri?.toString(true)}`); + + this._onDidChangeTimeline.fire(uri); + } } function* map(source: Iterable | IterableIterator, mapper: (item: T) => TMapped): Iterable {