From 7c43d44c09d8b99192237bae0c39c8ea05a60374 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 9 May 2024 23:19:23 +0200 Subject: [PATCH 01/33] Hacked up the starting point --- extensions/git/src/api/git.d.ts | 1 + extensions/git/src/git.ts | 5 + extensions/git/src/historyProvider.ts | 20 +- .../contrib/scm/browser/media/scm.css | 5 + src/vs/workbench/contrib/scm/browser/menus.ts | 60 +- .../contrib/scm/browser/scmViewPane.ts | 636 ++++++++++-------- .../workbench/contrib/scm/common/history.ts | 5 +- 7 files changed, 419 insertions(+), 313 deletions(-) diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index d6d2166e00bf5..279b81127b0d3 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -144,6 +144,7 @@ export interface LogOptions { readonly sortByAuthorDate?: boolean; readonly shortStats?: boolean; readonly author?: string; + readonly refNames?: string[]; } export interface CommitOptions { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 697e77815e4b2..532af7742cefe 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1165,6 +1165,11 @@ export class Repository { args.push(`--author="${options.author}"`); } + if (options?.refNames) { + args.push('--topo-order'); + args.push(...options.refNames); + } + if (options?.path) { args.push('--', options.path); } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index f238010e14c22..ee098f3925fac 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -77,20 +77,20 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.logger.trace(`GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup (${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); } - async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise { + async provideHistoryItems(_: string, __: SourceControlHistoryOptions): Promise { //TODO@lszomoru - support limit and cursor - if (typeof options.limit === 'number') { - throw new Error('Unsupported options.'); - } - if (typeof options.limit?.id !== 'string') { - throw new Error('Unsupported options.'); - } + // if (typeof options.limit === 'number') { + // throw new Error('Unsupported options.'); + // } + // if (typeof options.limit?.id !== 'string') { + // throw new Error('Unsupported options.'); + // } - const refParentId = options.limit.id; - const refId = await this.repository.revParse(historyItemGroupId) ?? ''; + // const refParentId = options.limit.id; + // const refId = await this.repository.revParse(historyItemGroupId) ?? ''; const historyItems: SourceControlHistoryItem[] = []; - const commits = await this.repository.log({ range: `${refParentId}..${refId}`, shortStats: true, sortByAuthorDate: true }); + const commits = await this.repository.log({ refNames: ['main', 'origin/main'] }); await ensureEmojis(); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index c8939c63fbe40..2bef117d02822 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -133,6 +133,11 @@ align-items: center; } +.scm-view .monaco-list-row .history-item > .graph { + display: flex; + flex-shrink: 0; +} + .scm-view .monaco-list-row .history-item .stats-container { display: flex; font-size: 11px; diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 39ee31783bd3a..b24712b13a52c 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -14,7 +14,7 @@ import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/c import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMMenus, ISCMProvider, ISCMRepository, ISCMRepositoryMenus, ISCMResource, ISCMResourceGroup, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; function actionEquals(a: IAction, b: IAction): boolean { @@ -255,16 +255,16 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDisposable { - private readonly historyItemMenus = new Map(); + // private readonly historyItemMenus = new Map(); private readonly disposables = new DisposableStore(); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService) { } - getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { - return this.getOrCreateHistoryItemMenu(historyItem); - } + // getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { + // return this.getOrCreateHistoryItemMenu(historyItem); + // } getHistoryItemGroupMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { return historyItemGroup.direction === 'incoming' ? @@ -278,31 +278,31 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo this.getOutgoingHistoryItemGroupMenu(MenuId.SCMOutgoingChangesContext, historyItemGroup); } - private getOrCreateHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { - let result = this.historyItemMenus.get(historyItem); - - if (!result) { - let menuId: MenuId; - if (historyItem.historyItemGroup.direction === 'incoming') { - menuId = historyItem.type === 'allChanges' ? - MenuId.SCMIncomingChangesAllChangesContext : - MenuId.SCMIncomingChangesHistoryItemContext; - } else { - menuId = historyItem.type === 'allChanges' ? - MenuId.SCMOutgoingChangesAllChangesContext : - MenuId.SCMOutgoingChangesHistoryItemContext; - } - - const contextKeyService = this.contextKeyService.createOverlay([ - ['scmHistoryItemFileCount', historyItem.statistics?.files ?? 0], - ]); - - result = this.menuService.createMenu(menuId, contextKeyService); - this.historyItemMenus.set(historyItem, result); - } - - return result; - } + // private getOrCreateHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { + // let result = this.historyItemMenus.get(historyItem); + + // if (!result) { + // let menuId: MenuId; + // if (historyItem.historyItemGroup.direction === 'incoming') { + // menuId = historyItem.type === 'allChanges' ? + // MenuId.SCMIncomingChangesAllChangesContext : + // MenuId.SCMIncomingChangesHistoryItemContext; + // } else { + // menuId = historyItem.type === 'allChanges' ? + // MenuId.SCMOutgoingChangesAllChangesContext : + // MenuId.SCMOutgoingChangesHistoryItemContext; + // } + + // const contextKeyService = this.contextKeyService.createOverlay([ + // ['scmHistoryItemFileCount', historyItem.statistics?.files ?? 0], + // ]); + + // result = this.menuService.createMenu(menuId, contextKeyService); + // this.historyItemMenus.set(historyItem, result); + // } + + // return result; + // } private getOutgoingHistoryItemGroupMenu(menuId: MenuId, historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { const contextKeyService = this.contextKeyService.createOverlay([ diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 0796247d73979..b04108c9fc9e7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -105,9 +105,8 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; import { clamp } from 'vs/base/common/numbers'; import { ILogService } from 'vs/platform/log/common/log'; -import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; @@ -878,7 +877,7 @@ class HistoryItemActionRunner extends ActionRunner { } const args: (ISCMProvider | ISCMHistoryItem)[] = []; - args.push(context.historyItemGroup.repository.provider); + args.push(context.repository.provider); args.push({ id: context.id, parentIds: context.parentIds, @@ -894,14 +893,15 @@ class HistoryItemActionRunner extends ActionRunner { } interface HistoryItemTemplate { - readonly iconContainer: HTMLElement; + // readonly iconContainer: HTMLElement; readonly label: IconLabel; - readonly statsContainer: HTMLElement; - readonly statsCustomHover: IUpdatableHover; - readonly filesLabel: HTMLElement; - readonly insertionsLabel: HTMLElement; - readonly deletionsLabel: HTMLElement; - readonly actionBar: ActionBar; + readonly graphContainer: SVGElement; + // readonly statsContainer: HTMLElement; + // readonly statsCustomHover: IUpdatableHover; + // readonly filesLabel: HTMLElement; + // readonly insertionsLabel: HTMLElement; + // readonly deletionsLabel: HTMLElement; + // readonly actionBar: ActionBar; readonly elementDisposables: DisposableStore; readonly disposables: IDisposable; } @@ -912,65 +912,124 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { const historyItem = node.element; - templateData.iconContainer.className = 'icon-container'; - if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { - templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); - } + // templateData.iconContainer.className = 'icon-container'; + // if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { + // templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); + // } + + + // const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + // line.setAttribute('x1', '11'); + // line.setAttribute('y1', '0'); + // line.setAttribute('x2', '11'); + // line.setAttribute('y2', '22'); + // line.setAttribute('stroke', 'black'); + // line.setAttribute('stroke-width', '1'); + // templateData.graphContainer.append(line); + + // templateData.graphContainer.style.width = '22px'; + this.renderGraph(templateData.graphContainer, historyItem,); const title = this.getTooltip(historyItem); - const [matches, descriptionMatches] = this.processMatches(historyItem, node.filterData); - templateData.label.setLabel(historyItem.message, historyItem.author, { matches, descriptionMatches, title }); + // const [matches, descriptionMatches] = this.processMatches(historyItem, node.filterData); + templateData.label.setLabel(historyItem.message, undefined, { title }); - templateData.actionBar.clear(); - templateData.actionBar.context = historyItem; + // templateData.actionBar.clear(); + // templateData.actionBar.context = historyItem; - const menus = this.scmViewService.menus.getRepositoryMenus(historyItem.historyItemGroup.repository.provider); - if (menus.historyProviderMenu) { - const historyItemMenu = menus.historyProviderMenu.getHistoryItemMenu(historyItem); - templateData.elementDisposables.add(connectPrimaryMenuToInlineActionBar(historyItemMenu, templateData.actionBar)); - } + // const menus = this.scmViewService.menus.getRepositoryMenus(historyItem.historyItemGroup.repository.provider); + // if (menus.historyProviderMenu) { + // const historyItemMenu = menus.historyProviderMenu.getHistoryItemMenu(historyItem); + // templateData.elementDisposables.add(connectPrimaryMenuToInlineActionBar(historyItemMenu, templateData.actionBar)); + // } - this.renderStatistics(node, index, templateData, height); + // this.renderStatistics(node, index, templateData, height); } renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } + private renderGraph(graphContainer: SVGElement, historyItem: SCMHistoryItemTreeElement): void { + graphContainer.textContent = ''; + + const swimlaneIndex = historyItem.graphSwimlanes.indexOf(historyItem.id); + const circleSwimlane = (swimlaneIndex === -1 ? historyItem.graphSwimlanes.length : swimlaneIndex) + 1; + + // Circle + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', `${11 * circleSwimlane}`); + circle.setAttribute('cy', '11'); + circle.setAttribute('r', '4'); + graphContainer.append(circle); + + // Line(s) + for (let index = 0; index < historyItem.graphSwimlanes.length; index++) { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', `M${11 * (index + 1)} 0 V22`); + path.style.stroke = 'black'; + path.style.strokeWidth = '1px'; + + graphContainer.append(path); + } + + // New swimlane + if (swimlaneIndex === -1) { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', `M${11 * (historyItem.graphSwimlanes.length + 1)} 11 V22`); + path.style.stroke = 'black'; + path.style.strokeWidth = '1px'; + + graphContainer.append(path); + } + + // Container width + const containerWidth = swimlaneIndex === -1 ? + 22 * (historyItem.graphSwimlanes.length + 1) : + 22 * historyItem.graphSwimlanes.length; + + graphContainer.style.width = `${containerWidth}px`; + } + private getTooltip(historyItem: SCMHistoryItemTreeElement): IUpdatableHoverTooltipMarkdownString { const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); @@ -988,46 +1047,46 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { - const historyItem = node.element; + // private renderStatistics(node: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + // const historyItem = node.element; - if (historyItem.statistics) { - const statsAriaLabel: string[] = [ - historyItem.statistics.files === 1 ? - localize('fileChanged', "{0} file changed", historyItem.statistics.files) : - localize('filesChanged', "{0} files changed", historyItem.statistics.files), - historyItem.statistics.insertions === 1 ? localize('insertion', "{0} insertion{1}", historyItem.statistics.insertions, '(+)') : - historyItem.statistics.insertions > 1 ? localize('insertions', "{0} insertions{1}", historyItem.statistics.insertions, '(+)') : '', - historyItem.statistics.deletions === 1 ? localize('deletion', "{0} deletion{1}", historyItem.statistics.deletions, '(-)') : - historyItem.statistics.deletions > 1 ? localize('deletions', "{0} deletions{1}", historyItem.statistics.deletions, '(-)') : '' - ]; + // if (historyItem.statistics) { + // const statsAriaLabel: string[] = [ + // historyItem.statistics.files === 1 ? + // localize('fileChanged', "{0} file changed", historyItem.statistics.files) : + // localize('filesChanged', "{0} files changed", historyItem.statistics.files), + // historyItem.statistics.insertions === 1 ? localize('insertion', "{0} insertion{1}", historyItem.statistics.insertions, '(+)') : + // historyItem.statistics.insertions > 1 ? localize('insertions', "{0} insertions{1}", historyItem.statistics.insertions, '(+)') : '', + // historyItem.statistics.deletions === 1 ? localize('deletion', "{0} deletion{1}", historyItem.statistics.deletions, '(-)') : + // historyItem.statistics.deletions > 1 ? localize('deletions', "{0} deletions{1}", historyItem.statistics.deletions, '(-)') : '' + // ]; - const statsTitle = statsAriaLabel.filter(l => l !== '').join(', '); - templateData.statsContainer.setAttribute('aria-label', statsTitle); - templateData.statsCustomHover.update(statsTitle); + // const statsTitle = statsAriaLabel.filter(l => l !== '').join(', '); + // templateData.statsContainer.setAttribute('aria-label', statsTitle); + // templateData.statsCustomHover.update(statsTitle); - templateData.filesLabel.textContent = historyItem.statistics.files.toString(); + // templateData.filesLabel.textContent = historyItem.statistics.files.toString(); - templateData.insertionsLabel.textContent = historyItem.statistics.insertions > 0 ? `+${historyItem.statistics.insertions}` : ''; - templateData.insertionsLabel.classList.toggle('hidden', historyItem.statistics.insertions === 0); + // templateData.insertionsLabel.textContent = historyItem.statistics.insertions > 0 ? `+${historyItem.statistics.insertions}` : ''; + // templateData.insertionsLabel.classList.toggle('hidden', historyItem.statistics.insertions === 0); - templateData.deletionsLabel.textContent = historyItem.statistics.deletions > 0 ? `-${historyItem.statistics.deletions}` : ''; - templateData.deletionsLabel.classList.toggle('hidden', historyItem.statistics.deletions === 0); - } + // templateData.deletionsLabel.textContent = historyItem.statistics.deletions > 0 ? `-${historyItem.statistics.deletions}` : ''; + // templateData.deletionsLabel.classList.toggle('hidden', historyItem.statistics.deletions === 0); + // } - templateData.statsContainer.classList.toggle('hidden', historyItem.statistics === undefined); - } + // templateData.statsContainer.classList.toggle('hidden', historyItem.statistics === undefined); + // } disposeElement(element: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { templateData.elementDisposables.clear(); @@ -1258,11 +1317,7 @@ export class SCMTreeSorter implements ITreeSorter { } if (isSCMHistoryItemTreeElement(one)) { - if (!isSCMHistoryItemTreeElement(other)) { - throw new Error('Invalid comparison'); - } - - return 0; + return isSCMHistoryItemTreeElement(other) ? 0 : 1; } if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) { @@ -1396,19 +1451,18 @@ function getSCMResourceId(element: TreeElement): string { const provider = element.repository.provider; return `historyItemGroup:${provider.id}/${element.id}`; } else if (isSCMHistoryItemTreeElement(element)) { - const historyItemGroup = element.historyItemGroup; - const provider = historyItemGroup.repository.provider; - return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}/${element.parentIds.join(',')}`; - } else if (isSCMHistoryItemChangeTreeElement(element)) { - const historyItem = element.historyItem; - const historyItemGroup = historyItem.historyItemGroup; - const provider = historyItemGroup.repository.provider; - return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; - } else if (isSCMHistoryItemChangeNode(element)) { - const historyItem = element.context; - const historyItemGroup = historyItem.historyItemGroup; - const provider = historyItemGroup.repository.provider; - return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; + const provider = element.repository.provider; + return `historyItem:${provider.id}/${element.id}/${element.parentIds.join(',')}`; + // } else if (isSCMHistoryItemChangeTreeElement(element)) { + // const historyItem = element.historyItem; + // const historyItemGroup = historyItem.historyItemGroup; + // const provider = historyItemGroup.repository.provider; + // return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; + // } else if (isSCMHistoryItemChangeNode(element)) { + // const historyItem = element.context; + // const historyItemGroup = historyItem.historyItemGroup; + // const provider = historyItemGroup.repository.provider; + // return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; } else if (isSCMViewSeparator(element)) { const provider = element.repository.provider; return `separator:${provider.id}`; @@ -3021,7 +3075,7 @@ export class SCMViewPane extends ViewPane { this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), resourceActionRunner), this.instantiationService.createInstance(HistoryItemGroupRenderer, historyItemGroupActionRunner), - this.instantiationService.createInstance(HistoryItemRenderer, historyItemActionRunner, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(HistoryItemRenderer, /*historyItemActionRunner, getActionViewItemProvider(this.instantiationService)*/), this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), this.instantiationService.createInstance(SeparatorRenderer) ], @@ -3147,17 +3201,17 @@ export class SCMViewPane extends ViewPane { this.scmViewService.focus(e.element.repository); return; } else if (isSCMHistoryItemTreeElement(e.element)) { - this.scmViewService.focus(e.element.historyItemGroup.repository); + // this.scmViewService.focus(e.element.historyItemGroup.repository); return; } else if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); } - this.scmViewService.focus(e.element.historyItem.historyItemGroup.repository); + // this.scmViewService.focus(e.element.historyItem.historyItemGroup.repository); return; } else if (isSCMHistoryItemChangeNode(e.element)) { - this.scmViewService.focus(e.element.context.historyItemGroup.repository); + // this.scmViewService.focus(e.element.context.historyItemGroup.repository); return; } } @@ -3320,12 +3374,12 @@ export class SCMViewPane extends ViewPane { createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, actions); } } else if (isSCMHistoryItemTreeElement(element)) { - const menus = this.scmViewService.menus.getRepositoryMenus(element.historyItemGroup.repository.provider); - const menu = menus.historyProviderMenu?.getHistoryItemMenu(element); - if (menu) { - actionRunner = new HistoryItemActionRunner(); - actions = collectContextMenuActions(menu); - } + // const menus = this.scmViewService.menus.getRepositoryMenus(element.historyItemGroup.repository.provider); + // const menu = menus.historyProviderMenu?.getHistoryItemMenu(element); + // if (menu) { + // actionRunner = new HistoryItemActionRunner(); + // actions = collectContextMenuActions(menu); + // } } actionRunner.onWillRun(() => this.tree.domFocus()); @@ -3529,7 +3583,7 @@ class SCMTreeDataSource implements IAsyncDataSource 0) { - let label = localize('syncSeparatorHeader', "Incoming/Outgoing"); - let ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + // History items + const historyItems = await this.getHistoryItems(inputOrElement); + children.push(...historyItems); - const incomingHistoryItems = historyItemGroups.find(g => g.direction === 'incoming'); - const outgoingHistoryItems = historyItemGroups.find(g => g.direction === 'outgoing'); + // // History item groups + // const historyItemGroups = await this.getHistoryItemGroups(inputOrElement); - if (incomingHistoryItems && !outgoingHistoryItems) { - label = localize('syncIncomingSeparatorHeader', "Incoming"); - ariaLabel = localize('syncIncomingSeparatorHeaderAriaLabel', "Incoming changes"); - } else if (!incomingHistoryItems && outgoingHistoryItems) { - label = localize('syncOutgoingSeparatorHeader', "Outgoing"); - ariaLabel = localize('syncOutgoingSeparatorHeaderAriaLabel', "Outgoing changes"); - } + // // Incoming/Outgoing Separator + // if (historyItemGroups.length > 0) { + // let label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + // let ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); - children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); - } + // const incomingHistoryItems = historyItemGroups.find(g => g.direction === 'incoming'); + // const outgoingHistoryItems = historyItemGroups.find(g => g.direction === 'outgoing'); - children.push(...historyItemGroups); + // if (incomingHistoryItems && !outgoingHistoryItems) { + // label = localize('syncIncomingSeparatorHeader', "Incoming"); + // ariaLabel = localize('syncIncomingSeparatorHeaderAriaLabel', "Incoming changes"); + // } else if (!incomingHistoryItems && outgoingHistoryItems) { + // label = localize('syncOutgoingSeparatorHeader', "Outgoing"); + // ariaLabel = localize('syncOutgoingSeparatorHeaderAriaLabel', "Outgoing changes"); + // } + + // children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + // } + + // children.push(...historyItemGroups); return children; } else if (isSCMResourceGroup(inputOrElement)) { @@ -3650,188 +3716,216 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); - - const scmProvider = element.provider; - const historyProvider = scmProvider.historyProvider; - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - - if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { + // private async getHistoryItemGroups(element: ISCMRepository): Promise { + // const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); + + // const scmProvider = element.provider; + // const historyProvider = scmProvider.historyProvider; + // const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + + // if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { + // return []; + // } + + // const children: SCMHistoryItemGroupTreeElement[] = []; + // const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); + + // let incomingHistoryItemGroup = historyProviderCacheEntry?.incomingHistoryItemGroup; + // let outgoingHistoryItemGroup = historyProviderCacheEntry?.outgoingHistoryItemGroup; + + // if (!incomingHistoryItemGroup && !outgoingHistoryItemGroup) { + // // Common ancestor, ahead, behind + // const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base?.id); + // if (!ancestor) { + // return []; + // } + + // // Only show "Incoming" node if there is a base branch + // incomingHistoryItemGroup = currentHistoryItemGroup.base ? { + // id: currentHistoryItemGroup.base.id, + // label: currentHistoryItemGroup.base.name, + // ariaLabel: localize('incomingChangesAriaLabel', "Incoming changes from {0}", currentHistoryItemGroup.base.name), + // icon: Codicon.arrowCircleDown, + // direction: 'incoming', + // ancestor: ancestor.id, + // count: ancestor.behind, + // repository: element, + // type: 'historyItemGroup' + // } : undefined; + + // outgoingHistoryItemGroup = { + // id: currentHistoryItemGroup.id, + // label: currentHistoryItemGroup.name, + // ariaLabel: localize('outgoingChangesAriaLabel', "Outgoing changes to {0}", currentHistoryItemGroup.name), + // icon: Codicon.arrowCircleUp, + // direction: 'outgoing', + // ancestor: ancestor.id, + // count: ancestor.ahead, + // repository: element, + // type: 'historyItemGroup' + // }; + + // this.historyProviderCache.set(element, { + // ...historyProviderCacheEntry, + // incomingHistoryItemGroup, + // outgoingHistoryItemGroup + // }); + // } + + // // Incoming + // if (incomingHistoryItemGroup && + // (showIncomingChanges === 'always' || + // (showIncomingChanges === 'auto' && (incomingHistoryItemGroup.count ?? 0) > 0))) { + // children.push(incomingHistoryItemGroup); + // } + + // // Outgoing + // if (outgoingHistoryItemGroup && + // (showOutgoingChanges === 'always' || + // (showOutgoingChanges === 'auto' && (outgoingHistoryItemGroup.count ?? 0) > 0))) { + // children.push(outgoingHistoryItemGroup); + // } + + // return children; + // } + + private async getHistoryItems(element: ISCMRepository): Promise { + const historyProvider = element.provider.historyProvider; + + if (!historyProvider?.currentHistoryItemGroup) { return []; } - const children: SCMHistoryItemGroupTreeElement[] = []; const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); - - let incomingHistoryItemGroup = historyProviderCacheEntry?.incomingHistoryItemGroup; - let outgoingHistoryItemGroup = historyProviderCacheEntry?.outgoingHistoryItemGroup; - - if (!incomingHistoryItemGroup && !outgoingHistoryItemGroup) { - // Common ancestor, ahead, behind - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base?.id); - if (!ancestor) { - return []; - } - - // Only show "Incoming" node if there is a base branch - incomingHistoryItemGroup = currentHistoryItemGroup.base ? { - id: currentHistoryItemGroup.base.id, - label: currentHistoryItemGroup.base.name, - ariaLabel: localize('incomingChangesAriaLabel', "Incoming changes from {0}", currentHistoryItemGroup.base.name), - icon: Codicon.arrowCircleDown, - direction: 'incoming', - ancestor: ancestor.id, - count: ancestor.behind, - repository: element, - type: 'historyItemGroup' - } : undefined; - - outgoingHistoryItemGroup = { - id: currentHistoryItemGroup.id, - label: currentHistoryItemGroup.name, - ariaLabel: localize('outgoingChangesAriaLabel', "Outgoing changes to {0}", currentHistoryItemGroup.name), - icon: Codicon.arrowCircleUp, - direction: 'outgoing', - ancestor: ancestor.id, - count: ancestor.ahead, - repository: element, - type: 'historyItemGroup' - }; - - this.historyProviderCache.set(element, { - ...historyProviderCacheEntry, - incomingHistoryItemGroup, - outgoingHistoryItemGroup - }); - } - - // Incoming - if (incomingHistoryItemGroup && - (showIncomingChanges === 'always' || - (showIncomingChanges === 'auto' && (incomingHistoryItemGroup.count ?? 0) > 0))) { - children.push(incomingHistoryItemGroup); - } - - // Outgoing - if (outgoingHistoryItemGroup && - (showOutgoingChanges === 'always' || - (showOutgoingChanges === 'auto' && (outgoingHistoryItemGroup.count ?? 0) > 0))) { - children.push(outgoingHistoryItemGroup); - } - - return children; - } - - private async getHistoryItems(element: SCMHistoryItemGroupTreeElement): Promise { - const repository = element.repository; - const historyProvider = repository.provider.historyProvider; - - if (!historyProvider) { - return []; - } - - const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); const historyItemsMap = historyProviderCacheEntry.historyItems; let historyItemsElement = historyProviderCacheEntry.historyItems.get(element.id); if (!historyItemsElement) { - const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? []; + const historyItems = await historyProvider.provideHistoryItems(historyProvider.currentHistoryItemGroup.id, { limit: 32 }) ?? []; // All Changes - const { showChangesSummary } = this.getConfiguration(); - const allChanges = showChangesSummary && historyItems.length >= 2 ? - await historyProvider.provideHistoryItemSummary(historyItems[0].id, element.ancestor) : undefined; + // const { showChangesSummary } = this.getConfiguration(); + // const allChanges = showChangesSummary && historyItems.length >= 2 ? + // await historyProvider.provideHistoryItemSummary(historyItems[0].id, element.ancestor) : undefined; - historyItemsElement = [allChanges, historyItems]; + historyItemsElement = [undefined, historyItems]; - this.historyProviderCache.set(repository, { + this.historyProviderCache.set(element, { ...historyProviderCacheEntry, historyItems: historyItemsMap.set(element.id, historyItemsElement) }); } const children: SCMHistoryItemTreeElement[] = []; - if (historyItemsElement[0]) { - children.push({ - ...historyItemsElement[0], - icon: historyItemsElement[0].icon ?? Codicon.files, - message: localize('allChanges', "All Changes"), - historyItemGroup: element, - type: 'allChanges' - } satisfies SCMHistoryItemTreeElement); - } - - children.push(...historyItemsElement[1] - .map(historyItem => ({ - ...historyItem, - historyItemGroup: element, - type: 'historyItem' - } satisfies SCMHistoryItemTreeElement))); - - return children; - } - - private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { - const repository = element.historyItemGroup.repository; - const historyProvider = repository.provider.historyProvider; - - if (!historyProvider) { - return []; - } - - const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); - const historyItemChangesMap = historyProviderCacheEntry.historyItemChanges; - - const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; - let historyItemChanges = historyItemChangesMap.get(`${element.id}/${historyItemParentId}`); - - if (!historyItemChanges) { - const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; - historyItemChanges = await historyProvider.provideHistoryItemChanges(element.id, historyItemParentId) ?? []; - - this.historyProviderCache.set(repository, { - ...historyProviderCacheEntry, - historyItemChanges: historyItemChangesMap.set(`${element.id}/${historyItemParentId}`, historyItemChanges) - }); - } - - if (this.viewMode() === ViewMode.List) { - // List - return historyItemChanges.map(change => ({ - ...change, - historyItem: element, - type: 'historyItemChange' - })); - } - - // Tree - const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); - for (const change of historyItemChanges) { - tree.add(change.uri, { - ...change, - historyItem: element, - type: 'historyItemChange' - }); - } + // if (historyItemsElement[0]) { + // children.push({ + // ...historyItemsElement[0], + // icon: historyItemsElement[0].icon ?? Codicon.files, + // message: localize('allChanges', "All Changes"), + // historyItemGroup: element, + // type: 'allChanges' + // } satisfies SCMHistoryItemTreeElement); + // } + + // Process each commit and add graph information + const graphSwimlanes: string[] = []; + for (let index = 0; index < historyItemsElement[1].length; index++) { + const historyItem = historyItemsElement[1][index]; + const swimlaneIndex = graphSwimlanes.indexOf(historyItem.id); + + if (swimlaneIndex === -1) { + // New swimlane + children.push({ + ...historyItem, + graphSwimlanes: [...graphSwimlanes], + repository: element, + type: 'historyItem' + } satisfies SCMHistoryItemTreeElement); - const children: (SCMHistoryItemChangeTreeElement | IResourceNode)[] = []; - for (const node of tree.root.children) { - children.push(node.element ?? node); + graphSwimlanes.push(...historyItem.parentIds); + } else { + // Existing swimlane + children.push({ + ...historyItem, + graphSwimlanes: [...graphSwimlanes], + repository: element, + type: 'historyItem' + } satisfies SCMHistoryItemTreeElement); + + // Update swimlane + for (let index = 0; index < historyItem.parentIds.length; index++) { + if (index === 0) { + graphSwimlanes.splice(swimlaneIndex, 1, historyItem.parentIds[index]); + } else { + graphSwimlanes.push(historyItem.parentIds[index]); + } + } + } } return children; } + // private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { + // const repository = element.historyItemGroup.repository; + // const historyProvider = repository.provider.historyProvider; + + // if (!historyProvider) { + // return []; + // } + + // const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); + // const historyItemChangesMap = historyProviderCacheEntry.historyItemChanges; + + // const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; + // let historyItemChanges = historyItemChangesMap.get(`${element.id}/${historyItemParentId}`); + + // if (!historyItemChanges) { + // const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; + // historyItemChanges = await historyProvider.provideHistoryItemChanges(element.id, historyItemParentId) ?? []; + + // this.historyProviderCache.set(repository, { + // ...historyProviderCacheEntry, + // historyItemChanges: historyItemChangesMap.set(`${element.id}/${historyItemParentId}`, historyItemChanges) + // }); + // } + + // if (this.viewMode() === ViewMode.List) { + // // List + // return historyItemChanges.map(change => ({ + // ...change, + // historyItem: element, + // type: 'historyItemChange' + // })); + // } + + // // Tree + // const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); + // for (const change of historyItemChanges) { + // tree.add(change.uri, { + // ...change, + // historyItem: element, + // type: 'historyItemChange' + // }); + // } + + // const children: (SCMHistoryItemChangeTreeElement | IResourceNode)[] = []; + // for (const node of tree.root.children) { + // children.push(node.element ?? node); + // } + + // return children; + // } + getParent(element: TreeElement): ISCMViewService | TreeElement { if (isSCMResourceNode(element)) { if (element.parent === element.context.resourceTree.root) { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 2cb81effd9130..19f34a654977c 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -13,7 +13,7 @@ export interface ISCMHistoryProviderMenus { getHistoryItemGroupMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu; getHistoryItemGroupContextMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu; - getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu; + // getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu; } export interface ISCMHistoryProvider { @@ -77,7 +77,8 @@ export interface ISCMHistoryItem { } export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { - readonly historyItemGroup: SCMHistoryItemGroupTreeElement; + readonly repository: ISCMRepository; + readonly graphSwimlanes: string[]; readonly type: 'allChanges' | 'historyItem'; } From 5bb78905994cc879082884bc21ead3693b820cfa Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 14 May 2024 13:41:11 +0200 Subject: [PATCH 02/33] More progress --- extensions/git/src/historyProvider.ts | 15 +++- .../contrib/scm/browser/scmViewPane.ts | 85 +++++++++++++------ 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index ee098f3925fac..821cb7d248c8d 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -77,7 +77,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.logger.trace(`GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup (${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); } - async provideHistoryItems(_: string, __: SourceControlHistoryOptions): Promise { + async provideHistoryItems(_: string, options: SourceControlHistoryOptions): Promise { //TODO@lszomoru - support limit and cursor // if (typeof options.limit === 'number') { // throw new Error('Unsupported options.'); @@ -89,11 +89,20 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // const refParentId = options.limit.id; // const refId = await this.repository.revParse(historyItemGroupId) ?? ''; - const historyItems: SourceControlHistoryItem[] = []; - const commits = await this.repository.log({ refNames: ['main', 'origin/main'] }); + const refNames = new Set(['main', 'origin/main']); + if (this.currentHistoryItemGroup?.name) { + refNames.add(this.currentHistoryItemGroup.name); + } + if (this.currentHistoryItemGroup?.base?.name) { + refNames.add(this.currentHistoryItemGroup.base.name); + } + + const maxEntries = typeof options.limit === 'number' ? options.limit : 32; + const commits = await this.repository.log({ refNames: Array.from(refNames), maxEntries }); await ensureEmojis(); + const historyItems: SourceControlHistoryItem[] = []; historyItems.push(...commits.map(commit => { const newLineIndex = commit.message.indexOf('\n'); const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 0c50dd4f962a3..82712454cb09d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -957,19 +957,28 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer h === historyItem.id); if (swimlaneIndex === -1) { // New swimlane @@ -3806,24 +3824,35 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Tue, 14 May 2024 15:47:45 +0200 Subject: [PATCH 03/33] More progress --- extensions/git/src/historyProvider.ts | 18 ++ .../contrib/scm/browser/media/scm.css | 21 +++ .../contrib/scm/browser/scmViewPane.ts | 156 ++++++++++++------ .../workbench/contrib/scm/common/history.ts | 1 + .../vscode.proposed.scmHistoryProvider.d.ts | 1 + 5 files changed, 150 insertions(+), 47 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 821cb7d248c8d..d114b84b4ae32 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -107,6 +107,23 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const newLineIndex = commit.message.indexOf('\n'); const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + const labels: string[] = []; + for (const label of commit.refNames) { + if (label === 'origin/HEAD') { + continue; + } + + if (label !== '') { + if (label.startsWith('HEAD -> ')) { + labels.push('HEAD'); + labels.push(label.substring(8)); + continue; + } + + labels.push(label); + } + } + return { id: commit.hash, parentIds: commit.parents, @@ -115,6 +132,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec icon: new ThemeIcon('git-commit'), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, + labels: labels.length !== 0 ? labels : undefined }; })); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 2bef117d02822..2de5f000c6a72 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -138,6 +138,27 @@ flex-shrink: 0; } +.scm-view .monaco-list-row .history-item > .label-container { + display: flex; + flex-shrink: 0; + gap: 4px; +} + +.scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label { + opacity: 0.75; +} + +.scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label .monaco-highlighted-label { + display: block; + height: 22px; +} + +.scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label .monaco-highlighted-label .codicon { + border: 1px solid var(--vscode-scm-historyItemStatisticsBorder); + border-radius: 2px; + padding: 2px +} + .scm-view .monaco-list-row .history-item .stats-container { display: flex; font-size: 11px; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 82712454cb09d..d17f1593e11de 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -860,6 +860,7 @@ interface HistoryItemTemplate { // readonly iconContainer: HTMLElement; readonly label: IconLabel; readonly graphContainer: SVGElement; + readonly labelContainer: HTMLElement; // readonly statsContainer: HTMLElement; // readonly statsCustomHover: IUpdatableHover; // readonly filesLabel: HTMLElement; @@ -896,6 +897,9 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { @@ -920,23 +924,31 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer 1) { + d.push(`M${11 * (index + 1)} 11`); + d.push(`A 11 11 0 0 1 ${11 * (index + 2)} 22`); + } + } + path.setAttribute('d', d.join(' ')); graphContainer.append(path); } + // Circle + if (swimlaneIndex !== -1) { + const circle = this.createCircle(swimlaneIndex, 4, '#f8f8f8', 'black'); + graphContainer.append(circle); + } + // New swimlane if (swimlaneIndex === -1) { - // Circle - const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - circle.setAttribute('cx', `${11 * (historyItem.graphSwimlanes.length + 1)}`); - circle.setAttribute('cy', '11'); - circle.setAttribute('r', '4'); - graphContainer.append(circle); + // Draw | + const path = this.createPath(); - // Line(s) - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', `M${11 * (historyItem.graphSwimlanes.length + 1)} 11 V22`); - path.style.fill = 'none'; - path.style.stroke = 'black'; - path.style.strokeWidth = '1px'; + const d: string[] = []; + d.push(`M${11 * (historyItem.graphSwimlanes.length + 1)} 11`); + d.push(`V 22`); + path.setAttribute('d', d.join(' ')); graphContainer.append(path); + + // Merge commit - draw \ + if (historyItem.parentIds.length > 1) { + const path = this.createPath(); + + const d: string[] = []; + d.push(`M${11 * (historyItem.graphSwimlanes.length + 1)} 11`); + d.push(`A 11 11 0 0 1 ${11 * (historyItem.graphSwimlanes.length + 2)} 22`); + + path.setAttribute('d', d.join(' ')); + graphContainer.append(path); + } + + // Draw * + if (historyItem.parentIds.length === 1) { + const circle = this.createCircle(historyItem.graphSwimlanes.length, 4, '#f8f8f8', 'black'); + graphContainer.append(circle); + } else { + const circleOuter = this.createCircle(historyItem.graphSwimlanes.length, 5, '#f8f8f8', 'black'); + graphContainer.append(circleOuter); + + const circleInner = this.createCircle(historyItem.graphSwimlanes.length, 3, '#f8f8f8', 'black'); + graphContainer.append(circleInner); + } } // Container width - const containerWidth = swimlaneIndex === -1 ? - 11 * (historyItem.graphSwimlanes.length + 2) : - 11 * (historyItem.graphSwimlanes.length + 1); + const containerWidth = swimlaneIndex === -1 + ? 11 * (historyItem.graphSwimlanes.length + 1 + historyItem.parentIds.length) + : 11 * (historyItem.graphSwimlanes.length + historyItem.parentIds.length); graphContainer.style.width = `${containerWidth}px`; } + private createCircle(index: number, radius: number, stroke: string, fill: string): SVGCircleElement { + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', `${11 * (index + 1)}`); + circle.setAttribute('cy', '11'); + circle.setAttribute('r', `${radius}`); + circle.setAttribute('fill', fill); + circle.setAttribute('stroke', stroke); + + return circle; + } + + private createPath(color: string = 'black'): SVGPathElement { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'none'); + path.setAttribute('stroke', color); + path.setAttribute('stroke-width', '1px'); + path.setAttribute('stroke-linecap', 'round'); + + return path; + } + private getTooltip(historyItem: SCMHistoryItemTreeElement): IUpdatableHoverTooltipMarkdownString { const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); @@ -3783,7 +3840,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 15 May 2024 13:46:47 +0200 Subject: [PATCH 04/33] Simplified algo, basic support for colors --- .../contrib/scm/browser/scmViewPane.ts | 149 +++++++++--------- .../workbench/contrib/scm/common/history.ts | 8 +- 2 files changed, 84 insertions(+), 73 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d17f1593e11de..b105dd87ed519 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGraphNode, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -969,12 +969,15 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer n.id === historyItem.id); + + for (let index = 0; index < historyItem.graphNodes.length; index++) { + const node = historyItem.graphNodes[index]; + const path = this.createPath(node.color); + const d: string[] = []; - const path = this.createPath(); - if (historyItem.graphSwimlanes[index] === historyItem.id && index !== swimlaneIndex) { + if (node.id === historyItem.id && index !== swimlaneIndex) { // Draw / d.push(`M ${11 * ((index - swimlaneIndex) + 1)} 0`); d.push(`A 11 11 0 0 1 ${11 * ((index - swimlaneIndex))} 11`); @@ -983,69 +986,36 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer 1) { - d.push(`M${11 * (index + 1)} 11`); - d.push(`A 11 11 0 0 1 ${11 * (index + 2)} 22`); - } + // Draw \ + if (historyItem.parentIds.length > 1) { + d.push(`M ${11 * (index + 1)} 11`); + d.push(`A 11 11 0 0 1 ${11 * (index + 2)} 22`); } path.setAttribute('d', d.join(' ')); graphContainer.append(path); } - // Circle - if (swimlaneIndex !== -1) { - const circle = this.createCircle(swimlaneIndex, 4, '#f8f8f8', 'black'); + // Draw * + if (historyItem.parentIds.length === 1) { + // Commit + const circle = this.createCircle(swimlaneIndex, 4, '#f8f8f8', historyItem.graphNodes[swimlaneIndex].color); graphContainer.append(circle); - } - - // New swimlane - if (swimlaneIndex === -1) { - // Draw | - const path = this.createPath(); - - const d: string[] = []; - d.push(`M${11 * (historyItem.graphSwimlanes.length + 1)} 11`); - d.push(`V 22`); - - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); - - // Merge commit - draw \ - if (historyItem.parentIds.length > 1) { - const path = this.createPath(); - - const d: string[] = []; - d.push(`M${11 * (historyItem.graphSwimlanes.length + 1)} 11`); - d.push(`A 11 11 0 0 1 ${11 * (historyItem.graphSwimlanes.length + 2)} 22`); - - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); - } - - // Draw * - if (historyItem.parentIds.length === 1) { - const circle = this.createCircle(historyItem.graphSwimlanes.length, 4, '#f8f8f8', 'black'); - graphContainer.append(circle); - } else { - const circleOuter = this.createCircle(historyItem.graphSwimlanes.length, 5, '#f8f8f8', 'black'); - graphContainer.append(circleOuter); + } else { + // Merge commit + const circleOuter = this.createCircle(swimlaneIndex, 5, '#f8f8f8', historyItem.graphNodes[swimlaneIndex].color); + graphContainer.append(circleOuter); - const circleInner = this.createCircle(historyItem.graphSwimlanes.length, 3, '#f8f8f8', 'black'); - graphContainer.append(circleInner); - } + const circleInner = this.createCircle(swimlaneIndex, 3, '#f8f8f8', historyItem.graphNodes[swimlaneIndex].color); + graphContainer.append(circleInner); } // Container width - const containerWidth = swimlaneIndex === -1 - ? 11 * (historyItem.graphSwimlanes.length + 1 + historyItem.parentIds.length) - : 11 * (historyItem.graphSwimlanes.length + historyItem.parentIds.length); - - graphContainer.style.width = `${containerWidth}px`; + graphContainer.style.width = `${11 * (historyItem.graphNodes.length + historyItem.parentIds.length)}px`; } private createCircle(index: number, radius: number, stroke: string, fill: string): SVGCircleElement { @@ -1059,10 +1029,10 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer { + const color = colors[colorIndex]; + colorIndex = colorIndex < colors.length ? colorIndex + 1 : 0; + + return color; + }; + // Process each commit and add graph information - const graphSwimlanes: string[] = []; + const graphNodes: ISCMHistoryItemGraphNode[] = []; for (let index = 0; index < historyItemsElement[1].length; index++) { const historyItem = historyItemsElement[1][index]; - const swimlaneIndex = graphSwimlanes.findIndex(h => h === historyItem.id); + const swimlaneIndex = graphNodes.findIndex(n => n.id === historyItem.id); + // New swimlane if (swimlaneIndex === -1) { - // New swimlane + const color = getColor(); + + // Add root node + graphNodes.push({ + id: historyItem.id, + color, + isRoot: true + }); + children.push({ ...historyItem, - graphSwimlanes: [...graphSwimlanes], + graphNodes: [...graphNodes], repository: element, type: 'historyItem' } satisfies SCMHistoryItemTreeElement); - // Update graph with parent(s) - graphSwimlanes.push(...historyItem.parentIds); + // Update graph node with parent(s) + graphNodes.splice(graphNodes.length - 1, 1); + for (let i = 0; i < historyItem.parentIds.length; i++) { + graphNodes.push({ + id: historyItem.parentIds[i], + color: i === 0 ? color : getColor(), + isRoot: false + }); + } continue; } - // Update swimlane + // Existing swimlane children.push({ ...historyItem, - graphSwimlanes: [...graphSwimlanes], + graphNodes: [...graphNodes], repository: element, type: 'historyItem' } satisfies SCMHistoryItemTreeElement); - // Update graph with parent(s) + // Update graph node with parent(s) let i = 0; - while (i < graphSwimlanes.length) { - if (graphSwimlanes[i] === historyItem.id) { + while (i < graphNodes.length) { + if (graphNodes[i].id === historyItem.id) { if (i === swimlaneIndex) { // Update first occurrence - graphSwimlanes.splice(i, 1, historyItem.parentIds[0]); + graphNodes.splice(i, 1, { + id: historyItem.parentIds[0], + color: graphNodes[i].color, + isRoot: false + }); } else { + // TODO@lszomoru - curved lines // Delete all other occurrences - graphSwimlanes.splice(i, 1); + graphNodes.splice(i, 1); continue; } } @@ -3914,7 +3915,11 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 15 May 2024 15:19:31 +0200 Subject: [PATCH 05/33] Add the concept of secondary colors for merge commits --- .../contrib/scm/browser/scmViewPane.ts | 27 ++++++++++++++----- .../workbench/contrib/scm/common/history.ts | 1 + 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index b105dd87ed519..9c81e1a4c58cf 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -990,14 +990,20 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer 1) { + const path = this.createPath(node.secondaryColor ?? node.color); + + const d: string[] = []; d.push(`M ${11 * (index + 1)} 11`); d.push(`A 11 11 0 0 1 ${11 * (index + 2)} 22`); - } - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); + path.setAttribute('d', d.join(' ')); + graphContainer.append(path); + } } // Draw * @@ -3851,15 +3857,16 @@ class SCMTreeDataSource implements IAsyncDataSource n.id === historyItem.id); + const secondaryColor = historyItem.parentIds.length > 1 ? getColor() : undefined; // New swimlane if (swimlaneIndex === -1) { const color = getColor(); - // Add root node graphNodes.push({ id: historyItem.id, color, + secondaryColor, isRoot: true }); @@ -3875,7 +3882,9 @@ class SCMTreeDataSource implements IAsyncDataSource 1) { + // Add secondary color to the graph node + const node = { ...graphNodes[swimlaneIndex], secondaryColor }; + graphNodes.splice(swimlaneIndex, 1, node); + } + children.push({ ...historyItem, graphNodes: [...graphNodes], @@ -3917,7 +3932,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 15 May 2024 15:45:21 +0200 Subject: [PATCH 06/33] Further optimizations of the algo --- .../contrib/scm/browser/scmViewPane.ts | 94 ++++++++----------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 9c81e1a4c58cf..6520bd9651a3c 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3856,49 +3856,26 @@ class SCMTreeDataSource implements IAsyncDataSource n.id === historyItem.id); + let swimlaneIndex = graphNodes.findIndex(n => n.id === historyItem.id); + + const color = swimlaneIndex === -1 ? getColor() : graphNodes[swimlaneIndex].color; const secondaryColor = historyItem.parentIds.length > 1 ? getColor() : undefined; // New swimlane if (swimlaneIndex === -1) { - const color = getColor(); // Add root node - graphNodes.push({ - id: historyItem.id, - color, - secondaryColor, - isRoot: true - }); - - children.push({ - ...historyItem, - graphNodes: [...graphNodes], - repository: element, - type: 'historyItem' - } satisfies SCMHistoryItemTreeElement); - - // Update graph node with parent(s) - graphNodes.splice(graphNodes.length - 1, 1); - for (let i = 0; i < historyItem.parentIds.length; i++) { - graphNodes.push({ - id: historyItem.parentIds[i], - color: - i === 0 ? color : - i === 1 ? secondaryColor! : getColor(), - isRoot: false - }); - } + graphNodes.push({ id: historyItem.id, color, isRoot: true }); - continue; + swimlaneIndex = graphNodes.length - 1; } - // Existing swimlane + // Add secondary color to the node if (historyItem.parentIds.length > 1) { - // Add secondary color to the graph node const node = { ...graphNodes[swimlaneIndex], secondaryColor }; graphNodes.splice(swimlaneIndex, 1, node); } + // Add element children.push({ ...historyItem, graphNodes: [...graphNodes], @@ -3906,35 +3883,40 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 15 May 2024 21:14:16 +0200 Subject: [PATCH 07/33] Bug fixes to clean-up algo --- .../contrib/scm/browser/scmViewPane.ts | 79 +++++++++++++------ 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 6520bd9651a3c..dae8e0ee8d935 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -969,54 +969,83 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer n.id === historyItem.id); + const firstIndex = historyItem.graphNodes + .findIndex(node => node.id === historyItem.id); + const firstNode = historyItem.graphNodes[firstIndex]; for (let index = 0; index < historyItem.graphNodes.length; index++) { const node = historyItem.graphNodes[index]; - const path = this.createPath(node.color); - const d: string[] = []; - if (node.id === historyItem.id && index !== swimlaneIndex) { - // Draw / - d.push(`M ${11 * ((index - swimlaneIndex) + 1)} 0`); - d.push(`A 11 11 0 0 1 ${11 * ((index - swimlaneIndex))} 11`); + // Not the current commit + if (node.id !== historyItem.id) { + const d: string[] = []; + const path = this.createPath(node.color); - // Draw - - d.push(`H ${11 * (swimlaneIndex + 1)}`); - } else { // Draw | - d.push(`M ${11 * (index + 1)} ${node.isRoot ? 11 : 0}`); + d.push(`M ${11 * (index + 1)} 0`); d.push(`V 22`); - } - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); + path.setAttribute('d', d.join(' ')); + graphContainer.append(path); - // Draw \ - if (historyItem.parentIds.length > 1) { - const path = this.createPath(node.secondaryColor ?? node.color); + continue; + } + // Base commit + if (index !== firstIndex) { const d: string[] = []; - d.push(`M ${11 * (index + 1)} 11`); - d.push(`A 11 11 0 0 1 ${11 * (index + 2)} 22`); + const path = this.createPath(node.color); + + // Draw / + d.push(`M ${11 * ((index - firstIndex) + 1)} 0`); + d.push(`A 11 11 0 0 1 ${11 * ((index - firstIndex))} 11`); + + // Draw - + d.push(`H ${11 * (firstIndex + 1)}`); path.setAttribute('d', d.join(' ')); graphContainer.append(path); } } + // Merge commit - draw -\ + if (historyItem.parentIds.length > 1) { + const path = this.createPath(firstNode.secondaryColor ?? firstNode.color); + const d: string[] = []; + + // Draw \ + d.push(`M ${11 * historyItem.graphNodes.length} 11`); + d.push(`A 11 11 0 0 1 ${11 * (historyItem.graphNodes.length + 1)} 22`); + + // Draw - + d.push(`M ${11 * historyItem.graphNodes.length} 11`); + d.push(`H ${11 * (firstIndex + 1)}`); + + path.setAttribute('d', d.join(' ')); + graphContainer.append(path); + } + + const d: string[] = []; + const path = this.createPath(firstNode.color); + + // Draw | + d.push(`M ${11 * (firstIndex + 1)} ${firstNode.isRoot ? 11 : 0}`); + d.push(`V 22`); + + path.setAttribute('d', d.join(' ')); + graphContainer.append(path); + // Draw * if (historyItem.parentIds.length === 1) { // Commit - const circle = this.createCircle(swimlaneIndex, 4, '#f8f8f8', historyItem.graphNodes[swimlaneIndex].color); + const circle = this.createCircle(firstIndex, 4, '#f8f8f8', firstNode.color); graphContainer.append(circle); } else { // Merge commit - const circleOuter = this.createCircle(swimlaneIndex, 5, '#f8f8f8', historyItem.graphNodes[swimlaneIndex].color); + const circleOuter = this.createCircle(firstIndex, 5, '#f8f8f8', firstNode.color); graphContainer.append(circleOuter); - const circleInner = this.createCircle(swimlaneIndex, 3, '#f8f8f8', historyItem.graphNodes[swimlaneIndex].color); + const circleInner = this.createCircle(firstIndex, 3, '#f8f8f8', firstNode.color); graphContainer.append(circleInner); } @@ -3816,7 +3845,7 @@ class SCMTreeDataSource implements IAsyncDataSource { const color = colors[colorIndex]; - colorIndex = colorIndex < colors.length ? colorIndex + 1 : 0; + colorIndex = colorIndex < colors.length - 1 ? colorIndex + 1 : 1; return color; }; From f894106529cd2d1a20e13fabcf926b8da498c687 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 15 May 2024 22:07:23 +0200 Subject: [PATCH 08/33] Add support for curved branches --- .../contrib/scm/browser/scmViewPane.ts | 35 ++++++++++++++----- .../workbench/contrib/scm/common/history.ts | 1 + 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index dae8e0ee8d935..46ca8324d057f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -968,7 +968,6 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer node.id === historyItem.id); const firstNode = historyItem.graphNodes[firstIndex]; @@ -981,9 +980,16 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer previous + current.offset, 0); + graphContainer.style.width = `${11 * (historyItem.graphNodes.length + historyItem.parentIds.length + offset)}px`; } private createCircle(index: number, radius: number, stroke: string, fill: string): SVGCircleElement { @@ -3845,7 +3853,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 15 May 2024 22:30:01 +0200 Subject: [PATCH 09/33] Keep track of nodes that are moved so that the second parent for the merge commit is rendered in the correct swimlane --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 46ca8324d057f..5b323f1d043ea 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -968,6 +968,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer node.id === historyItem.id); const firstNode = historyItem.graphNodes[firstIndex]; @@ -1011,6 +1012,8 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer Date: Wed, 15 May 2024 22:59:13 +0200 Subject: [PATCH 10/33] Account for offset when drawing a base commit --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 5b323f1d043ea..d86d6aa4b2623 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -975,7 +975,6 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer Date: Thu, 16 May 2024 23:09:18 +0200 Subject: [PATCH 11/33] Algo rework completed --- extensions/git/src/git.ts | 4 +- .../contrib/scm/browser/media/scm.css | 6 +- .../contrib/scm/browser/scmViewPane.ts | 268 +++--------------- src/vs/workbench/contrib/scm/browser/util.ts | 6 +- .../workbench/contrib/scm/common/history.ts | 13 + src/vs/workbench/contrib/scm/common/scm.ts | 2 + .../contrib/scm/common/scmHistory.ts | 247 ++++++++++++++++ .../contrib/scm/common/scmService.ts | 3 + 8 files changed, 319 insertions(+), 230 deletions(-) create mode 100644 src/vs/workbench/contrib/scm/common/scmHistory.ts diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 532af7742cefe..cfa7531766a0d 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1167,7 +1167,9 @@ export class Repository { if (options?.refNames) { args.push('--topo-order'); - args.push(...options.refNames); + args.push('--branches=*'); + // args.push('--all'); + // args.push(...options.refNames); } if (options?.path) { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 2de5f000c6a72..3afcfef46ed48 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -133,9 +133,10 @@ align-items: center; } -.scm-view .monaco-list-row .history-item > .graph { +.scm-view .monaco-list-row .history-item > .graph-container { display: flex; flex-shrink: 0; + height: 22px; } .scm-view .monaco-list-row .history-item > .label-container { @@ -151,11 +152,14 @@ .scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label .monaco-highlighted-label { display: block; height: 22px; + /*padding: 1px 0;*/ } .scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label .monaco-highlighted-label .codicon { + font-size: 14px; border: 1px solid var(--vscode-scm-historyItemStatisticsBorder); border-radius: 2px; + margin: 1px 0; padding: 2px } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d86d6aa4b2623..a4567f58dcbba 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGraphNode, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemViewModel, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -24,7 +24,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner, Action, Separator, IActionRunner } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModel } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, Sequencer, ThrottledDelayer, Throttler } from 'vs/base/common/async'; @@ -108,6 +108,7 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; +import { renderSCMHistoryItemGraph } from 'vs/workbench/contrib/scm/common/scmHistory'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -120,6 +121,7 @@ type TreeElement = IResourceNode | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | + ISCMHistoryItemViewModel | SCMHistoryItemChangeTreeElement | IResourceNode | SCMViewSeparatorElement; @@ -859,7 +861,7 @@ class HistoryItemActionRunner extends ActionRunner { interface HistoryItemTemplate { // readonly iconContainer: HTMLElement; readonly label: IconLabel; - readonly graphContainer: SVGElement; + readonly graphContainer: HTMLElement; readonly labelContainer: HTMLElement; // readonly statsContainer: HTMLElement; // readonly statsCustomHover: IUpdatableHover; @@ -871,7 +873,7 @@ interface HistoryItemTemplate { readonly disposables: IDisposable; } -class HistoryItemRenderer implements ICompressibleTreeRenderer { +class HistoryItemRenderer implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'history-item'; get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; } @@ -888,11 +890,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { - const historyItem = node.element; + renderElement(node: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + const historyItemViewModel = node.element; + const historyItem = historyItemViewModel.historyItem; // templateData.iconContainer.className = 'icon-container'; // if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { // templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); // } - this.renderGraph(templateData.graphContainer, historyItem); + templateData.graphContainer.textContent = ''; + templateData.graphContainer.appendChild(renderSCMHistoryItemGraph(historyItemViewModel)); - const title = this.getTooltip(historyItem); + const title = this.getTooltip(historyItemViewModel); // const [matches, descriptionMatches] = this.processMatches(historyItem, node.filterData); templateData.label.setLabel(historyItem.message, undefined, { title }); @@ -961,130 +961,12 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, LabelFuzzyScore>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } - private renderGraph(graphContainer: SVGElement, historyItem: SCMHistoryItemTreeElement): void { - graphContainer.textContent = ''; - - let removedNodes = 0; - const firstIndex = historyItem.graphNodes - .findIndex(node => node.id === historyItem.id); - const firstNode = historyItem.graphNodes[firstIndex]; - - for (let index = 0; index < historyItem.graphNodes.length; index++) { - const node = historyItem.graphNodes[index]; - // Not the current commit - if (node.id !== historyItem.id) { - const d: string[] = []; - const path = this.createPath(node.color); - - if (node.offset === 0) { - // Draw | - d.push(`M ${11 * (index + 1)} 0`); - d.push(`V 22`); - } else { - // Draw / - d.push(`M ${11 * (index + node.offset + 1)} 0`); - d.push(`A 11 11 0 0 1 ${11 * (index + 1)} 11`); - d.push(`V 22`); - } - - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); - - continue; - } - - // Base commit - if (index !== firstIndex) { - const d: string[] = []; - const path = this.createPath(node.color); - - // Draw / - d.push(`M ${11 * ((index - firstIndex) + node.offset + 1)} 0`); - d.push(`A 11 11 0 0 1 ${11 * ((index - firstIndex) + node.offset)} 11`); - - // Draw - - d.push(`H ${11 * (firstIndex + node.offset + 1)}`); - - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); - - removedNodes++; - } - } - - // Merge commit - draw -\ - if (historyItem.parentIds.length > 1) { - const path = this.createPath(firstNode.secondaryColor ?? firstNode.color); - const d: string[] = []; - - // Draw \ - d.push(`M ${11 * (historyItem.graphNodes.length - removedNodes)} 11`); - d.push(`A 11 11 0 0 1 ${11 * ((historyItem.graphNodes.length - removedNodes) + 1)} 22`); - - // Draw - - d.push(`M ${11 * (historyItem.graphNodes.length - removedNodes)} 11`); - d.push(`H ${11 * (firstIndex + 1)}`); - - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); - } - - const d: string[] = []; - const path = this.createPath(firstNode.color); - - // Draw | - d.push(`M ${11 * (firstIndex + 1)} ${firstNode.isRoot ? 11 : 0}`); - d.push(`V 22`); - - path.setAttribute('d', d.join(' ')); - graphContainer.append(path); - - // Draw * - if (historyItem.parentIds.length === 1) { - // Commit - const circle = this.createCircle(firstIndex, 4, '#f8f8f8', firstNode.color); - graphContainer.append(circle); - } else { - // Merge commit - const circleOuter = this.createCircle(firstIndex, 5, '#f8f8f8', firstNode.color); - graphContainer.append(circleOuter); - - const circleInner = this.createCircle(firstIndex, 3, '#f8f8f8', firstNode.color); - graphContainer.append(circleInner); - } - - // Container width - const offset = historyItem.graphNodes - .reduce((previous, current) => previous + current.offset, 0); - graphContainer.style.width = `${11 * (historyItem.graphNodes.length + historyItem.parentIds.length + offset)}px`; - } - - private createCircle(index: number, radius: number, stroke: string, fill: string): SVGCircleElement { - const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - circle.setAttribute('cx', `${11 * (index + 1)}`); - circle.setAttribute('cy', '11'); - circle.setAttribute('r', `${radius}`); - circle.setAttribute('fill', fill); - circle.setAttribute('stroke', stroke); - - return circle; - } - - private createPath(stroke: string): SVGPathElement { - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('fill', 'none'); - path.setAttribute('stroke', stroke); - path.setAttribute('stroke-width', '1px'); - path.setAttribute('stroke-linecap', 'round'); - - return path; - } - - private getTooltip(historyItem: SCMHistoryItemTreeElement): IUpdatableHoverTooltipMarkdownString { + private getTooltip(historyItemViewModel: ISCMHistoryItemViewModel): IUpdatableHoverTooltipMarkdownString { + const historyItem = historyItemViewModel.historyItem; const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); if (historyItem.author) { @@ -1142,7 +1024,7 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + disposeElement(element: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { templateData.elementDisposables.clear(); } @@ -1298,6 +1180,8 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemGroupRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemViewModel(element)) { + return HistoryItemRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; } else if (isSCMViewSeparator(element)) { @@ -1374,6 +1258,10 @@ export class SCMTreeSorter implements ITreeSorter { return isSCMHistoryItemTreeElement(other) ? 0 : 1; } + if (isSCMHistoryItemViewModel(one)) { + return isSCMHistoryItemViewModel(other) ? 0 : 1; + } + if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) { // List if (this.viewMode() === ViewMode.List) { @@ -1458,6 +1346,11 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb // the author. A match in the message takes precedence over // a match in the author. return [element.message, element.author]; + } else if (isSCMHistoryItemViewModel(element)) { + // For a history item we want to match both the message and + // the author. A match in the message takes precedence over + // a match in the author. + return [element.historyItem.message, element.historyItem.author]; } else if (isSCMViewSeparator(element)) { return element.label; } else { @@ -1507,6 +1400,9 @@ function getSCMResourceId(element: TreeElement): string { } else if (isSCMHistoryItemTreeElement(element)) { const provider = element.repository.provider; return `historyItem:${provider.id}/${element.id}/${element.parentIds.join(',')}`; + } else if (isSCMHistoryItemViewModel(element)) { + const provider = element.repository.provider; + return `historyItem2:${provider.id}/${element.historyItem.id}/${element.historyItem.parentIds.join(',')}`; // } else if (isSCMHistoryItemChangeTreeElement(element)) { // const historyItem = element.historyItem; // const historyItemGroup = historyItem.historyItemGroup; @@ -1557,6 +1453,8 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider { + private async getHistoryItems(element: ISCMRepository): Promise { + const graphController = element.graphController; const historyProvider = element.provider.historyProvider; if (!historyProvider?.currentHistoryItemGroup) { @@ -3855,7 +3756,7 @@ class SCMTreeDataSource implements IAsyncDataSource { - const color = colors[colorIndex]; - colorIndex = colorIndex < colors.length - 1 ? colorIndex + 1 : 1; - - return color; - }; - - // Process each commit and add graph information - const graphNodes: ISCMHistoryItemGraphNode[] = []; - for (let index = 0; index < historyItemsElement[1].length; index++) { - const historyItem = historyItemsElement[1][index]; - let swimlaneIndex = graphNodes.findIndex(n => n.id === historyItem.id); - - const color = swimlaneIndex === -1 ? getColor() : graphNodes[swimlaneIndex].color; - const secondaryColor = historyItem.parentIds.length > 1 ? getColor() : undefined; - - // New swimlane - if (swimlaneIndex === -1) { - // Add root node - graphNodes.push({ id: historyItem.id, color, offset: 0, isRoot: true }); - - swimlaneIndex = graphNodes.length - 1; - } - - // Add secondary color to the node - if (historyItem.parentIds.length > 1) { - const node = { ...graphNodes[swimlaneIndex], secondaryColor }; - graphNodes.splice(swimlaneIndex, 1, node); - } - - // Add element - children.push({ - ...historyItem, - graphNodes: [...graphNodes], - repository: element, - type: 'historyItem' - } satisfies SCMHistoryItemTreeElement); - - // Insert parent(s) into the graph - if (historyItem.parentIds.length !== 0) { - // Update graph node with parent(s) - // - Update first occurrence - // - Delete all other occurrences - // - Reset offset - let i = 0; - let offset = 0; - while (i < graphNodes.length) { - if (graphNodes[i].id === historyItem.id) { - if (i === swimlaneIndex) { - // Update first occurrence - graphNodes.splice(i, 1, { - id: historyItem.parentIds[0], - color, - offset, - isRoot: false - }); - i++; - - continue; - } else { - // Delete all other occurrences - graphNodes.splice(i, 1); - offset++; - - continue; - } - } - - // Reset offset - graphNodes[i] = { ...graphNodes[i], offset }; - - i++; - } - - // Add remaining parent(s) to the graph - for (let i = 1; i < historyItem.parentIds.length; i++) { - graphNodes.push({ - id: historyItem.parentIds[i], - color: i === 1 ? secondaryColor ?? getColor() : getColor(), - offset: 0, - isRoot: false - }); - } - } - } + graphController.clearHistoryItems(); + graphController.appendHistoryItems(historyItemsElement[1]); - return children; + return graphController.historyItems; } // private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 00c333886a6e3..250789ce15597 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItemViewModel, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -62,6 +62,10 @@ export function isSCMHistoryItemTreeElement(element: any): element is SCMHistory (element as SCMHistoryItemTreeElement).type === 'historyItem'; } +export function isSCMHistoryItemViewModel(element: any): element is ISCMHistoryItemViewModel { + return (element as ISCMHistoryItemViewModel).type === 'historyItem2'; +} + export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement { return (element as SCMHistoryItemChangeTreeElement).type === 'historyItemChange'; } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index f1a6e87dd063c..f3e6b12b31ba9 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -85,6 +85,19 @@ export interface ISCMHistoryItemGraphNode { readonly isRoot: boolean; } +export interface ISCMHistoryItemGraphNode2 { + readonly id: string; + readonly color: number; +} + +export interface ISCMHistoryItemViewModel { + readonly repository: ISCMRepository; + readonly historyItem: ISCMHistoryItem; + readonly inputSwimlanes: ISCMHistoryItemGraphNode2[]; + readonly outputSwimlanes: ISCMHistoryItemGraphNode2[]; + readonly type: 'historyItem2'; +} + export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { readonly repository: ISCMRepository; readonly graphNodes: ISCMHistoryItemGraphNode[]; diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 3fe568b77d51e..b8660ddbd9c6a 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -15,6 +15,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; import { ITextModel } from 'vs/editor/common/model'; +import { ISCMRepositoryGraphController } from 'vs/workbench/contrib/scm/common/scmHistory'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -162,6 +163,7 @@ export interface ISCMRepository extends IDisposable { readonly id: string; readonly provider: ISCMProvider; readonly input: ISCMInput; + readonly graphController: ISCMRepositoryGraphController; } export interface ISCMService { diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts new file mode 100644 index 0000000000000..7042170c8c3c5 --- /dev/null +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -0,0 +1,247 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { lastOrDefault } from 'vs/base/common/arrays'; +import { deepClone } from 'vs/base/common/objects'; +import { ISCMHistoryItem, ISCMHistoryItemGraphNode2, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; + +const SWIMLANE_HEIGHT = 22; +const SWIMLANE_WIDTH = 11; +const CIRCLE_RADIUS = 4; + +const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D']; + +function createPath(stroke: string): SVGPathElement { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'none'); + path.setAttribute('stroke', stroke); + path.setAttribute('stroke-width', '1px'); + path.setAttribute('stroke-linecap', 'round'); + + return path; +} + +function drawCircle(index: number, radius: number, stroke: string, fill: string): SVGCircleElement { + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`); + circle.setAttribute('cy', `${SWIMLANE_WIDTH}`); + circle.setAttribute('r', `${radius}`); + circle.setAttribute('fill', fill); + circle.setAttribute('stroke', stroke); + + return circle; +} + +function drawVerticalLine(x1: number, y1: number, y2: number, color: string): SVGPathElement { + const path = createPath(color); + path.setAttribute('d', `M ${x1} ${y1} V ${y2}`); + + return path; +} + +function findLastIndex(nodes: ISCMHistoryItemGraphNode2[], id: string): number { + for (let i = nodes.length - 1; i >= 0; i--) { + if (nodes[i].id === id) { + return i; + } + } + + return -1; +} + +export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemViewModel): SVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.classList.add('graph'); + + const historyItem = historyItemViewModel.historyItem; + const inputSwimlanes = historyItemViewModel.inputSwimlanes; + const outputSwimlanes = historyItemViewModel.outputSwimlanes; + + const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); + const outputIndex = historyItem.parentIds.length === 0 ? -1 : outputSwimlanes.findIndex(node => node.id === historyItem.parentIds[0]); + + const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length; + const circleColorIndex = inputIndex !== -1 ? inputSwimlanes[inputIndex].color : outputSwimlanes[circleIndex].color; + + for (let index = 0; index < inputSwimlanes.length; index++) { + const node = inputSwimlanes[index]; + const color = graphColors[inputSwimlanes[index].color]; + + // Not the current commit + if (node.id !== historyItem.id) { + if (index < outputSwimlanes.length && node.id === outputSwimlanes[index].id) { + // Draw | + const path = drawVerticalLine(SWIMLANE_WIDTH * (index + 1), 0, SWIMLANE_HEIGHT, color); + svg.append(path); + } else { + // Draw / + const d: string[] = []; + const path = createPath(color); + + d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * index} ${SWIMLANE_HEIGHT / 2}`); + + // Draw - + d.push(`H ${SWIMLANE_WIDTH * (findLastIndex(outputSwimlanes, node.id) + 1)}`); + + // Draw | + d.push(`V ${SWIMLANE_HEIGHT}`); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + + continue; + } + + // Base commit + if (index !== circleIndex) { + const d: string[] = []; + const path = createPath(color); + + // Draw / + d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (index)} ${SWIMLANE_WIDTH}`); + + // Draw - + d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)}`); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + } + + // Add remaining parent(s) + for (let i = 1; i < historyItem.parentIds.length; i++) { + const parentOutputIndex = outputSwimlanes.findIndex(node => node.id === historyItem.parentIds[i]); + if (parentOutputIndex === -1) { + continue; + } + + // Draw -\ + const d: string[] = []; + const path = createPath(graphColors[outputSwimlanes[parentOutputIndex].color]); + + // Draw \ + d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`); + d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * (parentOutputIndex + 1)} ${SWIMLANE_HEIGHT}`); + + // Draw - + d.push(`M ${SWIMLANE_WIDTH * parentOutputIndex} ${SWIMLANE_HEIGHT / 2}`); + d.push(`H ${SWIMLANE_WIDTH * (circleIndex + 1)} `); + + path.setAttribute('d', d.join(' ')); + svg.append(path); + } + + // Draw | to circle + if (inputIndex !== -1) { + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), 0, SWIMLANE_HEIGHT / 2, graphColors[circleColorIndex]); + svg.append(path); + } + + // Draw | from circle + if (outputIndex !== -1) { + const path = drawVerticalLine(SWIMLANE_WIDTH * (circleIndex + 1), SWIMLANE_HEIGHT / 2, SWIMLANE_HEIGHT, graphColors[circleColorIndex]); + svg.append(path); + } + + // Draw * + if (historyItem.parentIds.length === 1) { + // Node + // TODO@lszomoru - remove hardcoded color + const circle = drawCircle(circleIndex, CIRCLE_RADIUS, '#f8f8f8', graphColors[circleColorIndex]); + svg.append(circle); + } else { + // Multi-parent node + // TODO@lszomoru - remove hardcoded color + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, '#f8f8f8', graphColors[circleColorIndex]); + svg.append(circleOuter); + + // TODO@lszomoru - remove hardcoded color + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, '#f8f8f8', graphColors[circleColorIndex]); + svg.append(circleInner); + } + + // Set dimensions + svg.style.height = `${SWIMLANE_HEIGHT}px`; + svg.style.width = `${SWIMLANE_WIDTH * (Math.max(inputSwimlanes.length, outputSwimlanes.length) + 1)}px`; + + return svg; +} + +export interface ISCMRepositoryGraphController { + readonly historyItems: ISCMHistoryItemViewModel[]; + + appendHistoryItems(historyItems: ISCMHistoryItem[]): void; + clearHistoryItems(): void; +} + +export class SCMRepositoryGraphController implements ISCMRepositoryGraphController { + private readonly _historyItems: ISCMHistoryItemViewModel[] = []; + get historyItems(): ISCMHistoryItemViewModel[] { return this._historyItems; } + + private _colorIndex: number = -1; + + constructor(private readonly _repository: ISCMRepository) { } + + appendHistoryItems(historyItems: ISCMHistoryItem[]): void { + for (let index = 0; index < historyItems.length; index++) { + const historyItem = historyItems[index]; + + const outputSwimlanesFromPreviousItem = lastOrDefault(this.historyItems)?.outputSwimlanes ?? []; + const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); + const outputSwimlanes: ISCMHistoryItemGraphNode2[] = []; + + if (historyItem.parentIds.length > 0) { + let firstParentAdded = false; + + // Add first parent to the output + for (const node of inputSwimlanes) { + if (node.id === historyItem.id) { + if (!firstParentAdded) { + outputSwimlanes.push({ + ...deepClone(node), + id: historyItem.parentIds[0] + }); + firstParentAdded = true; + } + + continue; + } + + outputSwimlanes.push(deepClone(node)); + } + + // Add unprocessed parent(s) to the output + for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { + outputSwimlanes.push({ + id: historyItem.parentIds[i], + color: this.getGraphColorIndex() + }); + } + } + + this._historyItems.push({ + historyItem, + inputSwimlanes, + outputSwimlanes, + repository: this._repository, + type: 'historyItem2' + }); + } + } + + clearHistoryItems(): void { + this._colorIndex = -1; + this._historyItems.length = 0; + } + + private getGraphColorIndex(): number { + this._colorIndex = this._colorIndex < graphColors.length - 1 ? this._colorIndex + 1 : 1; + return this._colorIndex; + } +} diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 762dc9ed1b60c..6196aa721f65f 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -15,6 +15,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { Iterable } from 'vs/base/common/iterator'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ISCMRepositoryGraphController, SCMRepositoryGraphController } from 'vs/workbench/contrib/scm/common/scmHistory'; class SCMInput implements ISCMInput { @@ -180,6 +181,7 @@ class SCMRepository implements ISCMRepository { readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; readonly input: ISCMInput; + readonly graphController: ISCMRepositoryGraphController; constructor( public readonly id: string, @@ -188,6 +190,7 @@ class SCMRepository implements ISCMRepository { inputHistory: SCMInputHistory ) { this.input = new SCMInput(this, inputHistory); + this.graphController = new SCMRepositoryGraphController(this); } setSelected(selected: boolean): void { From 41abe800ca020e7b237886b590c713c8d2be987f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 May 2024 12:30:36 +0200 Subject: [PATCH 12/33] Revert some of the hacks that were put in place to quickly get going --- src/vs/workbench/contrib/scm/browser/menus.ts | 60 +- .../contrib/scm/browser/scmViewPane.ts | 630 ++++++++++-------- .../workbench/contrib/scm/common/history.ts | 13 +- 3 files changed, 394 insertions(+), 309 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index b24712b13a52c..39ee31783bd3a 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -14,7 +14,7 @@ import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/c import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryProviderMenus, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMMenus, ISCMProvider, ISCMRepository, ISCMRepositoryMenus, ISCMResource, ISCMResourceGroup, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; function actionEquals(a: IAction, b: IAction): boolean { @@ -255,16 +255,16 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDisposable { - // private readonly historyItemMenus = new Map(); + private readonly historyItemMenus = new Map(); private readonly disposables = new DisposableStore(); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService) { } - // getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { - // return this.getOrCreateHistoryItemMenu(historyItem); - // } + getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { + return this.getOrCreateHistoryItemMenu(historyItem); + } getHistoryItemGroupMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { return historyItemGroup.direction === 'incoming' ? @@ -278,31 +278,31 @@ export class SCMHistoryProviderMenus implements ISCMHistoryProviderMenus, IDispo this.getOutgoingHistoryItemGroupMenu(MenuId.SCMOutgoingChangesContext, historyItemGroup); } - // private getOrCreateHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { - // let result = this.historyItemMenus.get(historyItem); - - // if (!result) { - // let menuId: MenuId; - // if (historyItem.historyItemGroup.direction === 'incoming') { - // menuId = historyItem.type === 'allChanges' ? - // MenuId.SCMIncomingChangesAllChangesContext : - // MenuId.SCMIncomingChangesHistoryItemContext; - // } else { - // menuId = historyItem.type === 'allChanges' ? - // MenuId.SCMOutgoingChangesAllChangesContext : - // MenuId.SCMOutgoingChangesHistoryItemContext; - // } - - // const contextKeyService = this.contextKeyService.createOverlay([ - // ['scmHistoryItemFileCount', historyItem.statistics?.files ?? 0], - // ]); - - // result = this.menuService.createMenu(menuId, contextKeyService); - // this.historyItemMenus.set(historyItem, result); - // } - - // return result; - // } + private getOrCreateHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu { + let result = this.historyItemMenus.get(historyItem); + + if (!result) { + let menuId: MenuId; + if (historyItem.historyItemGroup.direction === 'incoming') { + menuId = historyItem.type === 'allChanges' ? + MenuId.SCMIncomingChangesAllChangesContext : + MenuId.SCMIncomingChangesHistoryItemContext; + } else { + menuId = historyItem.type === 'allChanges' ? + MenuId.SCMOutgoingChangesAllChangesContext : + MenuId.SCMOutgoingChangesHistoryItemContext; + } + + const contextKeyService = this.contextKeyService.createOverlay([ + ['scmHistoryItemFileCount', historyItem.statistics?.files ?? 0], + ]); + + result = this.menuService.createMenu(menuId, contextKeyService); + this.historyItemMenus.set(historyItem, result); + } + + return result; + } private getOutgoingHistoryItemGroupMenu(menuId: MenuId, historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu { const contextKeyService = this.contextKeyService.createOverlay([ diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a4567f58dcbba..2261b93bd3b0a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -103,12 +103,13 @@ import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/d import { clamp } from 'vs/base/common/numbers'; import { ILogService } from 'vs/platform/log/common/log'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import type { IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; +import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; import { renderSCMHistoryItemGraph } from 'vs/workbench/contrib/scm/common/scmHistory'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -843,7 +844,7 @@ class HistoryItemActionRunner extends ActionRunner { } const args: (ISCMProvider | ISCMHistoryItem)[] = []; - args.push(context.repository.provider); + args.push(context.historyItemGroup.repository.provider); args.push({ id: context.id, parentIds: context.parentIds, @@ -859,33 +860,164 @@ class HistoryItemActionRunner extends ActionRunner { } interface HistoryItemTemplate { - // readonly iconContainer: HTMLElement; + readonly iconContainer: HTMLElement; readonly label: IconLabel; - readonly graphContainer: HTMLElement; - readonly labelContainer: HTMLElement; - // readonly statsContainer: HTMLElement; - // readonly statsCustomHover: IUpdatableHover; - // readonly filesLabel: HTMLElement; - // readonly insertionsLabel: HTMLElement; - // readonly deletionsLabel: HTMLElement; - // readonly actionBar: ActionBar; + readonly statsContainer: HTMLElement; + readonly statsCustomHover: IUpdatableHover; + readonly filesLabel: HTMLElement; + readonly insertionsLabel: HTMLElement; + readonly deletionsLabel: HTMLElement; + readonly actionBar: ActionBar; readonly elementDisposables: DisposableStore; readonly disposables: IDisposable; } -class HistoryItemRenderer implements ICompressibleTreeRenderer { +class HistoryItemRenderer implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'history-item'; get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; } constructor( - // private actionRunner: IActionRunner, - // private actionViewItemProvider: IActionViewItemProvider, - // @IHoverService private hoverService: IHoverService, - // @ISCMViewService private scmViewService: ISCMViewService + private actionRunner: IActionRunner, + private actionViewItemProvider: IActionViewItemProvider, + @IHoverService private hoverService: IHoverService, + @ISCMViewService private scmViewService: ISCMViewService ) { } renderTemplate(container: HTMLElement): HistoryItemTemplate { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie'); + + const element = append(container, $('.history-item')); + + const iconLabel = new IconLabel(element, { supportIcons: true, supportHighlights: true, supportDescriptionHighlights: true }); + const iconContainer = prepend(iconLabel.element, $('.icon-container')); + + const disposables = new DisposableStore(); + const actionsContainer = append(element, $('.actions')); + const actionBar = new ActionBar(actionsContainer, { actionRunner: this.actionRunner, actionViewItemProvider: this.actionViewItemProvider }); + disposables.add(actionBar); + + const statsContainer = append(element, $('.stats-container')); + const filesLabel = append(statsContainer, $('.files-label')); + const insertionsLabel = append(statsContainer, $('.insertions-label')); + const deletionsLabel = append(statsContainer, $('.deletions-label')); + + const statsCustomHover = this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), statsContainer, ''); + disposables.add(statsCustomHover); + + return { iconContainer, label: iconLabel, actionBar, statsContainer, statsCustomHover, filesLabel, insertionsLabel, deletionsLabel, elementDisposables: new DisposableStore(), disposables }; + } + + renderElement(node: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + const historyItem = node.element; + + templateData.iconContainer.className = 'icon-container'; + if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { + templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); + } + + const title = this.getTooltip(historyItem); + const [matches, descriptionMatches] = this.processMatches(historyItem, node.filterData); + templateData.label.setLabel(historyItem.message, historyItem.author, { matches, descriptionMatches, title }); + + templateData.actionBar.clear(); + templateData.actionBar.context = historyItem; + + const menus = this.scmViewService.menus.getRepositoryMenus(historyItem.historyItemGroup.repository.provider); + if (menus.historyProviderMenu) { + const historyItemMenu = menus.historyProviderMenu.getHistoryItemMenu(historyItem); + templateData.elementDisposables.add(connectPrimaryMenuToInlineActionBar(historyItemMenu, templateData.actionBar)); + } + + this.renderStatistics(node, index, templateData, height); + } + + renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + + private getTooltip(historyItem: SCMHistoryItemTreeElement): IUpdatableHoverTooltipMarkdownString { + const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + + if (historyItem.author) { + markdown.appendMarkdown(`$(account) **${historyItem.author}**\n\n`); + } + + if (historyItem.timestamp) { + const dateFormatter = new Intl.DateTimeFormat(platform.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdown.appendMarkdown(`$(history) ${dateFormatter.format(historyItem.timestamp)}\n\n`); + } + + markdown.appendMarkdown(historyItem.message); + + return { markdown, markdownNotSupportedFallback: historyItem.message }; + } + + private processMatches(historyItem: SCMHistoryItemTreeElement, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { + if (!filterData) { + return [undefined, undefined]; + } + + return [ + historyItem.message === filterData.label ? createMatches(filterData.score) : undefined, + historyItem.author === filterData.label ? createMatches(filterData.score) : undefined + ]; + } + + private renderStatistics(node: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + const historyItem = node.element; + + if (historyItem.statistics) { + const statsAriaLabel: string[] = [ + historyItem.statistics.files === 1 ? + localize('fileChanged', "{0} file changed", historyItem.statistics.files) : + localize('filesChanged', "{0} files changed", historyItem.statistics.files), + historyItem.statistics.insertions === 1 ? localize('insertion', "{0} insertion{1}", historyItem.statistics.insertions, '(+)') : + historyItem.statistics.insertions > 1 ? localize('insertions', "{0} insertions{1}", historyItem.statistics.insertions, '(+)') : '', + historyItem.statistics.deletions === 1 ? localize('deletion', "{0} deletion{1}", historyItem.statistics.deletions, '(-)') : + historyItem.statistics.deletions > 1 ? localize('deletions', "{0} deletions{1}", historyItem.statistics.deletions, '(-)') : '' + ]; + + const statsTitle = statsAriaLabel.filter(l => l !== '').join(', '); + templateData.statsContainer.setAttribute('aria-label', statsTitle); + templateData.statsCustomHover.update(statsTitle); + + templateData.filesLabel.textContent = historyItem.statistics.files.toString(); + + templateData.insertionsLabel.textContent = historyItem.statistics.insertions > 0 ? `+${historyItem.statistics.insertions}` : ''; + templateData.insertionsLabel.classList.toggle('hidden', historyItem.statistics.insertions === 0); + + templateData.deletionsLabel.textContent = historyItem.statistics.deletions > 0 ? `-${historyItem.statistics.deletions}` : ''; + templateData.deletionsLabel.classList.toggle('hidden', historyItem.statistics.deletions === 0); + } + + templateData.statsContainer.classList.toggle('hidden', historyItem.statistics === undefined); + } + + disposeElement(element: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + templateData.elementDisposables.clear(); + } + + disposeTemplate(templateData: HistoryItemTemplate): void { + templateData.disposables.dispose(); + } +} + +interface HistoryItem2Template { + readonly label: IconLabel; + readonly graphContainer: HTMLElement; + readonly labelContainer: HTMLElement; + readonly elementDisposables: DisposableStore; + readonly disposables: IDisposable; +} + +class HistoryItem2Renderer implements ICompressibleTreeRenderer { + + static readonly TEMPLATE_ID = 'history-item-2'; + get templateId(): string { return HistoryItem2Renderer.TEMPLATE_ID; } + + renderTemplate(container: HTMLElement): HistoryItem2Template { // hack (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); @@ -893,42 +1025,23 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + renderElement(node: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { const historyItemViewModel = node.element; const historyItem = historyItemViewModel.historyItem; - // templateData.iconContainer.className = 'icon-container'; - // if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { - // templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); - // } - templateData.graphContainer.textContent = ''; templateData.graphContainer.appendChild(renderSCMHistoryItemGraph(historyItemViewModel)); const title = this.getTooltip(historyItemViewModel); - // const [matches, descriptionMatches] = this.processMatches(historyItem, node.filterData); - templateData.label.setLabel(historyItem.message, undefined, { title }); + const [matches, descriptionMatches] = this.processMatches(historyItemViewModel, node.filterData); + templateData.label.setLabel(historyItem.message, undefined, { title, matches, descriptionMatches }); templateData.labelContainer.textContent = ''; if (historyItem.labels) { @@ -948,20 +1061,9 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, LabelFuzzyScore>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } @@ -983,52 +1085,22 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { - // const historyItem = node.element; - - // if (historyItem.statistics) { - // const statsAriaLabel: string[] = [ - // historyItem.statistics.files === 1 ? - // localize('fileChanged', "{0} file changed", historyItem.statistics.files) : - // localize('filesChanged', "{0} files changed", historyItem.statistics.files), - // historyItem.statistics.insertions === 1 ? localize('insertion', "{0} insertion{1}", historyItem.statistics.insertions, '(+)') : - // historyItem.statistics.insertions > 1 ? localize('insertions', "{0} insertions{1}", historyItem.statistics.insertions, '(+)') : '', - // historyItem.statistics.deletions === 1 ? localize('deletion', "{0} deletion{1}", historyItem.statistics.deletions, '(-)') : - // historyItem.statistics.deletions > 1 ? localize('deletions', "{0} deletions{1}", historyItem.statistics.deletions, '(-)') : '' - // ]; - - // const statsTitle = statsAriaLabel.filter(l => l !== '').join(', '); - // templateData.statsContainer.setAttribute('aria-label', statsTitle); - // templateData.statsCustomHover.update(statsTitle); - - // templateData.filesLabel.textContent = historyItem.statistics.files.toString(); - - // templateData.insertionsLabel.textContent = historyItem.statistics.insertions > 0 ? `+${historyItem.statistics.insertions}` : ''; - // templateData.insertionsLabel.classList.toggle('hidden', historyItem.statistics.insertions === 0); - - // templateData.deletionsLabel.textContent = historyItem.statistics.deletions > 0 ? `-${historyItem.statistics.deletions}` : ''; - // templateData.deletionsLabel.classList.toggle('hidden', historyItem.statistics.deletions === 0); - // } + private processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { + if (!filterData) { + return [undefined, undefined]; + } - // templateData.statsContainer.classList.toggle('hidden', historyItem.statistics === undefined); - // } + return [ + historyItemViewModel.historyItem.message === filterData.label ? createMatches(filterData.score) : undefined, + historyItemViewModel.historyItem.author === filterData.label ? createMatches(filterData.score) : undefined + ]; + } - disposeElement(element: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + disposeElement(element: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { templateData.elementDisposables.clear(); } - disposeTemplate(templateData: HistoryItemTemplate): void { + disposeTemplate(templateData: HistoryItem2Template): void { templateData.disposables.dispose(); } } @@ -1181,7 +1253,7 @@ class ListDelegate implements IListVirtualDelegate { } else if (isSCMHistoryItemTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemViewModel(element)) { - return HistoryItemRenderer.TEMPLATE_ID; + return HistoryItem2Renderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; } else if (isSCMViewSeparator(element)) { @@ -1398,7 +1470,7 @@ function getSCMResourceId(element: TreeElement): string { const provider = element.repository.provider; return `historyItemGroup:${provider.id}/${element.id}`; } else if (isSCMHistoryItemTreeElement(element)) { - const provider = element.repository.provider; + const provider = element.historyItemGroup.repository.provider; return `historyItem:${provider.id}/${element.id}/${element.parentIds.join(',')}`; } else if (isSCMHistoryItemViewModel(element)) { const provider = element.repository.provider; @@ -3016,7 +3088,8 @@ export class SCMViewPane extends ViewPane { this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), resourceActionRunner), this.instantiationService.createInstance(HistoryItemGroupRenderer, historyItemGroupActionRunner), - this.instantiationService.createInstance(HistoryItemRenderer, /*historyItemActionRunner, getActionViewItemProvider(this.instantiationService)*/), + this.instantiationService.createInstance(HistoryItemRenderer, historyItemActionRunner, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(HistoryItem2Renderer), this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), this.instantiationService.createInstance(SeparatorRenderer) ], @@ -3526,7 +3599,7 @@ class SCMTreeDataSource implements IAsyncDataSource 0) { + let label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + let ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + + const incomingHistoryItems = historyItemGroups.find(g => g.direction === 'incoming'); + const outgoingHistoryItems = historyItemGroups.find(g => g.direction === 'outgoing'); + + if (incomingHistoryItems && !outgoingHistoryItems) { + label = localize('syncIncomingSeparatorHeader', "Incoming"); + ariaLabel = localize('syncIncomingSeparatorHeaderAriaLabel', "Incoming changes"); + } else if (!incomingHistoryItems && outgoingHistoryItems) { + label = localize('syncOutgoingSeparatorHeader', "Outgoing"); + ariaLabel = localize('syncOutgoingSeparatorHeaderAriaLabel', "Outgoing changes"); + } + + children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + } + + children.push(...historyItemGroups); + // History Separator children.push({ - label: localize('syncSeparatorHeader', "History"), - ariaLabel: localize('syncSeparatorHeaderAriaLabel', "History"), + label: localize('historyHeader', "History"), + ariaLabel: localize('historyHeaderAriaLabel', "History"), repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); // History items - const historyItems = await this.getHistoryItems(inputOrElement); + const historyItems = await this.getHistoryItems2(inputOrElement); children.push(...historyItems); - // // History item groups - // const historyItemGroups = await this.getHistoryItemGroups(inputOrElement); - - // // Incoming/Outgoing Separator - // if (historyItemGroups.length > 0) { - // let label = localize('syncSeparatorHeader', "Incoming/Outgoing"); - // let ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); - - // const incomingHistoryItems = historyItemGroups.find(g => g.direction === 'incoming'); - // const outgoingHistoryItems = historyItemGroups.find(g => g.direction === 'outgoing'); - - // if (incomingHistoryItems && !outgoingHistoryItems) { - // label = localize('syncIncomingSeparatorHeader', "Incoming"); - // ariaLabel = localize('syncIncomingSeparatorHeaderAriaLabel', "Incoming changes"); - // } else if (!incomingHistoryItems && outgoingHistoryItems) { - // label = localize('syncOutgoingSeparatorHeader', "Outgoing"); - // ariaLabel = localize('syncOutgoingSeparatorHeaderAriaLabel', "Outgoing changes"); - // } - - // children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); - // } - - // children.push(...historyItemGroups); - return children; } else if (isSCMResourceGroup(inputOrElement)) { if (this.viewMode() === ViewMode.List) { @@ -3661,182 +3734,203 @@ class SCMTreeDataSource implements IAsyncDataSource { - // const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); - - // const scmProvider = element.provider; - // const historyProvider = scmProvider.historyProvider; - // const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - - // if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { - // return []; - // } - - // const children: SCMHistoryItemGroupTreeElement[] = []; - // const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); - - // let incomingHistoryItemGroup = historyProviderCacheEntry?.incomingHistoryItemGroup; - // let outgoingHistoryItemGroup = historyProviderCacheEntry?.outgoingHistoryItemGroup; - - // if (!incomingHistoryItemGroup && !outgoingHistoryItemGroup) { - // // Common ancestor, ahead, behind - // const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base?.id); - // if (!ancestor) { - // return []; - // } - - // // Only show "Incoming" node if there is a base branch - // incomingHistoryItemGroup = currentHistoryItemGroup.base ? { - // id: currentHistoryItemGroup.base.id, - // label: currentHistoryItemGroup.base.name, - // ariaLabel: localize('incomingChangesAriaLabel', "Incoming changes from {0}", currentHistoryItemGroup.base.name), - // icon: Codicon.arrowCircleDown, - // direction: 'incoming', - // ancestor: ancestor.id, - // count: ancestor.behind, - // repository: element, - // type: 'historyItemGroup' - // } : undefined; - - // outgoingHistoryItemGroup = { - // id: currentHistoryItemGroup.id, - // label: currentHistoryItemGroup.name, - // ariaLabel: localize('outgoingChangesAriaLabel', "Outgoing changes to {0}", currentHistoryItemGroup.name), - // icon: Codicon.arrowCircleUp, - // direction: 'outgoing', - // ancestor: ancestor.id, - // count: ancestor.ahead, - // repository: element, - // type: 'historyItemGroup' - // }; - - // this.historyProviderCache.set(element, { - // ...historyProviderCacheEntry, - // incomingHistoryItemGroup, - // outgoingHistoryItemGroup - // }); - // } - - // // Incoming - // if (incomingHistoryItemGroup && - // (showIncomingChanges === 'always' || - // (showIncomingChanges === 'auto' && (incomingHistoryItemGroup.count ?? 0) > 0))) { - // children.push(incomingHistoryItemGroup); - // } - - // // Outgoing - // if (outgoingHistoryItemGroup && - // (showOutgoingChanges === 'always' || - // (showOutgoingChanges === 'auto' && (outgoingHistoryItemGroup.count ?? 0) > 0))) { - // children.push(outgoingHistoryItemGroup); - // } - - // return children; - // } - - private async getHistoryItems(element: ISCMRepository): Promise { - const graphController = element.graphController; - const historyProvider = element.provider.historyProvider; + private async getHistoryItemGroups(element: ISCMRepository): Promise { + const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); - if (!historyProvider?.currentHistoryItemGroup) { + const scmProvider = element.provider; + const historyProvider = scmProvider.historyProvider; + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + + if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { return []; } + const children: SCMHistoryItemGroupTreeElement[] = []; const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(element); + + let incomingHistoryItemGroup = historyProviderCacheEntry?.incomingHistoryItemGroup; + let outgoingHistoryItemGroup = historyProviderCacheEntry?.outgoingHistoryItemGroup; + + if (!incomingHistoryItemGroup && !outgoingHistoryItemGroup) { + // Common ancestor, ahead, behind + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base?.id); + if (!ancestor) { + return []; + } + + // Only show "Incoming" node if there is a base branch + incomingHistoryItemGroup = currentHistoryItemGroup.base ? { + id: currentHistoryItemGroup.base.id, + label: currentHistoryItemGroup.base.name, + ariaLabel: localize('incomingChangesAriaLabel', "Incoming changes from {0}", currentHistoryItemGroup.base.name), + icon: Codicon.arrowCircleDown, + direction: 'incoming', + ancestor: ancestor.id, + count: ancestor.behind, + repository: element, + type: 'historyItemGroup' + } : undefined; + + outgoingHistoryItemGroup = { + id: currentHistoryItemGroup.id, + label: currentHistoryItemGroup.name, + ariaLabel: localize('outgoingChangesAriaLabel', "Outgoing changes to {0}", currentHistoryItemGroup.name), + icon: Codicon.arrowCircleUp, + direction: 'outgoing', + ancestor: ancestor.id, + count: ancestor.ahead, + repository: element, + type: 'historyItemGroup' + }; + + this.historyProviderCache.set(element, { + ...historyProviderCacheEntry, + incomingHistoryItemGroup, + outgoingHistoryItemGroup + }); + } + + // Incoming + if (incomingHistoryItemGroup && + (showIncomingChanges === 'always' || + (showIncomingChanges === 'auto' && (incomingHistoryItemGroup.count ?? 0) > 0))) { + children.push(incomingHistoryItemGroup); + } + + // Outgoing + if (outgoingHistoryItemGroup && + (showOutgoingChanges === 'always' || + (showOutgoingChanges === 'auto' && (outgoingHistoryItemGroup.count ?? 0) > 0))) { + children.push(outgoingHistoryItemGroup); + } + + return children; + } + + private async getHistoryItems(element: SCMHistoryItemGroupTreeElement): Promise { + const repository = element.repository; + const historyProvider = repository.provider.historyProvider; + + if (!historyProvider) { + return []; + } + + const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); const historyItemsMap = historyProviderCacheEntry.historyItems; let historyItemsElement = historyProviderCacheEntry.historyItems.get(element.id); if (!historyItemsElement) { - const historyItems = await historyProvider.provideHistoryItems(historyProvider.currentHistoryItemGroup.id, { limit: 200 }) ?? []; + const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? []; // All Changes - // const { showChangesSummary } = this.getConfiguration(); - // const allChanges = showChangesSummary && historyItems.length >= 2 ? - // await historyProvider.provideHistoryItemSummary(historyItems[0].id, element.ancestor) : undefined; + const { showChangesSummary } = this.getConfiguration(); + const allChanges = showChangesSummary && historyItems.length >= 2 ? + await historyProvider.provideHistoryItemSummary(historyItems[0].id, element.ancestor) : undefined; - historyItemsElement = [undefined, historyItems]; + historyItemsElement = [allChanges, historyItems]; - this.historyProviderCache.set(element, { + this.historyProviderCache.set(repository, { ...historyProviderCacheEntry, historyItems: historyItemsMap.set(element.id, historyItemsElement) }); } - // if (historyItemsElement[0]) { - // children.push({ - // ...historyItemsElement[0], - // icon: historyItemsElement[0].icon ?? Codicon.files, - // message: localize('allChanges', "All Changes"), - // historyItemGroup: element, - // type: 'allChanges' - // } satisfies SCMHistoryItemTreeElement); - // } + const children: SCMHistoryItemTreeElement[] = []; + if (historyItemsElement[0]) { + children.push({ + ...historyItemsElement[0], + icon: historyItemsElement[0].icon ?? Codicon.files, + message: localize('allChanges', "All Changes"), + historyItemGroup: element, + type: 'allChanges' + } satisfies SCMHistoryItemTreeElement); + } + + children.push(...historyItemsElement[1] + .map(historyItem => ({ + ...historyItem, + historyItemGroup: element, + type: 'historyItem' + } satisfies SCMHistoryItemTreeElement))); + + return children; + } + + private async getHistoryItems2(element: ISCMRepository): Promise { + const graphController = element.graphController; + const historyProvider = element.provider.historyProvider; + + if (!historyProvider?.currentHistoryItemGroup) { + return []; + } + + const historyItems = await historyProvider.provideHistoryItems(historyProvider.currentHistoryItemGroup.id, { limit: 32 }) ?? []; graphController.clearHistoryItems(); - graphController.appendHistoryItems(historyItemsElement[1]); + graphController.appendHistoryItems(historyItems); return graphController.historyItems; } - // private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { - // const repository = element.historyItemGroup.repository; - // const historyProvider = repository.provider.historyProvider; - - // if (!historyProvider) { - // return []; - // } - - // const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); - // const historyItemChangesMap = historyProviderCacheEntry.historyItemChanges; - - // const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; - // let historyItemChanges = historyItemChangesMap.get(`${element.id}/${historyItemParentId}`); - - // if (!historyItemChanges) { - // const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; - // historyItemChanges = await historyProvider.provideHistoryItemChanges(element.id, historyItemParentId) ?? []; - - // this.historyProviderCache.set(repository, { - // ...historyProviderCacheEntry, - // historyItemChanges: historyItemChangesMap.set(`${element.id}/${historyItemParentId}`, historyItemChanges) - // }); - // } - - // if (this.viewMode() === ViewMode.List) { - // // List - // return historyItemChanges.map(change => ({ - // ...change, - // historyItem: element, - // type: 'historyItemChange' - // })); - // } - - // // Tree - // const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); - // for (const change of historyItemChanges) { - // tree.add(change.uri, { - // ...change, - // historyItem: element, - // type: 'historyItemChange' - // }); - // } - - // const children: (SCMHistoryItemChangeTreeElement | IResourceNode)[] = []; - // for (const node of tree.root.children) { - // children.push(node.element ?? node); - // } - - // return children; - // } + private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { + const repository = element.historyItemGroup.repository; + const historyProvider = repository.provider.historyProvider; + + if (!historyProvider) { + return []; + } + + const historyProviderCacheEntry = this.getHistoryProviderCacheEntry(repository); + const historyItemChangesMap = historyProviderCacheEntry.historyItemChanges; + + const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; + let historyItemChanges = historyItemChangesMap.get(`${element.id}/${historyItemParentId}`); + + if (!historyItemChanges) { + const historyItemParentId = element.parentIds.length > 0 ? element.parentIds[0] : undefined; + historyItemChanges = await historyProvider.provideHistoryItemChanges(element.id, historyItemParentId) ?? []; + + this.historyProviderCache.set(repository, { + ...historyProviderCacheEntry, + historyItemChanges: historyItemChangesMap.set(`${element.id}/${historyItemParentId}`, historyItemChanges) + }); + } + + if (this.viewMode() === ViewMode.List) { + // List + return historyItemChanges.map(change => ({ + ...change, + historyItem: element, + type: 'historyItemChange' + })); + } + + // Tree + const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); + for (const change of historyItemChanges) { + tree.add(change.uri, { + ...change, + historyItem: element, + type: 'historyItemChange' + }); + } + + const children: (SCMHistoryItemChangeTreeElement | IResourceNode)[] = []; + for (const node of tree.root.children) { + children.push(node.element ?? node); + } + + return children; + } getParent(element: TreeElement): ISCMViewService | TreeElement { if (isSCMResourceNode(element)) { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index f3e6b12b31ba9..e49a1b5f2c6c1 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -13,7 +13,7 @@ export interface ISCMHistoryProviderMenus { getHistoryItemGroupMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu; getHistoryItemGroupContextMenu(historyItemGroup: SCMHistoryItemGroupTreeElement): IMenu; - // getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu; + getHistoryItemMenu(historyItem: SCMHistoryItemTreeElement): IMenu; } export interface ISCMHistoryProvider { @@ -77,14 +77,6 @@ export interface ISCMHistoryItem { readonly labels?: string[]; } -export interface ISCMHistoryItemGraphNode { - readonly id: string; - readonly color: string; - readonly secondaryColor?: string; - readonly offset: number; - readonly isRoot: boolean; -} - export interface ISCMHistoryItemGraphNode2 { readonly id: string; readonly color: number; @@ -99,8 +91,7 @@ export interface ISCMHistoryItemViewModel { } export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { - readonly repository: ISCMRepository; - readonly graphNodes: ISCMHistoryItemGraphNode[]; + readonly historyItemGroup: SCMHistoryItemGroupTreeElement; readonly type: 'allChanges' | 'historyItem'; } From 391e3e4af0adffa8801dd1b008f5ed4fcc17c605 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 May 2024 12:56:04 +0200 Subject: [PATCH 13/33] Add caching to the history items used in the graph Remove more hacks that were in place --- extensions/git/src/historyProvider.ts | 42 +++++++++++++++---- src/vs/workbench/api/browser/mainThreadSCM.ts | 5 +++ .../workbench/api/common/extHost.protocol.ts | 1 + src/vs/workbench/api/common/extHostSCM.ts | 7 ++++ .../contrib/scm/browser/scmViewPane.ts | 16 ++++++- .../workbench/contrib/scm/common/history.ts | 2 + .../vscode.proposed.scmHistoryProvider.d.ts | 1 + 7 files changed, 63 insertions(+), 11 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index d114b84b4ae32..a40639770e5a7 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -77,18 +77,42 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.logger.trace(`GitHistoryProvider:onDidRunGitStatus - currentHistoryItemGroup (${force}): ${JSON.stringify(this.currentHistoryItemGroup)}`); } - async provideHistoryItems(_: string, options: SourceControlHistoryOptions): Promise { + async provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions): Promise { //TODO@lszomoru - support limit and cursor - // if (typeof options.limit === 'number') { - // throw new Error('Unsupported options.'); - // } - // if (typeof options.limit?.id !== 'string') { - // throw new Error('Unsupported options.'); - // } + if (typeof options.limit === 'number') { + throw new Error('Unsupported options.'); + } + if (typeof options.limit?.id !== 'string') { + throw new Error('Unsupported options.'); + } + + const refParentId = options.limit.id; + const refId = await this.repository.revParse(historyItemGroupId) ?? ''; + + const historyItems: SourceControlHistoryItem[] = []; + const commits = await this.repository.log({ range: `${refParentId}..${refId}`, shortStats: true, sortByAuthorDate: true }); + + await ensureEmojis(); + + historyItems.push(...commits.map(commit => { + const newLineIndex = commit.message.indexOf('\n'); + const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; + + return { + id: commit.hash, + parentIds: commit.parents, + message: emojify(subject), + author: commit.authorName, + icon: new ThemeIcon('git-commit'), + timestamp: commit.authorDate?.getTime(), + statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, + }; + })); - // const refParentId = options.limit.id; - // const refId = await this.repository.revParse(historyItemGroupId) ?? ''; + return historyItems; + } + async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { const refNames = new Set(['main', 'origin/main']); if (this.currentHistoryItemGroup?.name) { refNames.add(this.currentHistoryItemGroup.name); diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 348cd234e7627..98463da696d17 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -173,6 +173,11 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); } + async provideHistoryItems2(options: ISCMHistoryOptions): Promise { + const historyItems = await this.proxy.$provideHistoryItems2(this.handle, options, CancellationToken.None); + return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); + } + async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { const historyItem = await this.proxy.$provideHistoryItemSummary(this.handle, historyItemId, historyItemParentId, CancellationToken.None); return historyItem ? { ...historyItem, icon: getIconFromIconDto(historyItem.icon) } : undefined; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c4b0014ae3870..b090f925742f7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2297,6 +2297,7 @@ export interface ExtHostSCMShape { $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; $provideHistoryItems(sourceControlHandle: number, historyItemGroupId: string, options: any, token: CancellationToken): Promise; + $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index c746b79ed3556..116d111eeb599 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -972,6 +972,13 @@ export class ExtHostSCM implements ExtHostSCMShape { return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; } + async $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItems = await historyProvider?.provideHistoryItems2(options, token); + + return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; + } + async $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; if (typeof historyProvider?.provideHistoryItemSummary !== 'function') { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 2261b93bd3b0a..d9f2db965a7cf 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3873,10 +3873,21 @@ class SCMTreeDataSource implements IAsyncDataSource(), + historyItems2: new Map(), historyItemChanges: new Map() }; } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index e49a1b5f2c6c1..fee563fa733c9 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -24,6 +24,7 @@ export interface ISCMHistoryProvider { set currentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined); provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise; + provideHistoryItems2(options: ISCMHistoryOptions): Promise; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>; @@ -33,6 +34,7 @@ export interface ISCMHistoryProviderCacheEntry { readonly incomingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined; readonly outgoingHistoryItemGroup: SCMHistoryItemGroupTreeElement | undefined; readonly historyItems: Map; + readonly historyItems2: Map; readonly historyItemChanges: Map; } diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 836137917ced3..81dcc7760986f 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -25,6 +25,7 @@ declare module 'vscode' { // onDidChangeHistoryItemGroups: Event; provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; + provideHistoryItems2(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemSummary?(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; From d33fd8d5dcc41bb73fa0da9bc31af889449e115e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 May 2024 15:04:20 +0200 Subject: [PATCH 14/33] Added initial tests --- .../contrib/scm/browser/scmViewPane.ts | 2 +- .../scm/test/common/scmHistory.test.ts | 498 ++++++++++++++++++ 2 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d9f2db965a7cf..ee5806d0cd1a2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3878,7 +3878,7 @@ class SCMTreeDataSource implements IAsyncDataSource { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const controller = new SCMRepositoryGraphController({} as ISCMRepository); + + teardown(() => { + controller.clearHistoryItems(); + }); + + test('empty graph', () => { + controller.appendHistoryItems([]); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 0); + }); + + /** + * * a(b) + * * b(c) + * * c(d) + * * d(e) + * * e + */ + test('linear graph', () => { + const models = [ + { id: 'a', parentIds: ['b'] }, + { id: 'b', parentIds: ['c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'd', parentIds: ['e'] }, + { id: 'e', parentIds: [] }, + ] as ISCMHistoryItem[]; + + controller.appendHistoryItems(models); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 5); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + + // node c + assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + + // node d + assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 0); + }); + + /** + * * a(b) + * * b(c,d) + * |\ + * | * d(c) + * |/ + * * c(e) + * * e(f) + */ + test('merge commit (single commit in topic branch)', () => { + const models = [ + { id: 'a', parentIds: ['b'] }, + { id: 'b', parentIds: ['c', 'd'] }, + { id: 'd', parentIds: ['c'] }, + { id: 'c', parentIds: ['e'] }, + { id: 'e', parentIds: ['f'] }, + ] as ISCMHistoryItem[]; + + controller.appendHistoryItems(models); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 5); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + + // node b + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node d + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + }); + + /** + * * a(b,c) + * |\ + * | * c(d) + * * | b(e) + * * | e(f) + * * | f(d) + * |/ + * * d(g) + */ + test('merge commit (multiple commits in topic branch)', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'b', parentIds: ['e'] }, + { id: 'e', parentIds: ['f'] }, + { id: 'f', parentIds: ['d'] }, + { id: 'd', parentIds: ['g'] }, + ] as ISCMHistoryItem[]; + + controller.appendHistoryItems(models); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + + // node e + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + + // node f + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + }); + + /** + * * a(b,c) + * |\ + * | * c(b) + * |/ + * * b(d,e) + * |\ + * | * e(f) + * | * f(g) + * * | d(h) + */ + test('create brach from merge commit', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['b'] }, + { id: 'b', parentIds: ['d', 'e'] }, + { id: 'e', parentIds: ['f'] }, + { id: 'f', parentIds: ['g'] }, + { id: 'd', parentIds: ['h'] }, + ] as ISCMHistoryItem[]; + + controller.appendHistoryItems(models); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 2); + + // node e + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'f'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 2); + + // node f + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'f'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 2); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 2); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'h'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 2); + }); + + + /** + * * a(b,c) + * |\ + * | * c(d) + * * | b(e,f) + * |\| + * | |\ + * | | * f(g) + * * | | e(g) + * | * | d(g) + * |/ / + * | / + * |/ + * * g(h) + */ + test('create multiple branches from a commit', () => { + const models = [ + { id: 'a', parentIds: ['b', 'c'] }, + { id: 'c', parentIds: ['d'] }, + { id: 'b', parentIds: ['e', 'f'] }, + { id: 'f', parentIds: ['g'] }, + { id: 'e', parentIds: ['g'] }, + { id: 'd', parentIds: ['g'] }, + { id: 'g', parentIds: ['h'] }, + ] as ISCMHistoryItem[]; + + controller.appendHistoryItems(models); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 7); + + // node a + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[0].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[0].outputSwimlanes[1].color, 1); + + // node c + assert.strictEqual(viewModels[1].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[1].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, 1); + + // node b + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, 1); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[2].id, 'f'); + assert.strictEqual(viewModels[2].outputSwimlanes[2].color, 2); + + // node f + assert.strictEqual(viewModels[3].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[2].id, 'f'); + assert.strictEqual(viewModels[3].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[3].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[3].outputSwimlanes[2].color, 2); + + // node e + assert.strictEqual(viewModels[4].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[4].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[4].outputSwimlanes[2].color, 2); + + // node d + assert.strictEqual(viewModels[5].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[5].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 3); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[2].color, 2); + + // node g + assert.strictEqual(viewModels[6].inputSwimlanes.length, 3); + assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[0].color, 0); + assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[1].color, 1); + assert.strictEqual(viewModels[6].inputSwimlanes[2].id, 'g'); + assert.strictEqual(viewModels[6].inputSwimlanes[2].color, 2); + + assert.strictEqual(viewModels[6].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'h'); + assert.strictEqual(viewModels[6].outputSwimlanes[0].color, 0); + }); +}); From f6f28b77effc0ff796568571cbbb3c1772810859 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 May 2024 20:33:19 +0200 Subject: [PATCH 15/33] More work so that incoming/outgoing works along history --- .../contrib/scm/browser/scmViewPane.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index ee5806d0cd1a2..92ce18805ddad 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1319,7 +1319,7 @@ export class SCMTreeSorter implements ITreeSorter { } if (isSCMViewSeparator(one)) { - return isSCMResourceGroup(other) ? 1 : -1; + return 0;// isSCMResourceGroup(other) ? 1 : -1; } if (isSCMHistoryItemGroupTreeElement(one)) { @@ -1475,19 +1475,19 @@ function getSCMResourceId(element: TreeElement): string { } else if (isSCMHistoryItemViewModel(element)) { const provider = element.repository.provider; return `historyItem2:${provider.id}/${element.historyItem.id}/${element.historyItem.parentIds.join(',')}`; - // } else if (isSCMHistoryItemChangeTreeElement(element)) { - // const historyItem = element.historyItem; - // const historyItemGroup = historyItem.historyItemGroup; - // const provider = historyItemGroup.repository.provider; - // return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; - // } else if (isSCMHistoryItemChangeNode(element)) { - // const historyItem = element.context; - // const historyItemGroup = historyItem.historyItemGroup; - // const provider = historyItemGroup.repository.provider; - // return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; + } else if (isSCMHistoryItemChangeTreeElement(element)) { + const historyItem = element.historyItem; + const historyItemGroup = historyItem.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; + } else if (isSCMHistoryItemChangeNode(element)) { + const historyItem = element.context; + const historyItemGroup = historyItem.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; } else if (isSCMViewSeparator(element)) { const provider = element.repository.provider; - return `separator:${provider.id}`; + return `separator:${provider.id}/${element.label}`; } else { throw new Error('Invalid tree element'); } From 00e38de63544ac8621fbbeab386ebefe8277d507 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 May 2024 21:42:46 +0200 Subject: [PATCH 16/33] Uncomment more code --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 92ce18805ddad..8a3bf1ef26d72 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3213,17 +3213,17 @@ export class SCMViewPane extends ViewPane { this.scmViewService.focus(e.element.repository); return; } else if (isSCMHistoryItemTreeElement(e.element)) { - // this.scmViewService.focus(e.element.historyItemGroup.repository); + this.scmViewService.focus(e.element.historyItemGroup.repository); return; } else if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); } - // this.scmViewService.focus(e.element.historyItem.historyItemGroup.repository); + this.scmViewService.focus(e.element.historyItem.historyItemGroup.repository); return; } else if (isSCMHistoryItemChangeNode(e.element)) { - // this.scmViewService.focus(e.element.context.historyItemGroup.repository); + this.scmViewService.focus(e.element.context.historyItemGroup.repository); return; } } From 5d42c75cb74e8b29980d1fe65adf9c2ef53aa566 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 17 May 2024 22:50:28 +0200 Subject: [PATCH 17/33] Bug fixes to edge cases --- extensions/git/src/git.ts | 4 ++-- src/vs/workbench/contrib/scm/common/scmHistory.ts | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index cfa7531766a0d..726da5e6d019b 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1167,8 +1167,8 @@ export class Repository { if (options?.refNames) { args.push('--topo-order'); - args.push('--branches=*'); - // args.push('--all'); + args.push('--all'); + // args.push('--branches=*'); // args.push(...options.refNames); } diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts index 7042170c8c3c5..95c8fe7cade31 100644 --- a/src/vs/workbench/contrib/scm/common/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -84,8 +84,19 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * index} ${SWIMLANE_HEIGHT / 2}`); + // Start walking backwards from the current index and + // find the first occurrence in the output swimlanes + // array + let nodeOutputIndex = -1; + for (let j = index - 1; j >= 0; j--) { + if (outputSwimlanes[j].id === node.id) { + nodeOutputIndex = j; + break; + } + } + // Draw - - d.push(`H ${SWIMLANE_WIDTH * (findLastIndex(outputSwimlanes, node.id) + 1)}`); + d.push(`H ${SWIMLANE_WIDTH * (nodeOutputIndex + 1)}`); // Draw | d.push(`V ${SWIMLANE_HEIGHT}`); @@ -116,7 +127,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV // Add remaining parent(s) for (let i = 1; i < historyItem.parentIds.length; i++) { - const parentOutputIndex = outputSwimlanes.findIndex(node => node.id === historyItem.parentIds[i]); + const parentOutputIndex = findLastIndex(outputSwimlanes, historyItem.parentIds[i]); if (parentOutputIndex === -1) { continue; } From ac82315ad09591babf902a2a6722934d79b91650 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sat, 18 May 2024 22:00:46 +0200 Subject: [PATCH 18/33] Experiment with a new rendering for curved branches --- src/vs/workbench/contrib/scm/common/scmHistory.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts index 95c8fe7cade31..2dee7469a2669 100644 --- a/src/vs/workbench/contrib/scm/common/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -11,6 +11,7 @@ import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; const SWIMLANE_HEIGHT = 22; const SWIMLANE_WIDTH = 11; const CIRCLE_RADIUS = 4; +const SWIMLANE_CURVE_RADIUS = 5; const graphColors = ['#007ACC', '#BC3FBC', '#BF8803', '#CC6633', '#F14C4C', '#16825D']; @@ -77,12 +78,15 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV const path = drawVerticalLine(SWIMLANE_WIDTH * (index + 1), 0, SWIMLANE_HEIGHT, color); svg.append(path); } else { - // Draw / const d: string[] = []; const path = createPath(color); + // Draw | d.push(`M ${SWIMLANE_WIDTH * (index + 1)} 0`); - d.push(`A ${SWIMLANE_WIDTH} ${SWIMLANE_WIDTH} 0 0 1 ${SWIMLANE_WIDTH * index} ${SWIMLANE_HEIGHT / 2}`); + d.push(`V 6`); + + // Draw / + d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 1 ${(SWIMLANE_WIDTH * (index + 1)) - SWIMLANE_CURVE_RADIUS} ${SWIMLANE_HEIGHT / 2}`); // Start walking backwards from the current index and // find the first occurrence in the output swimlanes @@ -96,7 +100,10 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV } // Draw - - d.push(`H ${SWIMLANE_WIDTH * (nodeOutputIndex + 1)}`); + d.push(`H ${(SWIMLANE_WIDTH * (nodeOutputIndex + 1)) + SWIMLANE_CURVE_RADIUS}`); + + // Draw / + d.push(`A ${SWIMLANE_CURVE_RADIUS} ${SWIMLANE_CURVE_RADIUS} 0 0 0 ${SWIMLANE_WIDTH * (nodeOutputIndex + 1)} ${(SWIMLANE_HEIGHT / 2) + SWIMLANE_CURVE_RADIUS}`); // Draw | d.push(`V ${SWIMLANE_HEIGHT}`); From f0c329d51e1257233688bffeb57a31cd41941085 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 21 May 2024 15:23:22 +0200 Subject: [PATCH 19/33] Handle repository with a single commit --- extensions/git/src/historyProvider.ts | 2 +- .../contrib/scm/common/scmHistory.ts | 18 +++++++++--------- .../scm/test/common/scmHistory.test.ts | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index a40639770e5a7..be1bc4cbf784d 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -113,7 +113,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - const refNames = new Set(['main', 'origin/main']); + const refNames = new Set(['main']); if (this.currentHistoryItemGroup?.name) { refNames.add(this.currentHistoryItemGroup.name); } diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts index 2dee7469a2669..18929d271dac4 100644 --- a/src/vs/workbench/contrib/scm/common/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -65,7 +65,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV const outputIndex = historyItem.parentIds.length === 0 ? -1 : outputSwimlanes.findIndex(node => node.id === historyItem.parentIds[0]); const circleIndex = inputIndex !== -1 ? inputIndex : inputSwimlanes.length; - const circleColorIndex = inputIndex !== -1 ? inputSwimlanes[inputIndex].color : outputSwimlanes[circleIndex].color; + const circleColorIndex = inputIndex !== -1 ? inputSwimlanes[inputIndex].color : outputSwimlanes[circleIndex]?.color ?? 0; for (let index = 0; index < inputSwimlanes.length; index++) { const node = inputSwimlanes[index]; @@ -92,7 +92,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV // find the first occurrence in the output swimlanes // array let nodeOutputIndex = -1; - for (let j = index - 1; j >= 0; j--) { + for (let j = Math.min(index, outputSwimlanes.length) - 1; j >= 0; j--) { if (outputSwimlanes[j].id === node.id) { nodeOutputIndex = j; break; @@ -168,12 +168,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV } // Draw * - if (historyItem.parentIds.length === 1) { - // Node - // TODO@lszomoru - remove hardcoded color - const circle = drawCircle(circleIndex, CIRCLE_RADIUS, '#f8f8f8', graphColors[circleColorIndex]); - svg.append(circle); - } else { + if (historyItem.parentIds.length > 1) { // Multi-parent node // TODO@lszomoru - remove hardcoded color const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, '#f8f8f8', graphColors[circleColorIndex]); @@ -182,11 +177,16 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV // TODO@lszomoru - remove hardcoded color const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, '#f8f8f8', graphColors[circleColorIndex]); svg.append(circleInner); + } else { + // Node + // TODO@lszomoru - remove hardcoded color + const circle = drawCircle(circleIndex, CIRCLE_RADIUS, '#f8f8f8', graphColors[circleColorIndex]); + svg.append(circle); } // Set dimensions svg.style.height = `${SWIMLANE_HEIGHT}px`; - svg.style.width = `${SWIMLANE_WIDTH * (Math.max(inputSwimlanes.length, outputSwimlanes.length) + 1)}px`; + svg.style.width = `${SWIMLANE_WIDTH * (Math.max(inputSwimlanes.length, outputSwimlanes.length, 1) + 1)}px`; return svg; } diff --git a/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts index a087eaa1c98ab..b47d407236293 100644 --- a/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts @@ -26,6 +26,25 @@ suite('SCMRepositoryGraphController', () => { assert.strictEqual(viewModels.length, 0); }); + + /** + * * a + */ + + test('single commit', () => { + const models = [ + { id: 'a', parentIds: [], message: '' }, + ] as ISCMHistoryItem[]; + + controller.appendHistoryItems(models); + const viewModels = controller.historyItems; + + assert.strictEqual(viewModels.length, 1); + + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + assert.strictEqual(viewModels[0].outputSwimlanes.length, 0); + }); + /** * * a(b) * * b(c) From 923ea31022cf135d1817b9c50aebc286ea477e08 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:54:15 +0200 Subject: [PATCH 20/33] Maintain swimlanes --- extensions/git/src/git.ts | 4 +- extensions/git/src/historyProvider.ts | 16 ++++++-- .../contrib/scm/browser/scmViewPane.ts | 38 ++++++++++++------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 36744e6ea298b..096046bc56152 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1167,9 +1167,9 @@ export class Repository { if (options?.refNames) { args.push('--topo-order'); - args.push('--all'); + // args.push('--all'); // args.push('--branches=*'); - // args.push(...options.refNames); + args.push(...options.refNames); } if (options?.path) { diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index be1bc4cbf784d..2d24e15d51f7e 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -113,7 +113,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - const refNames = new Set(['main']); + if (!options.cursor) { + return []; + } + + const refNames = new Set(); if (this.currentHistoryItemGroup?.name) { refNames.add(this.currentHistoryItemGroup.name); } @@ -121,13 +125,17 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec refNames.add(this.currentHistoryItemGroup.base.name); } - const maxEntries = typeof options.limit === 'number' ? options.limit : 32; - const commits = await this.repository.log({ refNames: Array.from(refNames), maxEntries }); + // TODO@lszomoru - see if there is a better to do this + const [incoming, outgoing, ancestor] = await Promise.all([ + this.repository.log({ range: `${this.currentHistoryItemGroup?.name}..${this.currentHistoryItemGroup?.base?.name}`, refNames: Array.from(refNames) }), + this.repository.log({ range: `${this.currentHistoryItemGroup?.base?.name}..${this.currentHistoryItemGroup?.name}`, refNames: Array.from(refNames) }), + this.repository.getCommit(options.cursor) + ]); await ensureEmojis(); const historyItems: SourceControlHistoryItem[] = []; - historyItems.push(...commits.map(commit => { + historyItems.push(...[...outgoing, ...incoming, ancestor].map(commit => { const newLineIndex = commit.message.indexOf('\n'); const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d481c46bf0dfd..1f8e8ae7116df 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3009,7 +3009,8 @@ export class SCMViewPane extends ViewPane { e.affectsConfiguration('scm.showActionButton') || e.affectsConfiguration('scm.showChangesSummary') || e.affectsConfiguration('scm.showIncomingChanges') || - e.affectsConfiguration('scm.showOutgoingChanges'), + e.affectsConfiguration('scm.showOutgoingChanges') || + e.affectsConfiguration('scm.showChangesGraph'), this.visibilityDisposables) (() => this.updateChildren(), this, this.visibilityDisposables); @@ -3784,16 +3785,15 @@ class SCMTreeDataSource implements IAsyncDataSource 0) { + const label = localize('syncSeparatorHeader', "Incoming/Outgoing"); + const ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + + children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); + } + children.push(...historyItems); return children; @@ -3830,13 +3830,13 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showIncomingChanges, showOutgoingChanges } = this.getConfiguration(); + const { showIncomingChanges, showOutgoingChanges, showChangesGraph } = this.getConfiguration(); const scmProvider = element.provider; const historyProvider = scmProvider.historyProvider; const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never')) { + if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showChangesGraph) { return []; } @@ -3952,10 +3952,13 @@ class SCMTreeDataSource implements IAsyncDataSource { + const { showIncomingChanges, showOutgoingChanges, showChangesGraph } = this.getConfiguration(); + const graphController = element.graphController; const historyProvider = element.provider.historyProvider; + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - if (!historyProvider?.currentHistoryItemGroup) { + if (!currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || !showChangesGraph) { return []; } @@ -3964,7 +3967,12 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.alwaysShowRepositories'), showActionButton: this.configurationService.getValue('scm.showActionButton'), showChangesSummary: this.configurationService.getValue('scm.showChangesSummary'), showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), - showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges') + showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), + showChangesGraph: this.configurationService.getValue('scm.showChangesGraph') }; } From 705a812062447a4df2198a81f352e81065c7849f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:42:20 +0200 Subject: [PATCH 21/33] Fix condition --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 1f8e8ae7116df..057a370c3edc3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3968,7 +3968,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Tue, 18 Jun 2024 09:16:52 +0200 Subject: [PATCH 22/33] Saving my changes --- extensions/git/src/historyProvider.ts | 28 +++++++++++++------ extensions/git/src/repository.ts | 2 +- .../contrib/scm/browser/scmViewPane.ts | 10 +++---- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 2d24e15d51f7e..b9af5e179f22b 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -113,7 +113,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - if (!options.cursor) { + if (!this.currentHistoryItemGroup) { return []; } @@ -125,17 +125,25 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec refNames.add(this.currentHistoryItemGroup.base.name); } - // TODO@lszomoru - see if there is a better to do this - const [incoming, outgoing, ancestor] = await Promise.all([ - this.repository.log({ range: `${this.currentHistoryItemGroup?.name}..${this.currentHistoryItemGroup?.base?.name}`, refNames: Array.from(refNames) }), - this.repository.log({ range: `${this.currentHistoryItemGroup?.base?.name}..${this.currentHistoryItemGroup?.name}`, refNames: Array.from(refNames) }), - this.repository.getCommit(options.cursor) - ]); + // TODO@lszomoru - handle the scenario in which there is no default branch + const defaultBranch = await this.repository.getDefaultBranch(); + if (!defaultBranch) { + return []; + } + + const defaultBranchName = `${defaultBranch.remote}/${defaultBranch.name}`; + const ancestor = await this.repository.getMergeBase(this.currentHistoryItemGroup?.name, defaultBranchName); + if (!ancestor) { + return []; + } + + refNames.add(defaultBranchName); + const commits = await this.repository.log({ range: `${ancestor}^..`, refNames: Array.from(refNames) }); await ensureEmojis(); const historyItems: SourceControlHistoryItem[] = []; - historyItems.push(...[...outgoing, ...incoming, ancestor].map(commit => { + historyItems.push(...commits.map(commit => { const newLineIndex = commit.message.indexOf('\n'); const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; @@ -152,7 +160,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec continue; } - labels.push(label); + if (refNames.has(label)) { + labels.push(label); + } } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 3863e74872602..bfbc236017389 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1499,7 +1499,7 @@ export class Repository implements Disposable { return undefined; } - private async getDefaultBranch(): Promise { + async getDefaultBranch(): Promise { try { const defaultBranchResult = await this.repository.exec(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']); if (defaultBranchResult.stdout.trim() === '' || defaultBranchResult.stderr) { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 057a370c3edc3..854470713c4c4 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3967,12 +3967,12 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Tue, 18 Jun 2024 21:53:48 +0200 Subject: [PATCH 23/33] More polish and clean-up --- extensions/git/src/historyProvider.ts | 85 +++++++++++-------- src/vs/workbench/api/browser/mainThreadSCM.ts | 15 +++- .../workbench/api/common/extHost.protocol.ts | 9 ++ src/vs/workbench/api/common/extHostSCM.ts | 29 ++++--- .../contrib/scm/browser/media/scm.css | 17 ++-- .../contrib/scm/browser/scmViewPane.ts | 47 +++++----- .../workbench/contrib/scm/common/history.ts | 8 +- .../contrib/scm/common/scmHistory.ts | 12 +-- .../vscode.proposed.scmHistoryProvider.d.ts | 8 +- 9 files changed, 133 insertions(+), 97 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index b9af5e179f22b..3c167705e4fee 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, dispose, filterEvent } from './util'; import { toGitUri } from './uri'; import { Branch, RefType, UpstreamRef } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Operation } from './operation'; +import { Commit } from './git'; export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { @@ -113,31 +114,23 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup) { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds?.length) { return []; } - const refNames = new Set(); - if (this.currentHistoryItemGroup?.name) { - refNames.add(this.currentHistoryItemGroup.name); - } - if (this.currentHistoryItemGroup?.base?.name) { - refNames.add(this.currentHistoryItemGroup.base.name); - } - - // TODO@lszomoru - handle the scenario in which there is no default branch - const defaultBranch = await this.repository.getDefaultBranch(); - if (!defaultBranch) { + const baseBranch = await this.repository.getBranchBase(this.currentHistoryItemGroup.name); + if (!baseBranch) { return []; } - const defaultBranchName = `${defaultBranch.remote}/${defaultBranch.name}`; - const ancestor = await this.repository.getMergeBase(this.currentHistoryItemGroup?.name, defaultBranchName); + const baseBranchName = `${baseBranch.remote}/${baseBranch.name}`; + const ancestor = await this.repository.getMergeBase(this.currentHistoryItemGroup.name, baseBranchName); if (!ancestor) { return []; } - refNames.add(defaultBranchName); + // TODO@lszomoru - we need to sort the commits + const refNames = new Set([...options.historyItemGroupIds, baseBranchName]); const commits = await this.repository.log({ range: `${ancestor}^..`, refNames: Array.from(refNames) }); await ensureEmojis(); @@ -147,24 +140,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const newLineIndex = commit.message.indexOf('\n'); const subject = newLineIndex !== -1 ? commit.message.substring(0, newLineIndex) : commit.message; - const labels: string[] = []; - for (const label of commit.refNames) { - if (label === 'origin/HEAD') { - continue; - } - - if (label !== '') { - if (label.startsWith('HEAD -> ')) { - labels.push('HEAD'); - labels.push(label.substring(8)); - continue; - } - - if (refNames.has(label)) { - labels.push(label); - } - } - } + const labels = this.resolveHistoryItemLabels(commit, refNames); return { id: commit.hash, @@ -260,6 +236,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } + private resolveHistoryItemLabels(commit: Commit, refNames: Set): SourceControlHistoryItemLabel[] { + const labels: SourceControlHistoryItemLabel[] = []; + + for (const label of commit.refNames) { + if (label === 'origin/HEAD' || label === '') { + continue; + } + + if (label.startsWith('HEAD -> ')) { + labels.push( + { + title: label.substring(8), + icon: new ThemeIcon('git-branch') + } + ); + continue; + } + + if (refNames.has(label)) { + if (label.startsWith('tag: ')) { + labels.push({ + title: label.substring(5), + icon: new ThemeIcon('tag') + }); + } else if (label.startsWith('origin/')) { + labels.push({ + title: label, + icon: new ThemeIcon('cloud') + }); + } else { + labels.push({ + title: label, + icon: new ThemeIcon('git-branch') + }); + } + } + } + + return labels; + } + private async resolveHistoryItemGroupBase(historyItemId: string): Promise { try { // Upstream diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 2ee21805696a7..a673b7ea73148 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { observableValue } from 'vs/base/common/observable'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -43,6 +43,13 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { + const icon = getIconFromIconDto(historyItemDto.icon); + const labels = historyItemDto.labels?.map(l => ({ title: l.title, icon: getIconFromIconDto(l.icon) })); + + return { ...historyItemDto, icon, labels }; +} + class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { constructor( textModelService: ITextModelService, @@ -177,17 +184,17 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { async provideHistoryItems(historyItemGroupId: string, options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, historyItemGroupId, options, CancellationToken.None); - return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); } async provideHistoryItems2(options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems2(this.handle, options, CancellationToken.None); - return historyItems?.map(historyItem => ({ ...historyItem, icon: getIconFromIconDto(historyItem.icon) })); + return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); } async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { const historyItem = await this.proxy.$provideHistoryItemSummary(this.handle, historyItemId, historyItemParentId, CancellationToken.None); - return historyItem ? { ...historyItem, icon: getIconFromIconDto(historyItem.icon) } : undefined; + return historyItem ? toISCMHistoryItem(historyItem) : undefined; } async provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 338555d67ae8d..388acc5240765 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1512,6 +1512,15 @@ export interface SCMHistoryItemDto { readonly author?: string; readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; readonly timestamp?: number; + readonly statistics?: { + readonly files: number; + readonly insertions: number; + readonly deletions: number; + }; + readonly labels?: { + readonly title: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + }[]; } export interface SCMHistoryItemChangeDto { diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 47ae6688fb44a..8b9dd0e6f1be4 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -58,19 +58,26 @@ function getIconResource(decorations?: vscode.SourceControlResourceThemableDecor } } -function getHistoryItemIconDto(historyItem: vscode.SourceControlHistoryItem): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { - if (!historyItem.icon) { +function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined): UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon | undefined { + if (!icon) { return undefined; - } else if (URI.isUri(historyItem.icon)) { - return historyItem.icon; - } else if (ThemeIcon.isThemeIcon(historyItem.icon)) { - return historyItem.icon; + } else if (URI.isUri(icon)) { + return icon; + } else if (ThemeIcon.isThemeIcon(icon)) { + return icon; } else { - const icon = historyItem.icon as { light: URI; dark: URI }; - return { light: icon.light, dark: icon.dark }; + const iconDto = icon as { light: URI; dark: URI }; + return { light: iconDto.light, dark: iconDto.dark }; } } +function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { + const icon = getHistoryItemIconDto(historyItem.icon); + const labels = historyItem.labels?.map(l => ({ title: l.title, icon: getHistoryItemIconDto(l.icon) })); + + return { ...historyItem, icon, labels }; +} + function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { if (!a.iconPath && !b.iconPath) { return 0; @@ -972,14 +979,14 @@ export class ExtHostSCM implements ExtHostSCMShape { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(historyItemGroupId, options, token); - return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; } async $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems2(options, token); - return historyItems?.map(item => ({ ...item, icon: getHistoryItemIconDto(item) })) ?? undefined; + return historyItems?.map(item => toSCMHistoryItemDto(item)) ?? undefined; } async $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { @@ -989,7 +996,7 @@ export class ExtHostSCM implements ExtHostSCMShape { } const historyItem = await historyProvider.provideHistoryItemSummary(historyItemId, historyItemParentId, token); - return historyItem ? { ...historyItem, icon: getHistoryItemIconDto(historyItem) } : undefined; + return historyItem ? toSCMHistoryItemDto(historyItem) : undefined; } async $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 9b3e81857402c..a84c1dd22c972 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -135,23 +135,18 @@ height: 22px; } +.scm-view .monaco-list-row .history-item > .graph-container > .graph > circle { + stroke: var(--vscode-sideBar-background); +} + .scm-view .monaco-list-row .history-item > .label-container { display: flex; + opacity: 0.75; flex-shrink: 0; gap: 4px; } -.scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label { - opacity: 0.75; -} - -.scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label .monaco-highlighted-label { - display: block; - height: 22px; - /*padding: 1px 0;*/ -} - -.scm-view .monaco-list-row .history-item > .label-container > .monaco-icon-label .monaco-highlighted-label .codicon { +.scm-view .monaco-list-row .history-item > .label-container > .codicon { font-size: 14px; border: 1px solid var(--vscode-scm-historyItemStatisticsBorder); border-radius: 2px; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 854470713c4c4..ac46d6d56927d 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1018,6 +1018,10 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer 0) { - const label = localize('syncSeparatorHeader', "Incoming/Outgoing"); - const ariaLabel = localize('syncSeparatorHeaderAriaLabel', "Incoming and outgoing changes"); + const label = localize('historySeparatorHeader', "History"); + const ariaLabel = localize('historySeparatorHeaderAriaLabel', "History"); children.push({ label, ariaLabel, repository: inputOrElement, type: 'separator' } satisfies SCMViewSeparatorElement); } @@ -3830,13 +3827,13 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showIncomingChanges, showOutgoingChanges, showChangesGraph } = this.getConfiguration(); + const { showIncomingChanges, showOutgoingChanges, showHistoryGraph } = this.getConfiguration(); const scmProvider = element.provider; const historyProvider = scmProvider.historyProvider; const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showChangesGraph) { + if (!historyProvider || !currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || showHistoryGraph) { return []; } @@ -3952,13 +3949,13 @@ class SCMTreeDataSource implements IAsyncDataSource { - const { showIncomingChanges, showOutgoingChanges, showChangesGraph } = this.getConfiguration(); + const { showHistoryGraph } = this.getConfiguration(); const graphController = element.graphController; const historyProvider = element.provider.historyProvider; const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - if (!currentHistoryItemGroup || (showIncomingChanges === 'never' && showOutgoingChanges === 'never') || !showChangesGraph) { + if (!currentHistoryItemGroup || !showHistoryGraph) { return []; } @@ -3967,12 +3964,8 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.alwaysShowRepositories'), @@ -4091,7 +4084,7 @@ class SCMTreeDataSource implements IAsyncDataSource('scm.showChangesSummary'), showIncomingChanges: this.configurationService.getValue('scm.showIncomingChanges'), showOutgoingChanges: this.configurationService.getValue('scm.showOutgoingChanges'), - showChangesGraph: this.configurationService.getValue('scm.showChangesGraph') + showHistoryGraph: this.configurationService.getValue('scm.showHistoryGraph') }; } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 2d975d2c865ef..aae07929771de 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -43,6 +43,7 @@ export interface ISCMHistoryProviderCacheEntry { export interface ISCMHistoryOptions { readonly cursor?: string; readonly limit?: number | { id?: string }; + readonly historyItemGroupIds?: readonly string[]; } export interface ISCMHistoryItemGroup { @@ -70,6 +71,11 @@ export interface ISCMHistoryItemStatistics { readonly deletions: number; } +export interface ISCMHistoryItemLabel { + readonly title: string; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; +} + export interface ISCMHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -78,7 +84,7 @@ export interface ISCMHistoryItem { readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; - readonly labels?: string[]; + readonly labels?: ISCMHistoryItemLabel[]; } export interface ISCMHistoryItemGraphNode2 { diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts index 18929d271dac4..adb4b6b063e35 100644 --- a/src/vs/workbench/contrib/scm/common/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -25,13 +25,12 @@ function createPath(stroke: string): SVGPathElement { return path; } -function drawCircle(index: number, radius: number, stroke: string, fill: string): SVGCircleElement { +function drawCircle(index: number, radius: number, fill: string): SVGCircleElement { const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`); circle.setAttribute('cy', `${SWIMLANE_WIDTH}`); circle.setAttribute('r', `${radius}`); circle.setAttribute('fill', fill); - circle.setAttribute('stroke', stroke); return circle; } @@ -170,17 +169,14 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV // Draw * if (historyItem.parentIds.length > 1) { // Multi-parent node - // TODO@lszomoru - remove hardcoded color - const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, '#f8f8f8', graphColors[circleColorIndex]); + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, graphColors[circleColorIndex]); svg.append(circleOuter); - // TODO@lszomoru - remove hardcoded color - const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, '#f8f8f8', graphColors[circleColorIndex]); + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, graphColors[circleColorIndex]); svg.append(circleInner); } else { // Node - // TODO@lszomoru - remove hardcoded color - const circle = drawCircle(circleIndex, CIRCLE_RADIUS, '#f8f8f8', graphColors[circleColorIndex]); + const circle = drawCircle(circleIndex, CIRCLE_RADIUS, graphColors[circleColorIndex]); svg.append(circle); } diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 81dcc7760986f..7b90fa087bc6c 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -35,6 +35,7 @@ declare module 'vscode' { export interface SourceControlHistoryOptions { readonly cursor?: string; readonly limit?: number | { id?: string }; + readonly historyItemGroupIds?: readonly string[]; } export interface SourceControlHistoryItemGroup { @@ -54,6 +55,11 @@ declare module 'vscode' { readonly deletions: number; } + export interface SourceControlHistoryItemLabel { + readonly title: string; + readonly icon: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -62,7 +68,7 @@ declare module 'vscode' { readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; readonly timestamp?: number; readonly statistics?: SourceControlHistoryItemStatistics; - readonly labels?: string[]; + readonly labels?: SourceControlHistoryItemLabel[]; } export interface SourceControlHistoryItemChange { From 55facfcf314f004acaaa30d832738880db7ab92e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:56:31 +0200 Subject: [PATCH 24/33] Remove code that is not needed --- extensions/git/src/git.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 096046bc56152..0d27026c9ea1e 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1167,8 +1167,6 @@ export class Repository { if (options?.refNames) { args.push('--topo-order'); - // args.push('--all'); - // args.push('--branches=*'); args.push(...options.refNames); } From 5539cc4ce8427ba4984772b85f3edaf3bdd392b4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:59:21 +0200 Subject: [PATCH 25/33] Revert change --- extensions/git/src/repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index bfbc236017389..3863e74872602 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1499,7 +1499,7 @@ export class Repository implements Disposable { return undefined; } - async getDefaultBranch(): Promise { + private async getDefaultBranch(): Promise { try { const defaultBranchResult = await this.repository.exec(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']); if (defaultBranchResult.stdout.trim() === '' || defaultBranchResult.stderr) { From 8b264cbae0cfbe3ba4758635af984ca0626e0d70 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:12:22 +0200 Subject: [PATCH 26/33] Revert more changes --- .../contrib/scm/browser/scmViewPane.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index ac46d6d56927d..d5ffbf70d2d7a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1317,7 +1317,7 @@ export class SCMTreeSorter implements ITreeSorter { } if (isSCMViewSeparator(one)) { - return 0;// isSCMResourceGroup(other) ? 1 : -1; + return isSCMResourceGroup(other) ? 1 : -1; } if (isSCMHistoryItemGroupTreeElement(one)) { @@ -1325,7 +1325,11 @@ export class SCMTreeSorter implements ITreeSorter { } if (isSCMHistoryItemTreeElement(one)) { - return isSCMHistoryItemTreeElement(other) ? 0 : 1; + if (!isSCMHistoryItemTreeElement(other)) { + throw new Error('Invalid comparison'); + } + + return 0; } if (isSCMHistoryItemViewModel(one)) { @@ -1468,8 +1472,9 @@ function getSCMResourceId(element: TreeElement): string { const provider = element.repository.provider; return `historyItemGroup:${provider.id}/${element.id}`; } else if (isSCMHistoryItemTreeElement(element)) { - const provider = element.historyItemGroup.repository.provider; - return `historyItem:${provider.id}/${element.id}/${element.parentIds.join(',')}`; + const historyItemGroup = element.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}/${element.parentIds.join(',')}`; } else if (isSCMHistoryItemViewModel(element)) { const provider = element.repository.provider; return `historyItem2:${provider.id}/${element.historyItem.id}/${element.historyItem.parentIds.join(',')}`; @@ -1485,7 +1490,7 @@ function getSCMResourceId(element: TreeElement): string { return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; } else if (isSCMViewSeparator(element)) { const provider = element.repository.provider; - return `separator:${provider.id}/${element.label}`; + return `separator:${provider.id}`; } else { throw new Error('Invalid tree element'); } @@ -3384,12 +3389,12 @@ export class SCMViewPane extends ViewPane { createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, actions); } } else if (isSCMHistoryItemTreeElement(element)) { - // const menus = this.scmViewService.menus.getRepositoryMenus(element.historyItemGroup.repository.provider); - // const menu = menus.historyProviderMenu?.getHistoryItemMenu(element); - // if (menu) { - // actionRunner = new HistoryItemActionRunner(); - // actions = collectContextMenuActions(menu); - // } + const menus = this.scmViewService.menus.getRepositoryMenus(element.historyItemGroup.repository.provider); + const menu = menus.historyProviderMenu?.getHistoryItemMenu(element); + if (menu) { + actionRunner = new HistoryItemActionRunner(); + actions = collectContextMenuActions(menu); + } } actionRunner.onWillRun(() => this.tree.domFocus()); @@ -3713,7 +3718,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Tue, 18 Jun 2024 22:21:50 +0200 Subject: [PATCH 27/33] More fixes --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d5ffbf70d2d7a..a5cd2b9a81488 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1184,6 +1184,7 @@ class SeparatorRenderer implements ICompressibleTreeRenderer('scm.showHistoryGraph') !== true) { + const toolBar = new MenuWorkbenchToolBar(append(element, $('.actions')), MenuId.SCMChangesSeparator, { moreIcon: Codicon.gear }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); + disposables.add(toolBar); + } return { label, disposables }; } @@ -3012,7 +3015,7 @@ export class SCMViewPane extends ViewPane { e.affectsConfiguration('scm.showChangesSummary') || e.affectsConfiguration('scm.showIncomingChanges') || e.affectsConfiguration('scm.showOutgoingChanges') || - e.affectsConfiguration('scm.showChangesGraph'), + e.affectsConfiguration('scm.showHistoryGraph'), this.visibilityDisposables) (() => this.updateChildren(), this, this.visibilityDisposables); From d649cd2f96ae5ac77b8b4d2e9af7ce7351b02796 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:39:28 +0200 Subject: [PATCH 28/33] Rename interface --- src/vs/workbench/contrib/scm/common/history.ts | 6 +++--- src/vs/workbench/contrib/scm/common/scmHistory.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index aae07929771de..383190c7359f2 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -87,7 +87,7 @@ export interface ISCMHistoryItem { readonly labels?: ISCMHistoryItemLabel[]; } -export interface ISCMHistoryItemGraphNode2 { +export interface ISCMHistoryItemGraphNode { readonly id: string; readonly color: number; } @@ -95,8 +95,8 @@ export interface ISCMHistoryItemGraphNode2 { export interface ISCMHistoryItemViewModel { readonly repository: ISCMRepository; readonly historyItem: ISCMHistoryItem; - readonly inputSwimlanes: ISCMHistoryItemGraphNode2[]; - readonly outputSwimlanes: ISCMHistoryItemGraphNode2[]; + readonly inputSwimlanes: ISCMHistoryItemGraphNode[]; + readonly outputSwimlanes: ISCMHistoryItemGraphNode[]; readonly type: 'historyItem2'; } diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts index adb4b6b063e35..a9a999171123b 100644 --- a/src/vs/workbench/contrib/scm/common/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -5,7 +5,7 @@ import { lastOrDefault } from 'vs/base/common/arrays'; import { deepClone } from 'vs/base/common/objects'; -import { ISCMHistoryItem, ISCMHistoryItemGraphNode2, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; const SWIMLANE_HEIGHT = 22; @@ -42,7 +42,7 @@ function drawVerticalLine(x1: number, y1: number, y2: number, color: string): SV return path; } -function findLastIndex(nodes: ISCMHistoryItemGraphNode2[], id: string): number { +function findLastIndex(nodes: ISCMHistoryItemGraphNode[], id: string): number { for (let i = nodes.length - 1; i >= 0; i--) { if (nodes[i].id === id) { return i; @@ -208,7 +208,7 @@ export class SCMRepositoryGraphController implements ISCMRepositoryGraphControll const outputSwimlanesFromPreviousItem = lastOrDefault(this.historyItems)?.outputSwimlanes ?? []; const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); - const outputSwimlanes: ISCMHistoryItemGraphNode2[] = []; + const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; if (historyItem.parentIds.length > 0) { let firstParentAdded = false; From 9ee561b9e3679fcc5409cae5ea69027d193e7d7f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:49:07 +0200 Subject: [PATCH 29/33] One last minor change --- src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 7b90fa087bc6c..1e8a58cebfa2b 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -57,7 +57,7 @@ declare module 'vscode' { export interface SourceControlHistoryItemLabel { readonly title: string; - readonly icon: Uri | { light: Uri; dark: Uri } | ThemeIcon; + readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; } export interface SourceControlHistoryItem { From 3e26775ac316694d046ea1f37d90120355c70fcf Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:10:44 +0200 Subject: [PATCH 30/33] Pull request feedback --- .../contrib/scm/browser/scmViewPane.ts | 43 ++++---- src/vs/workbench/contrib/scm/browser/util.ts | 6 +- .../workbench/contrib/scm/common/history.ts | 6 +- src/vs/workbench/contrib/scm/common/scm.ts | 2 - .../contrib/scm/common/scmHistory.ts | 101 +++++++----------- .../contrib/scm/common/scmService.ts | 3 - .../scm/test/common/scmHistory.test.ts | 32 ++---- 7 files changed, 79 insertions(+), 114 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a5cd2b9a81488..89828bf8d2529 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent, isActiveElement } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemViewModel, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemViewModel, SCMHistoryItemViewModelTreeElement, ISCMHistoryProviderCacheEntry, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, ISCMInputValueProviderContext, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -24,7 +24,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner, Action, Separator, IActionRunner } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModel } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator, connectPrimaryMenu, isSCMHistoryItemViewModelTreeElement } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, Sequencer, ThrottledDelayer, Throttler } from 'vs/base/common/async'; @@ -109,7 +109,7 @@ import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; import { autorun } from 'vs/base/common/observable'; -import { renderSCMHistoryItemGraph } from 'vs/workbench/contrib/scm/common/scmHistory'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/common/scmHistory'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; // type SCMResourceTreeNode = IResourceNode; @@ -123,7 +123,7 @@ type TreeElement = IResourceNode | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | - ISCMHistoryItemViewModel | + SCMHistoryItemViewModelTreeElement | SCMHistoryItemChangeTreeElement | IResourceNode | SCMViewSeparatorElement; @@ -1253,7 +1253,7 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemGroupRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; - } else if (isSCMHistoryItemViewModel(element)) { + } else if (isSCMHistoryItemViewModelTreeElement(element)) { return HistoryItem2Renderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; @@ -1335,8 +1335,8 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } - if (isSCMHistoryItemViewModel(one)) { - return isSCMHistoryItemViewModel(other) ? 0 : 1; + if (isSCMHistoryItemViewModelTreeElement(one)) { + return isSCMHistoryItemViewModelTreeElement(other) ? 0 : 1; } if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) { @@ -1423,11 +1423,11 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb // the author. A match in the message takes precedence over // a match in the author. return [element.message, element.author]; - } else if (isSCMHistoryItemViewModel(element)) { + } else if (isSCMHistoryItemViewModelTreeElement(element)) { // For a history item we want to match both the message and // the author. A match in the message takes precedence over // a match in the author. - return [element.historyItem.message, element.historyItem.author]; + return [element.historyItemViewModel.historyItem.message, element.historyItemViewModel.historyItem.author]; } else if (isSCMViewSeparator(element)) { return element.label; } else { @@ -1478,9 +1478,10 @@ function getSCMResourceId(element: TreeElement): string { const historyItemGroup = element.historyItemGroup; const provider = historyItemGroup.repository.provider; return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}/${element.parentIds.join(',')}`; - } else if (isSCMHistoryItemViewModel(element)) { + } else if (isSCMHistoryItemViewModelTreeElement(element)) { const provider = element.repository.provider; - return `historyItem2:${provider.id}/${element.historyItem.id}/${element.historyItem.parentIds.join(',')}`; + const historyItem = element.historyItemViewModel.historyItem; + return `historyItem2:${provider.id}/${historyItem.id}/${historyItem.parentIds.join(',')}`; } else if (isSCMHistoryItemChangeTreeElement(element)) { const historyItem = element.historyItem; const historyItemGroup = historyItem.historyItemGroup; @@ -1531,8 +1532,9 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider { + private async getHistoryItems2(element: ISCMRepository): Promise { const { showHistoryGraph } = this.getConfiguration(); - const graphController = element.graphController; const historyProvider = element.provider.historyProvider; const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; @@ -3981,10 +3982,12 @@ class SCMTreeDataSource implements IAsyncDataSource ({ + repository: element, + historyItemViewModel: v, + type: 'historyItem2' + }) satisfies SCMHistoryItemViewModelTreeElement); } private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index ff60ba0e401e7..ed24dc67bf039 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import { ISCMHistoryItemViewModel, SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemViewModelTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -62,8 +62,8 @@ export function isSCMHistoryItemTreeElement(element: any): element is SCMHistory (element as SCMHistoryItemTreeElement).type === 'historyItem'; } -export function isSCMHistoryItemViewModel(element: any): element is ISCMHistoryItemViewModel { - return (element as ISCMHistoryItemViewModel).type === 'historyItem2'; +export function isSCMHistoryItemViewModelTreeElement(element: any): element is SCMHistoryItemViewModelTreeElement { + return (element as SCMHistoryItemViewModelTreeElement).type === 'historyItem2'; } export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 383190c7359f2..7d1e65364b930 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -93,10 +93,14 @@ export interface ISCMHistoryItemGraphNode { } export interface ISCMHistoryItemViewModel { - readonly repository: ISCMRepository; readonly historyItem: ISCMHistoryItem; readonly inputSwimlanes: ISCMHistoryItemGraphNode[]; readonly outputSwimlanes: ISCMHistoryItemGraphNode[]; +} + +export interface SCMHistoryItemViewModelTreeElement { + readonly repository: ISCMRepository; + readonly historyItemViewModel: ISCMHistoryItemViewModel; readonly type: 'historyItem2'; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 2fc0303d714e7..5bcd1c6fbe72b 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -16,7 +16,6 @@ import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; import { ITextModel } from 'vs/editor/common/model'; import { IObservable } from 'vs/base/common/observable'; -import { ISCMRepositoryGraphController } from 'vs/workbench/contrib/scm/common/scmHistory'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -163,7 +162,6 @@ export interface ISCMRepository extends IDisposable { readonly id: string; readonly provider: ISCMProvider; readonly input: ISCMInput; - readonly graphController: ISCMRepositoryGraphController; } export interface ISCMService { diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/common/scmHistory.ts index a9a999171123b..4fe5689e8347c 100644 --- a/src/vs/workbench/contrib/scm/common/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/common/scmHistory.ts @@ -6,7 +6,6 @@ import { lastOrDefault } from 'vs/base/common/arrays'; import { deepClone } from 'vs/base/common/objects'; import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from 'vs/workbench/contrib/scm/common/history'; -import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; const SWIMLANE_HEIGHT = 22; const SWIMLANE_WIDTH = 11; @@ -187,75 +186,53 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV return svg; } -export interface ISCMRepositoryGraphController { - readonly historyItems: ISCMHistoryItemViewModel[]; - - appendHistoryItems(historyItems: ISCMHistoryItem[]): void; - clearHistoryItems(): void; -} - -export class SCMRepositoryGraphController implements ISCMRepositoryGraphController { - private readonly _historyItems: ISCMHistoryItemViewModel[] = []; - get historyItems(): ISCMHistoryItemViewModel[] { return this._historyItems; } - - private _colorIndex: number = -1; - - constructor(private readonly _repository: ISCMRepository) { } - - appendHistoryItems(historyItems: ISCMHistoryItem[]): void { - for (let index = 0; index < historyItems.length; index++) { - const historyItem = historyItems[index]; - - const outputSwimlanesFromPreviousItem = lastOrDefault(this.historyItems)?.outputSwimlanes ?? []; - const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); - const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; - - if (historyItem.parentIds.length > 0) { - let firstParentAdded = false; - - // Add first parent to the output - for (const node of inputSwimlanes) { - if (node.id === historyItem.id) { - if (!firstParentAdded) { - outputSwimlanes.push({ - ...deepClone(node), - id: historyItem.parentIds[0] - }); - firstParentAdded = true; - } - - continue; +export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[]): ISCMHistoryItemViewModel[] { + let colorIndex = -1; + const viewModels: ISCMHistoryItemViewModel[] = []; + + for (let index = 0; index < historyItems.length; index++) { + const historyItem = historyItems[index]; + + const outputSwimlanesFromPreviousItem = lastOrDefault(viewModels)?.outputSwimlanes ?? []; + const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); + const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; + + if (historyItem.parentIds.length > 0) { + let firstParentAdded = false; + + // Add first parent to the output + for (const node of inputSwimlanes) { + if (node.id === historyItem.id) { + if (!firstParentAdded) { + outputSwimlanes.push({ + ...deepClone(node), + id: historyItem.parentIds[0] + }); + firstParentAdded = true; } - outputSwimlanes.push(deepClone(node)); + continue; } - // Add unprocessed parent(s) to the output - for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { - outputSwimlanes.push({ - id: historyItem.parentIds[i], - color: this.getGraphColorIndex() - }); - } + outputSwimlanes.push(deepClone(node)); } - this._historyItems.push({ - historyItem, - inputSwimlanes, - outputSwimlanes, - repository: this._repository, - type: 'historyItem2' - }); + // Add unprocessed parent(s) to the output + for (let i = firstParentAdded ? 1 : 0; i < historyItem.parentIds.length; i++) { + colorIndex = colorIndex < graphColors.length - 1 ? colorIndex + 1 : 1; + outputSwimlanes.push({ + id: historyItem.parentIds[i], + color: colorIndex + }); + } } - } - clearHistoryItems(): void { - this._colorIndex = -1; - this._historyItems.length = 0; + viewModels.push({ + historyItem, + inputSwimlanes, + outputSwimlanes, + }); } - private getGraphColorIndex(): number { - this._colorIndex = this._colorIndex < graphColors.length - 1 ? this._colorIndex + 1 : 1; - return this._colorIndex; - } + return viewModels; } diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index fbde1b9f78f62..b341a2029aec3 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -17,7 +17,6 @@ import { Iterable } from 'vs/base/common/iterator'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { ISCMRepositoryGraphController, SCMRepositoryGraphController } from 'vs/workbench/contrib/scm/common/scmHistory'; class SCMInput extends Disposable implements ISCMInput { @@ -185,7 +184,6 @@ class SCMRepository implements ISCMRepository { readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; readonly input: ISCMInput; - readonly graphController: ISCMRepositoryGraphController; constructor( public readonly id: string, @@ -194,7 +192,6 @@ class SCMRepository implements ISCMRepository { inputHistory: SCMInputHistory ) { this.input = new SCMInput(this, inputHistory); - this.graphController = new SCMRepositoryGraphController(this); } setSelected(selected: boolean): void { diff --git a/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts index b47d407236293..c0e4b806d552e 100644 --- a/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts @@ -6,22 +6,14 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ISCMHistoryItem } from 'vs/workbench/contrib/scm/common/history'; -import { ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; -import { SCMRepositoryGraphController } from 'vs/workbench/contrib/scm/common/scmHistory'; +import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/common/scmHistory'; -suite('SCMRepositoryGraphController', () => { +suite('toISCMHistoryItemViewModelArray', () => { ensureNoDisposablesAreLeakedInTestSuite(); - const controller = new SCMRepositoryGraphController({} as ISCMRepository); - - teardown(() => { - controller.clearHistoryItems(); - }); - test('empty graph', () => { - controller.appendHistoryItems([]); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray([]); assert.strictEqual(viewModels.length, 0); }); @@ -36,8 +28,7 @@ suite('SCMRepositoryGraphController', () => { { id: 'a', parentIds: [], message: '' }, ] as ISCMHistoryItem[]; - controller.appendHistoryItems(models); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray(models); assert.strictEqual(viewModels.length, 1); @@ -61,8 +52,7 @@ suite('SCMRepositoryGraphController', () => { { id: 'e', parentIds: [] }, ] as ISCMHistoryItem[]; - controller.appendHistoryItems(models); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray(models); assert.strictEqual(viewModels.length, 5); @@ -126,8 +116,7 @@ suite('SCMRepositoryGraphController', () => { { id: 'e', parentIds: ['f'] }, ] as ISCMHistoryItem[]; - controller.appendHistoryItems(models); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray(models); assert.strictEqual(viewModels.length, 5); @@ -202,8 +191,7 @@ suite('SCMRepositoryGraphController', () => { { id: 'd', parentIds: ['g'] }, ] as ISCMHistoryItem[]; - controller.appendHistoryItems(models); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray(models); assert.strictEqual(viewModels.length, 6); @@ -301,8 +289,7 @@ suite('SCMRepositoryGraphController', () => { { id: 'd', parentIds: ['h'] }, ] as ISCMHistoryItem[]; - controller.appendHistoryItems(models); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray(models); assert.strictEqual(viewModels.length, 6); @@ -408,8 +395,7 @@ suite('SCMRepositoryGraphController', () => { { id: 'g', parentIds: ['h'] }, ] as ISCMHistoryItem[]; - controller.appendHistoryItems(models); - const viewModels = controller.historyItems; + const viewModels = toISCMHistoryItemViewModelArray(models); assert.strictEqual(viewModels.length, 7); From 5423207a1f0468b4ac5b01b10577e84fe03f732d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:21:27 +0200 Subject: [PATCH 31/33] More refactoring --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 89828bf8d2529..4a2c273c33845 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1013,7 +1013,7 @@ interface HistoryItem2Template { readonly disposables: IDisposable; } -class HistoryItem2Renderer implements ICompressibleTreeRenderer { +class HistoryItem2Renderer implements ICompressibleTreeRenderer { static readonly TEMPLATE_ID = 'history-item-2'; get templateId(): string { return HistoryItem2Renderer.TEMPLATE_ID; } @@ -1037,8 +1037,8 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItem2Template, height: number | undefined): void { - const historyItemViewModel = node.element; + renderElement(node: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + const historyItemViewModel = node.element.historyItemViewModel; const historyItem = historyItemViewModel.historyItem; templateData.graphContainer.textContent = ''; @@ -1061,7 +1061,7 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + renderCompressedElements(node: ITreeNode, LabelFuzzyScore>, index: number, templateData: HistoryItem2Template, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } @@ -1094,7 +1094,7 @@ class HistoryItem2Renderer implements ICompressibleTreeRenderer, index: number, templateData: HistoryItem2Template, height: number | undefined): void { + disposeElement(element: ITreeNode, index: number, templateData: HistoryItem2Template, height: number | undefined): void { templateData.elementDisposables.clear(); } From 2fe669e5d06547deae1f81e244062498a8700f71 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:25:52 +0200 Subject: [PATCH 32/33] More pull request feedback --- extensions/git/src/historyProvider.ts | 58 ++++++++++++++----- src/vs/workbench/api/browser/mainThreadSCM.ts | 4 ++ .../workbench/api/common/extHost.protocol.ts | 1 + src/vs/workbench/api/common/extHostSCM.ts | 7 ++- .../contrib/scm/browser/scmViewPane.ts | 8 ++- .../workbench/contrib/scm/common/history.ts | 1 + .../vscode.proposed.scmHistoryProvider.d.ts | 8 +-- 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 3c167705e4fee..df86c284a3b1c 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -114,24 +114,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems2(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup || !options.historyItemGroupIds?.length) { + if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { return []; } - const baseBranch = await this.repository.getBranchBase(this.currentHistoryItemGroup.name); - if (!baseBranch) { - return []; - } + // Deduplicate refNames + const refNames = new Set(options.historyItemGroupIds); - const baseBranchName = `${baseBranch.remote}/${baseBranch.name}`; - const ancestor = await this.repository.getMergeBase(this.currentHistoryItemGroup.name, baseBranchName); - if (!ancestor) { + // Get the merge base of the refNames + const refsMergeBase = await this.resolveHistoryItemGroupsMergeBase(refNames); + if (!refsMergeBase) { return []; } - // TODO@lszomoru - we need to sort the commits - const refNames = new Set([...options.historyItemGroupIds, baseBranchName]); - const commits = await this.repository.log({ range: `${ancestor}^..`, refNames: Array.from(refNames) }); + // Get the commits + const commits = await this.repository.log({ range: `${refsMergeBase}^..`, refNames: Array.from(refNames) }); await ensureEmojis(); @@ -204,9 +201,23 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItemChanges; } + async resolveHistoryItemGroupBase(historyItemGroupId: string): Promise { + // Base (config -> reflog -> default) + const remoteBranch = await this.repository.getBranchBase(historyItemGroupId); + if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) { + this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemGroupId}'`); + return undefined; + } + + return { + id: `refs/remotes/${remoteBranch.remote}/${remoteBranch.name}`, + name: `${remoteBranch.remote}/${remoteBranch.name}`, + }; + } + async resolveHistoryItemGroupCommonAncestor(historyItemId1: string, historyItemId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { if (!historyItemId2) { - const upstreamRef = await this.resolveHistoryItemGroupBase(historyItemId1); + const upstreamRef = await this.resolveHistoryItemGroupUpstreamOrBase(historyItemId1); if (!upstreamRef) { this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupCommonAncestor - Failed to resolve history item group base for '${historyItemId1}'`); return undefined; @@ -236,6 +247,23 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } + private async resolveHistoryItemGroupsMergeBase(refNames: Set): Promise { + let refsMergeBase: string | undefined = undefined; + + for (const refName of refNames) { + if (refsMergeBase === undefined) { + const commit = await this.repository.revParse(refName); + refsMergeBase = commit ?? refName; + continue; + } + + const newMergeBase = await this.repository.getMergeBase(refsMergeBase, refName); + refsMergeBase = newMergeBase ?? refsMergeBase; + } + + return refsMergeBase; + } + private resolveHistoryItemLabels(commit: Commit, refNames: Set): SourceControlHistoryItemLabel[] { const labels: SourceControlHistoryItemLabel[] = []; @@ -277,7 +305,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return labels; } - private async resolveHistoryItemGroupBase(historyItemId: string): Promise { + private async resolveHistoryItemGroupUpstreamOrBase(historyItemId: string): Promise { try { // Upstream const branch = await this.repository.getBranch(historyItemId); @@ -288,7 +316,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Base (config -> reflog -> default) const remoteBranch = await this.repository.getBranchBase(historyItemId); if (!remoteBranch?.remote || !remoteBranch?.name || !remoteBranch?.commit || remoteBranch?.type !== RefType.RemoteHead) { - this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to resolve history item group base for '${historyItemId}'`); + this.logger.info(`GitHistoryProvider:resolveHistoryItemGroupUpstreamOrBase - Failed to resolve history item group base for '${historyItemId}'`); return undefined; } @@ -299,7 +327,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }; } catch (err) { - this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupBase - Failed to get branch base for '${historyItemId}': ${err.message}`); + this.logger.error(`GitHistoryProvider:resolveHistoryItemGroupUpstreamOrBase - Failed to get branch base for '${historyItemId}': ${err.message}`); } return undefined; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index a673b7ea73148..4bd3af2d38827 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -178,6 +178,10 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } + async resolveHistoryItemGroupBase(historyItemGroupId: string): Promise { + return this.proxy.$resolveHistoryItemGroupBase(this.handle, historyItemGroupId, CancellationToken.None); + } + async resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined> { return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupId1, historyItemGroupId2, CancellationToken.None); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 49135633a20a4..935f7fca1ac4c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2316,6 +2316,7 @@ export interface ExtHostSCMShape { $provideHistoryItems2(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemSummary(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; + $resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined>; } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 8b9dd0e6f1be4..a0284629323c7 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { asPromise } from 'vs/base/common/async'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemGroupDto } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; import type * as vscode from 'vscode'; @@ -970,6 +970,11 @@ export class ExtHostSCM implements ExtHostSCMShape { return Promise.resolve(undefined); } + async $resolveHistoryItemGroupBase(sourceControlHandle: number, historyItemGroupId: string, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + return await historyProvider?.resolveHistoryItemGroupBase(historyItemGroupId, token) ?? undefined; + } + async $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): Promise<{ id: string; ahead: number; behind: number } | undefined> { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupId1, historyItemGroupId2, token) ?? undefined; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 4a2c273c33845..f9e4b957148f3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -3973,7 +3973,13 @@ class SCMTreeDataSource implements IAsyncDataSource; provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; + resolveHistoryItemGroupBase(historyItemGroupId: string): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined): Promise<{ id: string; ahead: number; behind: number } | undefined>; } diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 1e8a58cebfa2b..ba720bb90caa5 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -29,6 +29,7 @@ declare module 'vscode' { provideHistoryItemSummary?(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; + resolveHistoryItemGroupBase(historyItemGroupId: string, token: CancellationToken): ProviderResult; resolveHistoryItemGroupCommonAncestor(historyItemGroupId1: string, historyItemGroupId2: string | undefined, token: CancellationToken): ProviderResult<{ id: string; ahead: number; behind: number }>; } @@ -41,12 +42,7 @@ declare module 'vscode' { export interface SourceControlHistoryItemGroup { readonly id: string; readonly name: string; - readonly base?: Omit; - } - - export interface SourceControlRemoteHistoryItemGroup { - readonly id: string; - readonly name: string; + readonly base?: Omit; } export interface SourceControlHistoryItemStatistics { From eae901115afdcd8d77592893845a9667b504fb0b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:34:05 +0200 Subject: [PATCH 33/33] Fix layering issues --- src/vs/workbench/contrib/scm/{common => browser}/scmHistory.ts | 0 src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- .../contrib/scm/test/{common => browser}/scmHistory.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/vs/workbench/contrib/scm/{common => browser}/scmHistory.ts (100%) rename src/vs/workbench/contrib/scm/test/{common => browser}/scmHistory.test.ts (99%) diff --git a/src/vs/workbench/contrib/scm/common/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts similarity index 100% rename from src/vs/workbench/contrib/scm/common/scmHistory.ts rename to src/vs/workbench/contrib/scm/browser/scmHistory.ts diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f9e4b957148f3..7102acab96e7e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -109,8 +109,8 @@ import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import { ITextModel } from 'vs/editor/common/model'; import { autorun } from 'vs/base/common/observable'; -import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/common/scmHistory'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; diff --git a/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts similarity index 99% rename from src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts rename to src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts index c0e4b806d552e..5c64ccda40249 100644 --- a/src/vs/workbench/contrib/scm/test/common/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/browser/scmHistory'; import { ISCMHistoryItem } from 'vs/workbench/contrib/scm/common/history'; -import { toISCMHistoryItemViewModelArray } from 'vs/workbench/contrib/scm/common/scmHistory'; suite('toISCMHistoryItemViewModelArray', () => {