SCM Graph - better handling of graph refresh (#228329)

* Initial implementation

* Removed refs should be also removed from the filter
This commit is contained in:
Ladislau Szomoru 2024-09-12 12:06:25 +02:00 committed by GitHub
parent edbf964e42
commit 1d3895d045
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 75 additions and 56 deletions

View file

@ -6,11 +6,12 @@
import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode';
import { Repository, Resource } from './repository';
import { IDisposable, deltaHistoryItemRefs, dispose } from './util';
import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent } from './util';
import { toGitUri } from './uri';
import { Branch, LogOptions, Ref, RefType } from './api/git';
import { emojify, ensureEmojis } from './emoji';
import { Commit } from './git';
import { OperationKind, OperationResult } from './operation';
function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef {
switch (ref.type) {
@ -71,13 +72,15 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
private disposables: Disposable[] = [];
constructor(protected readonly repository: Repository, private readonly logger: LogOutputChannel) {
this.disposables.push(repository.onDidRunGitStatus(() => this.onDidRunGitStatus(), this));
const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly);
this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this));
this.disposables.push(window.registerFileDecorationProvider(this));
}
private async onDidRunGitStatus(): Promise<void> {
private async onDidRunWriteOperation(result: OperationResult): Promise<void> {
if (!this.repository.HEAD) {
this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] repository.HEAD is undefined');
this.logger.trace('[GitHistoryProvider][onDidRunWriteOperation] repository.HEAD is undefined');
this._currentHistoryItemRef = this._currentHistoryItemRemoteRef = this._currentHistoryItemBaseRef = undefined;
this._onDidChangeCurrentHistoryItemRefs.fire();
@ -111,7 +114,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
mergeBase.name !== this.repository.HEAD.upstream?.name) ? {
id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`,
name: `${mergeBase.remote}/${mergeBase.name}`,
revision: mergeBase.commit
revision: mergeBase.commit,
icon: new ThemeIcon('cloud')
} : undefined;
}
} else {
@ -145,24 +149,28 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
};
this._onDidChangeCurrentHistoryItemRefs.fire();
this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`);
// Refs (alphabetically)
const refs = await this.repository.getRefs({ sort: 'alphabetically' });
const historyItemRefs = refs.map(ref => toSourceControlHistoryItemRef(ref));
// Auto-fetch
const silent = result.operation.kind === OperationKind.Fetch && result.operation.showProgress === false;
const delta = deltaHistoryItemRefs(this.historyItemRefs, historyItemRefs);
this._onDidChangeHistoryItemRefs.fire(delta);
this._onDidChangeHistoryItemRefs.fire({ ...delta, silent });
this.historyItemRefs = historyItemRefs;
const deltaLog = {
added: delta.added.map(ref => ref.id),
modified: delta.modified.map(ref => ref.id),
removed: delta.removed.map(ref => ref.id)
removed: delta.removed.map(ref => ref.id),
silent
};
this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] historyItemRefs: ${JSON.stringify(deltaLog)}`);
this.logger.trace(`[GitHistoryProvider][onDidRunWriteOperation] historyItemRefs: ${JSON.stringify(deltaLog)}`);
}
async provideHistoryItemRefs(): Promise<SourceControlHistoryItemRef[]> {

View file

@ -190,7 +190,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
}, undefined);
get historyItemBaseRef(): IObservable<ISCMHistoryItemRef | undefined> { return this._historyItemBaseRef; }
private readonly _historyItemRefChanges = observableValue<ISCMHistoryItemRefsChangeEvent>(this, { added: [], modified: [], removed: [] });
private readonly _historyItemRefChanges = observableValue<ISCMHistoryItemRefsChangeEvent>(this, { added: [], modified: [], removed: [], silent: false });
get historyItemRefChanges(): IObservable<ISCMHistoryItemRefsChangeEvent> { return this._historyItemRefChanges; }
constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { }
@ -232,7 +232,7 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider {
const modified = historyItemRefs.modified.map(ref => toISCMHistoryItemRef(ref)!);
const removed = historyItemRefs.removed.map(ref => toISCMHistoryItemRef(ref)!);
this._historyItemRefChanges.set({ added, modified, removed }, undefined);
this._historyItemRefChanges.set({ added, modified, removed, silent: historyItemRefs.silent }, undefined);
}
}

View file

@ -1555,6 +1555,7 @@ export interface SCMHistoryItemRefsChangeEventDto {
readonly added: readonly SCMHistoryItemRefDto[];
readonly modified: readonly SCMHistoryItemRefDto[];
readonly removed: readonly SCMHistoryItemRefDto[];
readonly silent: boolean;
}
export interface SCMHistoryItemDto {

View file

@ -612,7 +612,7 @@ class ExtHostSourceControl implements vscode.SourceControl {
const modified = e.modified.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) }));
const removed = e.removed.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) }));
this.#proxy.$onDidChangeHistoryProviderHistoryItemRefs(this.handle, { added, modified, removed });
this.#proxy.$onDidChangeHistoryProviderHistoryItemRefs(this.handle, { added, modified, removed, silent: e.silent });
}));
}
}

View file

@ -527,7 +527,7 @@
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
border: 1px solid var(--vscode-contrastBorder);
margin-left: 6px;
margin: 0 6px;
padding: 2px 4px;
}

View file

@ -16,7 +16,7 @@ import { fromNow } from '../../../../base/common/date.js';
import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange, signalFromObservable } from '../../../../base/common/observable.js';
import { autorun, autorunWithStore, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange } from '../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { localize } from '../../../../nls.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
@ -1132,58 +1132,59 @@ export class SCMHistoryViewPane extends ViewPane {
// Update context
this._scmProviderCtx.set(repository.provider.contextValue);
// Publish
const historyItemRemoteRefIdSignal = signalFromObservable(this, derived(reader => {
return historyProvider.historyItemRemoteRef.read(reader)?.id;
// HistoryItemId changed (checkout)
const historyItemRefId = derived(reader => {
return historyProvider.historyItemRef.read(reader)?.id;
});
store.add(runOnChange(historyItemRefId, () => {
this.refresh();
}));
// Fetch, Push
const historyItemRemoteRefRevision = derived(reader => {
return historyProvider.historyItemRemoteRef.read(reader)?.revision;
});
// HistoryItemRefs changed
store.add(
autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({
owner: this,
createEmptyChangeSummary: () => ({ refresh: false }),
handleChange(context, changeSummary) {
changeSummary.refresh = context.didChange(historyItemRemoteRefRevision) ? 'ifScrollTop' : true;
return true;
},
}, (reader, changeSummary) => {
historyItemRemoteRefIdSignal.read(reader);
const historyItemRefValue = historyProvider.historyItemRef.read(reader);
const historyItemRemoteRefRevisionValue = historyItemRemoteRefRevision.read(reader);
// Commit, Checkout, Publish, Pull
if (changeSummary.refresh === true) {
store.add(runOnChange(historyProvider.historyItemRefChanges, changes => {
if (changes.silent) {
// The history item reference changes occurred in the background (ex: Auto Fetch)
// If tree is scrolled to the top, we can safely refresh the tree, otherwise we
// will show a visual cue that the view is outdated.
if (this._tree.scrollTop === 0) {
this.refresh();
return;
}
if (changeSummary.refresh === 'ifScrollTop') {
// If the history item remote revision has changed, but it matches the history
// item revision, then it means that a Push operation was performed and it is
// safe to refresh the graph.
if (historyItemRefValue?.revision === historyItemRemoteRefRevisionValue) {
this.refresh();
return;
// Show the "Outdated" badge on the view
this._repositoryOutdated.set(true, undefined);
return;
}
// If there are any removed history item references we need to check whether they are
// currently being used in the filter. If they are, we need to update the filter which
// will result in the graph being refreshed.
if (changes.removed.length !== 0) {
const historyItemsFilter = this._treeViewModel.historyItemsFilter.get();
if (historyItemsFilter !== 'all' && historyItemsFilter !== 'auto') {
let updateFilter = false;
const historyItemRefs = [...historyItemsFilter];
for (const ref of changes.removed) {
const index = historyItemRefs
.findIndex(item => item.id === ref.id);
if (index !== -1) {
historyItemRefs.splice(index, 1);
updateFilter = true;
}
}
// If the history item remote revision has changed, but it does not matches the
// history item revision, then a Fetch operation was performed. This can be the
// result of a user action (Fetch) or a background action (Auto Fetch). If the
// tree is scrolled to the top, we can safely refresh the tree.
if (this._tree.scrollTop === 0) {
this.refresh();
if (updateFilter) {
this._treeViewModel.setHistoryItemsFilter(historyItemRefs);
return;
}
// Show the "Outdated" badge on the view
this._repositoryOutdated.set(true, undefined);
}
}));
}
this.refresh();
}));
// HistoryItemRefs filter changed
store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => {

View file

@ -53,6 +53,7 @@ export interface ISCMHistoryItemRefsChangeEvent {
readonly added: readonly ISCMHistoryItemRef[];
readonly removed: readonly ISCMHistoryItemRef[];
readonly modified: readonly ISCMHistoryItemRef[];
readonly silent: boolean;
}
export interface ISCMHistoryItem {

View file

@ -76,5 +76,13 @@ declare module 'vscode' {
readonly added: readonly SourceControlHistoryItemRef[];
readonly removed: readonly SourceControlHistoryItemRef[];
readonly modified: readonly SourceControlHistoryItemRef[];
/**
* Flag to indicate if the operation that caused the event to trigger was due
* to a user action or a background operation (ex: Auto Fetch). The flag is used
* to determine whether to automatically refresh the user interface or present
* the user with a visual cue that the user interface is outdated.
*/
readonly silent: boolean;
}
}