pi-readseek 0.7.7 → 0.7.9

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.
Files changed (60) hide show
  1. package/README.md +28 -46
  2. package/dist/index.js +7333 -0
  3. package/package.json +8 -7
  4. package/index.ts +0 -147
  5. package/src/binary-detect.ts +0 -22
  6. package/src/check.ts +0 -92
  7. package/src/coerce-obvious-int.ts +0 -41
  8. package/src/confusable-hyphens.ts +0 -6
  9. package/src/def.ts +0 -143
  10. package/src/diff-data.ts +0 -302
  11. package/src/edit-classify.ts +0 -58
  12. package/src/edit-diff.ts +0 -359
  13. package/src/edit-output.ts +0 -110
  14. package/src/edit-render-helpers.ts +0 -141
  15. package/src/edit-syntax-validate.ts +0 -65
  16. package/src/edit.ts +0 -686
  17. package/src/file-map.ts +0 -17
  18. package/src/fs-error.ts +0 -33
  19. package/src/grep-budget.ts +0 -35
  20. package/src/grep-output.ts +0 -181
  21. package/src/grep-render-helpers.ts +0 -77
  22. package/src/grep-symbol-scope.ts +0 -184
  23. package/src/grep.ts +0 -608
  24. package/src/hashline.ts +0 -736
  25. package/src/hover.ts +0 -140
  26. package/src/path-utils.ts +0 -24
  27. package/src/pending-diff-preview.ts +0 -269
  28. package/src/read-local-bundle.ts +0 -75
  29. package/src/read-output.ts +0 -186
  30. package/src/read-render-helpers.ts +0 -104
  31. package/src/read.ts +0 -661
  32. package/src/readseek/constants.ts +0 -17
  33. package/src/readseek/enums.ts +0 -38
  34. package/src/readseek/formatter.ts +0 -350
  35. package/src/readseek/symbol-error-format.ts +0 -18
  36. package/src/readseek/symbol-lookup.ts +0 -240
  37. package/src/readseek/symbol-tree.ts +0 -21
  38. package/src/readseek/types.ts +0 -49
  39. package/src/readseek-client.ts +0 -1250
  40. package/src/readseek-params.ts +0 -39
  41. package/src/readseek-settings.ts +0 -263
  42. package/src/readseek-value.ts +0 -184
  43. package/src/refs-output.ts +0 -47
  44. package/src/refs.ts +0 -161
  45. package/src/register-tool.ts +0 -29
  46. package/src/rename.ts +0 -182
  47. package/src/replace-symbol.ts +0 -77
  48. package/src/runtime.ts +0 -3
  49. package/src/search-output.ts +0 -50
  50. package/src/search.ts +0 -214
  51. package/src/session-anchors.ts +0 -34
  52. package/src/stat-search-path.ts +0 -29
  53. package/src/syntax-validate-mode.ts +0 -15
  54. package/src/tool-prompt-metadata.ts +0 -107
  55. package/src/tool-types.ts +0 -12
  56. package/src/tui-diff-component.ts +0 -108
  57. package/src/tui-diff-renderer.ts +0 -92
  58. package/src/tui-render-utils.ts +0 -286
  59. package/src/view.ts +0 -158
  60. package/src/write.ts +0 -357
@@ -1,38 +0,0 @@
1
- /**
2
- * Symbol kinds supported by mappers.
3
- */
4
- export enum SymbolKind {
5
- Class = "class",
6
- Function = "function",
7
- Method = "method",
8
- Variable = "variable",
9
- Constant = "constant",
10
- Interface = "interface",
11
- Type = "type",
12
- Enum = "enum",
13
- Struct = "struct",
14
- Import = "import",
15
- Module = "module",
16
- Namespace = "namespace",
17
- Property = "property",
18
- Heading = "heading",
19
- Table = "table",
20
- View = "view",
21
- Procedure = "procedure",
22
- Trigger = "trigger",
23
- Index = "index",
24
- Schema = "schema",
25
- Signal = "signal",
26
- Unknown = "unknown",
27
- }
28
-
29
- /**
30
- * Detail levels for map generation.
31
- */
32
- export enum DetailLevel {
33
- Full = "full",
34
- Compact = "compact",
35
- Minimal = "minimal",
36
- Outline = "outline",
37
- Truncated = "truncated",
38
- }
@@ -1,350 +0,0 @@
1
- import { basename } from "node:path";
2
-
3
- import type { FileMap, FileSymbol } from "./types.js";
4
-
5
- import { THRESHOLDS } from "./constants.js";
6
- import { DetailLevel } from "./enums.js";
7
-
8
- const BOX_LINE = "───────────────────────────────────────";
9
-
10
- /**
11
- * Format a file size for display.
12
- */
13
- function formatSize(bytes: number): string {
14
- if (bytes < 1024) {
15
- return `${bytes} B`;
16
- }
17
- if (bytes < 1024 * 1024) {
18
- return `${(bytes / 1024).toFixed(1)} KB`;
19
- }
20
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
21
- }
22
-
23
- /**
24
- * Format a number with comma separators.
25
- */
26
- function formatNumber(n: number): string {
27
- return n.toLocaleString("en-US");
28
- }
29
-
30
- /**
31
- * Format a symbol for display.
32
- */
33
- function formatSymbol(symbol: FileSymbol, indent = 0): string {
34
- const prefix = " ".repeat(indent);
35
- const lineRange =
36
- symbol.startLine === symbol.endLine
37
- ? `[${symbol.startLine}]`
38
- : `[${symbol.startLine}-${symbol.endLine}]`;
39
-
40
- const { name } = symbol;
41
-
42
- // Format based on kind
43
- let formatted: string;
44
- switch (symbol.kind) {
45
- case "class":
46
- case "interface":
47
- case "struct":
48
- case "enum":
49
- case "type": {
50
- formatted = `${prefix}${symbol.kind} ${name}: ${lineRange}`;
51
- break;
52
- }
53
- case "function":
54
- case "method": {
55
- formatted = `${prefix}${name}: ${lineRange}`;
56
- break;
57
- }
58
- case "variable":
59
- case "constant": {
60
- formatted = `${prefix}${name} = ... ${lineRange}`;
61
- break;
62
- }
63
- default: {
64
- formatted = `${prefix}${name}: ${lineRange}`;
65
- }
66
- }
67
-
68
- return formatted;
69
- }
70
-
71
- /**
72
- * Format symbols recursively.
73
- */
74
- function formatSymbols(
75
- symbols: FileSymbol[],
76
- level: DetailLevel,
77
- indent = 0
78
- ): string[] {
79
- const lines: string[] = [];
80
-
81
- for (const symbol of symbols) {
82
- lines.push(formatSymbol(symbol, indent));
83
-
84
- // Add children for full, compact, and minimal levels (not outline or truncated)
85
- if (
86
- level !== DetailLevel.Outline &&
87
- level !== DetailLevel.Truncated &&
88
- symbol.children?.length
89
- ) {
90
- // For minimal, flatten children
91
- if (level === DetailLevel.Minimal) {
92
- for (const child of symbol.children) {
93
- lines.push(formatSymbol(child, indent + 1));
94
- }
95
- } else {
96
- lines.push(...formatSymbols(symbol.children, level, indent + 1));
97
- }
98
- }
99
- }
100
-
101
- return lines;
102
- }
103
-
104
- /**
105
- * Format a complete file map to a string.
106
- */
107
- function formatFileMap(map: FileMap, level?: DetailLevel): string {
108
- const effectiveLevel = level ?? map.detailLevel;
109
- const fileName = basename(map.path);
110
-
111
- const lines: string[] = [
112
- "",
113
- BOX_LINE,
114
- `File Map: ${fileName}`,
115
- `${formatNumber(map.totalLines)} lines │ ${formatSize(map.totalBytes)} │ ${map.language}`,
116
- BOX_LINE,
117
- "",
118
- ];
119
-
120
- // Add detail level notice if reduced from Full
121
- if (map.truncatedInfo) {
122
- const { shownSymbols, totalSymbols } = map.truncatedInfo;
123
- lines.push(
124
- `[Map ≤${formatSize(THRESHOLDS.MAX_TRUNCATED_BYTES)} | ${shownSymbols} of ${formatNumber(totalSymbols)} symbols]`
125
- );
126
- lines.push("");
127
- } else if (effectiveLevel === DetailLevel.Outline) {
128
- lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_OUTLINE_BYTES)} | outline]`);
129
- lines.push("");
130
- } else if (effectiveLevel === DetailLevel.Minimal) {
131
- lines.push(`[Map ≤${formatSize(THRESHOLDS.MAX_MAP_BYTES)} | minimal]`);
132
- lines.push("");
133
- } else if (effectiveLevel === DetailLevel.Compact) {
134
- lines.push(
135
- `[Map ≤${formatSize(THRESHOLDS.COMPACT_TARGET_BYTES)} | compact]`
136
- );
137
- lines.push("");
138
- }
139
-
140
- // Add symbols
141
- if (map.truncatedInfo) {
142
- // Truncated format: first half, separator, second half
143
- const half = Math.floor(map.symbols.length / 2);
144
- const firstSymbols = map.symbols.slice(0, half);
145
- const lastSymbols = map.symbols.slice(half);
146
-
147
- // Format first batch
148
- const firstLines = formatSymbols(firstSymbols, effectiveLevel);
149
- lines.push(...firstLines);
150
-
151
- // Add separator
152
- lines.push("");
153
- lines.push(
154
- ` ─ ─ ─ ${formatNumber(map.truncatedInfo.omittedSymbols)} more symbols ─ ─ ─`
155
- );
156
- lines.push("");
157
-
158
- // Format last batch
159
- const lastLines = formatSymbols(lastSymbols, effectiveLevel);
160
- lines.push(...lastLines);
161
- } else {
162
- // Normal format
163
- const symbolLines = formatSymbols(map.symbols, effectiveLevel);
164
- lines.push(...symbolLines);
165
- }
166
-
167
- // Add footer with appropriate guidance
168
- lines.push("");
169
- lines.push(BOX_LINE);
170
- if (map.truncatedInfo) {
171
- // For truncated maps, provide specific guidance on finding omitted symbols
172
- const firstShown = map.symbols.slice(0, Math.floor(map.symbols.length / 2));
173
- const lastShown = map.symbols.slice(Math.floor(map.symbols.length / 2));
174
- const lastFirst = firstShown.at(-1);
175
- const firstLast = lastShown.at(0);
176
- if (lastFirst && firstLast) {
177
- const omitStart = lastFirst.endLine + 1;
178
- const omitEnd = firstLast.startLine - 1;
179
- lines.push(
180
- `Omitted symbols are in lines ${formatNumber(omitStart)}-${formatNumber(omitEnd)}.`
181
- );
182
- }
183
- lines.push(
184
- "Use read(path, offset=LINE, limit=N) to view specific sections."
185
- );
186
- } else {
187
- lines.push("Use read(path, offset=LINE, limit=N) for targeted reads.");
188
- }
189
- lines.push(BOX_LINE);
190
-
191
- return lines.join("\n");
192
- }
193
-
194
- /**
195
- * Reduce detail level of a file map.
196
- */
197
- function reduceToLevel(map: FileMap, level: DetailLevel): FileMap {
198
- if (level === DetailLevel.Outline) {
199
- // Remove all children
200
- return {
201
- ...map,
202
- detailLevel: DetailLevel.Outline,
203
- symbols: map.symbols.map((s) => ({
204
- name: s.name,
205
- kind: s.kind,
206
- startLine: s.startLine,
207
- endLine: s.endLine,
208
- })),
209
- };
210
- }
211
-
212
- if (level === DetailLevel.Minimal) {
213
- return {
214
- ...map,
215
- detailLevel: DetailLevel.Minimal,
216
- symbols: map.symbols.map((s) => ({
217
- name: s.name,
218
- kind: s.kind,
219
- startLine: s.startLine,
220
- endLine: s.endLine,
221
- children: s.children?.map((c) => ({
222
- name: c.name,
223
- kind: c.kind,
224
- startLine: c.startLine,
225
- endLine: c.endLine,
226
- })),
227
- })),
228
- };
229
- }
230
-
231
- if (level === DetailLevel.Compact) {
232
- return { ...map, detailLevel: DetailLevel.Compact };
233
- }
234
-
235
- return { ...map, detailLevel: DetailLevel.Full };
236
- }
237
-
238
- /**
239
- * Reduce a file map to truncated form: first N + last N symbols only.
240
- * Used when even Outline level exceeds budget.
241
- */
242
- function reduceToTruncated(
243
- map: FileMap,
244
- symbolsEach: number = THRESHOLDS.TRUNCATED_SYMBOLS_EACH
245
- ): FileMap {
246
- const { symbols } = map;
247
- const total = symbols.length;
248
-
249
- if (total <= symbolsEach * 2) {
250
- // Not enough symbols to truncate, return as outline
251
- return reduceToLevel(map, DetailLevel.Outline);
252
- }
253
-
254
- const firstSymbols = symbols.slice(0, symbolsEach).map((s) => ({
255
- name: s.name,
256
- kind: s.kind,
257
- startLine: s.startLine,
258
- endLine: s.endLine,
259
- }));
260
-
261
- const lastSymbols = symbols.slice(-symbolsEach).map((s) => ({
262
- name: s.name,
263
- kind: s.kind,
264
- startLine: s.startLine,
265
- endLine: s.endLine,
266
- }));
267
-
268
- return {
269
- ...map,
270
- symbols: [...firstSymbols, ...lastSymbols],
271
- detailLevel: DetailLevel.Truncated,
272
- truncatedInfo: {
273
- totalSymbols: total,
274
- shownSymbols: symbolsEach * 2,
275
- omittedSymbols: total - symbolsEach * 2,
276
- },
277
- };
278
- }
279
-
280
- /**
281
- * Format a file map with automatic budget enforcement.
282
- * Reduces detail level until the map fits within the budget.
283
- */
284
- export function formatFileMapWithBudget(
285
- map: FileMap,
286
- maxBytes = THRESHOLDS.MAX_TRUNCATED_BYTES
287
- ): string {
288
- // Tiered budgets: progressively reduce detail level
289
- const tiers: { level: DetailLevel; budget: number }[] = [
290
- { level: DetailLevel.Full, budget: THRESHOLDS.FULL_TARGET_BYTES },
291
- { level: DetailLevel.Compact, budget: THRESHOLDS.COMPACT_TARGET_BYTES },
292
- { level: DetailLevel.Minimal, budget: THRESHOLDS.MAX_MAP_BYTES },
293
- { level: DetailLevel.Outline, budget: THRESHOLDS.MAX_OUTLINE_BYTES },
294
- ];
295
-
296
- for (const { level, budget } of tiers) {
297
- const reduced = reduceToLevel(map, level);
298
- const formatted = formatFileMap(reduced, level);
299
- const size = Buffer.byteLength(formatted, "utf8");
300
-
301
- if (size <= budget && size <= maxBytes) {
302
- return formatted;
303
- }
304
- }
305
-
306
- // Outline exceeded budget - need to truncate symbols
307
- // First check if full outline fits in maxBytes (just not in outline budget)
308
- const outline = reduceToLevel(map, DetailLevel.Outline);
309
- const outlineFormatted = formatFileMap(outline, DetailLevel.Outline);
310
- const outlineSize = Buffer.byteLength(outlineFormatted, "utf8");
311
-
312
- if (outlineSize <= maxBytes) {
313
- // Outline fits in truncated budget, use it
314
- return outlineFormatted;
315
- }
316
-
317
- // Need to truncate - binary search for maximum symbols that fit
318
- const totalSymbols = map.symbols.length;
319
- const minSymbols = 10; // Guaranteed minimum
320
- const maxSymbolsEach = Math.floor(totalSymbols / 2); // Can't show more than half on each side
321
-
322
- let low = minSymbols;
323
- let high = maxSymbolsEach;
324
- let bestResult: string | null = null;
325
-
326
- while (low <= high) {
327
- const mid = Math.floor((low + high) / 2);
328
- const truncated = reduceToTruncated(map, mid);
329
- const formatted = formatFileMap(truncated, DetailLevel.Truncated);
330
- const size = Buffer.byteLength(formatted, "utf8");
331
-
332
- if (size <= maxBytes) {
333
- // This fits, try to show more
334
- bestResult = formatted;
335
- low = mid + 1;
336
- } else {
337
- // Too big, show fewer
338
- high = mid - 1;
339
- }
340
- }
341
-
342
- // Return best result or fallback to minimum
343
- if (bestResult) {
344
- return bestResult;
345
- }
346
-
347
- // Absolute fallback: minimum symbols (guaranteed to fit)
348
- const minimal = reduceToTruncated(map, minSymbols);
349
- return formatFileMap(minimal, DetailLevel.Truncated);
350
- }
@@ -1,18 +0,0 @@
1
- import type { FileMap } from "./types.js";
2
- import type { SymbolMatch } from "./symbol-lookup.js";
3
-
4
- export function formatAmbiguous(query: string, candidates: SymbolMatch[]): string {
5
- const lines = candidates.map((c) => `- ${c.name} (${c.kind}) — lines ${c.startLine}-${c.endLine}`);
6
- const hints = candidates.map((c) => `${query}@${c.startLine}`).join(" or ");
7
- return [
8
- `Symbol '${query}' is ambiguous.`,
9
- "Matches:",
10
- ...lines,
11
- `Use ${hints} to select by start line.`,
12
- ].join("\n");
13
- }
14
-
15
- export function formatNotFound(query: string, map: FileMap): string {
16
- const available = map.symbols.slice(0, 20).map((s) => s.name).join(", ");
17
- return `[Warning: symbol '${query}' not found. Available symbols: ${available}]`;
18
- }
@@ -1,240 +0,0 @@
1
- import type { FileMap, FileSymbol } from "./types.js";
2
- import type { SymbolKind } from "./enums.js";
3
- import { traverseSymbolTree } from "./symbol-tree.js";
4
-
5
- export interface SymbolMatch {
6
- name: string;
7
- kind: SymbolKind;
8
- startLine: number;
9
- endLine: number;
10
- parentName?: string;
11
- }
12
-
13
- type SymbolLookupResult =
14
- | { type: "found"; symbol: SymbolMatch }
15
- | { type: "ambiguous"; candidates: SymbolMatch[] }
16
- | {
17
- type: "fuzzy";
18
- symbol: SymbolMatch;
19
- tier: "camelCase" | "substring";
20
- otherCandidates: SymbolMatch[];
21
- }
22
- | { type: "not-found"; message?: string; candidates?: SymbolMatch[] };
23
-
24
- function toMatch(symbol: FileSymbol, parentName?: string): SymbolMatch {
25
- return {
26
- name: symbol.name,
27
- kind: symbol.kind,
28
- startLine: symbol.startLine,
29
- endLine: symbol.endLine,
30
- ...(parentName ? { parentName } : {}),
31
- };
32
- }
33
-
34
- interface SymbolCandidate {
35
- symbol: FileSymbol;
36
- parentName?: string;
37
- }
38
-
39
- function toMatches(candidates: SymbolCandidate[]): SymbolMatch[] {
40
- return candidates.map((candidate) => toMatch(candidate.symbol, candidate.parentName));
41
- }
42
-
43
- function flattenSymbols(symbols: FileSymbol[]): SymbolCandidate[] {
44
- return traverseSymbolTree(symbols, (symbol, parentName) => ({ symbol, parentName }));
45
- }
46
-
47
- function resolveDotPath(symbols: FileSymbol[], parts: string[]): SymbolCandidate[] {
48
- let candidates: SymbolCandidate[] = symbols
49
- .filter((symbol) => symbol.name === parts[0])
50
- .map((symbol) => ({ symbol }));
51
-
52
- for (let i = 1; i < parts.length; i++) {
53
- const nextCandidates: SymbolCandidate[] = [];
54
-
55
- for (const candidate of candidates) {
56
- for (const child of candidate.symbol.children ?? []) {
57
- if (child.name === parts[i]) {
58
- nextCandidates.push({ symbol: child, parentName: candidate.symbol.name });
59
- }
60
- }
61
- }
62
-
63
- candidates = nextCandidates;
64
- }
65
-
66
- return candidates;
67
- }
68
-
69
- const JAVA_TOP_LEVEL_TYPE_KINDS = new Set(["class", "interface", "enum", "module"]);
70
-
71
- function preferJavaTopLevelType(map: FileMap, candidates: SymbolCandidate[]): SymbolCandidate | undefined {
72
- if (map.language !== "Java") return undefined;
73
- const topLevelTypes = candidates.filter(
74
- (candidate) => !candidate.parentName && JAVA_TOP_LEVEL_TYPE_KINDS.has(candidate.symbol.kind),
75
- );
76
- return topLevelTypes.length === 1 ? topLevelTypes[0] : undefined;
77
- }
78
-
79
- function resolveDotPathSuffix(symbols: FileSymbol[], parts: string[]): SymbolCandidate[] {
80
- for (let start = 1; start < parts.length; start++) {
81
- const candidates = resolveDotPath(symbols, parts.slice(start));
82
- if (candidates.length > 0) return candidates;
83
- }
84
- return [];
85
- }
86
-
87
- export function findSymbol(map: FileMap, query: string): SymbolLookupResult {
88
- const q = query.trim();
89
- if (!q) return { type: "not-found" };
90
- if (map.symbols.length === 0) return { type: "not-found" };
91
-
92
- const allSymbols = flattenSymbols(map.symbols);
93
-
94
- const atLineMatch = /^(.+?)@(\d+)$/.exec(q);
95
- if (atLineMatch) {
96
- const namePart = atLineMatch[1];
97
- const lineNum = Number.parseInt(atLineMatch[2], 10);
98
- let pool: SymbolCandidate[];
99
- if (namePart.includes(".")) {
100
- const parts = namePart.split(".").map((p) => p.trim());
101
- pool = parts.every((p) => p.length > 0)
102
- ? resolveDotPath(map.symbols, parts)
103
- : [];
104
- if (pool.length === 0) {
105
- pool = allSymbols.filter((c) => c.symbol.name === namePart);
106
- }
107
- } else {
108
- pool = allSymbols.filter((c) => c.symbol.name === namePart);
109
- }
110
- // drop candidates without usable startLine. If that empties
111
- // the pool entirely, surface any same-leaf-name decls elsewhere in the file
112
- // so the user can see real candidate lines.
113
- pool = pool.filter((c) => Number.isFinite(c.symbol.startLine) && c.symbol.startLine > 0);
114
- if (pool.length === 0) {
115
- const leaf = namePart.split(".").pop() ?? namePart;
116
- const sameLeaf = allSymbols.filter(
117
- (c) => c.symbol.name === leaf
118
- && Number.isFinite(c.symbol.startLine)
119
- && c.symbol.startLine > 0,
120
- );
121
- if (sameLeaf.length > 0) {
122
- const list = sameLeaf
123
- .slice(0, 5)
124
- .map((c) => `${c.parentName ? c.parentName + "." : ""}${c.symbol.name}@${c.symbol.startLine}`)
125
- .join(", ");
126
- return {
127
- type: "not-found",
128
- message: `${q} not found. Candidates: ${list}`,
129
- candidates: toMatches(sameLeaf.slice(0, 5)),
130
- };
131
- }
132
- }
133
- if (pool.length > 0) {
134
- const containing = pool.filter(
135
- (c) => c.symbol.startLine <= lineNum && c.symbol.endLine >= lineNum,
136
- );
137
- if (containing.length > 0) {
138
- return { type: "found", symbol: toMatch(containing[0].symbol, containing[0].parentName) };
139
- }
140
- const below = pool
141
- .filter((c) => c.symbol.startLine >= lineNum)
142
- .sort((a, b) => a.symbol.startLine - b.symbol.startLine);
143
- if (below.length) {
144
- return { type: "found", symbol: toMatch(below[0].symbol, below[0].parentName) };
145
- }
146
- const above = pool
147
- .filter((c) => c.symbol.startLine < lineNum)
148
- .sort((a, b) => b.symbol.startLine - a.symbol.startLine);
149
- if (above.length) {
150
- return { type: "found", symbol: toMatch(above[0].symbol, above[0].parentName) };
151
- }
152
- }
153
- }
154
-
155
- const exact = allSymbols.filter((c) => c.symbol.name === q);
156
- if (exact.length === 1) return { type: "found", symbol: toMatch(exact[0].symbol, exact[0].parentName) };
157
- if (exact.length > 1) {
158
- const preferred = preferJavaTopLevelType(map, exact);
159
- if (preferred) return { type: "found", symbol: toMatch(preferred.symbol, preferred.parentName) };
160
- return { type: "ambiguous", candidates: toMatches(exact.slice(0, 5)) };
161
- }
162
-
163
- if (q.includes(".")) {
164
- const parts = q.split(".").map((p) => p.trim());
165
- if (parts.length >= 2 && parts.every((p) => p.length > 0)) {
166
- const candidates = resolveDotPath(map.symbols, parts);
167
- if (candidates.length === 1) {
168
- return { type: "found", symbol: toMatch(candidates[0].symbol, candidates[0].parentName) };
169
- }
170
- if (candidates.length > 1) {
171
- return {
172
- type: "ambiguous",
173
- candidates: toMatches(candidates.slice(0, 5)),
174
- };
175
- }
176
- const suffixCandidates = resolveDotPathSuffix(map.symbols, parts);
177
- if (suffixCandidates.length === 1) {
178
- return { type: "found", symbol: toMatch(suffixCandidates[0].symbol, suffixCandidates[0].parentName) };
179
- }
180
- if (suffixCandidates.length > 1) {
181
- return {
182
- type: "ambiguous",
183
- candidates: toMatches(suffixCandidates.slice(0, 5)),
184
- };
185
- }
186
- }
187
- }
188
-
189
- const qLower = q.toLowerCase();
190
-
191
- // Tier 1: case-insensitive exact
192
- const ci = allSymbols.filter((c) => c.symbol.name.toLowerCase() === qLower);
193
- if (ci.length === 1) return { type: "found", symbol: toMatch(ci[0].symbol, ci[0].parentName) };
194
- if (ci.length > 1) return { type: "ambiguous", candidates: toMatches(ci.slice(0, 5)) };
195
-
196
- // Tier 2: prefix match
197
- const prefix = allSymbols.filter((c) => c.symbol.name.toLowerCase().startsWith(qLower));
198
- if (prefix.length === 1) return { type: "found", symbol: toMatch(prefix[0].symbol, prefix[0].parentName) };
199
- if (prefix.length > 1) return { type: "ambiguous", candidates: toMatches(prefix.slice(0, 5)) };
200
-
201
- // Tier 3: camelCase word boundary match
202
- const camelCase = allSymbols.filter((c) => {
203
- const words = c.symbol.name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_]+/);
204
- return words.some((w) => w === qLower);
205
- });
206
-
207
- // Tier 4: substring match
208
- const partial = allSymbols.filter((c) => c.symbol.name.toLowerCase().includes(qLower));
209
- const buildOtherCandidates = (chosen: SymbolCandidate): SymbolMatch[] => {
210
- const pool: SymbolCandidate[] = [];
211
- const seen = new Set<FileSymbol>();
212
- seen.add(chosen.symbol);
213
- for (const c of [...camelCase, ...partial]) {
214
- if (seen.has(c.symbol)) continue;
215
- seen.add(c.symbol);
216
- pool.push(c);
217
- }
218
- return toMatches(pool.slice(0, 4));
219
- };
220
- if (camelCase.length === 1) {
221
- return {
222
- type: "fuzzy",
223
- symbol: toMatch(camelCase[0].symbol, camelCase[0].parentName),
224
- tier: "camelCase",
225
- otherCandidates: buildOtherCandidates(camelCase[0]),
226
- };
227
- }
228
- if (camelCase.length > 1) return { type: "ambiguous", candidates: toMatches(camelCase.slice(0, 5)) };
229
- if (partial.length === 1) {
230
- return {
231
- type: "fuzzy",
232
- symbol: toMatch(partial[0].symbol, partial[0].parentName),
233
- tier: "substring",
234
- otherCandidates: buildOtherCandidates(partial[0]),
235
- };
236
- }
237
- if (partial.length > 1) return { type: "ambiguous", candidates: toMatches(partial.slice(0, 5)) };
238
-
239
- return { type: "not-found" };
240
- }
@@ -1,21 +0,0 @@
1
- import type { FileSymbol } from "./types.js";
2
-
3
- /**
4
- * Depth-first walk over a readseek symbol tree. {@link visit} is called for
5
- * every symbol with the enclosing symbol's name as `parentName` (undefined at
6
- * the top level); its results are returned flattened in pre-order.
7
- */
8
- export function traverseSymbolTree<T>(
9
- symbols: FileSymbol[],
10
- visit: (symbol: FileSymbol, parentName?: string) => T,
11
- ): T[] {
12
- const result: T[] = [];
13
- const walk = (nodes: FileSymbol[], parentName?: string) => {
14
- for (const symbol of nodes) {
15
- result.push(visit(symbol, parentName));
16
- if (symbol.children?.length) walk(symbol.children, symbol.name);
17
- }
18
- };
19
- walk(symbols);
20
- return result;
21
- }