Skip to content

Commit 32453da

Browse files
committed
Improves quoted searching (exact match)
Avoids many "id already exists" errors with searches - Though we lose the count of results in the item label :(
1 parent 9f9d138 commit 32453da

File tree

3 files changed

+114
-50
lines changed

3 files changed

+114
-50
lines changed

src/constants.search.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ export const searchOperatorsToLongFormMap = new Map<SearchOperators, SearchOpera
3434
['type:', 'type:'],
3535
]);
3636

37-
export const searchOperationRegex =
38-
/(?:(?<op>=:|message:|@:|author:|#:|commit:|\?:|file:|~:|change:|is:|type:)\s?(?<value>".+?"|\S+}?))|(?<text>\S+)(?!(?:=|message|@|author|#|commit|\?|file|~|change|is|type):)/g;
39-
4037
export const searchOperationHelpRegex =
4138
/(?:^|(\b|\s)*)((=:|message:|@:|author:|#:|commit:|\?:|file:|~:|change:|is:|type:)(?:"[^"]*"?|\w*))(?:$|(\b|\s))/g;
4239

src/git/search.ts

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SearchOperators, SearchOperatorsLongForm, SearchQuery } from '../constants.search';
2-
import { searchOperationRegex, searchOperatorsToLongFormMap } from '../constants.search';
2+
import { searchOperators, searchOperatorsToLongFormMap } from '../constants.search';
33
import type { StoredSearchQuery } from '../constants.storage';
44
import type { GitRevisionReference } from './models/reference';
55
import type { GitUser } from './models/user';
@@ -63,35 +63,101 @@ export function createSearchQueryForCommits(refsOrCommits: (string | GitRevision
6363

6464
export function parseSearchQuery(search: SearchQuery): Map<SearchOperatorsLongForm, Set<string>> {
6565
const operations = new Map<SearchOperatorsLongForm, Set<string>>();
66+
const query = search.query.trim();
6667

67-
let op: SearchOperators | undefined;
68-
let value: string | undefined;
69-
let text: string | undefined;
68+
let pos = 0;
7069

71-
let match;
72-
do {
73-
match = searchOperationRegex.exec(search.query);
74-
if (match?.groups == null) break;
70+
while (pos < query.length) {
71+
// Skip whitespace
72+
if (/\s/.test(query[pos])) {
73+
pos++;
74+
continue;
75+
}
7576

76-
op = searchOperatorsToLongFormMap.get(match.groups.op as SearchOperators);
77-
({ value, text } = match.groups);
77+
// Try to match an operator
78+
let matchedOperator = false;
79+
let op: SearchOperators | undefined;
80+
let value: string | undefined;
81+
82+
// Check for operators (starting with longer ones first to avoid partial matches)
83+
for (const operator of searchOperators) {
84+
if (!operator.length) continue;
85+
86+
if (query.startsWith(operator, pos)) {
87+
op = operator as SearchOperators;
88+
const startPos = pos + operator.length;
89+
pos = startPos;
90+
91+
// Skip optional space after operator
92+
if (query[pos] === ' ') {
93+
pos++;
94+
}
95+
96+
// Extract the value and check if it is quoted
97+
if (query[pos] === '"') {
98+
const endQuotePos = query.indexOf('"', pos + 1);
99+
if (endQuotePos !== -1) {
100+
value = query.substring(pos, endQuotePos + 1);
101+
pos = endQuotePos + 1;
102+
} else {
103+
// Unterminated quote, take the rest of the string
104+
value = query.substring(pos);
105+
pos = query.length;
106+
}
107+
} else {
108+
// Unquoted value - take until whitespace
109+
const nextSpacePos = query.indexOf(' ', pos);
110+
const valueEndPos = nextSpacePos !== -1 ? nextSpacePos : query.length;
111+
value = query.substring(pos, valueEndPos);
112+
pos = valueEndPos;
113+
}
114+
115+
matchedOperator = true;
116+
break;
117+
}
118+
}
78119

79-
if (text) {
80-
if (!searchOperatorsToLongFormMap.has(text.trim() as SearchOperators)) {
81-
op = text === '@me' ? 'author:' : isSha(text) ? 'commit:' : 'message:';
82-
value = text;
120+
if (!matchedOperator) {
121+
// No operator found, parse as text
122+
let text: string;
123+
124+
// Check if text is quoted
125+
if (query[pos] === '"') {
126+
const endQuotePos = query.indexOf('"', pos + 1);
127+
if (endQuotePos !== -1) {
128+
text = query.substring(pos, endQuotePos + 1);
129+
pos = endQuotePos + 1;
130+
} else {
131+
// Unterminated quote, take the rest of the string
132+
text = query.substring(pos);
133+
pos = query.length;
134+
}
135+
} else {
136+
// Unquoted text - take until whitespace
137+
const nextSpacePos = query.indexOf(' ', pos);
138+
const valueEndPos = nextSpacePos !== -1 ? nextSpacePos : query.length;
139+
text = query.substring(pos, valueEndPos);
140+
pos = valueEndPos;
83141
}
142+
143+
// Handle special text tokens (@me, SHA)
144+
op = text === '@me' ? 'author:' : isSha(text) ? 'commit:' : 'message:';
145+
value = text;
84146
}
85147

148+
// Add the discovered operation to our map
86149
if (op && value) {
87-
let values = operations.get(op);
88-
if (values == null) {
89-
values = new Set();
90-
operations.set(op, values);
150+
const longFormOp = searchOperatorsToLongFormMap.get(op);
151+
if (longFormOp) {
152+
let values = operations.get(longFormOp);
153+
if (values == null) {
154+
values = new Set();
155+
operations.set(longFormOp, values);
156+
}
157+
values.add(value);
91158
}
92-
values.add(value);
93159
}
94-
} while (match != null);
160+
}
95161

96162
return operations;
97163
}

src/views/nodes/resultsCommitsNode.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { configuration } from '../../system/-webview/configuration';
99
import { gate } from '../../system/decorators/-webview/gate';
1010
import { debug } from '../../system/decorators/log';
1111
import { map } from '../../system/iterable';
12-
import type { Deferred } from '../../system/promise';
13-
import { defer, pauseOnCancelOrTimeout } from '../../system/promise';
12+
import { pauseOnCancelOrTimeout } from '../../system/promise';
1413
import type { ViewsWithCommits } from '../viewBase';
1514
import type { PageableViewNode } from './abstract/viewNode';
1615
import { ContextValues, getViewNodeId, ViewNode } from './abstract/viewNode';
@@ -85,15 +84,16 @@ export class ResultsCommitsNode<View extends ViewsWithCommits = ViewsWithCommits
8584
return this.context.comparisonFiltered;
8685
}
8786

88-
private _onChildrenCompleted: Deferred<void> | undefined;
87+
// Stop trying to update the label, because VS Code can't handle it and throws id conflict errors
88+
// private _onChildrenCompleted: Deferred<void> | undefined;
8989

9090
async getChildren(): Promise<ViewNode[]> {
91-
this._onChildrenCompleted?.cancel();
92-
this._onChildrenCompleted = defer<void>();
91+
// this._onChildrenCompleted?.cancel();
92+
// this._onChildrenCompleted = defer<void>();
9393

9494
const { log } = await this.getCommitsQueryResults();
95-
if (log == null) {
96-
this._onChildrenCompleted?.fulfill();
95+
if (!log?.commits.size) {
96+
// this._onChildrenCompleted?.fulfill();
9797
return [new MessageNode(this.view, this, 'No results found')];
9898
}
9999

@@ -159,7 +159,7 @@ export class ResultsCommitsNode<View extends ViewsWithCommits = ViewsWithCommits
159159
children.push(new LoadMoreNode(this.view, this, children[children.length - 1]));
160160
}
161161

162-
this._onChildrenCompleted?.fulfill();
162+
// this._onChildrenCompleted?.fulfill();
163163
return children;
164164
}
165165

@@ -171,28 +171,28 @@ export class ResultsCommitsNode<View extends ViewsWithCommits = ViewsWithCommits
171171
label = this._label;
172172
state = TreeItemCollapsibleState.Collapsed;
173173
} else {
174-
let log;
174+
// let log;
175175

176176
const result = await pauseOnCancelOrTimeout(this.getCommitsQueryResults(), undefined, 100);
177177
if (!result.paused) {
178-
({ label, log } = result.value);
179-
state =
180-
log == null || log.count === 0
181-
? TreeItemCollapsibleState.None
182-
: this._options.expand //|| log.count === 1
183-
? TreeItemCollapsibleState.Expanded
184-
: TreeItemCollapsibleState.Collapsed;
178+
state = this._options.expand ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed;
179+
180+
// ({ label, log } = result.value);
181+
//
182+
// state = !log?.commits.size
183+
// ? TreeItemCollapsibleState.None
184+
// : this._options.expand //|| log.count === 1
185+
// ? TreeItemCollapsibleState.Expanded
186+
// : TreeItemCollapsibleState.Collapsed;
185187
} else {
186-
queueMicrotask(async () => {
187-
try {
188-
await this._onChildrenCompleted?.promise;
189-
} catch {
190-
return;
191-
}
192-
193-
void (await result.value);
194-
this.view.triggerNodeChange(this.parent);
195-
});
188+
// Stop trying to update the label, because VS Code can't handle it and throws id conflict errors
189+
// queueMicrotask(async () => {
190+
// try {
191+
// await this._onChildrenCompleted?.promise;
192+
// void (await result.value);
193+
// this.view.triggerNodeChange(this.parent);
194+
// } catch {}
195+
// });
196196

197197
// Need to use Collapsed before we have results or the item won't show up in the view until the children are awaited
198198
// https://github.com/microsoft/vscode/issues/54806 & https://github.com/microsoft/vscode/issues/62214
@@ -233,7 +233,8 @@ export class ResultsCommitsNode<View extends ViewsWithCommits = ViewsWithCommits
233233
if (this._results.deferred) {
234234
this._results.deferred = false;
235235

236-
void this.parent.triggerChange(false);
236+
// Stop trying to update the label, because VS Code can't handle it and throws id conflict errors
237+
// void this.parent.triggerChange(false);
237238
}
238239
}
239240

0 commit comments

Comments
 (0)