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.
- package/README.md +28 -46
- package/dist/index.js +7333 -0
- package/package.json +8 -7
- package/index.ts +0 -147
- package/src/binary-detect.ts +0 -22
- package/src/check.ts +0 -92
- package/src/coerce-obvious-int.ts +0 -41
- package/src/confusable-hyphens.ts +0 -6
- package/src/def.ts +0 -143
- package/src/diff-data.ts +0 -302
- package/src/edit-classify.ts +0 -58
- package/src/edit-diff.ts +0 -359
- package/src/edit-output.ts +0 -110
- package/src/edit-render-helpers.ts +0 -141
- package/src/edit-syntax-validate.ts +0 -65
- package/src/edit.ts +0 -686
- package/src/file-map.ts +0 -17
- package/src/fs-error.ts +0 -33
- package/src/grep-budget.ts +0 -35
- package/src/grep-output.ts +0 -181
- package/src/grep-render-helpers.ts +0 -77
- package/src/grep-symbol-scope.ts +0 -184
- package/src/grep.ts +0 -608
- package/src/hashline.ts +0 -736
- package/src/hover.ts +0 -140
- package/src/path-utils.ts +0 -24
- package/src/pending-diff-preview.ts +0 -269
- package/src/read-local-bundle.ts +0 -75
- package/src/read-output.ts +0 -186
- package/src/read-render-helpers.ts +0 -104
- package/src/read.ts +0 -661
- package/src/readseek/constants.ts +0 -17
- package/src/readseek/enums.ts +0 -38
- package/src/readseek/formatter.ts +0 -350
- package/src/readseek/symbol-error-format.ts +0 -18
- package/src/readseek/symbol-lookup.ts +0 -240
- package/src/readseek/symbol-tree.ts +0 -21
- package/src/readseek/types.ts +0 -49
- package/src/readseek-client.ts +0 -1250
- package/src/readseek-params.ts +0 -39
- package/src/readseek-settings.ts +0 -263
- package/src/readseek-value.ts +0 -184
- package/src/refs-output.ts +0 -47
- package/src/refs.ts +0 -161
- package/src/register-tool.ts +0 -29
- package/src/rename.ts +0 -182
- package/src/replace-symbol.ts +0 -77
- package/src/runtime.ts +0 -3
- package/src/search-output.ts +0 -50
- package/src/search.ts +0 -214
- package/src/session-anchors.ts +0 -34
- package/src/stat-search-path.ts +0 -29
- package/src/syntax-validate-mode.ts +0 -15
- package/src/tool-prompt-metadata.ts +0 -107
- package/src/tool-types.ts +0 -12
- package/src/tui-diff-component.ts +0 -108
- package/src/tui-diff-renderer.ts +0 -92
- package/src/tui-render-utils.ts +0 -286
- package/src/view.ts +0 -158
- package/src/write.ts +0 -357
package/src/readseek/enums.ts
DELETED
|
@@ -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
|
-
}
|