Refactors timeline to work better w/ multi sources

Separates the tree rendering from the data model cache
Fixes many issues and simplifies the code
This commit is contained in:
Eric Amodio 2020-03-19 18:10:36 -04:00
parent 64b65cf8e4
commit 3f6843956a
6 changed files with 519 additions and 402 deletions

View file

@ -45,7 +45,7 @@ interface MutableRemote extends Remote {
isReadOnly: boolean;
}
// TODO[ECA]: Move to git.d.ts once we are good with the api
// TODO@eamodio: Move to git.d.ts once we are good with the api
/**
* Log file options.
*/

View file

@ -15,7 +15,7 @@ dayjs.extend(advancedFormat);
const localize = nls.loadMessageBundle();
// TODO[ECA]: Localize or use a setting for date format
// TODO@eamodio: Localize or use a setting for date format
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
@ -71,21 +71,21 @@ export class GitTimelineProvider implements TimelineProvider {
readonly id = 'git-history';
readonly label = localize('git.timeline.source', 'Git History');
private _disposable: Disposable;
private disposable: Disposable;
private _repo: Repository | undefined;
private _repoDisposable: Disposable | undefined;
private _repoStatusDate: Date | undefined;
private repo: Repository | undefined;
private repoDisposable: Disposable | undefined;
private repoStatusDate: Date | undefined;
constructor(private readonly _model: Model) {
this._disposable = Disposable.from(
this.disposable = Disposable.from(
_model.onDidOpenRepository(this.onRepositoriesChanged, this),
workspace.registerTimelineProvider(['file', 'git', 'gitlens-git'], this),
);
}
dispose() {
this._disposable.dispose();
this.disposable.dispose();
}
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
@ -93,33 +93,33 @@ export class GitTimelineProvider implements TimelineProvider {
const repo = this._model.getRepository(uri);
if (!repo) {
this._repoDisposable?.dispose();
this._repoStatusDate = undefined;
this._repo = undefined;
this.repoDisposable?.dispose();
this.repoStatusDate = undefined;
this.repo = undefined;
return { items: [] };
}
if (this._repo?.root !== repo.root) {
this._repoDisposable?.dispose();
if (this.repo?.root !== repo.root) {
this.repoDisposable?.dispose();
this._repo = repo;
this._repoStatusDate = new Date();
this._repoDisposable = Disposable.from(
this.repo = repo;
this.repoStatusDate = new Date();
this.repoDisposable = Disposable.from(
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo))
);
}
// TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo?
// TODO@eamodio: Ensure that the uri is a file -- if not we could get the history of the repo?
let limit: number | undefined;
if (options.limit !== undefined && typeof options.limit !== 'number') {
try {
const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.cursor}..`, '--', uri.fsPath]);
const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.id}..`, '--', uri.fsPath]);
if (!result.exitCode) {
// Ask for 1 more than so we can determine if there are more commits
limit = Number(result.stdout) + 1;
// Ask for 2 more (1 for the limit commit and 1 for the next commit) than so we can determine if there are more commits
limit = Number(result.stdout) + 2;
}
}
catch {
@ -130,21 +130,16 @@ export class GitTimelineProvider implements TimelineProvider {
limit = options.limit === undefined ? undefined : options.limit + 1;
}
const commits = await repo.logFile(uri, {
maxEntries: limit,
hash: options.cursor,
reverse: options.before,
// sortByAuthorDate: true
});
const more = limit === undefined || options.before ? false : commits.length >= limit;
const more = limit === undefined ? false : commits.length >= limit;
const paging = commits.length ? {
more: more,
cursors: {
before: commits[0]?.hash,
after: commits[commits.length - (more ? 1 : 2)]?.hash
}
cursor: more ? commits[commits.length - 1]?.hash : undefined
} : undefined;
// If we asked for an extra commit, strip it off
@ -171,16 +166,16 @@ export class GitTimelineProvider implements TimelineProvider {
return item;
});
if (options.cursor === undefined || options.before) {
if (options.cursor === undefined) {
const you = localize('git.timeline.you', 'You');
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (index) {
const date = this._repoStatusDate ?? new Date();
const date = this.repoStatusDate ?? new Date();
dateFormatter = dayjs(date);
const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index');
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = '';
item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(index.type));
@ -199,7 +194,7 @@ export class GitTimelineProvider implements TimelineProvider {
dateFormatter = dayjs(date);
const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommited Changes'), date.getTime(), 'working', 'git:file:working');
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = '';
item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(working.type));
@ -222,7 +217,7 @@ export class GitTimelineProvider implements TimelineProvider {
private onRepositoriesChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoriesChanged`);
// TODO[ECA]: Being naive for now and just always refreshing each time there is a new repository
// TODO@eamodio: Being naive for now and just always refreshing each time there is a new repository
this.fireChanged();
}
@ -236,7 +231,7 @@ export class GitTimelineProvider implements TimelineProvider {
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
// This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items
this._repoStatusDate = new Date();
this.repoStatusDate = new Date();
this.fireChanged();
}

View file

@ -1874,12 +1874,9 @@ declare module 'vscode' {
export interface Timeline {
readonly paging?: {
/**
* A set of provider-defined cursors specifing the range of timeline items returned.
* A provider-defined cursor specifing the starting point of timeline items which are after the ones returned.
*/
readonly cursors: {
readonly before: string;
readonly after?: string
};
readonly cursor?: string
/**
* A flag which indicates whether there are more items that weren't returned.
@ -1895,19 +1892,15 @@ declare module 'vscode' {
export interface TimelineOptions {
/**
* A provider-defined cursor specifing the range of timeline items that should be returned.
* A provider-defined cursor specifing the starting point of the timeline items that should be returned.
*/
cursor?: string;
/**
* A flag to specify whether the timeline items being requested should be before or after (default) the provided cursor.
* An optional maximum number timeline items or the all timeline items newer (inclusive) than the timestamp or id that should be returned.
* If `undefined` all timeline items should be returned.
*/
before?: boolean;
/**
* The maximum number or the ending cursor of timeline items that should be returned.
*/
limit?: number | { cursor: string };
limit?: number | { timestamp: number; id?: string };
}
export interface TimelineProvider {

File diff suppressed because it is too large Load diff

View file

@ -34,15 +34,14 @@ export interface TimelineItem {
}
export interface TimelineChangeEvent {
id?: string;
id: string;
uri?: URI;
reset?: boolean
}
export interface TimelineOptions {
cursor?: string;
before?: boolean;
limit?: number | { cursor: string };
limit?: number | { timestamp: number; id?: string };
}
export interface InternalTimelineOptions {
@ -55,10 +54,7 @@ export interface Timeline {
items: TimelineItem[];
paging?: {
cursors: {
before: string;
after?: string
};
cursor?: string
more?: boolean;
}
}

View file

@ -23,20 +23,81 @@ export class TimelineService implements ITimelineService {
private readonly _onDidChangeUri = new Emitter<URI>();
readonly onDidChangeUri: Event<URI> = this._onDidChangeUri.event;
private readonly _providers = new Map<string, TimelineProvider>();
private readonly _providerSubscriptions = new Map<string, IDisposable>();
private readonly providers = new Map<string, TimelineProvider>();
private readonly providerSubscriptions = new Map<string, IDisposable>();
constructor(
@ILogService private readonly logService: ILogService,
@IViewsService protected viewsService: IViewsService,
) {
// let source = 'fast-source';
// this.registerTimelineProvider({
// scheme: '*',
// id: source,
// label: 'Fast Source',
// provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) {
// if (options.cursor === undefined) {
// return Promise.resolve<Timeline>({
// source: source,
// items: [
// {
// handle: `${source}|1`,
// id: '1',
// label: 'Fast Timeline1',
// description: '',
// timestamp: Date.now(),
// source: source
// },
// {
// handle: `${source}|2`,
// id: '2',
// label: 'Fast Timeline2',
// description: '',
// timestamp: Date.now() - 3000000000,
// source: source
// }
// ],
// paging: {
// cursor: 'next',
// more: true
// }
// });
// }
// return Promise.resolve<Timeline>({
// source: source,
// items: [
// {
// handle: `${source}|3`,
// id: '3',
// label: 'Fast Timeline3',
// description: '',
// timestamp: Date.now() - 4000000000,
// source: source
// },
// {
// handle: `${source}|4`,
// id: '4',
// label: 'Fast Timeline4',
// description: '',
// timestamp: Date.now() - 300000000000,
// source: source
// }
// ],
// paging: {
// more: false
// }
// });
// },
// dispose() { }
// });
// let source = 'slow-source';
// this.registerTimelineProvider({
// scheme: '*',
// id: source,
// label: 'Slow Source',
// provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) {
// return new Promise(resolve => setTimeout(() => {
// return new Promise<Timeline>(resolve => setTimeout(() => {
// resolve({
// source: source,
// items: [
@ -69,7 +130,7 @@ export class TimelineService implements ITimelineService {
// id: source,
// label: 'Very Slow Source',
// provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) {
// return new Promise(resolve => setTimeout(() => {
// return new Promise<Timeline>(resolve => setTimeout(() => {
// resolve({
// source: source,
// items: [
@ -98,13 +159,13 @@ export class TimelineService implements ITimelineService {
}
getSources() {
return [...this._providers.values()].map(p => ({ id: p.id, label: p.label }));
return [...this.providers.values()].map(p => ({ id: p.id, label: p.label }));
}
getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) {
this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`);
const provider = this._providers.get(id);
const provider = this.providers.get(id);
if (provider === undefined) {
return undefined;
}
@ -141,10 +202,10 @@ export class TimelineService implements ITimelineService {
const id = provider.id;
const existing = this._providers.get(id);
const existing = this.providers.get(id);
if (existing) {
// For now to deal with https://github.com/microsoft/vscode/issues/89553 allow any overwritting here (still will be blocked in the Extension Host)
// TODO[ECA]: Ultimately will need to figure out a way to unregister providers when the Extension Host restarts/crashes
// TODO@eamodio: Ultimately will need to figure out a way to unregister providers when the Extension Host restarts/crashes
// throw new Error(`Timeline Provider ${id} already exists.`);
try {
existing?.dispose();
@ -152,15 +213,15 @@ export class TimelineService implements ITimelineService {
catch { }
}
this._providers.set(id, provider);
this.providers.set(id, provider);
if (provider.onDidChange) {
this._providerSubscriptions.set(id, provider.onDidChange(e => this._onDidChangeTimeline.fire(e)));
this.providerSubscriptions.set(id, provider.onDidChange(e => this._onDidChangeTimeline.fire(e)));
}
this._onDidChangeProviders.fire({ added: [id] });
return {
dispose: () => {
this._providers.delete(id);
this.providers.delete(id);
this._onDidChangeProviders.fire({ removed: [id] });
}
};
@ -169,12 +230,12 @@ export class TimelineService implements ITimelineService {
unregisterTimelineProvider(id: string): void {
this.logService.trace(`TimelineService#unregisterTimelineProvider: id=${id}`);
if (!this._providers.has(id)) {
if (!this.providers.has(id)) {
return;
}
this._providers.delete(id);
this._providerSubscriptions.delete(id);
this.providers.delete(id);
this.providerSubscriptions.delete(id);
this._onDidChangeProviders.fire({ removed: [id] });
}