Skip to content

Commit f290df4

Browse files
committed
Closes #4724 adds search history to Commit Graph
- Adds a persistent search history for the Commit Graph's search input - Allows navigating through past searches using arrow keys - Allows deleting history items with the Delete key
1 parent cd74277 commit f290df4

File tree

14 files changed

+496
-88
lines changed

14 files changed

+496
-88
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1111
- Adds a new _Safe Hard Reset_ (`--keep`) option to Git _reset_ command
1212
- Adds support for reference or range commit searches on the _Commit Graph_, _Search & Compare_ view, and in the _Search Commits_ command ([#4723](https://github.com/gitkraken/vscode-gitlens/issues/4723))
1313
- Adds natural language support to allow for more powerful queries
14+
- Adds a navigable search history to the search box on the _Commit Graph_ ([#4724](https://github.com/gitkraken/vscode-gitlens/issues/4724))
1415

1516
### Changed
1617

src/commands/git/search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ export class SearchGitCommand extends QuickCommand<State> {
454454
// Simulate an extra step if we have a value
455455
state.counter = value ? 3 : 2;
456456

457-
const operations = parseSearchQuery({
457+
const { operations } = parseSearchQuery({
458458
query: value,
459459
matchAll: state.matchAll,
460460
matchCase: state.matchCase,
@@ -539,7 +539,7 @@ async function updateSearchQuery(
539539
state: SearchStepState,
540540
context: Context,
541541
) {
542-
const ops = parseSearchQuery({
542+
const { operations: ops } = parseSearchQuery({
543543
query: quickpick.value,
544544
matchAll: state.matchAll,
545545
matchCase: state.matchCase,

src/constants.storage.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ export type WorkspaceStorage = {
184184
[key in IntegrationConnectedKey]: boolean;
185185
} & {
186186
[key in `views:${TreeViewTypes}:repositoryFilter`]: string[] | undefined;
187+
} & {
188+
[key in `graph:searchHistory:${string}`]: StoredGraphSearchHistory[];
187189
};
188190

189191
export interface Stored<T, SchemaVersion extends number = 1> {
@@ -352,6 +354,17 @@ export interface StoredGraphFilters {
352354

353355
export type StoredGraphRefType = 'head' | 'remote' | 'tag';
354356

357+
export type StoredGraphSearchHistory = {
358+
query: string;
359+
matchAll: boolean | undefined;
360+
matchCase: boolean | undefined;
361+
matchRegex: boolean | undefined;
362+
matchWholeWord: boolean | undefined;
363+
naturalLanguage: boolean | undefined;
364+
/** For NL queries, store the last known structured form to show in history */
365+
nlStructuredQuery?: string;
366+
};
367+
355368
export type StoredGraphSearchMode = 'normal' | 'filter';
356369

357370
export interface StoredGraphExcludedRef {

src/env/node/git/sub-providers/commits.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {
4141
} from '../../../../git/parsers/logParser';
4242
import { parseGitRefLog, parseGitRefLogDefaultFormat } from '../../../../git/parsers/reflogParser';
4343
import type { SearchQueryFilters } from '../../../../git/search';
44-
import { parseSearchQueryCommand, processNaturalLanguageToSearchQuery } from '../../../../git/search';
44+
import { parseSearchQueryCommand } from '../../../../git/search';
45+
import { processNaturalLanguageToSearchQuery } from '../../../../git/search.naturalLanguage';
4546
import { createUncommittedChangesCommit } from '../../../../git/utils/-webview/commit.utils';
4647
import { isRevisionRange, isSha, isUncommitted, isUncommittedStaged } from '../../../../git/utils/revision.utils';
4748
import { isUserMatch } from '../../../../git/utils/user.utils';
@@ -1010,9 +1011,9 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
10101011
@log<CommitsGitSubProvider['searchCommits']>({
10111012
args: {
10121013
1: s =>
1013-
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}${s.matchWholeWord ? 'W' : ''}]: ${
1014-
s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query
1015-
}`,
1014+
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}${
1015+
s.matchWholeWord ? 'W' : ''
1016+
}]: ${s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query}`,
10161017
},
10171018
})
10181019
async searchCommits(

src/git/search.naturalLanguage.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { SearchQuery } from '../constants.search';
2+
import type { Source } from '../constants.telemetry';
3+
import type { Container } from '../container';
4+
import type { NaturalLanguageSearchOptions } from '../plus/search/naturalLanguageSearchProcessor';
5+
import { NaturalLanguageSearchProcessor } from '../plus/search/naturalLanguageSearchProcessor';
6+
7+
/** Converts natural language to a structured search query */
8+
export async function processNaturalLanguageToSearchQuery(
9+
container: Container,
10+
search: SearchQuery,
11+
source: Source,
12+
options?: NaturalLanguageSearchOptions,
13+
): Promise<SearchQuery> {
14+
return new NaturalLanguageSearchProcessor(container).processNaturalLanguageToSearchQuery(search, source, options);
15+
}

src/git/search.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import type { SearchOperators, SearchOperatorsLongForm, SearchQuery } from '../constants.search';
22
import { searchOperators, searchOperatorsToLongFormMap } from '../constants.search';
33
import type { StoredSearchQuery } from '../constants.storage';
4-
import type { Source } from '../constants.telemetry';
5-
import type { Container } from '../container';
6-
import type { NaturalLanguageSearchOptions } from '../plus/search/naturalLanguageSearchProcessor';
7-
import { NaturalLanguageSearchProcessor } from '../plus/search/naturalLanguageSearchProcessor';
84
import { some } from '../system/iterable';
95
import type { GitRevisionReference } from './models/reference';
106
import type { GitUser } from './models/user';
@@ -68,6 +64,11 @@ export function getSearchQueryComparisonKey(search: SearchQuery | StoredSearchQu
6864
}${search.matchRegex ? 'R' : ''}${search.matchWholeWord ? 'W' : ''}${search.naturalLanguage ? 'NL' : ''}`;
6965
}
7066

67+
export interface ParsedSearchQuery {
68+
operations: Map<SearchOperatorsLongForm, Set<string>>;
69+
errors?: string[];
70+
}
71+
7172
export function createSearchQueryForCommit(ref: string): string;
7273
export function createSearchQueryForCommit(commit: GitRevisionReference): string;
7374
export function createSearchQueryForCommit(refOrCommit: string | GitRevisionReference): string {
@@ -80,10 +81,11 @@ export function createSearchQueryForCommits(refsOrCommits: (string | GitRevision
8081
return refsOrCommits.map(r => `#:${typeof r === 'string' ? shortenRevision(r) : r.name}`).join(' ');
8182
}
8283

83-
export function parseSearchQuery(search: SearchQuery): Map<SearchOperatorsLongForm, Set<string>> {
84+
export function parseSearchQuery(search: SearchQuery, validate: boolean = false): ParsedSearchQuery {
8485
const operations = new Map<SearchOperatorsLongForm, Set<string>>();
8586
const query = search.query.trim();
8687

88+
let errors: string[] | undefined;
8789
let pos = 0;
8890

8991
while (pos < query.length) {
@@ -164,6 +166,14 @@ export function parseSearchQuery(search: SearchQuery): Map<SearchOperatorsLongFo
164166
value = text;
165167
}
166168

169+
// Validate operator has a value
170+
if (op && !value) {
171+
if (!validate) continue;
172+
173+
errors ??= [];
174+
errors.push(`'${op}' requires a value`);
175+
}
176+
167177
// Add the discovered operation to our map
168178
if (op && value) {
169179
const longFormOp = searchOperatorsToLongFormMap.get(op);
@@ -178,7 +188,10 @@ export function parseSearchQuery(search: SearchQuery): Map<SearchOperatorsLongFo
178188
}
179189
}
180190

181-
return operations;
191+
return {
192+
operations: operations,
193+
...(errors?.length && { errors: errors }),
194+
};
182195
}
183196

184197
export interface SearchQueryFilters {
@@ -202,7 +215,7 @@ export interface SearchQueryCommand {
202215
}
203216

204217
export function parseSearchQueryCommand(search: SearchQuery, currentUser: GitUser | undefined): SearchQueryCommand {
205-
const operations = parseSearchQuery(search);
218+
const { operations } = parseSearchQuery(search);
206219

207220
const searchArgs = new Set<string>();
208221
const files: string[] = [];
@@ -391,13 +404,3 @@ export function parseSearchQueryCommand(search: SearchQuery, currentUser: GitUse
391404
filters: filters,
392405
};
393406
}
394-
395-
/** Converts natural language to a structured search query */
396-
export async function processNaturalLanguageToSearchQuery(
397-
container: Container,
398-
search: SearchQuery,
399-
source: Source,
400-
options?: NaturalLanguageSearchOptions,
401-
): Promise<SearchQuery> {
402-
return new NaturalLanguageSearchProcessor(container).processNaturalLanguageToSearchQuery(search, source, options);
403-
}

src/plus/integrations/providers/github/sub-providers/commits.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import type { GitLog } from '../../../../../git/models/log';
2222
import type { GitRevisionRange } from '../../../../../git/models/revision';
2323
import { deletedOrMissing } from '../../../../../git/models/revision';
2424
import type { GitUser } from '../../../../../git/models/user';
25-
import { parseSearchQuery, processNaturalLanguageToSearchQuery } from '../../../../../git/search';
25+
import { parseSearchQuery } from '../../../../../git/search';
26+
import { processNaturalLanguageToSearchQuery } from '../../../../../git/search.naturalLanguage';
2627
import { createUncommittedChangesCommit } from '../../../../../git/utils/-webview/commit.utils';
2728
import { createRevisionRange, isUncommitted } from '../../../../../git/utils/revision.utils';
2829
import { log } from '../../../../../system/decorators/log';
@@ -891,9 +892,9 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
891892
@log<CommitsGitSubProvider['searchCommits']>({
892893
args: {
893894
1: s =>
894-
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}${s.matchWholeWord ? 'W' : ''}]: ${
895-
s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query
896-
}`,
895+
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}${
896+
s.matchWholeWord ? 'W' : ''
897+
}]: ${s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query}`,
897898
},
898899
})
899900
async searchCommits(
@@ -915,7 +916,7 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
915916
search = await processNaturalLanguageToSearchQuery(this.container, search, source);
916917
}
917918

918-
const operations = parseSearchQuery(search);
919+
const { operations } = parseSearchQuery(search);
919920

920921
const values = operations.get('commit:');
921922
if (values?.size) {

src/plus/integrations/providers/github/sub-providers/graph.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,9 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
269269
getRemoteIconUri(this.container, remote, asWebviewUri)
270270
)?.toString(true);
271271
context = {
272-
webviewItem: `gitlens:branch+remote${isBranchStarred(this.container, remoteBranchId) ? '+starred' : ''}`,
272+
webviewItem: `gitlens:branch+remote${
273+
isBranchStarred(this.container, remoteBranchId) ? '+starred' : ''
274+
}`,
273275
webviewItemValue: {
274276
type: 'branch',
275277
ref: createReference(headBranch.name, repoPath, {
@@ -323,7 +325,9 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
323325
getRemoteIconUri(this.container, remote, asWebviewUri)
324326
)?.toString(true);
325327
context = {
326-
webviewItem: `gitlens:branch+remote${isBranchStarred(this.container, remoteBranchId) ? '+starred' : ''}`,
328+
webviewItem: `gitlens:branch+remote${
329+
isBranchStarred(this.container, remoteBranchId) ? '+starred' : ''
330+
}`,
327331
webviewItemValue: {
328332
type: 'branch',
329333
ref: createReference(b, repoPath, {
@@ -489,9 +493,9 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
489493
@log<GraphGitSubProvider['searchGraph']>({
490494
args: {
491495
1: s =>
492-
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}${s.matchWholeWord ? 'W' : ''}]: ${
493-
s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query
494-
}`,
496+
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}${
497+
s.matchWholeWord ? 'W' : ''
498+
}]: ${s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query}`,
495499
2: o => `limit=${o?.limit}, ordering=${o?.ordering}`,
496500
},
497501
})
@@ -511,7 +515,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
511515

512516
try {
513517
const results: GitGraphSearchResults = new Map<string, GitGraphSearchResultData>();
514-
const operations = parseSearchQuery(search);
518+
const { operations } = parseSearchQuery(search);
515519

516520
const values = operations.get('commit:');
517521
if (values != null) {

src/webviews/apps/plus/graph/graph-header.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ export class GlGraphHeader extends SignalWatcher(LitElement) {
371371
search: this.searchValid ? { ...this.graphState.filter } : undefined /*limit: options?.limit*/,
372372
});
373373

374-
if (rsp.search && rsp.results) {
374+
// Only log successful searches with at least 1 result
375+
if (rsp.search && rsp.results && !('error' in rsp.results) && rsp.results.count > 0) {
375376
this.searchEl.logSearch(rsp.search);
376377
}
377378

@@ -905,7 +906,9 @@ export class GlGraphHeader extends SignalWatcher(LitElement) {
905906
</sl-select>
906907
</gl-tooltip>
907908
<div
908-
class=${`shrink ${!Object.values(this.graphState.excludeRefs ?? {}).length && 'hidden'}`}
909+
class=${`shrink ${
910+
!Object.values(this.graphState.excludeRefs ?? {}).length && 'hidden'
911+
}`}
909912
>
910913
<gl-popover
911914
class="popover"

src/webviews/apps/shared/components/search/search-box.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class GlSearchBox extends GlElement {
166166
}
167167

168168
logSearch(query: SearchQuery): void {
169-
this.searchInput?.logSearch(query);
169+
void this.searchInput?.logSearch(query);
170170
}
171171

172172
setSearchQuery(query: string): void {

0 commit comments

Comments
 (0)