pi-supernova 0.4.0 → 0.6.0
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 +86 -24
- package/docs/CHANGELOG.md +70 -0
- package/docs/TOKEN_COSTS.md +64 -28
- package/index.js +64 -25
- package/package.json +2 -2
- package/src/bridge/catalog.js +14 -10
- package/src/bridge/host-bridge.js +919 -158
- package/src/bridge/native-tools.js +16 -6
- package/src/context/evidence.js +13 -5
- package/src/context/fuzzy.js +34 -13
- package/src/context/outline.js +11 -2
- package/src/context/repo-index.js +190 -19
- package/src/context/search.js +70 -17
- package/src/context/snap.js +128 -40
- package/src/context/spans.js +39 -0
- package/src/context/surface.js +23 -15
- package/src/fs/check.js +10 -2
- package/src/fs/json-read.js +5 -1
- package/src/fs/patch.js +4 -2
- package/src/fs/vfs.js +133 -33
- package/src/fs/workspace.js +34 -4
- package/src/output/bottleneck.js +53 -15
- package/src/output/format.js +63 -29
- package/src/runtime/guest-worker.js +172 -61
- package/src/runtime/parallel.js +4 -1
- package/src/runtime/program-batch.js +17 -6
- package/src/runtime/program-file.js +6 -3
- package/src/runtime/reference.js +15 -22
- package/src/runtime/runtime.js +17 -8
- package/src/shared/decode.js +26 -3
- package/src/ui/omp-frame.js +11 -4
- package/src/ui/render.js +2 -2
|
@@ -54,7 +54,11 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
function settingsFor(ctx) {
|
|
57
|
-
|
|
57
|
+
try {
|
|
58
|
+
return host.SettingsManager?.create(ctx.cwd, undefined, { projectTrusted: ctx.isProjectTrusted?.() === true });
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
async function readOne(args, signal, ctx, bridge, id) {
|
|
@@ -67,7 +71,7 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
67
71
|
signal?.throwIfAborted();
|
|
68
72
|
const image = /\.(png|jpe?g|gif|webp|bmp)$/i.test(target);
|
|
69
73
|
|
|
70
|
-
if (stat?.isFile() &&
|
|
74
|
+
if (stat?.isFile() && args.about === undefined && args.query === undefined && args.resolve === undefined && args.outline === undefined && args.evidence === undefined && args.json === undefined && args.complete === undefined) {
|
|
71
75
|
// Pi retains image handling, full text, line windows, truncation metadata,
|
|
72
76
|
// and actionable continuation offsets. Do not summarize or dedupe these.
|
|
73
77
|
return factories.read(ctx.cwd, { autoResizeImages: image ? settingsFor(ctx)?.getImageAutoResize() : undefined }).execute(id, { ...args, path: target }, signal, undefined, ctx);
|
|
@@ -77,7 +81,7 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
async function execute(name, id, args, signal, onUpdate, context) {
|
|
80
|
-
const ctx = { ...context, cwd: context?.cwd
|
|
84
|
+
const ctx = { ...context, cwd: isString(context?.cwd) && context.cwd ? context.cwd : cwd };
|
|
81
85
|
const bridge = base.fork({ getCwd: () => ctx.cwd });
|
|
82
86
|
bridge.bindCallContext(ctx, signal);
|
|
83
87
|
signal?.throwIfAborted();
|
|
@@ -166,11 +170,17 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
166
170
|
};
|
|
167
171
|
|
|
168
172
|
if (name === "read") {
|
|
169
|
-
tool.description += " Also reads directories or finds source from a symbol/question passed as path. Use about to focus a file or directory on a question. An array of paths returns all readable files and labels individual errors.";
|
|
173
|
+
tool.description += " Also reads directories or finds source from a symbol/question passed as path. Use about to focus a file or directory on a question (at most 16 keywords). An array of up to 64 paths returns all readable files and labels individual errors. Images return as attachments up to 20 MiB each; at most 16 image attachments are returned.";
|
|
170
174
|
tool.parameters = { ...definition.parameters, properties: {
|
|
171
175
|
...definition.parameters.properties,
|
|
172
176
|
path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" }, maxItems: 64 }], description: "File, directory, source question, or up to 64 paths to read" },
|
|
173
|
-
about: { type: "string", description: "Focus on this question or symbol; source selection reports uncertainty instead of guessing" },
|
|
177
|
+
about: { type: "string", description: "Focus on this question or symbol (at most 16 keywords); source selection reports uncertainty instead of guessing" },
|
|
178
|
+
query: { type: "string", description: "Source question; optional path scopes the search" },
|
|
179
|
+
outline: { type: "boolean" },
|
|
180
|
+
evidence: { type: "boolean" },
|
|
181
|
+
resolve: { type: "boolean" },
|
|
182
|
+
complete: { type: "boolean" },
|
|
183
|
+
json: { anyOf: [{ type: "boolean" }, { type: "string" }, { type: "array", items: { type: "string" }, maxItems: 64 }] },
|
|
174
184
|
} };
|
|
175
185
|
tool.promptGuidelines = [...(definition.promptGuidelines || []), "read can find source from a symbol or question; check its selection status before choosing a file. Plain file reads preserve full text within the stated limits."];
|
|
176
186
|
}
|
|
@@ -179,7 +189,7 @@ export function registerNativeTools(pi, host, config = loadConfig()) {
|
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
pi.on("session_start", (_event, ctx) => {
|
|
182
|
-
cwd = ctx?.cwd
|
|
192
|
+
cwd = isString(ctx?.cwd) && ctx.cwd ? ctx.cwd : cwd;
|
|
183
193
|
base.invalidateFiles();
|
|
184
194
|
});
|
|
185
195
|
pi.registerCommand("supernova", {
|
package/src/context/evidence.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { WorkspaceIndex } from "./repo-index.js";
|
|
3
4
|
import { tokenizeQuery, scorePathTopology, stem } from "./snap.js";
|
|
@@ -143,7 +144,7 @@ function lexicalSim(a, b) {
|
|
|
143
144
|
|
|
144
145
|
function activateEntities(profile, graph) {
|
|
145
146
|
const eta = new Map();
|
|
146
|
-
const anchors = profile.subjects.length ? profile.subjects : profile.
|
|
147
|
+
const anchors = profile.subjects.length ? profile.subjects : profile.stems;
|
|
147
148
|
|
|
148
149
|
for (const anchor of anchors) {
|
|
149
150
|
let best = null;
|
|
@@ -388,12 +389,12 @@ function candidateFiles(files, profile, index, limit, overlayText) {
|
|
|
388
389
|
|
|
389
390
|
scored.sort((a, b) => b.s - a.s);
|
|
390
391
|
const chosen = new Set();
|
|
391
|
-
const anchors = (profile.subjects.length ? profile.subjects : profile.
|
|
392
|
+
const anchors = (profile.subjects.length ? profile.subjects : profile.stems).map((a) => a.toLowerCase()).filter((a) => a.length > 2);
|
|
392
393
|
|
|
393
394
|
const pendingHits = files.filter(file => {
|
|
394
395
|
const pending = overlayText(file);
|
|
395
396
|
|
|
396
|
-
return pending !== undefined && anchors.some(anchor => pending.toLowerCase().includes(anchor));
|
|
397
|
+
return pending !== undefined && Buffer.byteLength(pending, "utf8") <= 512 * 1024 && anchors.some(anchor => pending.toLowerCase().includes(anchor));
|
|
397
398
|
});
|
|
398
399
|
|
|
399
400
|
const hits = anchors.length ? [...new Set([...pendingHits, ...index.filesContaining(files, anchors, true)])] : [];
|
|
@@ -511,7 +512,12 @@ export async function selectEvidence({ query, root, searchDir, index, overlayTex
|
|
|
511
512
|
return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
512
513
|
});
|
|
513
514
|
|
|
514
|
-
const
|
|
515
|
+
const rootStat = await fs.stat(searchRoot).catch(error => {
|
|
516
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return null;
|
|
517
|
+
throw error;
|
|
518
|
+
});
|
|
519
|
+
const diskFiles = rootStat?.isFile() ? [searchRoot] : rootStat ? await index.files(searchRoot) : [];
|
|
520
|
+
const files = [...new Set([...diskFiles, ...staged])];
|
|
515
521
|
|
|
516
522
|
if (files.length === 0) throw new Error(`no files found to search in ${searchDir || root}`);
|
|
517
523
|
|
|
@@ -520,7 +526,9 @@ export async function selectEvidence({ query, root, searchDir, index, overlayTex
|
|
|
520
526
|
|
|
521
527
|
for (const f of chosenFiles) {
|
|
522
528
|
const pending = overlayText(f);
|
|
523
|
-
const entry = pending === undefined
|
|
529
|
+
const entry = pending === undefined
|
|
530
|
+
? index.entry(f)
|
|
531
|
+
: Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(f, pending) : null;
|
|
524
532
|
|
|
525
533
|
if (!entry) continue;
|
|
526
534
|
spans.push(...spansOf(entry, f, opts.maxSpanLines));
|
package/src/context/fuzzy.js
CHANGED
|
@@ -15,6 +15,7 @@ const AI_DECAY = Math.LN2 / 3; // per day
|
|
|
15
15
|
const AI_MAX_HISTORY_DAYS = 7;
|
|
16
16
|
|
|
17
17
|
const MAX_TIMESTAMPS_PER_FILE = 128;
|
|
18
|
+
const MAX_FRECENCY_FILES = 10000;
|
|
18
19
|
|
|
19
20
|
const AI_MODIFICATION_THRESHOLDS = [[16, 30], [8, 300], [4, 900], [2, 3600], [1, 14400]]; // [boost, seconds]
|
|
20
21
|
|
|
@@ -26,7 +27,10 @@ export class Frecency {
|
|
|
26
27
|
record(filePath, at = Date.now() / 1000) {
|
|
27
28
|
let list = this.access.get(filePath);
|
|
28
29
|
|
|
29
|
-
if (!list)
|
|
30
|
+
if (!list) {
|
|
31
|
+
if (this.access.size >= MAX_FRECENCY_FILES) this.access.delete(this.access.keys().next().value);
|
|
32
|
+
this.access.set(filePath, (list = []));
|
|
33
|
+
}
|
|
30
34
|
list.push(at);
|
|
31
35
|
|
|
32
36
|
if (list.length > MAX_TIMESTAMPS_PER_FILE) list.splice(0, list.length - MAX_TIMESTAMPS_PER_FILE);
|
|
@@ -124,20 +128,35 @@ export function fuzzyMatch(needle, hay, { maxTypos = 0, caseSensitive = false }
|
|
|
124
128
|
|
|
125
129
|
if (direct) return { ...direct, typos: 0, exact: hay.toLowerCase() === needle.toLowerCase() };
|
|
126
130
|
|
|
127
|
-
if (maxTypos <= 0 || needle.length < 3) return null;
|
|
128
|
-
|
|
131
|
+
if (maxTypos <= 0 || needle.length < 3 || needle.length > 128) return null;
|
|
132
|
+
const memo = new Map();
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const m = fuzzyMatch(shorter, hay, { maxTypos: maxTypos - 1, caseSensitive });
|
|
134
|
+
const visit = (part, typosLeft) => {
|
|
135
|
+
const key = part + "\0" + typosLeft;
|
|
133
136
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
137
|
+
if (memo.has(key)) return memo.get(key);
|
|
138
|
+
let best = matchOnce(part, hay, caseSensitive);
|
|
136
139
|
|
|
137
|
-
if (
|
|
138
|
-
|
|
140
|
+
if (best) best = { ...best, typos: 0, exact: hay.toLowerCase() === part.toLowerCase() };
|
|
141
|
+
|
|
142
|
+
if (typosLeft > 0) {
|
|
143
|
+
for (let i = 0; i < part.length; i++) {
|
|
144
|
+
const shorter = part.slice(0, i) + part.slice(i + 1);
|
|
145
|
+
const m = visit(shorter, typosLeft - 1);
|
|
146
|
+
|
|
147
|
+
if (!m) continue;
|
|
148
|
+
const scored = { ...m, score: m.score - 12, typos: m.typos + 1, exact: false };
|
|
149
|
+
|
|
150
|
+
if (!best || scored.score > best.score) best = scored;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
memo.set(key, best);
|
|
139
155
|
|
|
140
|
-
|
|
156
|
+
return best;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return visit(needle, maxTypos);
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
export function smartCase(query) {
|
|
@@ -164,7 +183,7 @@ function distancePenalty(currentDir, candidateDir) {
|
|
|
164
183
|
export function rankPaths(query, paths, ctx = {}) {
|
|
165
184
|
const parts = query.trim().split(/\s+/).filter((p) => p.length >= 2);
|
|
166
185
|
|
|
167
|
-
if (parts.length === 0) return [];
|
|
186
|
+
if (parts.length === 0 || parts.length > 16) return [];
|
|
168
187
|
const caseSensitive = smartCase(query);
|
|
169
188
|
const maxTypos = ctx.maxTypos ?? (parts[0].length >= 6 ? 2 : parts[0].length >= 4 ? 1 : 0);
|
|
170
189
|
const currentDir = ctx.currentFile ? ctx.currentFile.slice(0, ctx.currentFile.lastIndexOf("/") + 1) : "";
|
|
@@ -212,7 +231,9 @@ function filenameBonus(base, rel, filenameStart, first, needle) {
|
|
|
212
231
|
|
|
213
232
|
/** fff: frecency boost base·f/100 and +15% for git-modified files. */
|
|
214
233
|
function contextBoost(base, rel, ctx) {
|
|
215
|
-
|
|
234
|
+
let frecency = 0;
|
|
235
|
+
|
|
236
|
+
try { frecency = ctx.frecency ? ctx.frecency.score(rel, ctx.mtimeOf?.(rel)) : 0; } catch {}
|
|
216
237
|
const gitBoost = ctx.modified?.has(rel) ? Math.floor((base * 15) / 100) : 0;
|
|
217
238
|
|
|
218
239
|
return Math.floor((base * frecency) / 100) + gitBoost;
|
package/src/context/outline.js
CHANGED
|
@@ -28,6 +28,7 @@ function relevance(span, lower, stems) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function chooseExpanded(spans, lower, stems, raw, opts) {
|
|
31
|
+
if (stems.length === 0) return new Set();
|
|
31
32
|
const scored = spans.map((s, i) => ({ i, r: relevance(s, lower, stems), chars: raw.slice(s.start - 1, s.end).join("\n").length }));
|
|
32
33
|
scored.sort((a, b) => b.r - a.r || a.i - b.i);
|
|
33
34
|
const expanded = new Set();
|
|
@@ -118,7 +119,15 @@ export function outlineFile(entry, relPath, about, options = {}) {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
for (let i = 0; i < spans.length; i++) parts.push(expanded.has(i) ? expandedBlock(spans[i], raw, opts) : foldedLine(spans[i]));
|
|
121
|
-
const
|
|
122
|
+
const label = about ? String(about).replace(/\s+/g, " ").slice(0, 120) : "";
|
|
123
|
+
const title = "// " + relPath + " · " + lineCount + " lines · " + spans.length + " declarations · " + expanded.size + " expanded" + (label ? " for \"" + label + "\"" : "") + " · read(path, line, count) for a folded body";
|
|
124
|
+
let text = title + "\n" + parts.join("\n");
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
if (text.length > opts.maxChars) {
|
|
127
|
+
const end = text.lastIndexOf("\n", Math.max(0, opts.maxChars - 160));
|
|
128
|
+
|
|
129
|
+
text = (end > title.length ? text.slice(0, end) : text.slice(0, opts.maxChars)) + "\n … outline truncated; use read(path, line, count) for later declarations";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { text, expanded: expanded.size, declarations: spans.length };
|
|
124
133
|
}
|
|
@@ -21,6 +21,7 @@ const WATCH_DEBOUNCE_MS = 150;
|
|
|
21
21
|
const MAX_INDEXED_FILES = 4000;
|
|
22
22
|
|
|
23
23
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
24
|
+
const MAX_ENTRY_CACHE_BYTES = 64 * 1024 * 1024;
|
|
24
25
|
|
|
25
26
|
const BINARY_EXT = new Set([
|
|
26
27
|
".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz", ".tgz", ".tar", ".bz2", ".xz", ".7z",
|
|
@@ -34,11 +35,13 @@ const IDENT_TOKEN = /[A-Za-z_$][\w$]*/g;
|
|
|
34
35
|
|
|
35
36
|
const EMPTY = Object.freeze([]);
|
|
36
37
|
|
|
37
|
-
const DEF_PATTERN = /^(?:pub\s+)?(?:export\s+)?(?:async\s+)?(?:default\s+)?(function|class|def|fn|const|let|interface|type|struct|enum)\s+([a-zA-Z0-9_$]+)/;
|
|
38
|
+
const DEF_PATTERN = /^(?:pub\s+)?(?:export\s+)?(?:async\s+)?(?:default\s+)?(?:(function|class|def|fn|const|let|interface|type|struct|enum)\s+([a-zA-Z0-9_$]+)|([A-Z][A-Z0-9_$]*)\s*(?::[^=\n]+)?=)/;
|
|
38
39
|
|
|
39
|
-
/** Declared identifier on a line (function/class/
|
|
40
|
+
/** Declared identifier on a line (function/class/UPPER_CASE constant/…), or ""; the same rule snap and grep use. */
|
|
40
41
|
export function declaredName(line) {
|
|
41
|
-
|
|
42
|
+
const match = DEF_PATTERN.exec(String(line).trim());
|
|
43
|
+
|
|
44
|
+
return match?.[2] ?? match?.[3] ?? "";
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
function isTextCandidate(filePath) {
|
|
@@ -71,7 +74,9 @@ function globGroup(glob, i, open) {
|
|
|
71
74
|
|
|
72
75
|
if (end < 0) throw new SyntaxError("unclosed " + open + " in glob");
|
|
73
76
|
const inner = glob.slice(i + 1, end);
|
|
74
|
-
const source = open === "{"
|
|
77
|
+
const source = open === "{"
|
|
78
|
+
? "(?:" + inner.split(",").map(globBody).join("|") + ")"
|
|
79
|
+
: "[" + (inner.startsWith("!") ? "^" + inner.slice(1) : inner) + "]";
|
|
75
80
|
|
|
76
81
|
return [source, end + 1];
|
|
77
82
|
}
|
|
@@ -96,11 +101,48 @@ export function globToRegExp(glob) {
|
|
|
96
101
|
return new RegExp(glob.includes("/") ? "^" + body + "$" : "(?:^|/)" + body + "$");
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
function declarationEnd(raw, lower, start, lineCount, ext) {
|
|
105
|
+
if (ext === ".py") {
|
|
106
|
+
const indentOf = (i) => raw[i].length - raw[i].trimStart().length;
|
|
107
|
+
const base = indentOf(start - 1);
|
|
108
|
+
let end = start;
|
|
109
|
+
|
|
110
|
+
for (let i = start; i < lineCount; i++) {
|
|
111
|
+
if (lower[i] === "") { end = i + 1; continue; }
|
|
112
|
+
if (indentOf(i) <= base) break;
|
|
113
|
+
end = i + 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return Math.min(end, lineCount);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let depth = 0;
|
|
120
|
+
|
|
121
|
+
for (const ch of raw[start - 1] ?? "") {
|
|
122
|
+
if (ch === "{") depth++;
|
|
123
|
+
else if (ch === "}") depth--;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (depth <= 0) return start;
|
|
127
|
+
|
|
128
|
+
for (let i = start; i < raw.length; i++) {
|
|
129
|
+
for (const ch of raw[i]) {
|
|
130
|
+
if (ch === "{") depth++;
|
|
131
|
+
else if (ch === "}") depth--;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (depth <= 0) return i + 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lineCount;
|
|
138
|
+
}
|
|
139
|
+
|
|
99
140
|
export class WorkspaceIndex {
|
|
100
141
|
constructor(runCommand) {
|
|
101
142
|
this.runCommand = runCommand;
|
|
102
143
|
this.lists = new Map();
|
|
103
144
|
this.entries = new Map();
|
|
145
|
+
this.entryBytes = 0;
|
|
104
146
|
this.watchers = new Map();
|
|
105
147
|
this.frecency = new Frecency();
|
|
106
148
|
this.gitModified = new Map(); // root → Set(relative "/"-joined paths)
|
|
@@ -109,6 +151,11 @@ export class WorkspaceIndex {
|
|
|
109
151
|
|
|
110
152
|
invalidate() {
|
|
111
153
|
this.lists.clear();
|
|
154
|
+
this.gitModified.clear();
|
|
155
|
+
|
|
156
|
+
for (const entry of this.entries.values()) this.entryBytes -= entry.weight ?? 0;
|
|
157
|
+
this.entries.clear();
|
|
158
|
+
this.entryBytes = 0;
|
|
112
159
|
}
|
|
113
160
|
|
|
114
161
|
/** fff frecency: every read/edit is an access; the newest one is the "current file" for distance penalties. */
|
|
@@ -130,12 +177,17 @@ export class WorkspaceIndex {
|
|
|
130
177
|
timer = null;
|
|
131
178
|
this.lists.clear();
|
|
132
179
|
this.gitModified.delete(root);
|
|
180
|
+
this.entries.clear();
|
|
181
|
+
this.entryBytes = 0;
|
|
133
182
|
}, WATCH_DEBOUNCE_MS);
|
|
183
|
+
timer.unref?.();
|
|
134
184
|
});
|
|
135
185
|
|
|
136
186
|
watcher.on("error", () => {
|
|
137
187
|
this.watchers.set(root, false);
|
|
138
188
|
this.lists.clear();
|
|
189
|
+
this.entries.clear();
|
|
190
|
+
this.entryBytes = 0;
|
|
139
191
|
});
|
|
140
192
|
|
|
141
193
|
if (isFunction(watcher.unref)) watcher.unref();
|
|
@@ -160,8 +212,21 @@ export class WorkspaceIndex {
|
|
|
160
212
|
const res = await this.runCommand(["git", "status", "--porcelain", "-z", "--untracked-files=all"], { cwd: root, timeoutMs: 5_000 });
|
|
161
213
|
|
|
162
214
|
if (res.exitCode === 0) {
|
|
163
|
-
|
|
164
|
-
|
|
215
|
+
const rows = res.stdout.split("\0");
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < rows.length; i++) {
|
|
218
|
+
const row = rows[i];
|
|
219
|
+
|
|
220
|
+
if (row.length <= 3) continue;
|
|
221
|
+
const status = row.slice(0, 2);
|
|
222
|
+
const file = row.slice(3);
|
|
223
|
+
|
|
224
|
+
if (file) set.add(file);
|
|
225
|
+
if ((status.includes("R") || status.includes("C")) && i + 1 < rows.length) {
|
|
226
|
+
const target = rows[++i];
|
|
227
|
+
|
|
228
|
+
if (target) set.add(target);
|
|
229
|
+
}
|
|
165
230
|
}
|
|
166
231
|
}
|
|
167
232
|
} catch {}
|
|
@@ -193,7 +258,7 @@ export class WorkspaceIndex {
|
|
|
193
258
|
const args = ["rg", "--files"];
|
|
194
259
|
|
|
195
260
|
if (includeHidden) args.push("--hidden");
|
|
196
|
-
args.push("-g", "!.git/**", "-g", "!**/.git/**", root);
|
|
261
|
+
args.push("-g", "!.git/**", "-g", "!**/.git/**", "--", root);
|
|
197
262
|
let files = [];
|
|
198
263
|
let error;
|
|
199
264
|
let truncated = false;
|
|
@@ -225,26 +290,107 @@ export class WorkspaceIndex {
|
|
|
225
290
|
try {
|
|
226
291
|
stat = fs.statSync(filePath);
|
|
227
292
|
} catch {
|
|
293
|
+
const previous = this.entries.get(filePath);
|
|
294
|
+
|
|
295
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
228
296
|
this.entries.delete(filePath);
|
|
229
297
|
|
|
230
298
|
return null;
|
|
231
299
|
}
|
|
232
300
|
|
|
233
|
-
if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
|
|
301
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) {
|
|
302
|
+
const previous = this.entries.get(filePath);
|
|
303
|
+
|
|
304
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
305
|
+
this.entries.delete(filePath);
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
234
309
|
const cached = this.entries.get(filePath);
|
|
235
310
|
|
|
236
|
-
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
|
|
311
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
312
|
+
this.entries.delete(filePath);
|
|
313
|
+
this.entries.set(filePath, cached);
|
|
314
|
+
|
|
315
|
+
return cached;
|
|
316
|
+
}
|
|
237
317
|
let text;
|
|
318
|
+
let actual = stat;
|
|
238
319
|
|
|
239
320
|
try {
|
|
240
|
-
|
|
321
|
+
const fd = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
actual = fs.fstatSync(fd);
|
|
325
|
+
if (!actual.isFile() || actual.size > MAX_FILE_BYTES) {
|
|
326
|
+
const previous = this.entries.get(filePath);
|
|
327
|
+
|
|
328
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
329
|
+
this.entries.delete(filePath);
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
|
|
334
|
+
let offset = 0;
|
|
335
|
+
|
|
336
|
+
while (offset < buffer.length) {
|
|
337
|
+
const read = fs.readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
338
|
+
|
|
339
|
+
if (read <= 0) break;
|
|
340
|
+
offset += read;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (offset > MAX_FILE_BYTES) {
|
|
344
|
+
const previous = this.entries.get(filePath);
|
|
345
|
+
|
|
346
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
347
|
+
this.entries.delete(filePath);
|
|
348
|
+
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
actual = fs.fstatSync(fd);
|
|
352
|
+
if (!actual.isFile() || actual.size !== offset) {
|
|
353
|
+
const previous = this.entries.get(filePath);
|
|
354
|
+
|
|
355
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
356
|
+
this.entries.delete(filePath);
|
|
357
|
+
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
text = buffer.subarray(0, offset).toString("utf8");
|
|
361
|
+
} finally { fs.closeSync(fd); }
|
|
241
362
|
} catch {
|
|
363
|
+
const previous = this.entries.get(filePath);
|
|
364
|
+
|
|
365
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
366
|
+
this.entries.delete(filePath);
|
|
367
|
+
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (text.includes("\0")) {
|
|
372
|
+
const previous = this.entries.get(filePath);
|
|
373
|
+
|
|
374
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
375
|
+
this.entries.delete(filePath);
|
|
376
|
+
|
|
242
377
|
return null;
|
|
243
378
|
}
|
|
379
|
+
const created = { text, lower: text.toLowerCase(), mtimeMs: actual.mtimeMs, size: actual.size, weight: Math.max(1, actual.size) * 2, ext: path.extname(filePath), surface: undefined, lines: undefined, spans: undefined };
|
|
380
|
+
const previous = this.entries.get(filePath);
|
|
244
381
|
|
|
245
|
-
if (
|
|
246
|
-
|
|
382
|
+
if (previous) this.entryBytes -= previous.weight ?? 0;
|
|
383
|
+
this.entries.delete(filePath);
|
|
247
384
|
this.entries.set(filePath, created);
|
|
385
|
+
this.entryBytes += created.weight;
|
|
386
|
+
|
|
387
|
+
while (this.entryBytes > MAX_ENTRY_CACHE_BYTES && this.entries.size > 1) {
|
|
388
|
+
const oldest = this.entries.keys().next().value;
|
|
389
|
+
const evicted = this.entries.get(oldest);
|
|
390
|
+
|
|
391
|
+
this.entries.delete(oldest);
|
|
392
|
+
this.entryBytes -= evicted?.weight ?? 0;
|
|
393
|
+
}
|
|
248
394
|
|
|
249
395
|
return created;
|
|
250
396
|
}
|
|
@@ -264,7 +410,8 @@ export class WorkspaceIndex {
|
|
|
264
410
|
for (let i = 0; i < raw.length; i++) {
|
|
265
411
|
const trimmed = raw[i].trim();
|
|
266
412
|
lower[i] = trimmed.toLowerCase();
|
|
267
|
-
|
|
413
|
+
const declared = DEF_PATTERN.exec(trimmed);
|
|
414
|
+
defNames[i] = (declared?.[2] ?? declared?.[3] ?? "").toLowerCase();
|
|
268
415
|
idents[i] = trimmed.match(IDENT_TOKEN) || EMPTY;
|
|
269
416
|
}
|
|
270
417
|
|
|
@@ -274,18 +421,18 @@ export class WorkspaceIndex {
|
|
|
274
421
|
}
|
|
275
422
|
|
|
276
423
|
/**
|
|
277
|
-
* Declaration spans [start, end] (1-based, inclusive)
|
|
278
|
-
*
|
|
424
|
+
* Declaration spans [start, end] (1-based, inclusive). Nested bodies stay inside the parent
|
|
425
|
+
* (brace-matched for JS-like, indent for Python). The file's leading header is not a span.
|
|
279
426
|
*/
|
|
280
427
|
static spansOf(entry) {
|
|
281
428
|
if (entry.spans) return entry.spans;
|
|
282
429
|
const { items, lineCount } = WorkspaceIndex.surfaceOf(entry);
|
|
283
|
-
const { lower } = WorkspaceIndex.linesOf(entry);
|
|
430
|
+
const { lower, raw } = WorkspaceIndex.linesOf(entry);
|
|
284
431
|
const spans = [];
|
|
285
432
|
|
|
286
433
|
for (let i = 0; i < items.length; i++) {
|
|
287
434
|
const start = items[i].line;
|
|
288
|
-
let end =
|
|
435
|
+
let end = declarationEnd(raw, lower, start, lineCount, entry.ext);
|
|
289
436
|
|
|
290
437
|
while (end > start && lower[end - 1] === "") end--;
|
|
291
438
|
spans.push({ start, end, name: items[i].name, kind: items[i].kind, isExport: items[i].isExport === true });
|
|
@@ -330,9 +477,33 @@ export class WorkspaceIndex {
|
|
|
330
477
|
|
|
331
478
|
for (const filePath of files) {
|
|
332
479
|
const pending = overlayText(filePath);
|
|
333
|
-
|
|
480
|
+
let e = null;
|
|
481
|
+
|
|
482
|
+
if (pending === undefined) e = this.entry(filePath);
|
|
483
|
+
else if (Buffer.byteLength(pending, "utf8") <= MAX_FILE_BYTES) e = WorkspaceIndex.fromText(filePath, pending);
|
|
484
|
+
else {
|
|
485
|
+
const rel = relativeSlash(root, filePath);
|
|
486
|
+
let start = 0;
|
|
487
|
+
let line = 0;
|
|
488
|
+
|
|
489
|
+
while (start <= pending.length) {
|
|
490
|
+
const end = pending.indexOf("\n", start);
|
|
491
|
+
const stop = end === -1 ? pending.length : end;
|
|
492
|
+
const text = pending.slice(start, stop).replace(/\r$/, "");
|
|
493
|
+
|
|
494
|
+
line++;
|
|
495
|
+
if (regex.test(text)) out.push({ rel, line, text, def: false });
|
|
496
|
+
if (end === -1) break;
|
|
497
|
+
start = end + 1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!e) continue;
|
|
504
|
+
const lineAnchored = /\^|\$/.test(regex.source.replace(/\\[\^$]|\[[^\]]*\]/g, ""));
|
|
334
505
|
|
|
335
|
-
if (!
|
|
506
|
+
if (!lineAnchored && !regex.test(e.text)) continue;
|
|
336
507
|
const { raw, defNames } = WorkspaceIndex.linesOf(e);
|
|
337
508
|
const rel = relativeSlash(root, filePath);
|
|
338
509
|
|