Skip to content

Commit f2cc6fd

Browse files
author
Eric Amodio
committed
Polishes the timeline UI/UX
Cleans up API and removes some unused features (e.g. paging) Adds date formatting Adds loading progress and message Removes lots of console.logs 😁 Adds titles to diffs
1 parent 7dde43e commit f2cc6fd

18 files changed

+282
-160
lines changed

extensions/git/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -1765,6 +1765,7 @@
17651765
},
17661766
"dependencies": {
17671767
"byline": "^5.0.0",
1768+
"dayjs": "1.8.19",
17681769
"file-type": "^7.2.0",
17691770
"iconv-lite": "^0.4.24",
17701771
"jschardet": "2.1.1",

extensions/git/src/api/git.d.ts

-8
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,6 @@ export interface LogOptions {
121121
readonly maxEntries?: number;
122122
}
123123

124-
/**
125-
* Log file options.
126-
*/
127-
export interface LogFileOptions {
128-
/** Max number of log entries to retrieve. If not specified, the default is 32. */
129-
readonly maxEntries?: number;
130-
}
131-
132124
export interface Repository {
133125

134126
readonly rootUri: Uri;

extensions/git/src/commands.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -2303,11 +2303,13 @@ export class CommandCenter {
23032303

23042304
@command('git.openDiff', { repository: false })
23052305
async openDiff(uri: Uri, hash: string) {
2306+
const basename = path.basename(uri.fsPath);
2307+
23062308
if (hash === '~') {
2307-
return commands.executeCommand('vscode.diff', toGitUri(uri, hash), toGitUri(uri, `HEAD`));
2309+
return commands.executeCommand('vscode.diff', toGitUri(uri, hash), toGitUri(uri, `HEAD`), `${basename} (Index)`);
23082310
}
23092311

2310-
return commands.executeCommand('vscode.diff', toGitUri(uri, hash), toGitUri(uri, `${hash}^`));
2312+
return commands.executeCommand('vscode.diff', toGitUri(uri, hash), toGitUri(uri, `${hash}^`), `${basename} (${hash.substr(0, 8)}^) \u27f7 ${basename} (${hash.substr(0, 8)})`);
23112313
}
23122314

23132315
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {

extensions/git/src/git.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes,
1515
import { CancellationToken, Progress, Uri } from 'vscode';
1616
import { URI } from 'vscode-uri';
1717
import { detectEncoding } from './encoding';
18-
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, LogFileOptions } from './api/git';
18+
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
1919
import * as byline from 'byline';
2020
import { StringDecoder } from 'string_decoder';
2121

@@ -45,6 +45,15 @@ interface MutableRemote extends Remote {
4545
isReadOnly: boolean;
4646
}
4747

48+
// TODO[ECA]: Move to git.d.ts once we are good with the api
49+
/**
50+
* Log file options.
51+
*/
52+
export interface LogFileOptions {
53+
/** Max number of log entries to retrieve. If not specified, the default is 32. */
54+
readonly maxEntries?: number;
55+
}
56+
4857
function parseVersion(raw: string): string {
4958
return raw.replace(/^git version /, '');
5059
}

extensions/git/src/repository.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import * as fs from 'fs';
77
import * as path from 'path';
88
import { CancellationToken, Command, Disposable, env, Event, EventEmitter, LogLevel, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode';
99
import * as nls from 'vscode-nls';
10-
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, LogFileOptions } from './api/git';
10+
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
1111
import { AutoFetcher } from './autofetch';
1212
import { debounce, memoize, throttle } from './decorators';
13-
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule } from './git';
13+
import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git';
1414
import { StatusBarCommands } from './statusbar';
1515
import { toGitUri } from './uri';
1616
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util';

extensions/git/src/timelineProvider.ts

+74-21
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import * as dayjs from 'dayjs';
7+
import * as advancedFormat from 'dayjs/plugin/advancedFormat';
8+
import * as relativeTime from 'dayjs/plugin/relativeTime';
69
import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode';
710
import { Model } from './model';
811
import { Repository } from './repository';
912
import { debounce } from './decorators';
13+
import { Status } from './api/git';
14+
15+
dayjs.extend(advancedFormat);
16+
dayjs.extend(relativeTime);
17+
18+
// TODO[ECA]: Localize all the strings
19+
// TODO[ECA]: Localize or use a setting for date format
1020

1121
export class GitTimelineProvider implements TimelineProvider {
1222
private _onDidChange = new EventEmitter<Uri | undefined>();
@@ -21,45 +31,47 @@ export class GitTimelineProvider implements TimelineProvider {
2131

2232
private _repo: Repository | undefined;
2333
private _repoDisposable: Disposable | undefined;
34+
private _repoStatusDate: Date | undefined;
2435

2536
constructor(private readonly _model: Model) {
26-
this._disposable = workspace.registerTimelineProvider('*', this);
37+
this._disposable = Disposable.from(
38+
_model.onDidOpenRepository(this.onRepositoriesChanged, this),
39+
workspace.registerTimelineProvider('*', this),
40+
);
2741
}
2842

2943
dispose() {
3044
this._disposable.dispose();
3145
}
3246

33-
async provideTimeline(uri: Uri, _since: number, _token: CancellationToken): Promise<TimelineItem[]> {
34-
console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
47+
async provideTimeline(uri: Uri, _token: CancellationToken): Promise<TimelineItem[]> {
48+
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
3549

3650
const repo = this._model.getRepository(uri);
3751
if (!repo) {
38-
console.log(`GitTimelineProvider.provideTimeline: repo NOT found`);
39-
4052
this._repoDisposable?.dispose();
53+
this._repoStatusDate = undefined;
4154
this._repo = undefined;
4255

4356
return [];
4457
}
4558

46-
console.log(`GitTimelineProvider.provideTimeline: repo found`);
47-
4859
if (this._repo?.root !== repo.root) {
4960
this._repoDisposable?.dispose();
5061

5162
this._repo = repo;
63+
this._repoStatusDate = new Date();
5264
this._repoDisposable = Disposable.from(
53-
repo.onDidChangeRepository(() => this.onRepositoryChanged(), this)
65+
repo.onDidChangeRepository(this.onRepositoryChanged, this),
66+
repo.onDidRunGitStatus(this.onRepositoryStatusChanged, this)
5467
);
5568
}
5669

57-
// TODO: Ensure that the uri is a file -- if not we could get the history of the repo?
58-
59-
const commits = await repo.logFile(uri, { maxEntries: 10 });
70+
// TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo?
6071

61-
console.log(`GitTimelineProvider.provideTimeline: commits=${commits.length}`);
72+
const commits = await repo.logFile(uri);
6273

74+
let dateFormatter: dayjs.Dayjs;
6375
const items = commits.map<TimelineItem>(c => {
6476
let message = c.message;
6577

@@ -68,13 +80,15 @@ export class GitTimelineProvider implements TimelineProvider {
6880
message = `${message.substring(0, index)} \u2026`;
6981
}
7082

83+
dateFormatter = dayjs(c.authorDate);
84+
7185
return {
7286
id: c.hash,
73-
date: c.authorDate?.getTime() ?? 0,
87+
timestamp: c.authorDate?.getTime() ?? 0,
7488
iconPath: new ThemeIcon('git-commit'),
7589
label: message,
76-
description: `${c.authorName} (${c.authorEmail}) \u2022 ${c.hash.substr(0, 8)}`,
77-
detail: `${c.authorName} (${c.authorEmail})\n${c.authorDate}\n\n${c.message}`,
90+
description: `${dateFormatter.fromNow()} \u2022 ${c.authorName}`,
91+
detail: `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n\n${c.message}`,
7892
command: {
7993
title: 'Open Diff',
8094
command: 'git.openDiff',
@@ -85,16 +99,42 @@ export class GitTimelineProvider implements TimelineProvider {
8599

86100
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
87101
if (index) {
102+
const date = this._repoStatusDate ?? new Date();
103+
dateFormatter = dayjs(date);
104+
105+
let status;
106+
switch (index.type) {
107+
case Status.INDEX_MODIFIED:
108+
status = 'Modified';
109+
break;
110+
case Status.INDEX_ADDED:
111+
status = 'Added';
112+
break;
113+
case Status.INDEX_DELETED:
114+
status = 'Deleted';
115+
break;
116+
case Status.INDEX_RENAMED:
117+
status = 'Renamed';
118+
break;
119+
case Status.INDEX_COPIED:
120+
status = 'Copied';
121+
break;
122+
default:
123+
status = '';
124+
break;
125+
}
126+
127+
88128
items.push({
89129
id: '~',
90-
// TODO: Fix the date
91-
date: Date.now(),
130+
timestamp: date.getTime(),
131+
// TODO[ECA]: Replace with a better icon -- reflecting its status maybe?
92132
iconPath: new ThemeIcon('git-commit'),
93133
label: 'Staged Changes',
94-
description: '',
95-
detail: '',
134+
description: `${dateFormatter.fromNow()} \u2022 You`,
135+
detail: `You \u2014 Index\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`,
96136
command: {
97-
title: 'Open Diff',
137+
title: 'Open Comparison',
98138
command: 'git.openDiff',
99139
arguments: [uri, '~']
100140
}
@@ -104,10 +144,23 @@ export class GitTimelineProvider implements TimelineProvider {
104144
return items;
105145
}
106146

147+
@debounce(500)
148+
private onRepositoriesChanged(_repo: Repository) {
149+
// console.log(`GitTimelineProvider.onRepositoriesChanged`);
150+
151+
// TODO[ECA]: Being naive for now and just always refreshing each time there is a new repository
152+
this._onDidChange.fire();
153+
}
154+
107155
@debounce(500)
108156
private onRepositoryChanged() {
109-
console.log(`GitTimelineProvider.onRepositoryChanged`);
157+
// console.log(`GitTimelineProvider.onRepositoryChanged`);
110158

111159
this._onDidChange.fire();
112160
}
161+
162+
private onRepositoryStatusChanged() {
163+
// This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items
164+
this._repoStatusDate = new Date();
165+
}
113166
}

extensions/git/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ concat-map@0.0.1:
8080
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
8181
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
8282

83+
dayjs@1.8.19:
84+
version "1.8.19"
85+
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.19.tgz#5117dc390d8f8e586d53891dbff3fa308f51abfe"
86+
integrity sha512-7kqOoj3oQSmqbvtvGFLU5iYqies+SqUiEGNT0UtUPPxcPYgY1BrkXR0Cq2R9HYSimBXN+xHkEN4Hi399W+Ovlg==
87+
8388
debug@2.6.8:
8489
version "2.6.8"
8590
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"

src/vs/vscode.proposed.d.ts

+7-35
Original file line numberDiff line numberDiff line change
@@ -1474,9 +1474,9 @@ declare module 'vscode' {
14741474

14751475
export class TimelineItem {
14761476
/**
1477-
* A date for when the timeline item occurred
1477+
* A timestamp (in milliseconds since 1 January 1970 00:00:00) for when the timeline item occurred
14781478
*/
1479-
date: number;
1479+
timestamp: number;
14801480

14811481
/**
14821482
* A human-readable string describing the timeline item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri).
@@ -1515,40 +1515,13 @@ declare module 'vscode' {
15151515

15161516
/**
15171517
* @param label A human-readable string describing the timeline item
1518-
* @param date A date for when the timeline item occurred
1518+
* @param timestamp A timestamp (in milliseconds since 1 January 1970 00:00:00) for when the timeline item occurred
15191519
* @param source A human-readable string describing the source of the timeline item
15201520
*/
1521-
constructor(label: string, date: number, source: string);
1521+
constructor(label: string, timestamp: number, source: string);
15221522
}
15231523

1524-
// export interface TimelimeAddEvent {
1525-
// /**
1526-
// * An array of timeline items which have been added.
1527-
// */
1528-
// readonly items: readonly TimelineItem[];
1529-
1530-
// /**
1531-
// * The uri of the file to which the timeline items belong.
1532-
// */
1533-
// readonly uri: Uri;
1534-
// }
1535-
1536-
// export interface TimelimeChangeEvent {
1537-
// /**
1538-
// * The date after which the timeline has changed. If `undefined` the entire timeline will be reset.
1539-
// */
1540-
// readonly since?: Date;
1541-
1542-
// /**
1543-
// * The uri of the file to which the timeline changed.
1544-
// */
1545-
// readonly uri: Uri;
1546-
// }
1547-
15481524
export interface TimelineProvider {
1549-
// onDidAdd?: Event<TimelimeAddEvent>;
1550-
// onDidChange?: Event<TimelimeChangeEvent>;
1551-
15521525
onDidChange?: Event<Uri | undefined>;
15531526

15541527
/**
@@ -1564,15 +1537,14 @@ declare module 'vscode' {
15641537
replaceable?: boolean;
15651538

15661539
/**
1567-
* Provide [timeline items](#TimelineItem) for a [Uri](#Uri) after a particular date.
1540+
* Provide [timeline items](#TimelineItem) for a [Uri](#Uri).
15681541
*
15691542
* @param uri The uri of the file to provide the timeline for.
1570-
* @param since A date after which timeline items should be provided.
15711543
* @param token A cancellation token.
15721544
* @return An array of timeline items or a thenable that resolves to such. The lack of a result
15731545
* can be signaled by returning `undefined`, `null`, or an empty array.
15741546
*/
1575-
provideTimeline(uri: Uri, since: number, token: CancellationToken): ProviderResult<TimelineItem[]>;
1547+
provideTimeline(uri: Uri, token: CancellationToken): ProviderResult<TimelineItem[]>;
15761548
}
15771549

15781550
export namespace workspace {
@@ -1586,7 +1558,7 @@ declare module 'vscode' {
15861558
* @param selector A selector that defines the documents this provider is applicable to.
15871559
* @param provider A timeline provider.
15881560
* @return A [disposable](#Disposable) that unregisters this provider when being disposed.
1589-
*/
1561+
*/
15901562
export function registerTimelineProvider(selector: DocumentSelector, provider: TimelineProvider): Disposable;
15911563
}
15921564

0 commit comments

Comments
 (0)