From 97ea2fdc2736feff3c3867f92370809e76126847 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 21 Sep 2022 01:14:22 -0400 Subject: [PATCH] Adds full commit searching to Graph (wip) --- src/env/node/git/git.ts | 5 +- src/env/node/git/localGitProvider.ts | 232 ++++++++++++++++++-------- src/git/gitProvider.ts | 7 +- src/git/gitProviderService.ts | 12 +- src/git/models/repository.ts | 14 +- src/git/search.ts | 14 ++ src/plus/github/githubGitProvider.ts | 100 ++++++++++- src/plus/webviews/graph/graphWebview.ts | 62 +++++-- src/plus/webviews/graph/protocol.ts | 15 ++ src/webviews/apps/plus/graph/GraphWrapper.tsx | 21 ++- src/webviews/apps/plus/graph/graph.tsx | 35 +++- 11 files changed, 426 insertions(+), 91 deletions(-) diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index 1016ab0..5780063 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -867,10 +867,13 @@ export class Git { params.push(options?.ref); } + if (!params.includes('--')) { + params.push('--'); + } + return this.git( { cwd: repoPath, configs: options?.configs ?? gitLogDefaultConfigs, stdin: options?.stdin }, ...params, - '--', ); } diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 75017d4..83f2b2a 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -106,7 +106,7 @@ import type { RemoteProvider } from '../../../git/remotes/remoteProvider'; import type { RemoteProviders } from '../../../git/remotes/remoteProviders'; import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remotes/remoteProviders'; import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider'; -import type { SearchPattern } from '../../../git/search'; +import type { GitSearch, SearchPattern } from '../../../git/search'; import { parseSearchOperations } from '../../../git/search'; import { Logger } from '../../../logger'; import type { LogScope } from '../../../logger'; @@ -2638,90 +2638,107 @@ export class LocalGitProvider implements GitProvider, Disposable { } @log() - async getLogForSearch( + async searchForCommitsSimple( repoPath: string, search: SearchPattern, - options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number }, - ): Promise { + options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' }, + ): Promise { search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; try { + const { args: searchArgs, files, commits } = this.getArgsFromSearchPattern(search); + if (commits?.length) { + return { + repoPath: repoPath, + pattern: search, + results: commits, + }; + } + + const refParser = getGraphRefParser(); const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0; const similarityThreshold = configuration.get('advanced.similarityThreshold'); - const operations = parseSearchOperations(search.pattern); - - const searchArgs = new Set(); - const files: string[] = []; - - let useShow = false; - - let op; - let values = operations.get('commit:'); - if (values != null) { - useShow = true; - - searchArgs.add('-m'); - searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); - for (const value of values) { - searchArgs.add(value.replace(doubleQuoteRegex, '')); - } - } else { - searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); - searchArgs.add('--all'); - searchArgs.add('--full-history'); - searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings'); - if (search.matchRegex && !search.matchCase) { - searchArgs.add('--regexp-ignore-case'); - } - - for ([op, values] of operations.entries()) { - switch (op) { - case 'message:': - searchArgs.add('-m'); - if (search.matchAll) { - searchArgs.add('--all-match'); - } - for (const value of values) { - searchArgs.add( - `--grep=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, - ); - } + const args = [ + ...refParser.arguments, + `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + '--use-mailmap', + ]; + if (limit) { + args.push(`-n${limit + 1}`); + } + if (options?.ordering) { + args.push(`--${options.ordering}-order`); + } - break; + async function searchForCommitsCore( + this: LocalGitProvider, + limit: number, + cursor?: { sha: string; skip: number }, + ): Promise { + const data = await this.git.log2( + repoPath, + undefined, + ...args, + ...(cursor?.skip ? [`--skip=${cursor.skip}`] : []), + ...searchArgs, + '--', + ...files, + ); + const results = [...refParser.parse(data)]; - case 'author:': - searchArgs.add('-m'); - for (const value of values) { - searchArgs.add( - `--author=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, - ); - } + const last = results[results.length - 1]; + cursor = + last != null + ? { + sha: last, + skip: results.length, + } + : undefined; - break; + return { + repoPath: repoPath, + pattern: search, + results: results, + paging: + limit !== 0 && results.length > limit + ? { + limit: limit, + startingCursor: cursor?.sha, + more: true, + } + : undefined, + more: async (limit: number): Promise => + searchForCommitsCore.call(this, limit, cursor), + }; + } - case 'change:': - for (const value of values) { - searchArgs.add( - search.matchRegex - ? `-G${value.replace(doubleQuoteRegex, '')}` - : `-S${value.replace(doubleQuoteRegex, '')}`, - ); - } + return searchForCommitsCore.call(this, limit); + } catch (ex) { + // TODO@eamodio handle error reporting -- just invalid patterns? or more detailed? + return { + repoPath: repoPath, + pattern: search, + results: [], + }; + } + } - break; + @log() + async getLogForSearch( + repoPath: string, + search: SearchPattern, + options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number }, + ): Promise { + search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; - case 'file:': - for (const value of values) { - files.push(value.replace(doubleQuoteRegex, '')); - } + try { + const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0; + const similarityThreshold = configuration.get('advanced.similarityThreshold'); - break; - } - } - } + const { args, files, commits } = this.getArgsFromSearchPattern(search); - const args = [...searchArgs.values(), '--']; + args.push(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '--'); if (files.length !== 0) { args.push(...files); } @@ -2730,7 +2747,7 @@ export class LocalGitProvider implements GitProvider, Disposable { ordering: configuration.get('advanced.commitOrdering'), ...options, limit: limit, - useShow: useShow, + useShow: Boolean(commits?.length), }); const log = GitLogParser.parse( this.container, @@ -2759,6 +2776,81 @@ export class LocalGitProvider implements GitProvider, Disposable { } } + private getArgsFromSearchPattern(search: SearchPattern): { + args: string[]; + files: string[]; + commits?: string[] | undefined; + } { + const operations = parseSearchOperations(search.pattern); + + const searchArgs = new Set(); + const files: string[] = []; + + let commits; + + let op; + let values = operations.get('commit:'); + if (values != null) { + // searchArgs.add('-m'); + for (const value of values) { + searchArgs.add(value.replace(doubleQuoteRegex, '')); + } + commits = [...searchArgs.values()]; + } else { + searchArgs.add('--all'); + searchArgs.add('--full-history'); + searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings'); + if (search.matchRegex && !search.matchCase) { + searchArgs.add('--regexp-ignore-case'); + } + + for ([op, values] of operations.entries()) { + switch (op) { + case 'message:': + searchArgs.add('-m'); + if (search.matchAll) { + searchArgs.add('--all-match'); + } + for (const value of values) { + searchArgs.add(`--grep=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`); + } + + break; + + case 'author:': + searchArgs.add('-m'); + for (const value of values) { + searchArgs.add( + `--author=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`, + ); + } + + break; + + case 'change:': + for (const value of values) { + searchArgs.add( + search.matchRegex + ? `-G${value.replace(doubleQuoteRegex, '')}` + : `-S${value.replace(doubleQuoteRegex, '')}`, + ); + } + + break; + + case 'file:': + for (const value of values) { + files.push(value.replace(doubleQuoteRegex, '')); + } + + break; + } + } + } + + return { args: [...searchArgs.values()], files: files, commits: commits }; + } + private getLogForSearchMoreFn( log: GitLog, search: SearchPattern, diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 7cac3ae..66da6bd 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -26,7 +26,7 @@ import type { GitWorktree } from './models/worktree'; import type { RemoteProvider } from './remotes/remoteProvider'; import type { RemoteProviders } from './remotes/remoteProviders'; import type { RichRemoteProvider } from './remotes/richRemoteProvider'; -import type { SearchPattern } from './search'; +import type { GitSearch, SearchPattern } from './search'; export const enum GitProviderId { Git = 'git', @@ -296,6 +296,11 @@ export interface GitProvider extends Disposable { since?: string | undefined; }, ): Promise | undefined>; + searchForCommitsSimple( + repoPath: string | Uri, + search: SearchPattern, + options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' }, + ): Promise; getLogForSearch( repoPath: string, search: SearchPattern, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 2231667..153b22b 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -69,7 +69,7 @@ import type { GitWorktree } from './models/worktree'; import { RichRemoteProviders } from './remotes/remoteProviderConnections'; import type { RemoteProviders } from './remotes/remoteProviders'; import type { RichRemoteProvider } from './remotes/richRemoteProvider'; -import type { SearchPattern } from './search'; +import type { GitSearch, SearchPattern } from './search'; const maxDefaultBranchWeight = 100; const weightedDefaultBranches = new Map([ @@ -1465,6 +1465,16 @@ export class GitProviderService implements Disposable { } @log() + searchForCommitsSimple( + repoPath: string | Uri, + search: SearchPattern, + options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.searchForCommitsSimple(path, search, options); + } + + @log() async getLogForSearch( repoPath: string | Uri, search: SearchPattern, diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index b40eb66..d43a4a1 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -23,7 +23,7 @@ import type { GitProviderDescriptor } from '../gitProvider'; import { loadRemoteProviders } from '../remotes/remoteProviders'; import type { RemoteProviders } from '../remotes/remoteProviders'; import type { RichRemoteProvider } from '../remotes/richRemoteProvider'; -import type { SearchPattern } from '../search'; +import type { GitSearch, SearchPattern } from '../search'; import type { BranchSortOptions, GitBranch } from './branch'; import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from './branch'; import type { GitCommit } from './commit'; @@ -852,10 +852,20 @@ export class Repository implements Disposable { this.runTerminalCommand('revert', ...args); } - searchForCommits(search: SearchPattern, options?: { limit?: number; skip?: number }): Promise { + searchForCommits( + search: SearchPattern, + options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo'; skip?: number }, + ): Promise { return this.container.git.getLogForSearch(this.path, search, options); } + searchForCommitsSimple( + search: SearchPattern, + options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' }, + ): Promise { + return this.container.git.searchForCommitsSimple(this.path, search, options); + } + async setRemoteAsDefault(remote: GitRemote, value: boolean = true) { await this.container.storage.storeWorkspace('remote:default', value ? remote.id : undefined); diff --git a/src/git/search.ts b/src/git/search.ts index c55aff9..6b681e6 100644 --- a/src/git/search.ts +++ b/src/git/search.ts @@ -35,6 +35,20 @@ export interface SearchPattern { matchRegex?: boolean; } +export interface GitSearch { + repoPath: string; + pattern: SearchPattern; + results: string[]; + + readonly paging?: { + readonly limit: number | undefined; + readonly startingCursor: string | undefined; + readonly more: boolean; + }; + + more?(limit: number): Promise; +} + export function getKeyForSearchPattern(search: SearchPattern) { return `${search.pattern}|${search.matchAll ? 'A' : ''}${search.matchCase ? 'C' : ''}${ search.matchRegex ? 'R' : '' diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index ac2235e..47f9cd2 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -72,7 +72,7 @@ import type { RemoteProvider } from '../../git/remotes/remoteProvider'; import type { RemoteProviders } from '../../git/remotes/remoteProviders'; import { getRemoteProviderMatcher, loadRemoteProviders } from '../../git/remotes/remoteProviders'; import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider'; -import type { SearchPattern } from '../../git/search'; +import type { GitSearch, SearchPattern } from '../../git/search'; import { parseSearchOperations } from '../../git/search'; import type { LogScope } from '../../logger'; import { Logger } from '../../logger'; @@ -1581,6 +1581,104 @@ export class GitHubGitProvider implements GitProvider, Disposable { } @log() + async searchForCommitsSimple( + repoPath: string, + search: SearchPattern, + _options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' }, + ): Promise { + search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; + return { + repoPath: repoPath, + pattern: search, + results: [], + }; + + // try { + // const { args: searchArgs, files, commits } = this.getArgsFromSearchPattern(search); + // if (commits?.length) { + // return { + // repoPath: repoPath, + // pattern: search, + // results: commits, + // }; + // } + + // const refParser = getGraphRefParser(); + // const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0; + // const similarityThreshold = configuration.get('advanced.similarityThreshold'); + + // const args = [ + // 'log', + // ...refParser.arguments, + // `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, + // '--use-mailmap', + // ]; + // if (limit) { + // args.push(`-n${limit + 1}`); + // } + // if (options?.ordering) { + // args.push(`--${options.ordering}-order`); + // } + + // searchArgs.push(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '--'); + // if (files.length !== 0) { + // searchArgs.push(...files); + // } + + // async function searchForCommitsCore( + // this: LocalGitProvider, + // limit: number, + // cursor?: { sha: string; skip: number }, + // ): Promise { + // const data = await this.git.log2( + // repoPath, + // undefined, + // ...args, + // ...(cursor?.skip ? [`--skip=${cursor.skip}`] : []), + // ...searchArgs, + // '--', + // ...files, + // ); + // const results = [...refParser.parse(data)]; + + // const last = results[results.length - 1]; + // cursor = + // last != null + // ? { + // sha: last, + // skip: results.length, + // } + // : undefined; + + // return { + // repoPath: repoPath, + // pattern: search, + // results: results, + // paging: + // limit !== 0 && results.length > limit + // ? { + // limit: limit, + // startingCursor: cursor?.sha, + // more: true, + // } + // : undefined, + // more: async (limit: number): Promise => + // searchForCommitsCore.call(this, limit, cursor), + // }; + // } + + // return searchForCommitsCore.call(this, limit); + // } catch (ex) { + // // TODO@eamodio handle error reporting -- just invalid patterns? or more detailed? + // return { + // repoPath: repoPath, + // pattern: search, + // results: [], + // }; + // } + } + + @log() async getLogForSearch( repoPath: string, search: SearchPattern, diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 94ea9e3..05f4efd 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -14,6 +14,7 @@ import { GitGraphRowType } from '../../../git/models/graph'; import type { GitGraph } from '../../../git/models/graph'; import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository'; import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository'; +import type { SearchPattern } from '../../../git/search'; import { registerCommand } from '../../../system/command'; import { gate } from '../../../system/decorators/gate'; import { debug } from '../../../system/decorators/log'; @@ -35,9 +36,11 @@ import { DidChangeNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, + DidSearchCommitsNotificationType, DismissBannerCommandType, GetMissingAvatarsCommandType, GetMoreCommitsCommandType, + SearchCommitsCommandType, UpdateColumnCommandType, UpdateSelectedRepositoryCommandType, UpdateSelectionCommandType, @@ -194,7 +197,10 @@ export class GraphWebview extends WebviewBase { onIpc(GetMissingAvatarsCommandType, e, params => this.onGetMissingAvatars(params.emails)); break; case GetMoreCommitsCommandType.method: - onIpc(GetMoreCommitsCommandType, e, params => this.onGetMoreCommits(params.sha)); + onIpc(GetMoreCommitsCommandType, e, params => this.onGetMoreCommits(params.sha, e.id)); + break; + case SearchCommitsCommandType.method: + onIpc(SearchCommitsCommandType, e, params => this.onSearchCommits(params.search, e.id)); break; case UpdateColumnCommandType.method: onIpc(UpdateColumnCommandType, e, params => this.onColumnUpdated(params.name, params.config)); @@ -363,7 +369,7 @@ export class GraphWebview extends WebviewBase { } @gate() - private async onGetMoreCommits(sha?: string) { + private async onGetMoreCommits(sha?: string, completionId?: string) { if (this._graph?.more == null || this._repository?.etag !== this._etagRepository) { this.updateState(true); @@ -378,7 +384,35 @@ export class GraphWebview extends WebviewBase { debugger; } - void this.notifyDidChangeCommits(); + void this.notifyDidChangeCommits(completionId); + } + + @gate() + private async onSearchCommits(searchPattern: SearchPattern, completionId?: string) { + // if (this._repository?.etag !== this._etagRepository) { + // this.updateState(true); + + // return; + // } + + if (this._repository == null) return; + + const search = await this._repository.searchForCommitsSimple(searchPattern, { + limit: 100, + ordering: configuration.get('graph.commitOrdering'), + }); + + void this.notify( + DidSearchCommitsNotificationType, + { + ids: search.results, + paging: { + startingCursor: search.paging?.startingCursor, + more: search.paging?.more ?? false, + }, + }, + completionId, + ); } private onRepositorySelectionChanged(path: string) { @@ -498,20 +532,24 @@ export class GraphWebview extends WebviewBase { } @debug() - private async notifyDidChangeCommits() { + private async notifyDidChangeCommits(completionId?: string) { let success = false; if (this.isReady && this.visible) { const data = this._graph!; - success = await this.notify(DidChangeCommitsNotificationType, { - rows: data.rows, - avatars: Object.fromEntries(data.avatars), - selectedRows: this._selectedRows, - paging: { - startingCursor: data.paging?.startingCursor, - more: data.paging?.more ?? false, + success = await this.notify( + DidChangeCommitsNotificationType, + { + rows: data.rows, + avatars: Object.fromEntries(data.avatars), + selectedRows: this._selectedRows, + paging: { + startingCursor: data.paging?.startingCursor, + more: data.paging?.more ?? false, + }, }, - }); + completionId, + ); } this._pendingNotifyCommits = !success; diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index b43c120..ef01892 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -2,6 +2,7 @@ import type { GraphRow, Remote } from '@gitkraken/gitkraken-components'; import type { DateStyle, GraphColumnConfig } from '../../../config'; import type { RepositoryVisibility } from '../../../git/gitProvider'; import type { GitGraphRowType } from '../../../git/models/graph'; +import type { SearchPattern } from '../../../git/search'; import type { Subscription } from '../../../subscription'; import type { DateTimeFormat } from '../../../system/date'; import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol'; @@ -24,6 +25,7 @@ export interface State { // Props below are computed in the webview (not passed) mixedColumnColors?: Record; + searchResults?: DidSearchCommitsParams; } export interface GraphPaging { @@ -87,6 +89,11 @@ export interface GetMoreCommitsParams { } export const GetMoreCommitsCommandType = new IpcCommandType('graph/getMoreCommits'); +export interface SearchCommitsParams { + search: SearchPattern; +} +export const SearchCommitsCommandType = new IpcCommandType('graph/searchCommits'); + export interface UpdateColumnParams { name: string; config: GraphColumnConfig; @@ -149,3 +156,11 @@ export interface DidChangeSelectionParams { export const DidChangeSelectionNotificationType = new IpcNotificationType( 'graph/selection/didChange', ); + +export interface DidSearchCommitsParams { + ids: string[]; + paging?: GraphPaging; +} +export const DidSearchCommitsNotificationType = new IpcNotificationType( + 'graph/commits/didSearch', +); diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index db2fdfd..af8ec67 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -14,6 +14,7 @@ import { DateStyle } from '../../../../config'; import type { GraphColumnConfig } from '../../../../config'; import { RepositoryVisibility } from '../../../../git/gitProvider'; import type { GitGraphRowType } from '../../../../git/models/graph'; +import type { SearchPattern } from '../../../../git/search'; import type { DismissBannerParams, GraphComponentConfig, @@ -34,7 +35,8 @@ export interface GraphWrapperProps extends State { onSelectRepository?: (repository: GraphRepository) => void; onColumnChange?: (name: string, settings: GraphColumnConfig) => void; onMissingAvatars?: (emails: { [email: string]: string }) => void; - onMoreCommits?: () => void; + onMoreCommits?: (id?: string) => void; + onSearchCommits?: (search: SearchPattern) => void; //Promise; onDismissBanner?: (key: DismissBannerParams['key']) => void; onSelectionChange?: (selection: { id: string; type: GitGraphRowType }[]) => void; } @@ -89,6 +91,16 @@ const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDat return (commitDateTime: number) => formatCommitDateTime(commitDateTime, config?.dateStyle, config?.dateFormat); }; +const getSearchHighlights = (searchResults: State['searchResults']): { [id: string]: boolean } | undefined => { + if (!searchResults?.ids?.length) return undefined; + + const highlights: { [id: string]: boolean } = {}; + for (const sha of searchResults.ids) { + highlights[sha] = true; + } + return highlights; +}; + type DebouncableFn = (...args: any) => void; type DebouncedFn = (...args: any) => void; const debounceFrame = (func: DebouncableFn): DebouncedFn => { @@ -188,10 +200,12 @@ export function GraphWrapper({ onColumnChange, onMissingAvatars, onMoreCommits, + onSearchCommits, onSelectionChange, nonce, mixedColumnColors, previewBanner = true, + searchResults: searchResults2, trialBanner = true, onDismissBanner, }: GraphWrapperProps) { @@ -228,11 +242,13 @@ export function GraphWrapper({ const [searchValue, setSearchValue] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searchResultKey, setSearchResultKey] = useState(undefined); + const [searchHighlights, setSearchHighlights] = useState(getSearchHighlights(searchResults2)); useEffect(() => { if (searchValue === '' || searchValue.length < 3 || graphRows.length < 1) { setSearchResults([]); setSearchResultKey(undefined); + setSearchHighlights(undefined); return; } @@ -276,6 +292,7 @@ export function GraphWrapper({ const currentValue = e.currentTarget.value; setSearchValue(currentValue); + onSearchCommits?.({ pattern: currentValue }); }; useLayoutEffect(() => { @@ -310,6 +327,7 @@ export function GraphWrapper({ setSubscriptionSnapshot(state.subscription); setIsPrivateRepo(state.selectedRepositoryVisibility === RepositoryVisibility.Private); setIsLoading(state.loading); + setSearchHighlights(getSearchHighlights(state.searchResults)); } useEffect(() => subscriber?.(transformData), []); @@ -576,6 +594,7 @@ export function GraphWrapper({ graphRows={graphRows} hasMoreCommits={pagingState?.more} height={mainHeight} + highlightedShas={searchHighlights} // highlightRowssOnRefHover={graphConfig?.highlightRowsOnRefHover} isLoadingRows={isLoading} isSelectedBySha={graphSelectedRows} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index 4f7fccc..de054b8 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import type { GraphColumnConfig } from '../../../../config'; import type { GitGraphRowType } from '../../../../git/models/graph'; +import type { SearchPattern } from '../../../../git/search'; import type { DismissBannerParams, GraphRepository, @@ -17,9 +18,11 @@ import { DidChangeNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, + DidSearchCommitsNotificationType, DismissBannerCommandType, GetMissingAvatarsCommandType, GetMoreCommitsCommandType, + SearchCommitsCommandType, UpdateColumnCommandType, UpdateSelectedRepositoryCommandType as UpdateRepositorySelectionCommandType, UpdateSelectionCommandType, @@ -72,6 +75,7 @@ export class GraphApp extends App { )} onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)} onMoreCommits={(...params) => this.onGetMoreCommits(...params)} + onSearchCommits={(...params) => this.onSearchCommits(...params)} onSelectionChange={debounce( (selection: { id: string; type: GitGraphRowType }[]) => this.onSelectionChanged(selection), 250, @@ -183,6 +187,13 @@ export class GraphApp extends App { }); break; + case DidSearchCommitsNotificationType.method: + onIpc(DidSearchCommitsNotificationType, msg, params => { + this.setState({ ...this.state, searchResults: params }); + this.refresh(this.state); + }); + break; + case DidChangeSelectionNotificationType.method: onIpc(DidChangeSelectionNotificationType, msg, params => { this.setState({ ...this.state, selectedRows: params.selection }); @@ -274,8 +285,28 @@ export class GraphApp extends App { this.sendCommand(GetMissingAvatarsCommandType, { emails: emails }); } - private onGetMoreCommits(sha?: string) { - this.sendCommand(GetMoreCommitsCommandType, { sha: sha }); + private onGetMoreCommits(sha?: string, wait?: boolean) { + if (wait) { + return this.sendCommandWithCompletion( + GetMoreCommitsCommandType, + { sha: sha }, + DidChangeCommitsNotificationType, + ); + } + + return this.sendCommand(GetMoreCommitsCommandType, { sha: sha }); + } + + private onSearchCommits(search: SearchPattern, wait?: boolean) { + if (wait) { + return this.sendCommandWithCompletion( + SearchCommitsCommandType, + { search: search }, + DidSearchCommitsNotificationType, + ); + } + + return this.sendCommand(SearchCommitsCommandType, { search: search }); } private onSelectionChanged(selection: { id: string; type: GitGraphRowType }[]) {