From 0439d723e2fa3d25d2b61a2c8afd8befadfbe9d7 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 13 May 2023 19:22:22 -0400 Subject: [PATCH] Defers Graph stats loading for Changes/minimap Improves the Graph minimap - Adds `gitlens.graph.minimap.dataType` setting to choose between commits or lines changed to be used for the minimap - Adds a configuration popover for the minimap - Adds a legend to the minimap - Removes minimap from being experimental - Renames `gitlens.graph.experimental.minimap.enabled` to `gitlens.graph.minimap.enabled` - Renames `gitlens.graph.experimental.minimap.additionalTypes` to `gitlens.graph.minimap.additionalTypes` --- package.json | 41 +++- src/config.ts | 32 +-- src/env/node/git/localGitProvider.ts | 64 ++++- src/git/models/graph.ts | 6 +- src/git/parsers/logParser.ts | 20 +- src/plus/github/githubGitProvider.ts | 26 +- src/plus/webviews/graph/graphWebview.ts | 112 ++++++--- src/plus/webviews/graph/protocol.ts | 63 +++-- src/webviews/apps/plus/graph/GraphWrapper.tsx | 273 ++++++++++++++++----- src/webviews/apps/plus/graph/graph.scss | 55 +++-- src/webviews/apps/plus/graph/graph.tsx | 13 + src/webviews/apps/plus/graph/minimap/minimap.ts | 80 ++++-- .../apps/shared/components/menu/menu-label.ts | 1 + .../components/overlays/pop-menu/pop-menu.ts | 43 +++- .../apps/shared/components/search/search-input.ts | 34 ++- yarn.lock | 8 +- 16 files changed, 649 insertions(+), 222 deletions(-) diff --git a/package.json b/package.json index 37e6779..62e8277 100644 --- a/package.json +++ b/package.json @@ -2189,14 +2189,14 @@ "enum": [ "localBranches", "remoteBranches", - "tags", - "stashes" + "stashes", + "tags" ], "enumDescriptions": [ "Marks the location of local branches", "Marks the location of remote branches", - "Marks the location of tags", - "Marks the location of stashes" + "Marks the location of stashes", + "Marks the location of tags" ] }, "minItems": 0, @@ -2349,14 +2349,29 @@ "scope": "window", "order": 99 }, - "gitlens.graph.experimental.minimap.enabled": { + "gitlens.graph.minimap.enabled": { "type": "boolean", "default": false, - "markdownDescription": "Specifies whether to show an experimental minimap of commit activity above the _Commit Graph_", + "markdownDescription": "Specifies whether to show a minimap of commit activity above the _Commit Graph_", "scope": "window", "order": 100 }, - "gitlens.graph.experimental.minimap.additionalTypes": { + "gitlens.graph.minimap.dataType": { + "type": "string", + "default": "commits", + "enum": [ + "commits", + "lines" + ], + "enumDescriptions": [ + "Shows the number of commits per day in the minimap", + "Shows the number of lines changed per day in the minimap" + ], + "markdownDescription": "Specifies the data to show on the minimap in the _Commit Graph_", + "scope": "window", + "order": 101 + }, + "gitlens.graph.minimap.additionalTypes": { "type": "array", "default": [ "localBranches", @@ -2367,14 +2382,14 @@ "enum": [ "localBranches", "remoteBranches", - "tags", - "stashes" + "stashes", + "tags" ], "enumDescriptions": [ "Marks the location of local branches", "Marks the location of remote branches", - "Marks the location of tags", - "Marks the location of stashes" + "Marks the location of stashes", + "Marks the location of tags" ] }, "minItems": 0, @@ -2382,7 +2397,7 @@ "uniqueItems": true, "markdownDescription": "Specifies additional markers to show on the minimap in the _Commit Graph_", "scope": "window", - "order": 101 + "order": 102 } } }, @@ -14041,7 +14056,7 @@ "vscode:prepublish": "yarn run bundle" }, "dependencies": { - "@gitkraken/gitkraken-components": "9.1.5", + "@gitkraken/gitkraken-components": "10.0.0", "@microsoft/fast-element": "1.12.0", "@microsoft/fast-react-wrapper": "0.3.18", "@octokit/core": "4.2.1", diff --git a/src/config.ts b/src/config.ts index 707792c..a0e5c94 100644 --- a/src/config.ts +++ b/src/config.ts @@ -305,25 +305,8 @@ export const enum GitCommandSorting { Usage = 'usage', } -export const enum GraphScrollMarkerTypes { - Selection = 'selection', - Head = 'head', - LocalBranches = 'localBranches', - RemoteBranches = 'remoteBranches', - Highlights = 'highlights', - Stashes = 'stashes', - Tags = 'tags', -} - -export const enum GraphMinimapTypes { - Selection = 'selection', - Head = 'head', - LocalBranches = 'localBranches', - RemoteBranches = 'remoteBranches', - Highlights = 'highlights', - Stashes = 'stashes', - Tags = 'tags', -} +export type GraphScrollMarkersAdditionalTypes = 'localBranches' | 'remoteBranches' | 'stashes' | 'tags'; +export type GraphMinimapMarkersAdditionalTypes = 'localBranches' | 'remoteBranches' | 'stashes' | 'tags'; export const enum GravatarDefaultStyle { Faces = 'wavatar', @@ -429,11 +412,10 @@ export interface GraphConfig { dateStyle: DateStyle | null; defaultItemLimit: number; dimMergeCommits: boolean; - experimental: { - minimap: { - enabled: boolean; - additionalTypes: GraphMinimapTypes[]; - }; + minimap: { + enabled: boolean; + dataType: 'commits' | 'lines'; + additionalTypes: GraphMinimapMarkersAdditionalTypes[]; }; highlightRowsOnRefHover: boolean; layout: 'editor' | 'panel'; @@ -442,7 +424,7 @@ export interface GraphConfig { showGhostRefsOnRowHover: boolean; scrollMarkers: { enabled: boolean; - additionalTypes: GraphScrollMarkerTypes[]; + additionalTypes: GraphScrollMarkersAdditionalTypes[]; }; pullRequests: { enabled: boolean; diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index a3d0a1d..06055ef 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -69,6 +69,8 @@ import type { GitGraphRowContexts, GitGraphRowHead, GitGraphRowRemoteHead, + GitGraphRowsStats, + GitGraphRowStats, GitGraphRowTag, } from '../../../git/models/graph'; import { GitGraphRowType } from '../../../git/models/graph'; @@ -109,6 +111,7 @@ import { createLogParserWithFiles, getContributorsParser, getGraphParser, + getGraphStatsParser, getRefAndDateParser, getRefParser, GitLogParser, @@ -1723,13 +1726,16 @@ export class LocalGitProvider implements GitProvider, Disposable { ref?: string; }, ): Promise { - const parser = getGraphParser(options?.include?.stats); - const refParser = getRefParser(); - const defaultLimit = options?.limit ?? configuration.get('graph.defaultItemLimit') ?? 5000; const defaultPageLimit = configuration.get('graph.pageItemLimit') ?? 1000; const ordering = configuration.get('graph.commitOrdering', undefined, 'date'); + const deferStats = options?.include?.stats; // && defaultLimit > 1000; + + const parser = getGraphParser(options?.include?.stats && !deferStats); + const refParser = getRefParser(); + const statsParser = getGraphStatsParser(); + const [refResult, stashResult, branchesResult, remotesResult, currentUserResult] = await Promise.allSettled([ this.git.log2(repoPath, undefined, ...refParser.arguments, '-n1', options?.ref ?? 'HEAD'), this.getStash(repoPath), @@ -1769,6 +1775,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const remappedIds = new Map(); let total = 0; let iterations = 0; + let pendingRowsStatsCount = 0; async function getCommitsForGraphCore( this: LocalGitProvider, @@ -1776,6 +1783,8 @@ export class LocalGitProvider implements GitProvider, Disposable { sha?: string, cursor?: { sha: string; skip: number }, ): Promise { + const startTotal = total; + iterations++; let log: string | string[] | undefined; @@ -1880,6 +1889,7 @@ export class LocalGitProvider implements GitProvider, Disposable { let remoteBranchId: string; let remoteName: string; let stashCommit: GitStashCommit | undefined; + let stats: GitGraphRowsStats | undefined; let tagId: string; let tagName: string; let tip: string; @@ -2152,8 +2162,14 @@ export class LocalGitProvider implements GitProvider, Disposable { remotes: refRemoteHeads, tags: refTags, contexts: contexts, - stats: commit.stats, }); + + if (commit.stats != null) { + if (stats == null) { + stats = new Map(); + } + stats.set(commit.sha, commit.stats); + } } const startingCursor = cursor?.sha; @@ -2166,6 +2182,44 @@ export class LocalGitProvider implements GitProvider, Disposable { } : undefined; + let rowsStatsDeferred: GitGraph['rowsStatsDeferred']; + + if (deferStats) { + if (stats == null) { + stats = new Map(); + } + pendingRowsStatsCount++; + + // eslint-disable-next-line no-async-promise-executor + const promise = new Promise(async resolve => { + try { + const args = [...statsParser.arguments]; + if (startTotal === 0) { + args.push(`-n${total}`); + } else { + args.push(`-n${total - startTotal}`, `--skip=${startTotal}`); + } + args.push(`--${ordering}-order`, '--all'); + + const statsData = await this.git.log2(repoPath, stdin ? { stdin: stdin } : undefined, ...args); + if (statsData) { + const commitStats = statsParser.parse(statsData); + for (const stat of commitStats) { + stats!.set(stat.sha, stat.stats); + } + } + } finally { + pendingRowsStatsCount--; + resolve(); + } + }); + + rowsStatsDeferred = { + isLoaded: () => pendingRowsStatsCount === 0, + promise: promise, + }; + } + return { repoPath: repoPath, avatars: avatars, @@ -2177,6 +2231,8 @@ export class LocalGitProvider implements GitProvider, Disposable { downstreams: downstreamMap, rows: rows, id: sha, + rowsStats: stats, + rowsStatsDeferred: rowsStatsDeferred, paging: { limit: limit === 0 ? count : limit, diff --git a/src/git/models/graph.ts b/src/git/models/graph.ts index 7576437..48c5028 100644 --- a/src/git/models/graph.ts +++ b/src/git/models/graph.ts @@ -22,7 +22,6 @@ export interface GitGraphRow extends GraphRow { remotes?: GitGraphRowRemoteHead[]; tags?: GitGraphRowTag[]; contexts?: GitGraphRowContexts; - stats?: GitGraphRowStats; } export interface GitGraph { @@ -43,6 +42,9 @@ export interface GitGraph { readonly rows: GitGraphRow[]; readonly id?: string; + readonly rowsStats?: GitGraphRowsStats; + readonly rowsStatsDeferred?: { isLoaded: () => boolean; promise: Promise }; + readonly paging?: { readonly limit: number | undefined; readonly startingCursor: string | undefined; @@ -51,3 +53,5 @@ export interface GitGraph { more?(limit: number, id?: string): Promise; } + +export type GitGraphRowsStats = Map; diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index f55c526..7bba207 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -79,10 +79,13 @@ export type ParsedEntryWithFiles = { [K in keyof T]: string } & { files: Pars export type ParserWithFiles = Parser>; export type ParsedStats = { files: number; additions: number; deletions: number }; -export type ParsedEntryWithStats = T & { stats?: ParsedStats }; +export type ParsedEntryWithMaybeStats = T & { stats?: ParsedStats }; +export type ParserWithMaybeStats = Parser>; + +export type ParsedEntryWithStats = T & { stats: ParsedStats }; export type ParserWithStats = Parser>; -type ContributorsParserMaybeWithStats = ParserWithStats<{ +type ContributorsParserMaybeWithStats = ParserWithMaybeStats<{ sha: string; author: string; email: string; @@ -115,7 +118,7 @@ export function getContributorsParser(stats?: boolean): ContributorsParserMaybeW return _contributorsParser; } -type GraphParserMaybeWithStats = ParserWithStats<{ +type GraphParserMaybeWithStats = ParserWithMaybeStats<{ sha: string; author: string; authorEmail: string; @@ -161,6 +164,15 @@ export function getGraphParser(stats?: boolean): GraphParserMaybeWithStats { return _graphParser; } +let _graphStatsParser: ParserWithStats<{ sha: string }> | undefined; + +export function getGraphStatsParser(): ParserWithStats<{ sha: string }> { + if (_graphStatsParser == null) { + _graphStatsParser = createLogParserWithStats({ sha: '%H' }); + } + return _graphStatsParser; +} + type RefParser = Parser; let _refParser: RefParser | undefined; @@ -327,7 +339,7 @@ export function createLogParserWithFiles>( export function createLogParserWithStats>( fieldMapping: ExtractAll, ): ParserWithStats { - function parseStats(fields: IterableIterator, entry: ParsedEntryWithStats) { + function parseStats(fields: IterableIterator, entry: ParsedEntryWithMaybeStats) { const stats = fields.next().value; const match = shortstatRegex.exec(stats); if (match?.groups != null) { diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index 74ab779..f7bd56f 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -39,7 +39,7 @@ import { GitUri } from '../../git/gitUri'; import type { GitBlame, GitBlameAuthor, GitBlameLine, GitBlameLines } from '../../git/models/blame'; import type { BranchSortOptions } from '../../git/models/branch'; import { getBranchId, getBranchNameWithoutRemote, GitBranch, sortBranches } from '../../git/models/branch'; -import type { GitCommitLine, GitCommitStats } from '../../git/models/commit'; +import type { GitCommitLine } from '../../git/models/commit'; import { getChangedFilesCount, GitCommit, GitCommitIdentity } from '../../git/models/commit'; import { deletedOrMissing, uncommitted } from '../../git/models/constants'; import { GitContributor } from '../../git/models/contributor'; @@ -52,6 +52,8 @@ import type { GitGraphRowContexts, GitGraphRowHead, GitGraphRowRemoteHead, + GitGraphRowsStats, + GitGraphRowStats, GitGraphRowTag, } from '../../git/models/graph'; import { GitGraphRowType } from '../../git/models/graph'; @@ -1256,7 +1258,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { let refRemoteHeads: GitGraphRowRemoteHead[]; let refTags: GitGraphRowTag[]; let remoteBranchId: string; - let stats: GitCommitStats | undefined; + let stats: GitGraphRowsStats | undefined; let tagId: string; const headRefUpstreamName = headBranch.upstream?.name; @@ -1442,7 +1444,6 @@ export class GitHubGitProvider implements GitProvider, Disposable { }), }; - stats = commit.stats; rows.push({ sha: commit.sha, parents: commit.parents, @@ -1456,15 +1457,18 @@ export class GitHubGitProvider implements GitProvider, Disposable { remotes: refRemoteHeads, tags: refTags, contexts: contexts, - stats: - stats != null - ? { - files: getChangedFilesCount(stats.changedFiles), - additions: stats.additions, - deletions: stats.deletions, - } - : undefined, }); + + if (commit.stats != null) { + if (stats == null) { + stats = new Map(); + } + stats.set(commit.sha, { + files: getChangedFilesCount(commit.stats.changedFiles), + additions: commit.stats.additions, + deletions: commit.stats.deletions, + }); + } } if (options?.ref === 'HEAD') { diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 9ac3961..07ec1eb 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -11,7 +11,7 @@ import type { ShowCommitsInViewCommandArgs, } from '../../../commands'; import { parseCommandContext } from '../../../commands/base'; -import type { Config } from '../../../config'; +import type { Config, GraphMinimapMarkersAdditionalTypes } from '../../../config'; import type { StoredGraphFilters, StoredGraphIncludeOnlyRef, StoredGraphRefType } from '../../../constants'; import { Commands, GlyphChars } from '../../../constants'; import type { Container } from '../../../container'; @@ -115,12 +115,14 @@ import type { GraphItemRefGroupContext, GraphItemTypedContext, GraphItemTypedContextValue, + GraphMinimapMarkerTypes, GraphMissingRefsMetadataType, GraphPullRequestContextValue, GraphPullRequestMetadata, GraphRefMetadata, GraphRefMetadataType, GraphRepository, + GraphScrollMarkerTypes, GraphSelectedRows, GraphStashContextValue, GraphTagContextValue, @@ -145,6 +147,7 @@ import { DidChangeRefsMetadataNotificationType, DidChangeRefsVisibilityNotificationType, DidChangeRowsNotificationType, + DidChangeRowsStatsNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, DidChangeWindowFocusNotificationType, @@ -159,9 +162,7 @@ import { GetMissingAvatarsCommandType, GetMissingRefsMetadataCommandType, GetMoreRowsCommandType, - GraphMinimapMarkerTypes, GraphRefMetadataTypes, - GraphScrollMarkerTypes, SearchCommandType, SearchOpenInViewCommandType, supportedRefMetadataTypes, @@ -514,8 +515,28 @@ export class GraphWebviewProvider implements WebviewProvider { if (config[key] !== params.changes[key]) { switch (key) { case 'minimap': - void configuration.updateEffective('graph.experimental.minimap.enabled', params.changes[key]); + void configuration.updateEffective('graph.minimap.enabled', params.changes[key]); break; + case 'minimapDataType': + void configuration.updateEffective('graph.minimap.dataType', params.changes[key]); + break; + case 'minimapMarkerTypes': { + const additionalTypes: GraphMinimapMarkersAdditionalTypes[] = []; + + const markers = params.changes[key] ?? []; + for (const marker of markers) { + switch (marker) { + case 'localBranches': + case 'remoteBranches': + case 'stashes': + case 'tags': + additionalTypes.push(marker); + break; + } + } + void configuration.updateEffective('graph.minimap.additionalTypes', additionalTypes); + break; + } default: // TODO:@eamodio add more config options as needed debugger; @@ -582,14 +603,17 @@ export class GraphWebviewProvider implements WebviewProvider { configuration.changed(e, 'graph.pullRequests.enabled') || configuration.changed(e, 'graph.showRemoteNames') || configuration.changed(e, 'graph.showUpstreamStatus') || - configuration.changed(e, 'graph.experimental.minimap.enabled') || - configuration.changed(e, 'graph.experimental.minimap.additionalTypes') + configuration.changed(e, 'graph.minimap.enabled') || + configuration.changed(e, 'graph.minimap.dataType') || + configuration.changed(e, 'graph.minimap.additionalTypes') ) { void this.notifyDidChangeConfiguration(); if ( - configuration.changed(e, 'graph.experimental.minimap.enabled') && - configuration.get('graph.experimental.minimap.enabled') && + (configuration.changed(e, 'graph.minimap.enabled') || + configuration.changed(e, 'graph.minimap.dataType')) && + configuration.get('graph.minimap.enabled') && + configuration.get('graph.minimap.dataType') === 'lines' && !this._graph?.includes?.stats ) { this.updateState(); @@ -1279,18 +1303,21 @@ export class GraphWebviewProvider implements WebviewProvider { private async notifyDidChangeRows(sendSelectedRows: boolean = false, completionId?: string) { if (this._graph == null) return; - const data = this._graph; + const graph = this._graph; return this.host.notify( DidChangeRowsNotificationType, { - rows: data.rows, - downstreams: Object.fromEntries(data.downstreams), - avatars: Object.fromEntries(data.avatars), + rows: graph.rows, + avatars: Object.fromEntries(graph.avatars), + downstreams: Object.fromEntries(graph.downstreams), refsMetadata: this._refsMetadata != null ? Object.fromEntries(this._refsMetadata) : this._refsMetadata, + rowsStats: graph.rowsStats?.size ? Object.fromEntries(graph.rowsStats) : undefined, + rowsStatsLoading: + graph.rowsStatsDeferred?.isLoaded != null ? !graph.rowsStatsDeferred.isLoaded() : false, selectedRows: sendSelectedRows ? this._selectedRows : undefined, paging: { - startingCursor: data.paging?.startingCursor, - hasMore: data.paging?.hasMore ?? false, + startingCursor: graph.paging?.startingCursor, + hasMore: graph.paging?.hasMore ?? false, }, }, completionId, @@ -1298,6 +1325,16 @@ export class GraphWebviewProvider implements WebviewProvider { } @debug() + private async notifyDidChangeRowsStats(graph: GitGraph) { + if (graph.rowsStats == null) return; + + return this.host.notify(DidChangeRowsStatsNotificationType, { + rowsStats: Object.fromEntries(graph.rowsStats), + rowsStatsLoading: graph.rowsStatsDeferred?.isLoaded != null ? !graph.rowsStatsDeferred.isLoaded() : false, + }); + } + + @debug() private async notifyDidChangeWorkingTree() { if (!this.host.ready || !this.host.visible) { this.host.addPendingIpcNotification(DidChangeWorkingTreeNotificationType, this._ipcNotificationMap, this); @@ -1608,10 +1645,11 @@ export class GraphWebviewProvider implements WebviewProvider { dimMergeCommits: configuration.get('graph.dimMergeCommits'), enableMultiSelection: false, highlightRowsOnRefHover: configuration.get('graph.highlightRowsOnRefHover'), - minimap: configuration.get('graph.experimental.minimap.enabled'), - enabledMinimapMarkerTypes: this.getEnabledGraphMinimapMarkers(), + minimap: configuration.get('graph.minimap.enabled'), + minimapDataType: configuration.get('graph.minimap.dataType'), + minimapMarkerTypes: this.getMinimapMarkerTypes(), scrollRowPadding: configuration.get('graph.scrollRowPadding'), - enabledScrollMarkerTypes: this.getEnabledGraphScrollMarkers(), + scrollMarkerTypes: this.getScrollMarkerTypes(), showGhostRefsOnRowHover: configuration.get('graph.showGhostRefsOnRowHover'), showRemoteNamesOnRefs: configuration.get('graph.showRemoteNames'), idLength: configuration.get('advanced.abbreviatedShaLength'), @@ -1619,33 +1657,29 @@ export class GraphWebviewProvider implements WebviewProvider { return config; } - private getEnabledGraphScrollMarkers(): GraphScrollMarkerTypes[] { - const markersEnabled = configuration.get('graph.scrollMarkers.enabled'); - if (!markersEnabled) return []; + private getScrollMarkerTypes(): GraphScrollMarkerTypes[] { + if (!configuration.get('graph.scrollMarkers.enabled')) return []; const markers: GraphScrollMarkerTypes[] = [ - GraphScrollMarkerTypes.Selection, - GraphScrollMarkerTypes.Highlights, - GraphScrollMarkerTypes.Head, - GraphScrollMarkerTypes.Upstream, - ...(configuration.get('graph.scrollMarkers.additionalTypes') as unknown as GraphScrollMarkerTypes[]), + 'selection', + 'highlights', + 'head', + 'upstream', + ...configuration.get('graph.scrollMarkers.additionalTypes'), ]; return markers; } - private getEnabledGraphMinimapMarkers(): GraphMinimapMarkerTypes[] { - const markersEnabled = configuration.get('graph.experimental.minimap.enabled'); - if (!markersEnabled) return []; + private getMinimapMarkerTypes(): GraphMinimapMarkerTypes[] { + if (!configuration.get('graph.minimap.enabled')) return []; const markers: GraphMinimapMarkerTypes[] = [ - GraphMinimapMarkerTypes.Selection, - GraphMinimapMarkerTypes.Highlights, - GraphMinimapMarkerTypes.Head, - GraphMinimapMarkerTypes.Upstream, - ...(configuration.get( - 'graph.experimental.minimap.additionalTypes', - ) as unknown as GraphMinimapMarkerTypes[]), + 'selection', + 'highlights', + 'head', + 'upstream', + ...configuration.get('graph.minimap.additionalTypes'), ]; return markers; @@ -1747,7 +1781,10 @@ export class GraphWebviewProvider implements WebviewProvider { uri => this.host.asWebviewUri(uri), { include: { - stats: configuration.get('graph.experimental.minimap.enabled') || !columnSettings.changes.isHidden, + stats: + (configuration.get('graph.minimap.enabled') && + configuration.get('graph.minimap.dataType') === 'lines') || + !columnSettings.changes.isHidden, }, limit: limit, ref: ref, @@ -1817,6 +1854,7 @@ export class GraphWebviewProvider implements WebviewProvider { avatars: data != null ? Object.fromEntries(data.avatars) : undefined, refsMetadata: this.resetRefsMetadata() === null ? null : {}, loading: deferRows, + rowsStatsLoading: data?.rowsStatsDeferred?.isLoaded != null ? !data.rowsStatsDeferred.isLoaded() : false, rows: data?.rows, downstreams: data != null ? Object.fromEntries(data.downstreams) : undefined, paging: @@ -1943,6 +1981,8 @@ export class GraphWebviewProvider implements WebviewProvider { if (graph == null) { this.resetRefsMetadata(); this.resetSearchState(); + } else { + void graph.rowsStatsDeferred?.promise.then(() => void this.notifyDidChangeRowsStats(graph)); } } diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index 4890bd7..2ab6c23 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -16,11 +16,12 @@ import type { RefMetadataItem, RefMetadataType, Remote, + RowStats, Tag, UpstreamMetadata, WorkDirStats, } from '@gitkraken/gitkraken-components'; -import type { DateStyle } from '../../../config'; +import type { Config, DateStyle } from '../../../config'; import type { RepositoryVisibility } from '../../../git/gitProvider'; import type { GitTrackingState } from '../../../git/models/branch'; import type { GitGraphRowType } from '../../../git/models/graph'; @@ -59,27 +60,25 @@ export enum GraphRefMetadataTypes { PullRequest = 'pullRequest', } -export const enum GraphScrollMarkerTypes { - Selection = 'selection', - Head = 'head', - Highlights = 'highlights', - LocalBranches = 'localBranches', - RemoteBranches = 'remoteBranches', - Stashes = 'stashes', - Tags = 'tags', - Upstream = 'upstream', -} - -export const enum GraphMinimapMarkerTypes { - Selection = 'selection', - Head = 'head', - Highlights = 'highlights', - LocalBranches = 'localBranches', - RemoteBranches = 'remoteBranches', - Stashes = 'stashes', - Tags = 'tags', - Upstream = 'upstream', -} +export type GraphScrollMarkerTypes = + | 'selection' + | 'head' + | 'highlights' + | 'localBranches' + | 'remoteBranches' + | 'stashes' + | 'tags' + | 'upstream'; + +export type GraphMinimapMarkerTypes = + | 'selection' + | 'head' + | 'highlights' + | 'localBranches' + | 'remoteBranches' + | 'stashes' + | 'tags' + | 'upstream'; export const supportedRefMetadataTypes: GraphRefMetadataType[] = Object.values(GraphRefMetadataTypes); @@ -100,6 +99,8 @@ export interface State { loading?: boolean; refsMetadata?: GraphRefsMetadata | null; rows?: GraphRow[]; + rowsStats?: Record; + rowsStatsLoading?: boolean; downstreams?: GraphDownstreams; paging?: GraphPaging; columns?: GraphColumnsSettings; @@ -176,9 +177,10 @@ export interface GraphComponentConfig { enableMultiSelection?: boolean; highlightRowsOnRefHover?: boolean; minimap?: boolean; - enabledMinimapMarkerTypes?: GraphMinimapMarkerTypes[]; + minimapDataType?: Config['graph']['minimap']['dataType']; + minimapMarkerTypes?: GraphMinimapMarkerTypes[]; + scrollMarkerTypes?: GraphScrollMarkerTypes[]; scrollRowPadding?: number; - enabledScrollMarkerTypes?: GraphScrollMarkerTypes[]; showGhostRefsOnRowHover?: boolean; showRemoteNamesOnRefs?: boolean; idLength?: number; @@ -200,6 +202,7 @@ export type GraphIncludeOnlyRefs = IncludeOnlyRefsById; export type GraphIncludeOnlyRef = GraphRefOptData; export type GraphColumnName = GraphZoneType; +export type GraphRowStats = RowStats; export type InternalNotificationType = 'didChangeTheme'; @@ -371,14 +374,24 @@ export const DidChangeRefsVisibilityNotificationType = new IpcNotificationType; + rowsStatsLoading: boolean; selectedRows?: GraphSelectedRows; } export const DidChangeRowsNotificationType = new IpcNotificationType('graph/rows/didChange'); +export interface DidChangeRowsStatsParams { + rowsStats: Record; + rowsStatsLoading: boolean; +} +export const DidChangeRowsStatsNotificationType = new IpcNotificationType( + 'graph/rows/stats/didChange', +); + export interface DidChangeSelectionParams { selection: GraphSelectedRows; } diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 712ae2f..d64b20e 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -30,6 +30,7 @@ import type { GraphComponentConfig, GraphExcludedRef, GraphExcludeTypes, + GraphMinimapMarkerTypes, GraphMissingRefsMetadata, GraphRefMetadataItem, GraphRepository, @@ -47,13 +48,13 @@ import { DidChangeRefsMetadataNotificationType, DidChangeRefsVisibilityNotificationType, DidChangeRowsNotificationType, + DidChangeRowsStatsNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, DidChangeWindowFocusNotificationType, DidChangeWorkingTreeNotificationType, DidFetchNotificationType, DidSearchNotificationType, - GraphMinimapMarkerTypes, } from '../../../../plus/webviews/graph/protocol'; import type { Subscription } from '../../../../subscription'; import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription'; @@ -193,6 +194,8 @@ export function GraphWrapper({ const graphRef = useRef(null); const [rows, setRows] = useState(state.rows ?? []); + const [rowsStats, setRowsStats] = useState(state.rowsStats); + const [rowsStatsLoading, setRowsStatsLoading] = useState(state.rowsStatsLoading); const [avatars, setAvatars] = useState(state.avatars); const [downstreams, setDownstreams] = useState(state.downstreams ?? {}); const [refsMetadata, setRefsMetadata] = useState(state.refsMetadata); @@ -274,6 +277,8 @@ export function GraphWrapper({ break; case DidChangeRowsNotificationType: setRows(state.rows ?? []); + setRowsStats(state.rowsStats); + setRowsStatsLoading(state.rowsStatsLoading); setSelectedRows(state.selectedRows); setAvatars(state.avatars); setDownstreams(state.downstreams ?? {}); @@ -281,6 +286,10 @@ export function GraphWrapper({ setPagingHasMore(state.paging?.hasMore ?? false); setIsLoading(state.loading); break; + case DidChangeRowsStatsNotificationType: + setRowsStats(state.rowsStats); + setRowsStatsLoading(state.rowsStatsLoading); + break; case DidSearchNotificationType: { const { results, resultsError } = getSearchResultModel(state); setSearchResultsError(resultsError); @@ -319,6 +328,8 @@ export function GraphWrapper({ setLastFetched(state.lastFetched); setColumns(state.columns); setRows(state.rows ?? []); + setRowsStats(state.rowsStats); + setRowsStatsLoading(state.rowsStatsLoading); setWorkingTreeStats(state.workingTreeStats ?? { added: 0, modified: 0, deleted: 0 }); setGraphConfig(state.config); setSelectedRows(state.selectedRows); @@ -377,10 +388,13 @@ export function GraphWrapper({ const minimapData = useMemo(() => { if (!graphConfig?.minimap) return undefined; + const showLinesChanged = (graphConfig?.minimapDataType ?? 'commits') === 'lines'; + if (showLinesChanged && rowsStats == null) return undefined; + // Loops through all the rows and group them by day and aggregate the row.stats const statsByDayMap = new Map(); const markersByDay = new Map(); - const enabledMinimapMarkers: GraphMinimapMarkerTypes[] = graphConfig?.enabledMinimapMarkerTypes ?? []; + const enabledMinimapMarkers: GraphMinimapMarkerTypes[] = graphConfig?.minimapMarkerTypes ?? []; let rankedShas: { head: string | undefined; @@ -409,7 +423,6 @@ export function GraphWrapper({ // Iterate in reverse order so that we can track the HEAD upstream properly for (let i = rows.length - 1; i >= 0; i--) { row = rows[i]; - stats = row.stats; day = getDay(row.date); if (day !== prevDay) { @@ -424,8 +437,7 @@ export function GraphWrapper({ if ( row.heads?.length && - (enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Head) || - enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches)) + (enabledMinimapMarkers.includes('head') || enabledMinimapMarkers.includes('localBranches')) ) { rankedShas.branch = row.sha; @@ -438,13 +450,13 @@ export function GraphWrapper({ } if ( - enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches) || - (enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Head) && h.isCurrentHead) + enabledMinimapMarkers.includes('localBranches') || + (enabledMinimapMarkers.includes('head') && h.isCurrentHead) ) { headMarkers.push({ type: 'branch', name: h.name, - current: h.isCurrentHead && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Head), + current: h.isCurrentHead && enabledMinimapMarkers.includes('head'), }); } }); @@ -459,9 +471,9 @@ export function GraphWrapper({ if ( row.remotes?.length && - (enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Upstream) || - enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.RemoteBranches) || - enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches)) + (enabledMinimapMarkers.includes('upstream') || + enabledMinimapMarkers.includes('remoteBranches') || + enabledMinimapMarkers.includes('localBranches')) ) { rankedShas.remote = row.sha; @@ -477,14 +489,14 @@ export function GraphWrapper({ } if ( - enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.RemoteBranches) || - (enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Upstream) && current) || - (enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.LocalBranches) && hasDownstream) + enabledMinimapMarkers.includes('remoteBranches') || + (enabledMinimapMarkers.includes('upstream') && current) || + (enabledMinimapMarkers.includes('localBranches') && hasDownstream) ) { remoteMarkers.push({ type: 'remote', name: `${r.owner}/${r.name}`, - current: current && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Upstream), + current: current && enabledMinimapMarkers.includes('upstream'), }); } }); @@ -497,7 +509,7 @@ export function GraphWrapper({ } } - if (row.type === 'stash-node' && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Stashes)) { + if (row.type === 'stash-node' && enabledMinimapMarkers.includes('stashes')) { stashMarker = { type: 'stash', name: row.message }; markers = markersByDay.get(day); if (markers == null) { @@ -507,7 +519,7 @@ export function GraphWrapper({ } } - if (row.tags?.length && enabledMinimapMarkers.includes(GraphMinimapMarkerTypes.Tags)) { + if (row.tags?.length && enabledMinimapMarkers.includes('tags')) { rankedShas.tag = row.sha; tagMarkers = row.tags.map(t => ({ @@ -525,42 +537,54 @@ export function GraphWrapper({ stat = statsByDayMap.get(day); if (stat == null) { - stat = - stats != null - ? { - activity: { additions: stats.additions, deletions: stats.deletions }, - commits: 1, - files: stats.files, - sha: row.sha, - } - : { - commits: 1, - sha: row.sha, - }; - statsByDayMap.set(day, stat); + if (showLinesChanged) { + stats = rowsStats![row.sha]; + if (stats != null) { + stat = { + activity: { additions: stats.additions, deletions: stats.deletions }, + commits: 1, + files: stats.files, + sha: row.sha, + }; + statsByDayMap.set(day, stat); + } + } else { + stat = { + commits: 1, + sha: row.sha, + }; + statsByDayMap.set(day, stat); + } } else { stat.commits++; stat.sha = rankedShas.head ?? rankedShas.branch ?? rankedShas.remote ?? rankedShas.tag ?? stat.sha; - if (stats != null) { - if (stat.activity == null) { - stat.activity = { additions: stats.additions, deletions: stats.deletions }; - } else { - stat.activity.additions += stats.additions; - stat.activity.deletions += stats.deletions; + if (showLinesChanged) { + stats = rowsStats![row.sha]; + if (stats != null) { + if (stat.activity == null) { + stat.activity = { additions: stats.additions, deletions: stats.deletions }; + } else { + stat.activity.additions += stats.additions; + stat.activity.deletions += stats.deletions; + } + stat.files = (stat.files ?? 0) + stats.files; } - stat.files = (stat.files ?? 0) + stats.files; } } } return { stats: statsByDayMap, markers: markersByDay }; - }, [rows, downstreams, graphConfig?.minimap, graphConfig?.enabledMinimapMarkerTypes]); + }, [ + rows, + rowsStats, + downstreams, + graphConfig?.minimap, + graphConfig?.minimapDataType, + graphConfig?.minimapMarkerTypes, + ]); const minimapSearchResults = useMemo(() => { - if ( - !graphConfig?.minimap || - !graphConfig.enabledMinimapMarkerTypes?.includes(GraphMinimapMarkerTypes.Highlights) - ) { + if (!graphConfig?.minimap || !graphConfig.minimapMarkerTypes?.includes('highlights')) { return undefined; } @@ -582,7 +606,7 @@ export function GraphWrapper({ } return searchResultsByDay; - }, [searchResults, graphConfig?.minimap, graphConfig?.enabledMinimapMarkerTypes]); + }, [searchResults, graphConfig?.minimap, graphConfig?.minimapMarkerTypes]); const handleOnMinimapDaySelected = (e: CustomEvent) => { let { sha } = e.detail; @@ -600,10 +624,45 @@ export function GraphWrapper({ graphRef.current?.selectCommits([sha], false, true); }; - const handleOnToggleMinimap = (_e: React.MouseEvent) => { + const handleOnMinimapToggle = (_e: React.MouseEvent) => { onUpdateGraphConfiguration?.({ minimap: !graphConfig?.minimap }); }; + // This can only be applied to one radio button for now due to a bug in the component: https://github.com/microsoft/fast/issues/6381 + const handleOnMinimapDataTypeChange = (e: Event | FormEvent) => { + if (graphConfig == null) return; + + const $el = e.target as HTMLInputElement; + if ($el.value === 'commits') { + const minimapDataType = $el.checked ? 'commits' : 'lines'; + if (graphConfig.minimapDataType === minimapDataType) return; + + setGraphConfig({ ...graphConfig, minimapDataType: minimapDataType }); + onUpdateGraphConfiguration?.({ minimapDataType: minimapDataType }); + } + }; + + const handleOnMinimapAdditionalTypesChange = (e: Event | FormEvent) => { + if (graphConfig?.minimapMarkerTypes == null) return; + + const $el = e.target as HTMLInputElement; + const value = $el.value as GraphMinimapMarkerTypes; + + if ($el.checked) { + const index = graphConfig.minimapMarkerTypes.indexOf(value); + if (index !== -1) { + const minimapMarkerTypes = [...graphConfig.minimapMarkerTypes]; + minimapMarkerTypes.splice(index, 1); + setGraphConfig({ ...graphConfig, minimapMarkerTypes: minimapMarkerTypes }); + onUpdateGraphConfiguration?.({ minimapMarkerTypes: minimapMarkerTypes }); + } + } else if (!graphConfig.minimapMarkerTypes.includes(value)) { + const minimapMarkerTypes = [...graphConfig.minimapMarkerTypes, value]; + setGraphConfig({ ...graphConfig, minimapMarkerTypes: minimapMarkerTypes }); + onUpdateGraphConfiguration?.({ minimapMarkerTypes: minimapMarkerTypes }); + } + }; + const handleOnGraphMouseLeave = (_event: any) => { minimap.current?.unselect(undefined, true); }; @@ -1351,21 +1410,111 @@ export function GraphWrapper({ - + + + + + + Show by day + + + + Commits + + + Lines Changed (can take a while) + + + + + Markers + + + Hide Local Branches + + + + + Hide Remote Branches + + + + + Hide Stashes + + + + + Hide Tags + + + + + )} -
+
@@ -1374,6 +1523,7 @@ export function GraphWrapper({ ref={minimap as any} activeDay={activeDay} data={minimapData?.stats} + dataType={graphConfig?.minimapDataType ?? 'commits'} markers={minimapData?.markers} searchResults={minimapSearchResults} visibleDays={visibleDays} @@ -1397,9 +1547,7 @@ export function GraphWrapper({ dimMergeCommits={graphConfig?.dimMergeCommits} downstreamsByUpstream={downstreams} enabledRefMetadataTypes={graphConfig?.enabledRefMetadataTypes} - enabledScrollMarkerTypes={ - graphConfig?.enabledScrollMarkerTypes as GraphMarkerType[] | undefined - } + enabledScrollMarkerTypes={graphConfig?.scrollMarkerTypes as GraphMarkerType[] | undefined} enableMultiSelection={graphConfig?.enableMultiSelection} excludeRefsById={excludeRefsById} excludeByType={excludeTypes} @@ -1432,6 +1580,8 @@ export function GraphWrapper({ onGraphVisibleRowsChanged={minimap.current ? handleOnGraphVisibleRowsChanged : undefined} platform={clientPlatform} refMetadataById={refsMetadata} + rowsStats={rowsStats} + rowsStatsLoading={rowsStatsLoading} shaLength={graphConfig?.idLength} themeOpacityFactor={styleProps?.themeOpacityFactor} useAuthorInitialsForAvatars={!graphConfig?.avatars} @@ -1448,10 +1598,7 @@ export function GraphWrapper({ data-vscode-context={context?.header || JSON.stringify({ webviewItem: 'gitlens:graph:columns' })} onClick={handleToggleColumnSettings} > - + diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index f051937..8766fe5 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -233,6 +233,10 @@ button:not([disabled]), vertical-align: bottom; } + .codicon[class*='codicon-graph-line'] { + transform: translateY(1px); + } + &__pill { .is-ahead & { background-color: var(--branch-status-ahead-pill-background); @@ -245,18 +249,6 @@ button:not([disabled]), } } - // &__icon { - // .is-ahead & { - // color: var(--branch-status-ahead-foreground); - // } - // .is-behind & { - // color: var(--branch-status-behind-foreground); - // } - // .is-ahead.is-behind & { - // color: var(--branch-status-both-foreground); - // } - // } - &__more, &__more.codicon[class*='codicon-'] { font-size: 1rem; @@ -338,6 +330,38 @@ button:not([disabled]), border-color: var(--vscode-inputOption-activeBorder); } +.button-group { + display: flex; + flex-direction: row; + align-items: stretch; + + &:hover { + background-color: var(--color-graph-actionbar-selectedBackground); + } + + > *:not(:first-child), + > *:not(:first-child) .action-button { + display: flex; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + > *:not(:first-child) .action-button { + padding-left: 0.25rem; + } + + > *:not(:last-child), + > *:not(:last-child) .action-button { + padding-right: 0.5rem; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + // > *:not(:first-child) { + // border-left: 0.1rem solid var(--titlebar-fg); + // } +} + .repo-access { font-size: 1.1em; margin-right: 0.2rem; @@ -348,10 +372,10 @@ button:not([disabled]), } .column-button { - --column-button-height: 20px; + --column-button-height: 19px; position: absolute; - top: 1px; + top: 3px; right: 0; z-index: 2; @@ -361,7 +385,7 @@ button:not([disabled]), border: none; color: var(--text-disabled, hsla(0, 0%, 100%, 0.4)); margin: 0; - padding: 0 4px; + padding: 0 2px; height: var(--column-button-height); cursor: pointer; background-color: var(--color-graph-actionbar-background); @@ -385,7 +409,6 @@ button:not([disabled]), .codicon[class*='codicon-'] { font-size: 1.1rem; position: relative; - top: 2px; } } diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index c129482..c6f2a08 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -26,6 +26,7 @@ import { DidChangeRefsMetadataNotificationType, DidChangeRefsVisibilityNotificationType, DidChangeRowsNotificationType, + DidChangeRowsStatsNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, DidChangeWindowFocusNotificationType, @@ -272,6 +273,10 @@ export class GraphApp extends App { } this.state.rows = rows; this.state.paging = params.paging; + if (params.rowsStats != null) { + this.state.rowsStats = { ...this.state.rowsStats, ...params.rowsStats }; + } + this.state.rowsStatsLoading = params.rowsStatsLoading; if (params.selectedRows != null) { this.state.selectedRows = params.selectedRows; } @@ -280,6 +285,14 @@ export class GraphApp extends App { }); break; + case DidChangeRowsStatsNotificationType.method: + onIpc(DidChangeRowsStatsNotificationType, msg, (params, type) => { + this.state.rowsStats = { ...this.state.rowsStats, ...params.rowsStats }; + this.state.rowsStatsLoading = params.rowsStatsLoading; + this.setState(this.state, type); + }); + break; + case DidSearchNotificationType.method: onIpc(DidSearchNotificationType, msg, (params, type) => { this.state.searchResults = params.results; diff --git a/src/webviews/apps/plus/graph/minimap/minimap.ts b/src/webviews/apps/plus/graph/minimap/minimap.ts index 199091e..0943b6c 100644 --- a/src/webviews/apps/plus/graph/minimap/minimap.ts +++ b/src/webviews/apps/plus/graph/minimap/minimap.ts @@ -3,7 +3,7 @@ import type { Chart, DataItem, RegionOptions } from 'billboard.js'; import { groupByMap } from '../../../../../system/array'; import { debug } from '../../../../../system/decorators/log'; import { debounce } from '../../../../../system/function'; -import { first, flatMap, map, some, union } from '../../../../../system/iterable'; +import { first, flatMap, map, union } from '../../../../../system/iterable'; import { pluralize } from '../../../../../system/string'; import { formatDate, formatNumeric, fromNow } from '../../../shared/date'; @@ -54,7 +54,14 @@ export interface GraphMinimapDaySelectedEventDetail { } const template = html``; const styles = css` @@ -69,11 +76,36 @@ const styles = css` #chart { height: 100%; - width: 100%; + width: calc(100% - 1rem); overflow: hidden; position: initial !important; } + #spinner { + position: absolute; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + } + + #spinner[aria-hidden='true'] { + display: none; + } + + .legend { + position: absolute; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + z-index: 1; + opacity: 0.7; + cursor: help; + } + .bb svg { font: 10px var(--font-family); -webkit-tap-highlight-color: rgba(0, 0, 0, 0); @@ -392,6 +424,7 @@ const markerZOrder = [ @customElement({ name: 'graph-minimap', template: template, styles: styles }) export class GraphMinimap extends FASTElement { chart!: HTMLDivElement; + spinner!: HTMLDivElement; private _chart!: Chart; private _loadTimer: ReturnType | undefined; @@ -401,7 +434,6 @@ export class GraphMinimap extends FASTElement { @observable activeDay: number | undefined; - @debug({ singleLine: true }) protected activeDayChanged() { this.select(this.activeDay); } @@ -428,6 +460,12 @@ export class GraphMinimap extends FASTElement { } @observable + dataType: 'commits' | 'lines' = 'commits'; + protected dataTypeChanged() { + this.dataChanged(); + } + + @observable markers: Map | undefined; protected markersChanged() { this.dataChanged(undefined, undefined, true); @@ -444,7 +482,6 @@ export class GraphMinimap extends FASTElement { @observable visibleDays: { top: number; bottom: number } | undefined; - @debug({ singleLine: true }) protected visibleDaysChanged() { this._chart?.regions.remove({ classes: ['visible-area'] }); if (this.visibleDays == null) return; @@ -466,10 +503,13 @@ export class GraphMinimap extends FASTElement { } private getInternalChart(): any { - return (this._chart as any).internal; + try { + return (this._chart as any)?.internal; + } catch { + return undefined; + } } - @debug({ singleLine: true }) select(date: number | Date | undefined, trackOnly: boolean = false) { if (date == null) { this.unselect(); @@ -481,6 +521,8 @@ export class GraphMinimap extends FASTElement { if (d == null) return; const internal = this.getInternalChart(); + if (internal == null) return; + internal.showGridFocus([d]); if (!trackOnly) { @@ -491,10 +533,9 @@ export class GraphMinimap extends FASTElement { } } - @debug({ singleLine: true }) unselect(date?: number | Date, focus: boolean = false) { if (focus) { - this.getInternalChart().hideGridFocus(); + this.getInternalChart()?.hideGridFocus(); return; } @@ -616,13 +657,15 @@ export class GraphMinimap extends FASTElement { @debug({ singleLine: true }) private async loadChartCore() { if (!this.data?.size) { + this.spinner.setAttribute('aria-hidden', 'false'); + this._chart?.destroy(); this._chart = undefined!; return; } - const hasActivity = some(this.data.values(), v => v?.activity != null); + const showLinesChanged = this.dataType === 'lines'; // Convert the map to an array dates and an array of stats const dates = []; @@ -653,7 +696,7 @@ export class GraphMinimap extends FASTElement { stat = this.data.get(day); dates.push(day); - if (hasActivity) { + if (showLinesChanged) { adds = stat?.activity?.additions ?? 0; deletes = stat?.activity?.deletions ?? 0; changes = adds + deletes; @@ -832,6 +875,7 @@ export class GraphMinimap extends FASTElement { } const stashesCount = groups?.get('stash')?.length ?? 0; + const showLinesChanged = this.dataType === 'lines'; return /*html*/ `
@@ -845,12 +889,12 @@ export class GraphMinimap extends FASTElement { : `${pluralize('commit', stat?.commits ?? 0, { format: c => formatNumeric(c), zero: 'No', - })}, ${pluralize('file', stat?.commits ?? 0, { - format: c => formatNumeric(c), - zero: 'No', })}${ - hasActivity - ? `, ${pluralize( + showLinesChanged + ? `, ${pluralize('file', stat?.files ?? 0, { + format: c => formatNumeric(c), + zero: 'No', + })}, ${pluralize( 'line', (stat?.activity?.additions ?? 0) + (stat?.activity?.deletions ?? 0), @@ -858,9 +902,9 @@ export class GraphMinimap extends FASTElement { format: c => formatNumeric(c), zero: 'No', }, - )}` + )} changed` : '' - } changed` + }` }
${ @@ -957,6 +1001,8 @@ export class GraphMinimap extends FASTElement { this._chart.regions(regions); } + this.spinner.setAttribute('aria-hidden', 'true'); + this.activeDayChanged(); } } diff --git a/src/webviews/apps/shared/components/menu/menu-label.ts b/src/webviews/apps/shared/components/menu/menu-label.ts index 4ad2c0d..9b3f337 100644 --- a/src/webviews/apps/shared/components/menu/menu-label.ts +++ b/src/webviews/apps/shared/components/menu/menu-label.ts @@ -20,6 +20,7 @@ const styles = css` margin: 0px; color: var(--vscode-menu-foreground); opacity: 0.6; + user-select: none; } `; diff --git a/src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts b/src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts index 15da11c..8adb29d 100644 --- a/src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts +++ b/src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts @@ -16,13 +16,44 @@ const styles = css` position: relative; } + slot[name='content']::slotted(*)::before { + font: normal normal normal 14px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + + vertical-align: middle; + line-height: 2rem; + letter-spacing: normal; + content: '\\ea76'; + position: absolute; + top: 2px; + right: 5px; + cursor: pointer; + pointer-events: all; + z-index: 10001; + } + slot[name='content']::slotted(*) { position: absolute; - left: 0; top: 100%; z-index: 10000; } + :host([position='left']) slot[name='content']::slotted(*) { + left: 0; + } + + :host([position='right']) slot[name='content']::slotted(*) { + right: 0; + } + :host(:not([open])) slot[name='content']::slotted(*) { display: none; } @@ -33,6 +64,9 @@ export class PopMenu extends FASTElement { @attr({ mode: 'boolean' }) open = false; + @attr() + position: 'left' | 'right' = 'left'; + @observable triggerNodes?: HTMLElement[]; @@ -96,7 +130,12 @@ export class PopMenu extends FASTElement { if (this.open === false) return; const composedPath = e.composedPath(); - if (!composedPath.includes(this)) { + if ( + !composedPath.includes(this) || + // If the ::before element is clicked and is the close icon, close the menu + (e.type === 'click' && + window.getComputedStyle(composedPath[0] as Element, '::before').content === '"\uEA76"') + ) { this.open = false; this.disposeTrackOutside(); } diff --git a/src/webviews/apps/shared/components/search/search-input.ts b/src/webviews/apps/shared/components/search/search-input.ts index 8060fd7..d22a61e 100644 --- a/src/webviews/apps/shared/components/search/search-input.ts +++ b/src/webviews/apps/shared/components/search/search-input.ts @@ -342,6 +342,32 @@ const styles = css` display: block; } + .helper::before { + font: normal normal normal 14px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + + vertical-align: middle; + line-height: 2rem; + letter-spacing: normal; + + content: '\\ea76'; + position: absolute; + top: 2px; + right: 5px; + cursor: pointer; + pointer-events: all; + z-index: 10001; + opacity: 0.6; + } + .helper-label { text-transform: uppercase; font-size: 0.84em; @@ -350,6 +376,7 @@ const styles = css` padding-right: 0.6rem; margin: 0; opacity: 0.6; + user-select: none; } .helper-button { @@ -429,7 +456,12 @@ export class SearchInput extends FASTElement { if (this.showHelp === false) return; const composedPath = e.composedPath(); - if (!composedPath.includes(this)) { + if ( + !composedPath.includes(this) || + // If the ::before element is clicked and is the close icon, close the menu + (e.type === 'click' && + window.getComputedStyle(composedPath[0] as Element, '::before').content === '"\uEA76"') + ) { this.showHelp = false; } } diff --git a/yarn.lock b/yarn.lock index 0b07ac0..3cfab3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -202,10 +202,10 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@gitkraken/gitkraken-components@9.1.5": - version "9.1.5" - resolved "https://registry.yarnpkg.com/@gitkraken/gitkraken-components/-/gitkraken-components-9.1.5.tgz#96f8b92ef47ebaa4c041e7b61e4a2cce73c35473" - integrity sha512-dVIQEOeS4Fd8973eWP+bu7EOWqRuypz+gJvQ+gqrP3hfs7OWMTE6dD3PTO/vPlDNTlgqhjmPXR8mGYk3HBUqHQ== +"@gitkraken/gitkraken-components@10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@gitkraken/gitkraken-components/-/gitkraken-components-10.0.0.tgz#936c766427c7fbdaab2e7ac72bbce054de5fbcb1" + integrity sha512-1feNudTT69Dsaue5F+4KG0YtWnQTb2Tn2+0B8kMhFh61X+c7tcO6AUZ3D0kwXx/I5moowpsBnQXnYfaIKa4IXw== dependencies: "@axosoft/react-virtualized" "9.22.3-gitkraken.3" classnames "2.3.2"