Skip to content

Commit d6394ad

Browse files
authored
SCM - improve loading more experience (microsoft#226421)
1 parent 3069c9f commit d6394ad

File tree

4 files changed

+102
-46
lines changed

4 files changed

+102
-46
lines changed

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -564,19 +564,32 @@
564564
.scm-history-view .history-item-load-more {
565565
display: flex;
566566
height: 22px;
567-
gap: 2px;
568567
}
569568

570569
.scm-history-view .history-item-load-more .graph-placeholder {
571-
margin: 2px 0;
572-
background: var(--vscode-scm-historyItemStatisticsBorder);
573-
border-radius: 2px;
574-
opacity: 0.5;
570+
mask-image: linear-gradient(black, transparent);
575571
}
576572

577573
.scm-history-view .history-item-load-more .history-item-placeholder {
578574
flex-grow: 1;
579-
margin: 2px 0;
575+
}
576+
577+
.scm-history-view .history-item-load-more .history-item-placeholder .monaco-highlighted-label {
578+
display: flex;
579+
align-items: center;
580+
justify-content: center;
581+
}
582+
583+
.scm-history-view .history-item-load-more .history-item-placeholder .monaco-highlighted-label .codicon {
584+
font-size: 12px;
585+
}
586+
587+
.scm-history-view .history-item-load-more .history-item-placeholder.shimmer {
588+
padding: 2px 0;
589+
}
590+
591+
.scm-history-view .history-item-load-more .history-item-placeholder.shimmer .monaco-icon-label-container {
592+
height: 18px;
580593
background: var(--vscode-scm-historyItemStatisticsBorder);
581594
border-radius: 2px;
582595
opacity: 0.5;

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import { chartsBlue, chartsGreen, chartsOrange, chartsPurple, chartsRed, chartsY
1212
import { asCssVariable, ColorIdentifier, registerColor } from 'vs/platform/theme/common/colorUtils';
1313
import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history';
1414
import { rot } from 'vs/base/common/numbers';
15+
import { svgElem } from 'vs/base/browser/dom';
1516

16-
const SWIMLANE_HEIGHT = 22;
17-
const SWIMLANE_WIDTH = 11;
17+
export const SWIMLANE_HEIGHT = 22;
18+
export const SWIMLANE_WIDTH = 11;
1819
const CIRCLE_RADIUS = 4;
1920
const SWIMLANE_CURVE_RADIUS = 5;
2021

@@ -228,6 +229,20 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV
228229
return svg;
229230
}
230231

232+
export function renderSCMHistoryGraphPlaceholder(columns: ISCMHistoryItemGraphNode[]): HTMLElement {
233+
const elements = svgElem('svg', {
234+
style: { height: `${SWIMLANE_HEIGHT}px`, width: `${SWIMLANE_WIDTH * (columns.length + 1)}px`, }
235+
});
236+
237+
// Draw |
238+
for (let index = 0; index < columns.length; index++) {
239+
const path = drawVerticalLine(SWIMLANE_WIDTH * (index + 1), 0, SWIMLANE_HEIGHT, columns[index].color);
240+
elements.root.append(path);
241+
}
242+
243+
return elements.root;
244+
}
245+
231246
export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map<string, string>()): ISCMHistoryItemViewModel[] {
232247
let colorIndex = -1;
233248
const viewModels: ISCMHistoryItemViewModel[] = [];

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

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { fromNow } from 'vs/base/common/date';
1818
import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters';
1919
import { MarkdownString } from 'vs/base/common/htmlContent';
2020
import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
21-
import { autorun, autorunWithStore } from 'vs/base/common/observable';
21+
import { autorun, autorunWithStore, IObservable, observableValue } from 'vs/base/common/observable';
2222
import { ThemeIcon } from 'vs/base/common/themables';
2323
import { localize } from 'vs/nls';
2424
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -35,7 +35,7 @@ import { ColorIdentifier, registerColor } from 'vs/platform/theme/common/colorRe
3535
import { IThemeService } from 'vs/platform/theme/common/themeService';
3636
import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
3737
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
38-
import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, historyItemGroupHoverLabelForeground, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory';
38+
import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, historyItemGroupHoverLabelForeground, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder } from 'vs/workbench/contrib/scm/browser/scmHistory';
3939
import { RepositoryActionRunner } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer';
4040
import { collectContextMenuActions, connectPrimaryMenu, getActionViewItemProvider, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository, isSCMViewService } from 'vs/workbench/contrib/scm/browser/util';
4141
import { ISCMHistoryItem, ISCMHistoryItemViewModel, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from 'vs/workbench/contrib/scm/common/history';
@@ -338,7 +338,9 @@ class HistoryItemRenderer implements ITreeRenderer<SCMHistoryItemViewModelTreeEl
338338
interface LoadMoreTemplate {
339339
readonly element: HTMLElement;
340340
readonly graphPlaceholder: HTMLElement;
341-
readonly historyItemPlaceholder: HTMLElement;
341+
readonly historyItemPlaceholderContainer: HTMLElement;
342+
readonly historyItemPlaceholderLabel: IconLabel;
343+
readonly elementDisposables: DisposableStore;
342344
readonly disposables: IDisposable;
343345
}
344346

@@ -347,22 +349,45 @@ class HistoryItemLoadMoreRenderer implements ITreeRenderer<SCMHistoryItemLoadMor
347349
static readonly TEMPLATE_ID = 'historyItemLoadMore';
348350
get templateId(): string { return HistoryItemLoadMoreRenderer.TEMPLATE_ID; }
349351

350-
constructor(private readonly _loadMoreCallback: (repository: ISCMRepository, cursor: string) => void) { }
352+
constructor(
353+
private readonly _loadingMore: IObservable<boolean>,
354+
private readonly _loadMoreCallback: (repository: ISCMRepository) => void,
355+
@IConfigurationService private readonly _configurationService: IConfigurationService,
356+
@ISCMViewService private readonly _scmViewService: ISCMViewService) { }
351357

352358
renderTemplate(container: HTMLElement): LoadMoreTemplate {
353359
// hack
354360
(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie');
355361

356362
const element = append(container, $('.history-item-load-more'));
357363
const graphPlaceholder = append(element, $('.graph-placeholder'));
358-
const historyItemPlaceholder = append(element, $('.history-item-placeholder'));
364+
const historyItemPlaceholderContainer = append(element, $('.history-item-placeholder'));
365+
const historyItemPlaceholderLabel = new IconLabel(historyItemPlaceholderContainer, { supportIcons: true });
359366

360-
return { element, graphPlaceholder, historyItemPlaceholder, disposables: new DisposableStore() };
367+
return { element, graphPlaceholder, historyItemPlaceholderContainer, historyItemPlaceholderLabel, elementDisposables: new DisposableStore(), disposables: new DisposableStore() };
361368
}
362369

363370
renderElement(element: ITreeNode<SCMHistoryItemLoadMoreTreeElement, void>, index: number, templateData: LoadMoreTemplate, height: number | undefined): void {
364-
templateData.graphPlaceholder.style.width = `${11 * (element.element.graphColumnCount + 1) - 4}px`;
365-
this._loadMoreCallback(element.element.repository, element.element.cursor);
371+
const repositoryCount = this._scmViewService.visibleRepositories.length;
372+
const alwaysShowRepositories = this._configurationService.getValue<boolean>('scm.alwaysShowRepositories') === true;
373+
374+
templateData.graphPlaceholder.textContent = '';
375+
templateData.graphPlaceholder.style.width = `${SWIMLANE_WIDTH * (element.element.graphColumns.length + 1)}px`;
376+
templateData.graphPlaceholder.appendChild(renderSCMHistoryGraphPlaceholder(element.element.graphColumns));
377+
378+
templateData.historyItemPlaceholderContainer.classList.toggle('shimmer', repositoryCount === 1 && !alwaysShowRepositories);
379+
380+
if (repositoryCount > 1 || alwaysShowRepositories) {
381+
templateData.elementDisposables.add(autorun(reader => {
382+
const loadingMore = this._loadingMore.read(reader);
383+
const icon = `$(${loadingMore ? 'loading~spin' : 'fold-down'})`;
384+
385+
templateData.historyItemPlaceholderLabel.setLabel(localize('loadMore', "{0} Load More...", icon));
386+
}));
387+
} else {
388+
templateData.historyItemPlaceholderLabel.setLabel('');
389+
this._loadMoreCallback(element.element.repository);
390+
}
366391
}
367392

368393
disposeTemplate(templateData: LoadMoreTemplate): void {
@@ -500,21 +525,21 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
500525
private readonly _historyItems = new Map<ISCMRepository, HistoryItemCacheEntry>();
501526

502527
constructor(
503-
@IConfigurationService private readonly configurationService: IConfigurationService,
504-
@ISCMViewService private readonly scmViewService: ISCMViewService
528+
@IConfigurationService private readonly _configurationService: IConfigurationService,
529+
@ISCMViewService private readonly _scmViewService: ISCMViewService
505530
) {
506531
super();
507532
}
508533

509534
async getChildren(inputOrElement: ISCMViewService | TreeElement): Promise<Iterable<TreeElement>> {
510-
const repositoryCount = this.scmViewService.visibleRepositories.length;
511-
const alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories') === true;
535+
const repositoryCount = this._scmViewService.visibleRepositories.length;
536+
const alwaysShowRepositories = this._configurationService.getValue<boolean>('scm.alwaysShowRepositories') === true;
512537

513538
if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) {
514-
return this.scmViewService.visibleRepositories;
539+
return this._scmViewService.visibleRepositories;
515540
} else if ((isSCMViewService(inputOrElement) && repositoryCount === 1 && !alwaysShowRepositories) || isSCMRepository(inputOrElement)) {
516541
const children: TreeElement[] = [];
517-
inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this.scmViewService.visibleRepositories[0];
542+
inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this._scmViewService.visibleRepositories[0];
518543

519544
const historyItems = await this._getHistoryItems(inputOrElement);
520545
children.push(...historyItems);
@@ -523,8 +548,7 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
523548
if (lastHistoryItem && lastHistoryItem.historyItemViewModel.historyItem.parentIds.length !== 0) {
524549
children.push({
525550
repository: inputOrElement,
526-
cursor: lastHistoryItem.historyItemViewModel.historyItem.parentIds[0],
527-
graphColumnCount: lastHistoryItem.historyItemViewModel.outputSwimlanes.length,
551+
graphColumns: lastHistoryItem.historyItemViewModel.outputSwimlanes,
528552
type: 'historyItemLoadMore'
529553
} satisfies SCMHistoryItemLoadMoreTreeElement);
530554
}
@@ -536,7 +560,7 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource<IS
536560

537561
hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean {
538562
if (isSCMViewService(inputOrElement)) {
539-
return this.scmViewService.visibleRepositories.length !== 0;
563+
return this._scmViewService.visibleRepositories.length !== 0;
540564
} else if (isSCMRepository(inputOrElement)) {
541565
return true;
542566
} else if (isSCMHistoryItemViewModelTreeElement(inputOrElement)) {
@@ -622,7 +646,7 @@ export class SCMHistoryViewPane extends ViewPane {
622646
private _tree!: WorkbenchAsyncDataTree<ISCMViewService, TreeElement, FuzzyScore>;
623647
private _treeDataSource!: SCMHistoryTreeDataSource;
624648
private _treeIdentityProvider!: SCMHistoryTreeIdentityProvider;
625-
private _isLoadMoreInProgress = false;
649+
private _isLoadMore = observableValue(this, false);
626650

627651
private readonly _repositories = new DisposableMap<ISCMRepository>();
628652
private readonly _visibilityDisposables = new DisposableStore();
@@ -636,8 +660,8 @@ export class SCMHistoryViewPane extends ViewPane {
636660

637661
constructor(
638662
options: IViewPaneOptions,
639-
@ICommandService private readonly commandService: ICommandService,
640-
@ISCMViewService private readonly scmViewService: ISCMViewService,
663+
@ICommandService private readonly _commandService: ICommandService,
664+
@ISCMViewService private readonly _scmViewService: ISCMViewService,
641665
@IConfigurationService configurationService: IConfigurationService,
642666
@IContextMenuService contextMenuService: IContextMenuService,
643667
@IKeybindingService keybindingService: IKeybindingService,
@@ -679,7 +703,7 @@ export class SCMHistoryViewPane extends ViewPane {
679703
this.onDidChangeBodyVisibility(visible => {
680704
if (visible) {
681705
this._treeOperationSequencer.queue(async () => {
682-
await this._tree.setInput(this.scmViewService);
706+
await this._tree.setInput(this._scmViewService);
683707

684708
Event.filter(this.configurationService.onDidChangeConfiguration,
685709
e =>
@@ -691,8 +715,8 @@ export class SCMHistoryViewPane extends ViewPane {
691715
}, this, this._visibilityDisposables);
692716

693717
// Add visible repositories
694-
this.scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this._visibilityDisposables);
695-
this._onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() });
718+
this._scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this._visibilityDisposables);
719+
this._onDidChangeVisibleRepositories({ added: this._scmViewService.visibleRepositories, removed: Iterable.empty() });
696720

697721
this._tree.scrollTop = 0;
698722
});
@@ -728,7 +752,7 @@ export class SCMHistoryViewPane extends ViewPane {
728752
[
729753
this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)),
730754
this.instantiationService.createInstance(HistoryItemRenderer, historyItemHoverDelegate),
731-
this.instantiationService.createInstance(HistoryItemLoadMoreRenderer, (repository, cursor) => this._loadMoreCallback(repository, cursor)),
755+
this.instantiationService.createInstance(HistoryItemLoadMoreRenderer, this._isLoadMore, repository => this._loadMoreCallback(repository)),
732756
],
733757
this._treeDataSource,
734758
{
@@ -750,8 +774,7 @@ export class SCMHistoryViewPane extends ViewPane {
750774
if (!e.element) {
751775
return;
752776
} else if (isSCMRepository(e.element)) {
753-
this.scmViewService.focus(e.element);
754-
return;
777+
this._scmViewService.focus(e.element);
755778
} else if (isSCMHistoryItemViewModelTreeElement(e.element)) {
756779
const historyItem = e.element.historyItemViewModel.historyItem;
757780
const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined;
@@ -765,11 +788,17 @@ export class SCMHistoryViewPane extends ViewPane {
765788
const path = rootUri ? rootUri.path : e.element.repository.provider.label;
766789
const multiDiffSourceUri = URI.from({ scheme: 'scm-history-item', path: `${path}/${historyItemParentId}..${historyItem.id}` }, true);
767790

768-
await this.commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges });
791+
await this._commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges });
769792
}
770793

771-
this.scmViewService.focus(e.element.repository);
772-
return;
794+
this._scmViewService.focus(e.element.repository);
795+
} else if (isSCMHistoryItemLoadMoreTreeElement(e.element)) {
796+
const repositoryCount = this._scmViewService.visibleRepositories.length;
797+
const alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories') === true;
798+
799+
if (repositoryCount > 1 || alwaysShowRepositories) {
800+
this._loadMoreCallback(e.element.repository);
801+
}
773802
}
774803
}
775804

@@ -785,14 +814,14 @@ export class SCMHistoryViewPane extends ViewPane {
785814
let actionRunner: IActionRunner = new HistoryItemActionRunner(() => this._getSelectedHistoryItems());
786815

787816
if (isSCMRepository(element)) {
788-
const menus = this.scmViewService.menus.getRepositoryMenus(element.provider);
817+
const menus = this._scmViewService.menus.getRepositoryMenus(element.provider);
789818
const menu = menus.repositoryContextMenu;
790819

791820
actions = collectContextMenuActions(menu);
792821
actionRunner = new RepositoryActionRunner(() => this._getSelectedRepositories());
793822
context = element.provider;
794823
} else if (isSCMHistoryItemViewModelTreeElement(element)) {
795-
const menus = this.scmViewService.menus.getRepositoryMenus(element.repository.provider);
824+
const menus = this._scmViewService.menus.getRepositoryMenus(element.repository.provider);
796825
const menu = menus.historyProviderMenu?.getHistoryItemMenu2(element);
797826

798827
actions = menu ? collectContextMenuActions(menu) : [];
@@ -821,7 +850,7 @@ export class SCMHistoryViewPane extends ViewPane {
821850
return;
822851
}
823852

824-
if (this.scmViewService.visibleRepositories.length === 1) {
853+
if (this._scmViewService.visibleRepositories.length === 1) {
825854
this._scmHistoryItemGroupHasRemoteContextKey.set(!!currentHistoryItemGroup?.remote);
826855
} else {
827856
this._scmHistoryItemGroupHasRemoteContextKey.reset();
@@ -856,16 +885,16 @@ export class SCMHistoryViewPane extends ViewPane {
856885
.filter(r => !!r && isSCMHistoryItemViewModelTreeElement(r))!;
857886
}
858887

859-
private _loadMoreCallback(repository: ISCMRepository, cursor: string): void {
860-
if (this._isLoadMoreInProgress) {
888+
private _loadMoreCallback(repository: ISCMRepository): void {
889+
if (this._isLoadMore.get()) {
861890
return;
862891
}
863892

864-
this._isLoadMoreInProgress = true;
893+
this._isLoadMore.set(true, undefined);
865894
this._treeDataSource.loadMore(repository);
866895

867896
this._updateChildren(repository)
868-
.finally(() => this._isLoadMoreInProgress = false);
897+
.finally(() => this._isLoadMore.set(false, undefined));
869898
}
870899

871900
private _updateChildren(element?: ISCMRepository): Promise<void> {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,7 @@ export interface SCMHistoryItemViewModelTreeElement {
100100

101101
export interface SCMHistoryItemLoadMoreTreeElement {
102102
readonly repository: ISCMRepository;
103-
readonly cursor: string;
104-
readonly graphColumnCount: number;
103+
readonly graphColumns: ISCMHistoryItemGraphNode[];
105104
readonly type: 'historyItemLoadMore';
106105
}
107106

0 commit comments

Comments
 (0)