@reqlan/analytical 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/out/analysis/fuzzy-search-analyser.d.ts +17 -0
- package/out/analysis/fuzzy-search-analyser.js +25 -0
- package/out/analysis/fuzzy-search-analyser.js.map +1 -0
- package/out/analysis/fuzzy-search-worker-client.d.ts +39 -0
- package/out/analysis/fuzzy-search-worker-client.js +178 -0
- package/out/analysis/fuzzy-search-worker-client.js.map +1 -0
- package/out/analysis/fuzzy-search-worker.d.ts +32 -0
- package/out/analysis/fuzzy-search-worker.js +81 -0
- package/out/analysis/fuzzy-search-worker.js.map +1 -0
- package/out/analysis/fuzzy-search.d.ts +38 -0
- package/out/analysis/fuzzy-search.js +217 -0
- package/out/analysis/fuzzy-search.js.map +1 -0
- package/out/analysis/git-dates-analyser.js +12 -3
- package/out/analysis/git-dates-analyser.js.map +1 -1
- package/out/analysis-api.d.ts +14 -0
- package/out/analysis-api.js +32 -0
- package/out/analysis-api.js.map +1 -1
- package/out/core/barrel-page.d.ts +46 -0
- package/out/core/barrel-page.js +162 -0
- package/out/core/barrel-page.js.map +1 -0
- package/out/core/filter-specials.d.ts +2 -0
- package/out/core/filter-specials.js +15 -0
- package/out/core/filter-specials.js.map +1 -1
- package/out/core/types.d.ts +17 -1
- package/out/core/types.js +1 -0
- package/out/core/types.js.map +1 -1
- package/out/create-runtime.js +2 -0
- package/out/create-runtime.js.map +1 -1
- package/out/export/export-csv.d.ts +4 -0
- package/out/export/export-csv.js +13 -0
- package/out/export/export-csv.js.map +1 -0
- package/out/export/export-json.d.ts +6 -0
- package/out/export/export-json.js +28 -0
- package/out/export/export-json.js.map +1 -0
- package/out/export/export-markdown.d.ts +4 -0
- package/out/export/export-markdown.js +23 -0
- package/out/export/export-markdown.js.map +1 -0
- package/out/export/html-export-assets.d.ts +1 -1
- package/out/export/html-export-assets.js +126 -5
- package/out/export/html-export-assets.js.map +1 -1
- package/out/export/html-export-template.js +89 -47
- package/out/export/html-export-template.js.map +1 -1
- package/out/export/html-export-utils.d.ts +33 -3
- package/out/export/html-export-utils.js +108 -10
- package/out/export/html-export-utils.js.map +1 -1
- package/out/export/types.d.ts +1 -1
- package/out/export/write-csv-export.d.ts +5 -0
- package/out/export/write-csv-export.js +132 -0
- package/out/export/write-csv-export.js.map +1 -0
- package/out/export/write-json-export.d.ts +3 -0
- package/out/export/write-json-export.js +92 -0
- package/out/export/write-json-export.js.map +1 -0
- package/out/export/write-markdown-export.d.ts +5 -0
- package/out/export/write-markdown-export.js +177 -0
- package/out/export/write-markdown-export.js.map +1 -0
- package/out/index-store/idea-extractor.d.ts +6 -1
- package/out/index-store/idea-extractor.js +56 -11
- package/out/index-store/idea-extractor.js.map +1 -1
- package/out/index-store/sqlite-store.d.ts +16 -3
- package/out/index-store/sqlite-store.js +104 -8
- package/out/index-store/sqlite-store.js.map +1 -1
- package/out/index-store/webview-graph-queries.d.ts +5 -0
- package/out/index-store/webview-graph-queries.js +21 -4
- package/out/index-store/webview-graph-queries.js.map +1 -1
- package/out/index-store/webview-table-queries.js +1 -0
- package/out/index-store/webview-table-queries.js.map +1 -1
- package/out/index-store/workspace-index-file.js +22 -1
- package/out/index-store/workspace-index-file.js.map +1 -1
- package/out/index.d.ts +14 -1
- package/out/index.js +9 -1
- package/out/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,6 +30,25 @@ npm install @reqlan/analytical
|
|
|
30
30
|
|
|
31
31
|
## Changelog
|
|
32
32
|
|
|
33
|
+
### 0.8.1
|
|
34
|
+
|
|
35
|
+
#### Patch Changes
|
|
36
|
+
|
|
37
|
+
- d46b740: Fix missed build and test errors.
|
|
38
|
+
- Updated dependencies [d46b740]
|
|
39
|
+
- @reqlan/language@1.8.1
|
|
40
|
+
|
|
41
|
+
### 0.8.0
|
|
42
|
+
|
|
43
|
+
#### Minor Changes
|
|
44
|
+
|
|
45
|
+
- 93368c6: Update exports; add support for wildcard import; update search functionality; add barrel import
|
|
46
|
+
|
|
47
|
+
#### Patch Changes
|
|
48
|
+
|
|
49
|
+
- Updated dependencies [93368c6]
|
|
50
|
+
- @reqlan/language@1.8.0
|
|
51
|
+
|
|
33
52
|
### 0.7.0
|
|
34
53
|
|
|
35
54
|
#### Minor Changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyser entry for fuzzy idea search.
|
|
3
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search]
|
|
4
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search_whitespace]
|
|
5
|
+
*/
|
|
6
|
+
import type { Analyser } from './analyser-registry.js';
|
|
7
|
+
import { type FuzzySearchHit } from './fuzzy-search.js';
|
|
8
|
+
export interface FuzzySearchParams {
|
|
9
|
+
query: string;
|
|
10
|
+
/** Cap results after ranking; omit for uncapped (caller paginates). */
|
|
11
|
+
limit?: number;
|
|
12
|
+
/** When true, empty query returns no hits instead of every non-ideaset idea. */
|
|
13
|
+
requireQuery?: boolean;
|
|
14
|
+
/** When true mid-run, scoring aborts and returns no hits (superseded request). */
|
|
15
|
+
isCancelled?: () => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const fuzzySearchAnalyser: Analyser<FuzzySearchParams, FuzzySearchHit[]>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { filterAndScoreIdeasAsync } from './fuzzy-search.js';
|
|
2
|
+
export const fuzzySearchAnalyser = {
|
|
3
|
+
id: 'fuzzy_search',
|
|
4
|
+
async run({ store }, { query, limit, requireQuery = false, isCancelled }) {
|
|
5
|
+
const trimmed = query.trim();
|
|
6
|
+
if (requireQuery && !trimmed) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
if (isCancelled?.()) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const ideas = await store.listAllIdeas();
|
|
13
|
+
// listAllIdeas is sync under sql.js — yield so openIdea / newer searches can run.
|
|
14
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
15
|
+
if (isCancelled?.()) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const hits = await filterAndScoreIdeasAsync(ideas, trimmed, { isCancelled });
|
|
19
|
+
if (hits === undefined || isCancelled?.()) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return limit === undefined ? hits : hits.slice(0, Math.max(0, limit));
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=fuzzy-search-analyser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fuzzy-search-analyser.js","sourceRoot":"","sources":["../../src/analysis/fuzzy-search-analyser.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,wBAAwB,EAAuB,MAAM,mBAAmB,CAAC;AAYlF,MAAM,CAAC,MAAM,mBAAmB,GAAkD;IAC9E,EAAE,EAAE,cAAc;IAClB,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,GAAG,KAAK,EAAE,WAAW,EAAE;QACpE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,YAAY,IAAI,CAAC,OAAO,EAAE,CAAC;YAC3B,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,WAAW,EAAE,EAAE,EAAE,CAAC;YAClB,OAAO,EAAE,CAAC;QACd,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,EAAE,CAAC;QACzC,kFAAkF;QAClF,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,WAAW,EAAE,EAAE,EAAE,CAAC;YAClB,OAAO,EAAE,CAAC;QACd,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QAC7E,IAAI,IAAI,KAAK,SAAS,IAAI,WAAW,EAAE,EAAE,EAAE,CAAC;YACxC,OAAO,EAAE,CAAC;QACd,CAAC;QACD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAC1E,CAAC;CACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IdeaSummary } from '../core/types.js';
|
|
2
|
+
import type { FuzzySearchHit } from './fuzzy-search.js';
|
|
3
|
+
/** Resolve the bundled worker script next to the extension host output. */
|
|
4
|
+
export declare function resolveFuzzySearchWorkerPath(explicit?: string): string;
|
|
5
|
+
export interface FuzzySearchWorkerSearchResult {
|
|
6
|
+
hits: FuzzySearchHit[];
|
|
7
|
+
total: number;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Owns a single worker that holds the idea catalog and scores queries off-thread.
|
|
12
|
+
*/
|
|
13
|
+
export declare class FuzzySearchWorkerClient {
|
|
14
|
+
private worker;
|
|
15
|
+
private readonly pending;
|
|
16
|
+
private catalogEpoch;
|
|
17
|
+
private disposed;
|
|
18
|
+
private readonly workerPath;
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
workerPath?: string;
|
|
21
|
+
});
|
|
22
|
+
get hasCatalog(): boolean;
|
|
23
|
+
setCatalog(ideas: readonly IdeaSummary[]): Promise<number>;
|
|
24
|
+
clearCatalog(): void;
|
|
25
|
+
search(query: string, requestId: number, options?: {
|
|
26
|
+
limit?: number;
|
|
27
|
+
requireQuery?: boolean;
|
|
28
|
+
}): Promise<FuzzySearchWorkerSearchResult>;
|
|
29
|
+
/** Drop in-flight search promises so callers can move on (worker may still finish). */
|
|
30
|
+
cancelSearches(requestId?: number): void;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
private ensureWorker;
|
|
33
|
+
private post;
|
|
34
|
+
private onMessage;
|
|
35
|
+
}
|
|
36
|
+
export declare class SearchCancelledError extends Error {
|
|
37
|
+
readonly requestId: number;
|
|
38
|
+
constructor(requestId: number);
|
|
39
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side client for the fuzzy-search worker thread.
|
|
3
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search]
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { Worker } from 'node:worker_threads';
|
|
9
|
+
function moduleDirectory() {
|
|
10
|
+
if (typeof __dirname === 'string') {
|
|
11
|
+
return __dirname;
|
|
12
|
+
}
|
|
13
|
+
const metaUrl = (0, eval)('import.meta.url');
|
|
14
|
+
return dirname(fileURLToPath(metaUrl));
|
|
15
|
+
}
|
|
16
|
+
/** Resolve the bundled worker script next to the extension host output. */
|
|
17
|
+
export function resolveFuzzySearchWorkerPath(explicit) {
|
|
18
|
+
if (explicit) {
|
|
19
|
+
return explicit;
|
|
20
|
+
}
|
|
21
|
+
const dir = moduleDirectory();
|
|
22
|
+
const candidates = [
|
|
23
|
+
join(dir, 'fuzzy-search-worker.cjs'),
|
|
24
|
+
join(dir, 'fuzzy-search-worker.js'),
|
|
25
|
+
join(dir, '..', 'fuzzy-search-worker.cjs'),
|
|
26
|
+
join(dir, 'extension', 'fuzzy-search-worker.cjs')
|
|
27
|
+
];
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
if (existsSync(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return candidates[0];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Owns a single worker that holds the idea catalog and scores queries off-thread.
|
|
37
|
+
*/
|
|
38
|
+
export class FuzzySearchWorkerClient {
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.pending = new Map();
|
|
41
|
+
this.catalogEpoch = 0;
|
|
42
|
+
this.disposed = false;
|
|
43
|
+
this.workerPath = resolveFuzzySearchWorkerPath(options?.workerPath);
|
|
44
|
+
}
|
|
45
|
+
get hasCatalog() {
|
|
46
|
+
return this.catalogEpoch > 0;
|
|
47
|
+
}
|
|
48
|
+
async setCatalog(ideas) {
|
|
49
|
+
this.ensureWorker();
|
|
50
|
+
const epoch = ++this.catalogEpoch;
|
|
51
|
+
const compact = ideas.map(idea => ({
|
|
52
|
+
id: idea.id,
|
|
53
|
+
name: idea.name,
|
|
54
|
+
kind: idea.kind,
|
|
55
|
+
fileUri: idea.fileUri,
|
|
56
|
+
summary: idea.summary,
|
|
57
|
+
lineStart: idea.lineStart,
|
|
58
|
+
tags: idea.tags
|
|
59
|
+
}));
|
|
60
|
+
this.post({ type: 'setCatalog', ideas: compact });
|
|
61
|
+
return epoch;
|
|
62
|
+
}
|
|
63
|
+
clearCatalog() {
|
|
64
|
+
this.catalogEpoch = 0;
|
|
65
|
+
if (this.worker) {
|
|
66
|
+
this.post({ type: 'clearCatalog' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
search(query, requestId, options) {
|
|
70
|
+
this.ensureWorker();
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
this.pending.set(requestId, { resolve, reject });
|
|
73
|
+
this.post({
|
|
74
|
+
type: 'search',
|
|
75
|
+
requestId,
|
|
76
|
+
query,
|
|
77
|
+
limit: options?.limit,
|
|
78
|
+
requireQuery: options?.requireQuery
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Drop in-flight search promises so callers can move on (worker may still finish). */
|
|
83
|
+
cancelSearches(requestId) {
|
|
84
|
+
if (this.worker) {
|
|
85
|
+
this.post({ type: 'cancel', requestId });
|
|
86
|
+
}
|
|
87
|
+
if (requestId === undefined) {
|
|
88
|
+
for (const [id, pending] of this.pending) {
|
|
89
|
+
pending.reject(new SearchCancelledError(id));
|
|
90
|
+
}
|
|
91
|
+
this.pending.clear();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const pending = this.pending.get(requestId);
|
|
95
|
+
if (pending) {
|
|
96
|
+
this.pending.delete(requestId);
|
|
97
|
+
pending.reject(new SearchCancelledError(requestId));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
dispose() {
|
|
101
|
+
this.disposed = true;
|
|
102
|
+
for (const [id, pending] of this.pending) {
|
|
103
|
+
pending.reject(new SearchCancelledError(id));
|
|
104
|
+
}
|
|
105
|
+
this.pending.clear();
|
|
106
|
+
const worker = this.worker;
|
|
107
|
+
this.worker = undefined;
|
|
108
|
+
void worker?.terminate();
|
|
109
|
+
}
|
|
110
|
+
ensureWorker() {
|
|
111
|
+
if (this.disposed) {
|
|
112
|
+
throw new Error('FuzzySearchWorkerClient is disposed');
|
|
113
|
+
}
|
|
114
|
+
if (this.worker) {
|
|
115
|
+
return this.worker;
|
|
116
|
+
}
|
|
117
|
+
const worker = new Worker(this.workerPath);
|
|
118
|
+
worker.on('message', (message) => {
|
|
119
|
+
this.onMessage(message);
|
|
120
|
+
});
|
|
121
|
+
worker.on('error', error => {
|
|
122
|
+
for (const [, pending] of this.pending) {
|
|
123
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
124
|
+
}
|
|
125
|
+
this.pending.clear();
|
|
126
|
+
this.worker = undefined;
|
|
127
|
+
});
|
|
128
|
+
worker.on('exit', code => {
|
|
129
|
+
if (this.disposed) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.worker = undefined;
|
|
133
|
+
if (code !== 0 && this.pending.size > 0) {
|
|
134
|
+
const error = new Error(`fuzzy-search worker exited with code ${code}`);
|
|
135
|
+
for (const [, pending] of this.pending) {
|
|
136
|
+
pending.reject(error);
|
|
137
|
+
}
|
|
138
|
+
this.pending.clear();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
this.worker = worker;
|
|
142
|
+
return worker;
|
|
143
|
+
}
|
|
144
|
+
post(message) {
|
|
145
|
+
this.ensureWorker().postMessage(message);
|
|
146
|
+
}
|
|
147
|
+
onMessage(message) {
|
|
148
|
+
if (!message || typeof message !== 'object' || !('type' in message)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const pending = this.pending.get(message.requestId);
|
|
152
|
+
if (!pending) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.pending.delete(message.requestId);
|
|
156
|
+
if (message.type === 'result') {
|
|
157
|
+
pending.resolve({
|
|
158
|
+
hits: message.hits,
|
|
159
|
+
total: message.total,
|
|
160
|
+
truncated: message.truncated
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (message.type === 'cancelled') {
|
|
165
|
+
pending.reject(new SearchCancelledError(message.requestId));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
pending.reject(new Error(message.message));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export class SearchCancelledError extends Error {
|
|
172
|
+
constructor(requestId) {
|
|
173
|
+
super(`Search ${requestId} cancelled`);
|
|
174
|
+
this.name = 'SearchCancelledError';
|
|
175
|
+
this.requestId = requestId;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=fuzzy-search-worker-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fuzzy-search-worker-client.js","sourceRoot":"","sources":["../../src/analysis/fuzzy-search-worker-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAW7C,SAAS,eAAe;IACpB,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,iBAAiB,CAAW,CAAC;IACvD,OAAO,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,4BAA4B,CAAC,QAAiB;IAC1D,IAAI,QAAQ,EAAE,CAAC;QACX,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;IAC9B,MAAM,UAAU,GAAG;QACf,IAAI,CAAC,GAAG,EAAE,yBAAyB,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,wBAAwB,CAAC;QACnC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,yBAAyB,CAAC;QAC1C,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,yBAAyB,CAAC;KACpD,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACjC,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACxB,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC,CAAC,CAAE,CAAC;AAC1B,CAAC;AAaD;;GAEG;AACH,MAAM,OAAO,uBAAuB;IAOhC,YAAY,OAAiC;QAL5B,YAAO,GAAG,IAAI,GAAG,EAAmB,CAAC;QAC9C,iBAAY,GAAG,CAAC,CAAC;QACjB,aAAQ,GAAG,KAAK,CAAC;QAIrB,IAAI,CAAC,UAAU,GAAG,4BAA4B,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACxE,CAAC;IAED,IAAI,UAAU;QACV,OAAO,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAA6B;QAC1C,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC;QAClC,MAAM,OAAO,GAA4B,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxD,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,IAAI,EAAE,IAAI,CAAC,IAAI;SAClB,CAAC,CAAC,CAAC;QACJ,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAClD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,YAAY;QACR,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;QACxC,CAAC;IACL,CAAC;IAED,MAAM,CACF,KAAa,EACb,SAAiB,EACjB,OAAoD;QAEpD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC;gBACN,IAAI,EAAE,QAAQ;gBACd,SAAS;gBACT,KAAK;gBACL,KAAK,EAAE,OAAO,EAAE,KAAK;gBACrB,YAAY,EAAE,OAAO,EAAE,YAAY;aACtC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;IAED,uFAAuF;IACvF,cAAc,CAAC,SAAkB;QAC7B,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC1B,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACvC,OAAO,CAAC,MAAM,CAAC,IAAI,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC;YACjD,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrB,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,OAAO,EAAE,CAAC;YACV,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,IAAI,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC;QACxD,CAAC;IACL,CAAC;IAED,OAAO;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACvC,OAAO,CAAC,MAAM,CAAC,IAAI,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QACxB,KAAK,MAAM,EAAE,SAAS,EAAE,CAAC;IAC7B,CAAC;IAEO,YAAY;QAChB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,IAAI,CAAC,MAAM,CAAC;QACvB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC3C,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAkC,EAAE,EAAE;YACxD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE;YACvB,KAAK,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACrC,OAAO,CAAC,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC9E,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE;YACrB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAChB,OAAO;YACX,CAAC;YACD,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;YACxB,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC;gBACxE,KAAK,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;oBACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC1B,CAAC;gBACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC;QACL,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,OAAO,MAAM,CAAC;IAClB,CAAC;IAEO,IAAI,CAAC,OAAiC;QAC1C,IAAI,CAAC,YAAY,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;IAEO,SAAS,CAAC,OAAkC;QAChD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,EAAE,CAAC;YAClE,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO;QACX,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC5B,OAAO,CAAC,OAAO,CAAC;gBACZ,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,SAAS,EAAE,OAAO,CAAC,SAAS;aAC/B,CAAC,CAAC;YACH,OAAO;QACX,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,IAAI,oBAAoB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;YAC5D,OAAO;QACX,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,CAAC;CACJ;AAED,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAG3C,YAAY,SAAiB;QACzB,KAAK,CAAC,UAAU,SAAS,YAAY,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type FuzzySearchHit } from './fuzzy-search.js';
|
|
2
|
+
import type { IdeaSummary } from '../core/types.js';
|
|
3
|
+
export type FuzzySearchWorkerIdea = Pick<IdeaSummary, 'id' | 'name' | 'kind' | 'fileUri' | 'summary' | 'lineStart' | 'tags'>;
|
|
4
|
+
export type FuzzySearchWorkerInbound = {
|
|
5
|
+
type: 'setCatalog';
|
|
6
|
+
ideas: FuzzySearchWorkerIdea[];
|
|
7
|
+
} | {
|
|
8
|
+
type: 'clearCatalog';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'search';
|
|
11
|
+
requestId: number;
|
|
12
|
+
query: string;
|
|
13
|
+
limit?: number;
|
|
14
|
+
requireQuery?: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'cancel';
|
|
17
|
+
requestId?: number;
|
|
18
|
+
};
|
|
19
|
+
export type FuzzySearchWorkerOutbound = {
|
|
20
|
+
type: 'result';
|
|
21
|
+
requestId: number;
|
|
22
|
+
hits: FuzzySearchHit[];
|
|
23
|
+
total: number;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'error';
|
|
27
|
+
requestId: number;
|
|
28
|
+
message: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'cancelled';
|
|
31
|
+
requestId: number;
|
|
32
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread fuzzy idea search — keeps scoring off the extension-host event loop.
|
|
3
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search]
|
|
4
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search_whitespace]
|
|
5
|
+
*/
|
|
6
|
+
import { parentPort } from 'node:worker_threads';
|
|
7
|
+
import { filterAndScoreIdeas } from './fuzzy-search.js';
|
|
8
|
+
let catalog = [];
|
|
9
|
+
let activeRequestId;
|
|
10
|
+
if (!parentPort) {
|
|
11
|
+
throw new Error('fuzzy-search-worker must run as a worker thread');
|
|
12
|
+
}
|
|
13
|
+
parentPort.on('message', (message) => {
|
|
14
|
+
if (!message || typeof message !== 'object' || !('type' in message)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
switch (message.type) {
|
|
18
|
+
case 'setCatalog':
|
|
19
|
+
catalog = message.ideas;
|
|
20
|
+
break;
|
|
21
|
+
case 'clearCatalog':
|
|
22
|
+
catalog = [];
|
|
23
|
+
break;
|
|
24
|
+
case 'cancel':
|
|
25
|
+
if (message.requestId === undefined || message.requestId === activeRequestId) {
|
|
26
|
+
activeRequestId = undefined;
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
case 'search': {
|
|
30
|
+
const { requestId, query, limit, requireQuery = false } = message;
|
|
31
|
+
activeRequestId = requestId;
|
|
32
|
+
try {
|
|
33
|
+
const trimmed = query.trim();
|
|
34
|
+
if (requireQuery && !trimmed) {
|
|
35
|
+
parentPort.postMessage({
|
|
36
|
+
type: 'result',
|
|
37
|
+
requestId,
|
|
38
|
+
hits: [],
|
|
39
|
+
total: 0,
|
|
40
|
+
truncated: false
|
|
41
|
+
});
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const ranked = filterAndScoreIdeas(catalog, trimmed);
|
|
45
|
+
if (activeRequestId !== requestId) {
|
|
46
|
+
parentPort.postMessage({
|
|
47
|
+
type: 'cancelled',
|
|
48
|
+
requestId
|
|
49
|
+
});
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const total = ranked.length;
|
|
53
|
+
const truncated = limit !== undefined && total > limit;
|
|
54
|
+
const hits = limit === undefined ? ranked : ranked.slice(0, Math.max(0, limit));
|
|
55
|
+
parentPort.postMessage({
|
|
56
|
+
type: 'result',
|
|
57
|
+
requestId,
|
|
58
|
+
hits,
|
|
59
|
+
total,
|
|
60
|
+
truncated
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
parentPort.postMessage({
|
|
65
|
+
type: 'error',
|
|
66
|
+
requestId,
|
|
67
|
+
message: error instanceof Error ? error.message : String(error)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (activeRequestId === requestId) {
|
|
72
|
+
activeRequestId = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=fuzzy-search-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fuzzy-search-worker.js","sourceRoot":"","sources":["../../src/analysis/fuzzy-search-worker.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAuB,MAAM,mBAAmB,CAAC;AAyB7E,IAAI,OAAO,GAA4B,EAAE,CAAC;AAC1C,IAAI,eAAmC,CAAC;AAExC,IAAI,CAAC,UAAU,EAAE,CAAC;IACd,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;AACvE,CAAC;AAED,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAiC,EAAE,EAAE;IAC3D,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,EAAE,CAAC;QAClE,OAAO;IACX,CAAC;IACD,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,YAAY;YACb,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC;YACxB,MAAM;QACV,KAAK,cAAc;YACf,OAAO,GAAG,EAAE,CAAC;YACb,MAAM;QACV,KAAK,QAAQ;YACT,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,SAAS,KAAK,eAAe,EAAE,CAAC;gBAC3E,eAAe,GAAG,SAAS,CAAC;YAChC,CAAC;YACD,MAAM;QACV,KAAK,QAAQ,CAAC,CAAC,CAAC;YACZ,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;YAClE,eAAe,GAAG,SAAS,CAAC;YAC5B,IAAI,CAAC;gBACD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;gBAC7B,IAAI,YAAY,IAAI,CAAC,OAAO,EAAE,CAAC;oBAC3B,UAAW,CAAC,WAAW,CAAC;wBACpB,IAAI,EAAE,QAAQ;wBACd,SAAS;wBACT,IAAI,EAAE,EAAE;wBACR,KAAK,EAAE,CAAC;wBACR,SAAS,EAAE,KAAK;qBACiB,CAAC,CAAC;oBACvC,MAAM;gBACV,CAAC;gBACD,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAwB,EAAE,OAAO,CAAC,CAAC;gBACtE,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;oBAChC,UAAW,CAAC,WAAW,CAAC;wBACpB,IAAI,EAAE,WAAW;wBACjB,SAAS;qBACwB,CAAC,CAAC;oBACvC,MAAM;gBACV,CAAC;gBACD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;gBAC5B,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,KAAK,CAAC;gBACvD,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBAChF,UAAW,CAAC,WAAW,CAAC;oBACpB,IAAI,EAAE,QAAQ;oBACd,SAAS;oBACT,IAAI;oBACJ,KAAK;oBACL,SAAS;iBACwB,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,UAAW,CAAC,WAAW,CAAC;oBACpB,IAAI,EAAE,OAAO;oBACb,SAAS;oBACT,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;iBAC9B,CAAC,CAAC;YAC3C,CAAC;oBAAS,CAAC;gBACP,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;oBAChC,eAAe,GAAG,SAAS,CAAC;gBAChC,CAAC;YACL,CAAC;YACD,MAAM;QACV,CAAC;QACD;YACI,MAAM;IACd,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuzzy / partial idea search with interchangeable separators.
|
|
3
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search]
|
|
4
|
+
* rq:["../../../../reqlan rq/core_analysis/search.rq".fuzzy_search_whitespace]
|
|
5
|
+
*/
|
|
6
|
+
import type { IdeaSummary } from '../core/types.js';
|
|
7
|
+
export interface FuzzySearchHit {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
kind: IdeaSummary['kind'];
|
|
11
|
+
fileUri: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
lineStart: number;
|
|
14
|
+
score: number;
|
|
15
|
+
}
|
|
16
|
+
/** Lowercase and strip interchangeable separators for comparison. */
|
|
17
|
+
export declare function normalizeSearchSeparators(value: string): string;
|
|
18
|
+
/** Split on interchangeable separators into lowercase word tokens. */
|
|
19
|
+
export declare function splitSearchTokens(value: string): string[];
|
|
20
|
+
export declare function filterAndScoreIdeas(ideas: IdeaSummary[], query: string): FuzzySearchHit[];
|
|
21
|
+
/**
|
|
22
|
+
* Score ideas in chunks, yielding to the event loop so UI actions (e.g. openIdea)
|
|
23
|
+
* are not blocked by a long synchronous scan of a large index.
|
|
24
|
+
* Returns `undefined` when `isCancelled` becomes true mid-run.
|
|
25
|
+
*/
|
|
26
|
+
export declare function filterAndScoreIdeasAsync(ideas: IdeaSummary[], query: string, options?: {
|
|
27
|
+
isCancelled?: () => boolean;
|
|
28
|
+
/** Ideas processed between yields; default 250. */
|
|
29
|
+
chunkSize?: number;
|
|
30
|
+
}): Promise<FuzzySearchHit[] | undefined>;
|
|
31
|
+
/**
|
|
32
|
+
* Match every query token against hay tokens.
|
|
33
|
+
* Prefers order-preserving matches; still accepts reordered tokens (missing
|
|
34
|
+
* intermediate hay words are fine either way).
|
|
35
|
+
*/
|
|
36
|
+
export declare function matchQueryTokens(hayTokens: string[], queryTokens: string[]): 'ordered' | 'reordered' | null;
|
|
37
|
+
/** True when needle characters appear in order inside hay (simple fuzzy). */
|
|
38
|
+
export declare function fuzzySubsequence(hay: string, needle: string): boolean;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characters treated as the same separator family: underscore, hyphen,
|
|
3
|
+
* ellipsis / dots, and whitespace. Runs collapse to nothing for matching so
|
|
4
|
+
* `cli package`, `cli_package`, `cli-package`, and `cli...package` align.
|
|
5
|
+
*/
|
|
6
|
+
const SEPARATOR_RE = /[_\-\s.…]+/g;
|
|
7
|
+
/** Lowercase and strip interchangeable separators for comparison. */
|
|
8
|
+
export function normalizeSearchSeparators(value) {
|
|
9
|
+
return value.toLowerCase().replace(SEPARATOR_RE, '');
|
|
10
|
+
}
|
|
11
|
+
/** Split on interchangeable separators into lowercase word tokens. */
|
|
12
|
+
export function splitSearchTokens(value) {
|
|
13
|
+
return value
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.split(SEPARATOR_RE)
|
|
16
|
+
.map(token => token.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
export function filterAndScoreIdeas(ideas, query) {
|
|
20
|
+
return rankScoredIdeas(scoreIdeas(ideas, query));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Score ideas in chunks, yielding to the event loop so UI actions (e.g. openIdea)
|
|
24
|
+
* are not blocked by a long synchronous scan of a large index.
|
|
25
|
+
* Returns `undefined` when `isCancelled` becomes true mid-run.
|
|
26
|
+
*/
|
|
27
|
+
export async function filterAndScoreIdeasAsync(ideas, query, options) {
|
|
28
|
+
const chunkSize = Math.max(1, options?.chunkSize ?? 250);
|
|
29
|
+
const isCancelled = options?.isCancelled;
|
|
30
|
+
const rawNeedle = query.trim().toLowerCase();
|
|
31
|
+
const needle = normalizeSearchSeparators(rawNeedle);
|
|
32
|
+
const queryTokens = splitSearchTokens(rawNeedle);
|
|
33
|
+
const scored = [];
|
|
34
|
+
for (let index = 0; index < ideas.length; index += 1) {
|
|
35
|
+
if (index > 0 && index % chunkSize === 0) {
|
|
36
|
+
if (isCancelled?.()) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
await yieldEventLoop();
|
|
40
|
+
if (isCancelled?.()) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const idea = ideas[index];
|
|
45
|
+
if (idea.kind === 'ideaset') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const score = rawNeedle ? scoreIdeaMatch(idea, rawNeedle, needle, queryTokens) : 1;
|
|
49
|
+
if (score <= 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
scored.push({
|
|
53
|
+
id: idea.id,
|
|
54
|
+
name: idea.name,
|
|
55
|
+
kind: idea.kind,
|
|
56
|
+
fileUri: idea.fileUri,
|
|
57
|
+
summary: idea.summary,
|
|
58
|
+
lineStart: idea.lineStart,
|
|
59
|
+
score
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (isCancelled?.()) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return rankScoredIdeas(scored);
|
|
66
|
+
}
|
|
67
|
+
function scoreIdeas(ideas, query) {
|
|
68
|
+
const rawNeedle = query.trim().toLowerCase();
|
|
69
|
+
const needle = normalizeSearchSeparators(rawNeedle);
|
|
70
|
+
const queryTokens = splitSearchTokens(rawNeedle);
|
|
71
|
+
const scored = [];
|
|
72
|
+
for (const idea of ideas) {
|
|
73
|
+
if (idea.kind === 'ideaset') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const score = rawNeedle ? scoreIdeaMatch(idea, rawNeedle, needle, queryTokens) : 1;
|
|
77
|
+
if (score <= 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
scored.push({
|
|
81
|
+
id: idea.id,
|
|
82
|
+
name: idea.name,
|
|
83
|
+
kind: idea.kind,
|
|
84
|
+
fileUri: idea.fileUri,
|
|
85
|
+
summary: idea.summary,
|
|
86
|
+
lineStart: idea.lineStart,
|
|
87
|
+
score
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return scored;
|
|
91
|
+
}
|
|
92
|
+
function rankScoredIdeas(scored) {
|
|
93
|
+
return scored.sort((left, right) => right.score - left.score
|
|
94
|
+
|| left.name.localeCompare(right.name)
|
|
95
|
+
|| left.fileUri.localeCompare(right.fileUri));
|
|
96
|
+
}
|
|
97
|
+
function yieldEventLoop() {
|
|
98
|
+
return new Promise(resolve => {
|
|
99
|
+
setTimeout(resolve, 0);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function scoreIdeaMatch(idea, rawNeedle, needle, queryTokens) {
|
|
103
|
+
const nameRaw = idea.name.toLowerCase();
|
|
104
|
+
const name = normalizeSearchSeparators(idea.name);
|
|
105
|
+
const summaryRaw = idea.summary.toLowerCase();
|
|
106
|
+
const summary = normalizeSearchSeparators(idea.summary);
|
|
107
|
+
let score = 0;
|
|
108
|
+
if (nameRaw === rawNeedle || name === needle) {
|
|
109
|
+
score = 100;
|
|
110
|
+
}
|
|
111
|
+
else if (nameRaw.startsWith(rawNeedle) || name.startsWith(needle)) {
|
|
112
|
+
score = 80;
|
|
113
|
+
}
|
|
114
|
+
else if (nameRaw.includes(rawNeedle) || name.includes(needle)) {
|
|
115
|
+
score = 50;
|
|
116
|
+
}
|
|
117
|
+
else if (fuzzySubsequence(nameRaw, rawNeedle) || fuzzySubsequence(name, needle)) {
|
|
118
|
+
score = 30;
|
|
119
|
+
}
|
|
120
|
+
const nameTokenKind = matchQueryTokens(splitSearchTokens(idea.name), queryTokens);
|
|
121
|
+
if (nameTokenKind === 'ordered') {
|
|
122
|
+
score = Math.max(score, 45);
|
|
123
|
+
}
|
|
124
|
+
else if (nameTokenKind === 'reordered') {
|
|
125
|
+
score = Math.max(score, 35);
|
|
126
|
+
}
|
|
127
|
+
if (summaryRaw.includes(rawNeedle) || (needle && summary.includes(needle))) {
|
|
128
|
+
score = Math.max(score, 20) + 5;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const summaryTokenKind = matchQueryTokens(splitSearchTokens(idea.summary), queryTokens);
|
|
132
|
+
if (summaryTokenKind === 'ordered') {
|
|
133
|
+
score = Math.max(score, 22);
|
|
134
|
+
}
|
|
135
|
+
else if (summaryTokenKind === 'reordered') {
|
|
136
|
+
score = Math.max(score, 18);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const tag of idea.tags) {
|
|
140
|
+
const tagRaw = tag.toLowerCase();
|
|
141
|
+
const tagNorm = normalizeSearchSeparators(tag);
|
|
142
|
+
if (tagRaw.includes(rawNeedle) || (needle && tagNorm.includes(needle))) {
|
|
143
|
+
score = Math.max(score, 15) + 2;
|
|
144
|
+
}
|
|
145
|
+
else if (matchQueryTokens(splitSearchTokens(tag), queryTokens) !== null) {
|
|
146
|
+
score = Math.max(score, 15) + 2;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return score;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Match every query token against hay tokens.
|
|
153
|
+
* Prefers order-preserving matches; still accepts reordered tokens (missing
|
|
154
|
+
* intermediate hay words are fine either way).
|
|
155
|
+
*/
|
|
156
|
+
export function matchQueryTokens(hayTokens, queryTokens) {
|
|
157
|
+
if (queryTokens.length === 0 || hayTokens.length === 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
let hayIndex = 0;
|
|
161
|
+
let ordered = true;
|
|
162
|
+
for (const queryToken of queryTokens) {
|
|
163
|
+
let found = -1;
|
|
164
|
+
for (let index = hayIndex; index < hayTokens.length; index += 1) {
|
|
165
|
+
if (tokenMatches(hayTokens[index], queryToken)) {
|
|
166
|
+
found = index;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (found < 0) {
|
|
171
|
+
ordered = false;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
hayIndex = found + 1;
|
|
175
|
+
}
|
|
176
|
+
if (ordered) {
|
|
177
|
+
return 'ordered';
|
|
178
|
+
}
|
|
179
|
+
const used = new Set();
|
|
180
|
+
for (const queryToken of queryTokens) {
|
|
181
|
+
let found = -1;
|
|
182
|
+
for (let index = 0; index < hayTokens.length; index += 1) {
|
|
183
|
+
if (used.has(index)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (tokenMatches(hayTokens[index], queryToken)) {
|
|
187
|
+
found = index;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (found < 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
used.add(found);
|
|
195
|
+
}
|
|
196
|
+
return 'reordered';
|
|
197
|
+
}
|
|
198
|
+
function tokenMatches(hayToken, queryToken) {
|
|
199
|
+
return hayToken === queryToken || hayToken.startsWith(queryToken);
|
|
200
|
+
}
|
|
201
|
+
/** True when needle characters appear in order inside hay (simple fuzzy). */
|
|
202
|
+
export function fuzzySubsequence(hay, needle) {
|
|
203
|
+
if (!needle) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
let index = 0;
|
|
207
|
+
for (const char of hay) {
|
|
208
|
+
if (char === needle[index]) {
|
|
209
|
+
index += 1;
|
|
210
|
+
if (index >= needle.length) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=fuzzy-search.js.map
|