Skip to content

Commit 1027a1a

Browse files
authored
SCM - consistently render date on the right in the repositories view (#280870)
SCM - scaffold artifact timestamp
1 parent b03e656 commit 1027a1a

File tree

6 files changed

+92
-21
lines changed

6 files changed

+92
-21
lines changed

extensions/git/src/artifactProvider.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable } from 'vscode';
7-
import { dispose, filterEvent, fromNow, getStashDescription, IDisposable } from './util';
7+
import { dispose, filterEvent, IDisposable } from './util';
88
import { Repository } from './repository';
99
import { Ref, RefType } from './api/git';
1010
import { OperationKind } from './operation';
1111

1212
function getArtifactDescription(ref: Ref, shortCommitLength: number): string {
1313
const segments: string[] = [];
14-
if (ref.commitDetails?.commitDate) {
15-
segments.push(fromNow(ref.commitDetails.commitDate));
16-
}
1714
if (ref.commit) {
1815
segments.push(ref.commit.substring(0, shortCommitLength));
1916
}
@@ -130,7 +127,8 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
130127
description: getArtifactDescription(r, shortCommitLength),
131128
icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name
132129
? new ThemeIcon('target')
133-
: new ThemeIcon('git-branch')
130+
: new ThemeIcon('git-branch'),
131+
timestamp: r.commitDetails?.commitDate?.getTime()
134132
}));
135133
} else if (group === 'tags') {
136134
const refs = await this.repository
@@ -142,16 +140,18 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
142140
description: getArtifactDescription(r, shortCommitLength),
143141
icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name
144142
? new ThemeIcon('target')
145-
: new ThemeIcon('tag')
143+
: new ThemeIcon('tag'),
144+
timestamp: r.commitDetails?.commitDate?.getTime()
146145
}));
147146
} else if (group === 'stashes') {
148147
const stashes = await this.repository.getStashes();
149148

150149
return stashes.map(s => ({
151150
id: `stash@{${s.index}}`,
152151
name: s.description,
153-
description: getStashDescription(s),
154-
icon: new ThemeIcon('git-stash')
152+
description: s.branchName,
153+
icon: new ThemeIcon('git-stash'),
154+
timestamp: s.commitDate?.getTime()
155155
}));
156156
}
157157
} catch (err) {

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,6 +1765,7 @@ export interface SCMArtifactDto {
17651765
readonly name: string;
17661766
readonly description?: string;
17671767
readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon;
1768+
readonly timestamp?: number;
17681769
}
17691770

17701771
export interface MainThreadSCMShape extends IDisposable {

src/vs/workbench/contrib/scm/browser/media/scm.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,33 @@
585585
white-space: nowrap;
586586
}
587587

588+
.scm-repositories-view .scm-artifact .timestamp-container {
589+
flex-shrink: 0;
590+
margin-left: 2px;
591+
margin-right: 4px;
592+
opacity: 0.5;
593+
}
594+
595+
.scm-repositories-view .scm-artifact .timestamp-container.duplicate {
596+
height: 22px;
597+
min-width: 6px;
598+
border-left: 1px solid currentColor;
599+
opacity: 0.25;
600+
601+
.timestamp {
602+
display: none;
603+
}
604+
}
605+
606+
.scm-repositories-view .monaco-list .monaco-list-row:hover .scm-artifact .timestamp-container.duplicate {
607+
border-left: 0;
608+
opacity: 0.5;
609+
610+
.timestamp {
611+
display: block;
612+
}
613+
}
614+
588615
/* History item hover */
589616

590617
.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:first-child > p {

src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from '../../../../b
4646
import { Codicon } from '../../../../base/common/codicons.js';
4747
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
4848
import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js';
49+
import { fromNow } from '../../../../base/common/date.js';
4950

5051
type TreeElement = ISCMRepository | SCMArtifactGroupTreeElement | SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>;
5152

@@ -136,6 +137,8 @@ class ArtifactGroupRenderer implements ICompressibleTreeRenderer<SCMArtifactGrou
136137
interface ArtifactTemplate {
137138
readonly icon: HTMLElement;
138139
readonly label: IconLabel;
140+
readonly timestampContainer: HTMLElement;
141+
readonly timestamp: HTMLElement;
139142
readonly actionBar: WorkbenchToolBar;
140143
readonly elementDisposables: DisposableStore;
141144
readonly templateDisposable: IDisposable;
@@ -162,10 +165,13 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
162165
const icon = append(element, $('.icon'));
163166
const label = new IconLabel(element, { supportIcons: false });
164167

168+
const timestampContainer = append(element, $('.timestamp-container'));
169+
const timestamp = append(timestampContainer, $('.timestamp'));
170+
165171
const actionsContainer = append(element, $('.actions'));
166172
const actionBar = new WorkbenchToolBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider }, this._menuService, this._contextKeyService, this._contextMenuService, this._keybindingService, this._commandService, this._telemetryService);
167173

168-
return { icon, label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
174+
return { icon, label, timestampContainer, timestamp, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
169175
}
170176

171177
renderElement(nodeOrElement: ITreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, FuzzyScore>, index: number, templateData: ArtifactTemplate): void {
@@ -186,10 +192,18 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
186192
? artifact.name.split('/').pop() ?? artifact.name
187193
: artifact.name;
188194
templateData.label.setLabel(artifactLabel, artifact.description);
195+
196+
templateData.timestamp.textContent = artifact.timestamp ? fromNow(artifact.timestamp) : '';
197+
templateData.timestampContainer.classList.toggle('duplicate', artifactOrFolder.hideTimestamp);
198+
templateData.timestampContainer.style.display = '';
189199
} else if (isSCMArtifactNode(artifactOrFolder)) {
190200
// Folder
191201
templateData.icon.className = `icon ${ThemeIcon.asClassName(Codicon.folder)}`;
192202
templateData.label.setLabel(basename(artifactOrFolder.uri));
203+
204+
templateData.timestamp.textContent = '';
205+
templateData.timestampContainer.classList.remove('duplicate');
206+
templateData.timestampContainer.style.display = 'none';
193207
}
194208

195209
// Actions
@@ -211,10 +225,18 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
211225
: '';
212226

213227
templateData.label.setLabel(artifact.name, artifact.description);
228+
229+
templateData.timestamp.textContent = artifact.timestamp ? fromNow(artifact.timestamp) : '';
230+
templateData.timestampContainer.classList.toggle('duplicate', artifactOrFolder.hideTimestamp);
231+
templateData.timestampContainer.style.display = '';
214232
} else if (isSCMArtifactNode(artifactOrFolder)) {
215233
// Folder
216234
templateData.icon.className = `icon ${ThemeIcon.asClassName(Codicon.folder)}`;
217235
templateData.label.setLabel(artifactOrFolder.uri.fsPath.substring(1));
236+
237+
templateData.timestamp.textContent = '';
238+
templateData.timestampContainer.classList.remove('duplicate');
239+
templateData.timestampContainer.style.display = 'none';
218240
}
219241

220242
// Actions
@@ -300,13 +322,29 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
300322
if (inputOrElement.artifactGroup.supportsFolders) {
301323
// Resource tree for artifacts
302324
const artifactsTree = new ResourceTree<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>(inputOrElement);
303-
for (const artifact of artifacts) {
304-
artifactsTree.add(URI.from({
305-
scheme: 'scm-artifact', path: artifact.name
306-
}), {
325+
for (let index = 0; index < artifacts.length; index++) {
326+
const artifact = artifacts[index];
327+
const artifactUri = URI.from({ scheme: 'scm-artifact', path: artifact.name });
328+
const artifactBasename = artifact.id.lastIndexOf('/') > 0
329+
? artifact.id.substring(0, artifact.id.lastIndexOf('/'))
330+
: artifact.id;
331+
332+
const prevArtifact = index > 0 ? artifacts[index - 1] : undefined;
333+
const prevArtifactBasename = prevArtifact && prevArtifact.id.lastIndexOf('/') > 0
334+
? prevArtifact.id.substring(0, prevArtifact.id.lastIndexOf('/'))
335+
: prevArtifact?.id;
336+
337+
const hideTimestamp = index > 0 &&
338+
artifact.timestamp !== undefined &&
339+
prevArtifact?.timestamp !== undefined &&
340+
artifactBasename === prevArtifactBasename &&
341+
fromNow(prevArtifact.timestamp) === fromNow(artifact.timestamp);
342+
343+
artifactsTree.add(artifactUri, {
307344
repository,
308345
group: inputOrElement.artifactGroup,
309346
artifact,
347+
hideTimestamp,
310348
type: 'artifact'
311349
});
312350
}
@@ -315,14 +353,16 @@ class RepositoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
315353
}
316354

317355
// Flat list of artifacts
318-
return artifacts.map(artifact => (
319-
{
320-
repository,
321-
group: inputOrElement.artifactGroup,
322-
artifact,
323-
type: 'artifact'
324-
} satisfies SCMArtifactTreeElement
325-
));
356+
return artifacts.map((artifact, index, artifacts) => ({
357+
repository,
358+
group: inputOrElement.artifactGroup,
359+
artifact,
360+
hideTimestamp: index > 0 &&
361+
artifact.timestamp !== undefined &&
362+
artifacts[index - 1].timestamp !== undefined &&
363+
fromNow(artifacts[index - 1].timestamp!) === fromNow(artifact.timestamp),
364+
type: 'artifact'
365+
} satisfies SCMArtifactTreeElement));
326366
} else if (isSCMArtifactNode(inputOrElement)) {
327367
return Iterable.map(inputOrElement.children,
328368
node => node.element && node.childrenCount === 0 ? node.element : node);

src/vs/workbench/contrib/scm/common/artifact.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface ISCMArtifact {
2626
readonly name: string;
2727
readonly description?: string;
2828
readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon;
29+
readonly timestamp?: number;
2930
}
3031

3132
export interface SCMArtifactGroupTreeElement {
@@ -38,5 +39,6 @@ export interface SCMArtifactTreeElement {
3839
readonly repository: ISCMRepository;
3940
readonly group: ISCMArtifactGroup;
4041
readonly artifact: ISCMArtifact;
42+
readonly hideTimestamp: boolean;
4143
readonly type: 'artifact';
4244
}

src/vscode-dts/vscode.proposed.scmArtifactProvider.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ declare module 'vscode' {
2929
readonly name: string;
3030
readonly description?: string;
3131
readonly icon?: IconPath;
32+
readonly timestamp?: number;
3233
}
3334
}

0 commit comments

Comments
 (0)