From d8af24e15daeb1ad5bd0ee4858852e60ea545c66 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 01:51:31 +0200 Subject: [PATCH] SCM - wire-up the onDidChangeHistoryItemRefs event (#228042) Wire-up the event --- extensions/git/src/historyProvider.ts | 85 ++++++++++++------- extensions/git/src/util.ts | 57 ++++++++++++- src/vs/workbench/api/browser/mainThreadSCM.ts | 37 +++++++- .../workbench/api/common/extHost.protocol.ts | 7 ++ src/vs/workbench/api/common/extHostSCM.ts | 11 +++ .../workbench/contrib/scm/common/history.ts | 8 ++ .../vscode.proposed.scmHistoryProvider.d.ts | 10 +-- 7 files changed, 177 insertions(+), 38 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index e9aefa63dd1..872582dae6e 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,21 +4,53 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryChangeEvent, SourceControlHistoryItemRef, l10n } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, dispose } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose } from './util'; import { toGitUri } from './uri'; -import { Branch, LogOptions, RefType } from './api/git'; +import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; +function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { + switch (ref.type) { + case RefType.RemoteHead: + return { + id: `refs/remotes/${ref.remote}/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('cloud'), + category: l10n.t('remote branches') + }; + case RefType.Tag: + return { + id: `refs/tags/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('tag'), + category: l10n.t('tags') + }; + default: + return { + id: `refs/heads/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? ref.commit.substring(0, 8) : undefined, + revision: ref.commit, + icon: new ThemeIcon('git-branch'), + category: l10n.t('branches') + }; + } +} + export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter(); readonly onDidChangeCurrentHistoryItemGroup: Event = this._onDidChangeCurrentHistoryItemGroup.event; - private readonly _onDidChangeHistory = new EventEmitter(); - readonly onDidChangeHistory: Event = this._onDidChangeHistory.event; + private readonly _onDidChangeHistoryItemRefs = new EventEmitter(); + readonly onDidChangeHistoryItemRefs: Event = this._onDidChangeHistoryItemRefs.event; private readonly _onDidChangeDecorations = new EventEmitter(); readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; @@ -30,6 +62,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this._onDidChangeCurrentHistoryItemGroup.fire(); } + private historyItemRefs: SourceControlHistoryItemRef[] = []; private historyItemDecorations = new Map(); private disposables: Disposable[] = []; @@ -80,6 +113,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }; this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`); + + // Refs (alphabetically) + const refs = await this.repository.getRefs({ sort: 'alphabetically' }); + const historyItemRefs = refs.map(ref => toSourceControlHistoryItemRef(ref)); + + const delta = deltaHistoryItemRefs(this.historyItemRefs, historyItemRefs); + this._onDidChangeHistoryItemRefs.fire(delta); + 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) + }; + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] historyItemRefs: ${JSON.stringify(deltaLog)}`); } async provideHistoryItemRefs(): Promise { @@ -92,34 +140,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec for (const ref of refs) { switch (ref.type) { case RefType.RemoteHead: - remoteBranches.push({ - id: `refs/remotes/${ref.remote}/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, - revision: ref.commit, - icon: new ThemeIcon('cloud'), - category: l10n.t('remote branches') - }); + remoteBranches.push(toSourceControlHistoryItemRef(ref)); break; case RefType.Tag: - tags.push({ - id: `refs/tags/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, - revision: ref.commit, - icon: new ThemeIcon('tag'), - category: l10n.t('tags') - }); + tags.push(toSourceControlHistoryItemRef(ref)); break; default: - branches.push({ - id: `refs/heads/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? ref.commit.substring(0, 8) : undefined, - revision: ref.commit, - icon: new ThemeIcon('git-branch'), - category: l10n.t('branches') - }); + branches.push(toSourceControlHistoryItemRef(ref)); break; } } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index eac6f0384b9..bf33411c3b3 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef } from 'vscode'; import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -513,3 +513,58 @@ export namespace Versions { return from(major, minor, patch, pre); } } + +export function deltaHistoryItemRefs(before: SourceControlHistoryItemRef[], after: SourceControlHistoryItemRef[]): { + added: SourceControlHistoryItemRef[]; + modified: SourceControlHistoryItemRef[]; + removed: SourceControlHistoryItemRef[]; +} { + if (before.length === 0) { + return { added: after, modified: [], removed: [] }; + } + + const added: SourceControlHistoryItemRef[] = []; + const modified: SourceControlHistoryItemRef[] = []; + const removed: SourceControlHistoryItemRef[] = []; + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + added.push(...after.slice(afterIdx)); + break; + } + if (afterIdx === after.length) { + removed.push(...before.slice(beforeIdx)); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + + const result = beforeElement.id.localeCompare(afterElement.id); + + if (result === 0) { + if (beforeElement.revision !== afterElement.revision) { + // modified + modified.push(afterElement); + } + + beforeIdx += 1; + afterIdx += 1; + } else if (result < 0) { + // beforeElement is smaller -> before element removed + removed.push(beforeElement); + + beforeIdx += 1; + } else if (result > 0) { + // beforeElement is greater -> after element added + added.push(afterElement); + + afterIdx += 1; + } + } + + return { added, modified, removed }; +} diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 7ab6d69e6ae..d713066de80 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,10 +6,10 @@ import { Barrier } from '../../../base/common/async.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; -import { derivedOpts, observableValue, observableValueOpts } from '../../../base/common/observable.js'; +import { derivedOpts, IObservable, observableValue, observableValueOpts } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol.js'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto, SCMHistoryItemRefsChangeEventDto } from '../common/extHost.protocol.js'; import { Command } from '../../../editor/common/languages.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; @@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js'; @@ -218,6 +218,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { } : undefined; }); + private readonly _historyItemRefChanges = observableValue(this, { added: [], modified: [], removed: [] }); + get historyItemRefChanges(): IObservable { return this._historyItemRefChanges; } + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { @@ -247,6 +250,14 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { this._currentHistoryItemGroup.set(historyItemGroup, undefined); } + + $onDidChangeHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { + const added = historyItemRefs.added.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + const modified = historyItemRefs.modified.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + const removed = historyItemRefs.removed.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + + this._historyItemRefChanges.set({ added, modified, removed }, undefined); + } } class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { @@ -484,6 +495,14 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._historyProvider.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); } + $onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { + if (!this.historyProvider.get()) { + return; + } + + this._historyProvider.get()?.$onDidChangeHistoryItemRefs(historyItemRefs); + } + toJSON(): any { return { $mid: MarshalledId.ScmProvider, @@ -728,4 +747,16 @@ export class MainThreadSCM implements MainThreadSCMShape { const provider = repository.provider as MainThreadSCMProvider; provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup); } + + async $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); + const repository = this._repositories.get(sourceControlHandle); + + if (!repository) { + return; + } + + const provider = repository.provider as MainThreadSCMProvider; + provider.$onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs); + } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2f517f05a6d..2055d997aa0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1559,6 +1559,12 @@ export interface SCMHistoryItemRefDto { readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; } +export interface SCMHistoryItemRefsChangeEventDto { + readonly added: readonly SCMHistoryItemRefDto[]; + readonly modified: readonly SCMHistoryItemRefDto[]; + readonly removed: readonly SCMHistoryItemRefDto[]; +} + export interface SCMHistoryItemDto { readonly id: string; readonly parentIds: string[]; @@ -1601,6 +1607,7 @@ export interface MainThreadSCMShape extends IDisposable { $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; + $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise; } export interface MainThreadQuickDiffShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 3976177cc7e..1f15f83d60a 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -597,6 +597,17 @@ class ExtHostSourceControl implements vscode.SourceControl { this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); })); + this._historyProviderDisposable.value.add(historyProvider.onDidChangeHistoryItemRefs((e) => { + if (e.added.length === 0 && e.modified.length === 0 && e.removed.length === 0) { + return; + } + + const added = e.added.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + 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 }); + })); } } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index fbc0d6c0f1f..6a388fcb460 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -21,6 +21,8 @@ export interface ISCMHistoryProvider { readonly currentHistoryItemRemoteRef: IObservable; readonly currentHistoryItemBaseRef: IObservable; + readonly historyItemRefChanges: IObservable; + provideHistoryItemRefs(): Promise; provideHistoryItems(options: ISCMHistoryOptions): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; @@ -57,6 +59,12 @@ export interface ISCMHistoryItemRef { readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; } +export interface ISCMHistoryItemRefsChangeEvent { + readonly added: readonly ISCMHistoryItemRef[]; + readonly removed: readonly ISCMHistoryItemRef[]; + readonly modified: readonly ISCMHistoryItemRef[]; +} + export interface ISCMHistoryItem { readonly id: string; readonly parentIds: string[]; diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index d465d599ba2..b6a4893dd54 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -22,7 +22,7 @@ declare module 'vscode' { /** * Fires when history item refs change */ - onDidChangeHistory: Event; + onDidChangeHistoryItemRefs: Event; provideHistoryItemRefs(token: CancellationToken): ProviderResult; provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; @@ -78,9 +78,9 @@ declare module 'vscode' { readonly renameUri: Uri | undefined; } - export interface SourceControlHistoryChangeEvent { - readonly added: Iterable; - readonly removed: Iterable; - readonly modified: Iterable; + export interface SourceControlHistoryItemRefsChangeEvent { + readonly added: readonly SourceControlHistoryItemRef[]; + readonly removed: readonly SourceControlHistoryItemRef[]; + readonly modified: readonly SourceControlHistoryItemRef[]; } }