pi-ast-sgrep 2.0.0 → 2.0.2
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 +3 -2
- package/dist/present.d.ts +4 -1
- package/dist/present.js +96 -2
- package/dist/runtime.js +37 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,9 +132,9 @@ Code Mode and `ast-sgrep-mcp` are separate front ends over the same Rust search
|
|
|
132
132
|
|
|
133
133
|
Start Pi in the repository you want to search. The first search validates the index and lazily creates `<project-root>/.asgrep/` when needed. Run `/asgrep-index` if you want to build it before searching.
|
|
134
134
|
|
|
135
|
-
After a successful Pi `write` or `edit`, the extension marks the affected path dirty and updates only known changed paths before the next search. It also watches the project for external filesystem changes: known file changes receive the same targeted update, while renames, directory changes, ignore-file edits, watcher errors, and ambiguous events trigger a correctness scan. `.asgrep` writes are excluded so indexing cannot dirty itself. If recursive watching is unavailable, an immediate scan
|
|
135
|
+
After a successful Pi `write` or `edit`, the extension marks the affected path dirty and updates only known changed paths before the next search. It also watches the project for external filesystem changes: known file changes receive the same targeted update, while renames, directory changes, ignore-file edits, watcher errors, and ambiguous events trigger a correctness scan. `.asgrep` writes are excluded so indexing cannot dirty itself. If recursive watching is unavailable, an immediate scan on first use preserves correctness. A ready index is not walked on first search or on the refresh interval; the interval only re-checks index health. Concurrent searches for the same root share one in-flight refresh. The last cancelled waiter aborts that shared index so Pi cannot leave workers running after a search is cancelled. In-process indexing uses the host's available parallelism by default.
|
|
136
136
|
|
|
137
|
-
The
|
|
137
|
+
The refresh interval does not force a full tree walk. Run `/asgrep-index` when you need freshness immediately after a large external operation the watcher did not see; use `/asgrep-reindex` only for an incompatible or corrupt index, or when you explicitly need a strict full rebuild.
|
|
138
138
|
|
|
139
139
|
The package never edits `.gitignore`. Add this entry yourself if index data must stay untracked:
|
|
140
140
|
|
|
@@ -209,6 +209,7 @@ Defaults are a 30-second operation timeout, 4 MiB output limit, and 30-second fr
|
|
|
209
209
|
| `ASGREP_TIMEOUT_MS` | Set the native operation timeout. |
|
|
210
210
|
| `ASGREP_MAX_OUTPUT_BYTES` | Bound native output. |
|
|
211
211
|
| `ASGREP_REFRESH_INTERVAL_MS` | Set the idle freshness-check interval. |
|
|
212
|
+
| `ASGREP_INDEX_THREADS` | Cap in-process Code Mode indexing threads (default: host parallelism). |
|
|
212
213
|
| `ASGREP_BIN` | Override the packaged binary for development. |
|
|
213
214
|
|
|
214
215
|
Explicit project configuration can opt into `allowOutsideProject`; global settings and environment variables cannot relax the default project boundary. See the [complete package guide](https://github.com/AdityaVG13/ast-sgrep/blob/main/docs/pi-package.md) for schema and precedence details.
|
package/dist/present.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ export declare function formatSearchResult(response: EnvelopeLike, meta: {
|
|
|
45
45
|
}, theme?: PresentTheme): string;
|
|
46
46
|
export declare function formatStatusResult(response: EnvelopeLike, theme?: PresentTheme): string;
|
|
47
47
|
export declare function formatIndexResult(command: string, response: EnvelopeLike, theme?: PresentTheme): string;
|
|
48
|
+
/** Local stand-in so we do not take a pi-tui dependency. Over-counts wide glyphs rather than under-count. */
|
|
49
|
+
export declare function visibleWidth(text: string): number;
|
|
50
|
+
export declare function truncateToWidth(text: string, maxWidth: number, ellipsis?: string): string;
|
|
48
51
|
export declare function formatCodemodeResult(value: unknown, meta?: {
|
|
49
52
|
stats?: {
|
|
50
53
|
calls: number;
|
|
@@ -62,6 +65,6 @@ export declare class AsgrepText {
|
|
|
62
65
|
constructor(text?: string);
|
|
63
66
|
setText(text: string): void;
|
|
64
67
|
invalidate(): void;
|
|
65
|
-
render(
|
|
68
|
+
render(width: number): string[];
|
|
66
69
|
}
|
|
67
70
|
export declare function presentText(formatted: string, last: unknown): AsgrepText;
|
package/dist/present.js
CHANGED
|
@@ -84,6 +84,97 @@ export function formatIndexResult(command, response, theme) {
|
|
|
84
84
|
const tail = count === undefined ? "done" : `${count} file${count === 1 ? "" : "s"}`;
|
|
85
85
|
return header(theme, command, [tail]);
|
|
86
86
|
}
|
|
87
|
+
function ansiLengthAt(text, index) {
|
|
88
|
+
if (text.charCodeAt(index) !== 0x1b)
|
|
89
|
+
return 0;
|
|
90
|
+
const next = text[index + 1];
|
|
91
|
+
if (next === "[") {
|
|
92
|
+
let cursor = index + 2;
|
|
93
|
+
while (cursor < text.length) {
|
|
94
|
+
const code = text.charCodeAt(cursor);
|
|
95
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
96
|
+
return cursor - index + 1;
|
|
97
|
+
cursor += 1;
|
|
98
|
+
}
|
|
99
|
+
return text.length - index;
|
|
100
|
+
}
|
|
101
|
+
if (next === "]") {
|
|
102
|
+
let cursor = index + 2;
|
|
103
|
+
while (cursor < text.length) {
|
|
104
|
+
if (text.charCodeAt(cursor) === 0x07)
|
|
105
|
+
return cursor - index + 1;
|
|
106
|
+
if (text.charCodeAt(cursor) === 0x1b && text[cursor + 1] === "\\")
|
|
107
|
+
return cursor - index + 2;
|
|
108
|
+
cursor += 1;
|
|
109
|
+
}
|
|
110
|
+
return text.length - index;
|
|
111
|
+
}
|
|
112
|
+
if (next === "P" || next === "X" || next === "^" || next === "_") {
|
|
113
|
+
let cursor = index + 2;
|
|
114
|
+
while (cursor < text.length) {
|
|
115
|
+
if (text.charCodeAt(cursor) === 0x1b && text[cursor + 1] === "\\")
|
|
116
|
+
return cursor - index + 2;
|
|
117
|
+
cursor += 1;
|
|
118
|
+
}
|
|
119
|
+
return text.length - index;
|
|
120
|
+
}
|
|
121
|
+
return Math.min(2, text.length - index);
|
|
122
|
+
}
|
|
123
|
+
function cellWidthAt(text, index) {
|
|
124
|
+
const code = text.charCodeAt(index);
|
|
125
|
+
if (code === 0x09)
|
|
126
|
+
return { width: 3, length: 1 };
|
|
127
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
128
|
+
return { width: 2, length: 2 };
|
|
129
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
|
|
130
|
+
return { width: 0, length: 1 };
|
|
131
|
+
if (code <= 0x7e)
|
|
132
|
+
return { width: 1, length: 1 };
|
|
133
|
+
return { width: 2, length: 1 };
|
|
134
|
+
}
|
|
135
|
+
/** Local stand-in so we do not take a pi-tui dependency. Over-counts wide glyphs rather than under-count. */
|
|
136
|
+
export function visibleWidth(text) {
|
|
137
|
+
let width = 0;
|
|
138
|
+
for (let index = 0; index < text.length;) {
|
|
139
|
+
const ansi = ansiLengthAt(text, index);
|
|
140
|
+
if (ansi > 0) {
|
|
141
|
+
index += ansi;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const cell = cellWidthAt(text, index);
|
|
145
|
+
width += cell.width;
|
|
146
|
+
index += cell.length;
|
|
147
|
+
}
|
|
148
|
+
return width;
|
|
149
|
+
}
|
|
150
|
+
export function truncateToWidth(text, maxWidth, ellipsis = "...") {
|
|
151
|
+
const limit = Math.max(0, maxWidth);
|
|
152
|
+
if (limit <= 0)
|
|
153
|
+
return "";
|
|
154
|
+
if (visibleWidth(text) <= limit)
|
|
155
|
+
return text;
|
|
156
|
+
const ellipsisWidth = visibleWidth(ellipsis);
|
|
157
|
+
if (ellipsisWidth >= limit)
|
|
158
|
+
return ellipsis.slice(0, limit);
|
|
159
|
+
const budget = limit - ellipsisWidth;
|
|
160
|
+
let kept = "";
|
|
161
|
+
let width = 0;
|
|
162
|
+
for (let index = 0; index < text.length;) {
|
|
163
|
+
const ansi = ansiLengthAt(text, index);
|
|
164
|
+
if (ansi > 0) {
|
|
165
|
+
kept += text.slice(index, index + ansi);
|
|
166
|
+
index += ansi;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const cell = cellWidthAt(text, index);
|
|
170
|
+
if (width + cell.width > budget)
|
|
171
|
+
break;
|
|
172
|
+
kept += text.slice(index, index + cell.length);
|
|
173
|
+
width += cell.width;
|
|
174
|
+
index += cell.length;
|
|
175
|
+
}
|
|
176
|
+
return `${kept}${ellipsis}`;
|
|
177
|
+
}
|
|
87
178
|
function compactValue(value) {
|
|
88
179
|
if (value === null || value === undefined)
|
|
89
180
|
return String(value);
|
|
@@ -149,8 +240,11 @@ export class AsgrepText {
|
|
|
149
240
|
this.#text = text;
|
|
150
241
|
}
|
|
151
242
|
invalidate() { }
|
|
152
|
-
render(
|
|
153
|
-
|
|
243
|
+
render(width) {
|
|
244
|
+
const maxWidth = Math.max(1, width);
|
|
245
|
+
if (this.#text.length === 0)
|
|
246
|
+
return [""];
|
|
247
|
+
return this.#text.split("\n").map((line) => truncateToWidth(line, maxWidth));
|
|
154
248
|
}
|
|
155
249
|
}
|
|
156
250
|
export function presentText(formatted, last) {
|
package/dist/runtime.js
CHANGED
|
@@ -312,6 +312,21 @@ function waitForRefresh(refresh, signal) {
|
|
|
312
312
|
});
|
|
313
313
|
});
|
|
314
314
|
}
|
|
315
|
+
/** Shared refresh continues while other waiters remain; the last cancel stops it. */
|
|
316
|
+
function attachRefreshWaiter(state, refresh, signal) {
|
|
317
|
+
state.waiterCount += 1;
|
|
318
|
+
let cancelledByWaiter = false;
|
|
319
|
+
const wait = waitForRefresh(refresh, signal).catch((cause) => {
|
|
320
|
+
cancelledByWaiter = cause instanceof RuntimeError && cause.code === "CANCELLED" && signal?.aborted === true;
|
|
321
|
+
throw cause;
|
|
322
|
+
});
|
|
323
|
+
return wait.finally(() => {
|
|
324
|
+
state.waiterCount = Math.max(0, state.waiterCount - 1);
|
|
325
|
+
if (cancelledByWaiter && state.waiterCount === 0 && state.inFlight !== undefined) {
|
|
326
|
+
state.refreshAbort?.abort();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
315
330
|
export class FreshnessCoordinator {
|
|
316
331
|
#states = new Map();
|
|
317
332
|
#pending = new Map();
|
|
@@ -373,7 +388,7 @@ export class FreshnessCoordinator {
|
|
|
373
388
|
}
|
|
374
389
|
}
|
|
375
390
|
async ensureFresh(runtime, context, options = {}) {
|
|
376
|
-
const root = await runtime.resolveRoot(context);
|
|
391
|
+
const root = canonicalizeRootPath(await runtime.resolveRoot(context));
|
|
377
392
|
const rootContext = { cwd: root, [RESOLVED_ROOT]: true };
|
|
378
393
|
let state = this.#states.get(root);
|
|
379
394
|
if (!state) {
|
|
@@ -385,6 +400,8 @@ export class FreshnessCoordinator {
|
|
|
385
400
|
initialized: false,
|
|
386
401
|
lastRefreshAt: 0,
|
|
387
402
|
inFlight: undefined,
|
|
403
|
+
refreshAbort: undefined,
|
|
404
|
+
waiterCount: 0,
|
|
388
405
|
watcher: undefined,
|
|
389
406
|
};
|
|
390
407
|
this.#states.set(root, state);
|
|
@@ -413,12 +430,16 @@ export class FreshnessCoordinator {
|
|
|
413
430
|
this.#pending.delete(pendingRoot);
|
|
414
431
|
}
|
|
415
432
|
if (state.inFlight) {
|
|
416
|
-
await
|
|
433
|
+
await attachRefreshWaiter(state, state.inFlight, options.signal);
|
|
417
434
|
return this.ensureFresh(runtime, rootContext, options);
|
|
418
435
|
}
|
|
436
|
+
if (options.signal?.aborted)
|
|
437
|
+
throw cancelledRefreshWait();
|
|
419
438
|
const now = this.#now();
|
|
420
439
|
const elapsed = now - state.lastRefreshAt;
|
|
421
440
|
// Lease expiry: initialized and interval elapsed (or clock went backwards).
|
|
441
|
+
// Expiry re-probes status (missing/incompatible) but must not walk a ready
|
|
442
|
+
// index. First search of a ready, clean index is the same: status only.
|
|
422
443
|
const expired = state.initialized && (elapsed < 0 || elapsed >= this.#interval);
|
|
423
444
|
if (state.initialized && state.cleanGeneration === state.dirtyGeneration && !expired)
|
|
424
445
|
return root;
|
|
@@ -427,8 +448,12 @@ export class FreshnessCoordinator {
|
|
|
427
448
|
const fullScanRequired = state.fullScanRequired;
|
|
428
449
|
// Correctness work belongs to the root, not to whichever request happened
|
|
429
450
|
// to start it. Individual callers may stop waiting, but cannot cancel the
|
|
430
|
-
// shared refresh while other callers depend on it.
|
|
431
|
-
|
|
451
|
+
// shared refresh while other callers still depend on it. The last waiter
|
|
452
|
+
// abort stops the in-flight index so Pi/tool cancel cannot leave rayon
|
|
453
|
+
// workers burning CPU.
|
|
454
|
+
const refreshAbort = new AbortController();
|
|
455
|
+
state.refreshAbort = refreshAbort;
|
|
456
|
+
const sharedOptions = { signal: refreshAbort.signal };
|
|
432
457
|
if (options.timeoutMs !== undefined)
|
|
433
458
|
sharedOptions.timeoutMs = options.timeoutMs;
|
|
434
459
|
if (options.env !== undefined)
|
|
@@ -443,7 +468,10 @@ export class FreshnessCoordinator {
|
|
|
443
468
|
else
|
|
444
469
|
await runIndex(runtime, true, rootContext, sharedOptions);
|
|
445
470
|
}
|
|
446
|
-
else if (health === "missing"
|
|
471
|
+
else if (health === "missing") {
|
|
472
|
+
await runIndex(runtime, false, rootContext, sharedOptions);
|
|
473
|
+
}
|
|
474
|
+
else if (dirty && (fullScanRequired || refreshPaths.length === 0)) {
|
|
447
475
|
await runIndex(runtime, false, rootContext, sharedOptions);
|
|
448
476
|
}
|
|
449
477
|
else if (dirty) {
|
|
@@ -459,14 +487,16 @@ export class FreshnessCoordinator {
|
|
|
459
487
|
})();
|
|
460
488
|
let tracked;
|
|
461
489
|
tracked = refresh.finally(() => {
|
|
462
|
-
if (state.inFlight === tracked)
|
|
490
|
+
if (state.inFlight === tracked) {
|
|
463
491
|
state.inFlight = undefined;
|
|
492
|
+
state.refreshAbort = undefined;
|
|
493
|
+
}
|
|
464
494
|
});
|
|
465
495
|
state.inFlight = tracked;
|
|
466
496
|
// If every waiter is cancelled, the root-owned refresh still needs a
|
|
467
497
|
// rejection handler while it finishes in the background.
|
|
468
498
|
void tracked.catch(() => undefined);
|
|
469
|
-
await
|
|
499
|
+
await attachRefreshWaiter(state, tracked, options.signal);
|
|
470
500
|
if (state.cleanGeneration !== state.dirtyGeneration) {
|
|
471
501
|
return this.ensureFresh(runtime, rootContext, options);
|
|
472
502
|
}
|