pi-hashline-edit-pro 7.0.0 → 7.1.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 +28 -9
- package/index.ts +31 -28
- package/package.json +1 -1
- package/src/auto-read-all.ts +2 -2
- package/src/config-ui.ts +6 -1
- package/src/config.ts +6 -1
- package/src/edit-common.ts +15 -7
- package/src/model-gate.ts +1 -0
- package/src/outline/parsers.ts +0 -1
- package/src/outline/render.ts +154 -82
- package/src/outline/vendor/README.md +4 -0
- package/src/outline/vendor/parsers/blade.ts +1 -1
- package/src/outline/vendor/parsers/java.ts +144 -331
- package/src/outline/vendor/parsers/manager.ts +12 -183
- package/src/outline/vendor/parsers/php.ts +16 -81
- package/src/outline/vendor/parsers/sass.ts +0 -11
- package/src/outline/vendor/parsers/scss.ts +0 -8
- package/src/outline/vendor/types.ts +0 -1
- package/src/outline-tool.ts +184 -0
- package/src/prompts.ts +1 -0
- package/src/read.ts +1 -0
- package/src/structured.ts +32 -0
- package/tool-prompts/outline-guidelines.md +2 -0
- package/tool-prompts/outline-snippet.md +1 -0
- package/tool-prompts/outline.md +3 -0
- package/tool-prompts/read-guidelines.md +1 -1
package/src/outline/render.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { HASH_SEP } from "../hashline";
|
|
2
3
|
import { clipLine } from "../utils";
|
|
3
4
|
import type { OutlineSymbol } from "./parsers";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_MAX_ROWS = 400;
|
|
6
7
|
const DEFAULT_MAX_DEPTH = 6;
|
|
7
|
-
const MAX_DETAIL_CHARS = 60;
|
|
8
8
|
const PREVIEW_HEAD_LINES = 20;
|
|
9
9
|
const PREVIEW_TAIL_LINES = 10;
|
|
10
10
|
const PREVIEW_LINE_CHARS = 200;
|
|
11
11
|
|
|
12
|
+
function formatMoreLines(count: number): string {
|
|
13
|
+
return `... (${count} more ${count === 1 ? "line" : "lines"})`;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export interface OutlineRow {
|
|
13
17
|
line: number;
|
|
14
18
|
anchor: string;
|
|
@@ -20,6 +24,7 @@ export interface OutlineRender {
|
|
|
20
24
|
rows: OutlineRow[];
|
|
21
25
|
servedHashes: string[];
|
|
22
26
|
truncated: boolean;
|
|
27
|
+
complete: boolean;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export interface SymbolOutlineInput {
|
|
@@ -43,64 +48,118 @@ export interface PreviewOutlineInput {
|
|
|
43
48
|
maxBytes?: number;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
interface OutlineBudget {
|
|
52
|
+
output: string[];
|
|
53
|
+
rows: OutlineRow[];
|
|
54
|
+
servedHashes: string[];
|
|
55
|
+
maxRows: number;
|
|
56
|
+
maxBytes: number;
|
|
57
|
+
bytes: number;
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
truncation?: "rows" | "bytes";
|
|
60
|
+
skipped: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function startBudget(header: string, maxRows: number, maxBytes: number): OutlineBudget {
|
|
64
|
+
return { output: [header], rows: [], servedHashes: [], maxRows, maxBytes, bytes: Buffer.byteLength(header, "utf-8") + 1, truncated: false, skipped: 0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pushLine(budget: OutlineBudget, text: string): boolean {
|
|
68
|
+
const size = Buffer.byteLength(text, "utf-8") + 1;
|
|
69
|
+
if (budget.bytes + size > budget.maxBytes) return false;
|
|
70
|
+
budget.bytes += size;
|
|
71
|
+
budget.output.push(text);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function markTruncated(budget: OutlineBudget, cause: "rows" | "bytes"): void {
|
|
76
|
+
budget.truncated = true;
|
|
77
|
+
if (budget.truncation === undefined) budget.truncation = cause;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isComplete(budget: OutlineBudget): boolean {
|
|
81
|
+
return !budget.truncated && budget.skipped === 0 && budget.servedHashes.length === budget.rows.length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type EmitOutcome = "emitted" | "skipped" | "stopped";
|
|
85
|
+
|
|
86
|
+
function emitRow(budget: OutlineBudget, hashes: string[], lines: string[], line: number, text: string): EmitOutcome {
|
|
87
|
+
const anchor = hashes[line - 1];
|
|
88
|
+
if (anchor === undefined) {
|
|
89
|
+
budget.skipped += 1;
|
|
90
|
+
return "skipped";
|
|
91
|
+
}
|
|
92
|
+
if (budget.rows.length >= budget.maxRows) {
|
|
93
|
+
markTruncated(budget, "rows");
|
|
94
|
+
return "stopped";
|
|
95
|
+
}
|
|
96
|
+
if (!pushLine(budget, `${anchor}${HASH_SEP}${text}`)) {
|
|
97
|
+
markTruncated(budget, "bytes");
|
|
98
|
+
return "stopped";
|
|
99
|
+
}
|
|
100
|
+
budget.rows.push({ line, anchor, text });
|
|
101
|
+
if (text === (lines[line - 1] ?? "")) budget.servedHashes.push(anchor);
|
|
102
|
+
return "emitted";
|
|
48
103
|
}
|
|
49
104
|
|
|
50
|
-
function
|
|
105
|
+
function countSymbolRows(
|
|
106
|
+
symbols: OutlineSymbol[],
|
|
107
|
+
depth: number,
|
|
108
|
+
maxDepth: number,
|
|
109
|
+
hashes: string[],
|
|
110
|
+
seenLines: Set<number>,
|
|
111
|
+
): number {
|
|
51
112
|
let total = 0;
|
|
52
113
|
for (const symbol of symbols) {
|
|
53
|
-
|
|
54
|
-
if (
|
|
114
|
+
const children = symbol.children ?? [];
|
|
115
|
+
if (seenLines.has(symbol.startLine)) {
|
|
116
|
+
if (children.length > 0 && depth < maxDepth) total += countSymbolRows(children, depth + 1, maxDepth, hashes, seenLines);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
seenLines.add(symbol.startLine);
|
|
120
|
+
if (hashes[symbol.startLine - 1] !== undefined) total += 1;
|
|
121
|
+
if (children.length === 0 || depth >= maxDepth) continue;
|
|
122
|
+
total += countSymbolRows(children, depth + 1, maxDepth, hashes, seenLines);
|
|
55
123
|
}
|
|
56
124
|
return total;
|
|
57
125
|
}
|
|
58
126
|
|
|
59
|
-
function symbolLabel(symbol: OutlineSymbol, depth: number): string {
|
|
60
|
-
const indent = " ".repeat(depth);
|
|
61
|
-
const name = symbol.name.length > 0 ? ` ${symbol.name}` : "";
|
|
62
|
-
const detail = symbol.detail !== undefined && symbol.detail.length > 0
|
|
63
|
-
? ` ${symbol.detail.length > MAX_DETAIL_CHARS ? `${symbol.detail.slice(0, MAX_DETAIL_CHARS - 3)}...` : symbol.detail}`
|
|
64
|
-
: "";
|
|
65
|
-
const children = symbol.children !== undefined && symbol.children.length > 0 ? ` (${symbol.children.length} children)` : "";
|
|
66
|
-
const span = symbol.endLine - symbol.startLine + 1;
|
|
67
|
-
return `${indent}${symbol.type}${name}${detail}${children}${span > 1 ? ` [limit ${span}]` : ""}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
127
|
export function renderSymbolOutline(input: SymbolOutlineInput): OutlineRender {
|
|
71
|
-
const maxRows = input.maxRows ?? DEFAULT_MAX_ROWS;
|
|
72
|
-
const maxDepth = input.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
73
|
-
const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
74
128
|
const header = `=== ${input.displayPath}${input.languageName !== undefined ? ` (${input.languageName})` : ""} — ${input.totalLines} lines ===`;
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
let truncated = false;
|
|
79
|
-
|
|
80
|
-
const addRow = (line: number, text: string): boolean => {
|
|
81
|
-
const anchor = input.hashes[line - 1];
|
|
82
|
-
if (anchor === undefined) return true;
|
|
83
|
-
const rendered = `${anchor}│${text}`;
|
|
84
|
-
const size = Buffer.byteLength(rendered, "utf-8") + 1;
|
|
85
|
-
if (rows.length >= maxRows || bytes + size > maxBytes) {
|
|
86
|
-
truncated = true;
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
bytes += size;
|
|
90
|
-
output.push(rendered);
|
|
91
|
-
rows.push({ line, anchor, text });
|
|
92
|
-
return true;
|
|
93
|
-
};
|
|
129
|
+
const budget = startBudget(header, input.maxRows ?? DEFAULT_MAX_ROWS, input.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
130
|
+
const maxDepth = input.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
131
|
+
const seenLines = new Set<number>();
|
|
94
132
|
|
|
95
133
|
const walk = (symbols: OutlineSymbol[], depth: number): boolean => {
|
|
96
134
|
for (const symbol of symbols) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
if (seenLines.has(symbol.startLine)) {
|
|
136
|
+
const children = symbol.children ?? [];
|
|
137
|
+
if (children.length > 0) {
|
|
138
|
+
if (depth < maxDepth) {
|
|
139
|
+
if (!walk(children, depth + 1)) return false;
|
|
140
|
+
} else if (!pushLine(budget, `[${children.length} nested]`)) {
|
|
141
|
+
markTruncated(budget, "bytes");
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
seenLines.add(symbol.startLine);
|
|
148
|
+
const text = clipLine(input.lines[symbol.startLine - 1] ?? "", PREVIEW_LINE_CHARS);
|
|
149
|
+
const outcome = emitRow(budget, input.hashes, input.lines, symbol.startLine, text);
|
|
150
|
+
if (outcome === "stopped") return false;
|
|
151
|
+
const span = symbol.endLine - symbol.startLine + 1;
|
|
152
|
+
if (outcome === "emitted" && span > 1 && !pushLine(budget, `[limit ${span}]`)) {
|
|
153
|
+
markTruncated(budget, "bytes");
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
100
156
|
const children = symbol.children ?? [];
|
|
101
157
|
if (children.length === 0) continue;
|
|
102
158
|
if (depth >= maxDepth) {
|
|
103
|
-
|
|
159
|
+
if (!pushLine(budget, `[${children.length} nested]`)) {
|
|
160
|
+
markTruncated(budget, "bytes");
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
104
163
|
continue;
|
|
105
164
|
}
|
|
106
165
|
if (!walk(children, depth + 1)) return false;
|
|
@@ -109,62 +168,75 @@ export function renderSymbolOutline(input: SymbolOutlineInput): OutlineRender {
|
|
|
109
168
|
};
|
|
110
169
|
|
|
111
170
|
walk(input.symbols, 0);
|
|
112
|
-
if (truncated) {
|
|
113
|
-
|
|
171
|
+
if (budget.truncated) {
|
|
172
|
+
const remaining = countSymbolRows(input.symbols, 0, maxDepth, input.hashes, new Set<number>()) - budget.rows.length;
|
|
173
|
+
const more = remaining > 0 ? ` (${remaining} more ${remaining === 1 ? "symbol" : "symbols"})` : "";
|
|
174
|
+
const cause = budget.truncation === "rows" ? "row cap" : "byte budget";
|
|
175
|
+
if (budget.rows.length === 0) {
|
|
176
|
+
budget.output.push(more.length > 0
|
|
177
|
+
? `...${more}; outline truncated by the ${cause} before the first row.`
|
|
178
|
+
: `... (outline truncated by the ${cause} before the first row.)`);
|
|
179
|
+
} else if (remaining > 0) {
|
|
180
|
+
budget.output.push(`...${more}; read from the last shown anchor to continue.`);
|
|
181
|
+
} else {
|
|
182
|
+
budget.output.push(`... (outline truncated by the ${cause}; read from the last shown anchor to continue.)`);
|
|
183
|
+
}
|
|
114
184
|
}
|
|
115
|
-
return { text: output.join("\n"), rows, servedHashes:
|
|
185
|
+
return { text: budget.output.join("\n"), rows: budget.rows, servedHashes: budget.servedHashes, truncated: budget.truncated, complete: isComplete(budget) };
|
|
116
186
|
}
|
|
117
187
|
|
|
118
188
|
export function renderPreviewOutline(input: PreviewOutlineInput): OutlineRender {
|
|
119
|
-
const headCount = input.headLines ?? PREVIEW_HEAD_LINES;
|
|
120
|
-
const tailCount = input.tailLines ?? PREVIEW_TAIL_LINES;
|
|
121
|
-
const maxBytes = input.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
122
189
|
const header = `=== ${input.displayPath} — ${input.lines.length} lines ===`;
|
|
123
|
-
const
|
|
124
|
-
const output: string[] = [header];
|
|
125
|
-
let bytes = Buffer.byteLength(header, "utf-8") + 1;
|
|
126
|
-
let truncated = false;
|
|
190
|
+
const budget = startBudget(header, Number.POSITIVE_INFINITY, input.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
127
191
|
if (input.lines.length === 0) {
|
|
128
192
|
const anchor = input.hashes[0];
|
|
129
193
|
if (anchor !== undefined) {
|
|
130
|
-
|
|
131
|
-
|
|
194
|
+
const marker = `${anchor}${HASH_SEP}`;
|
|
195
|
+
const note = "[File is empty. Use replace to insert content.]";
|
|
196
|
+
if (pushLine(budget, marker)) {
|
|
197
|
+
budget.rows.push({ line: 1, anchor, text: "" });
|
|
198
|
+
budget.servedHashes.push(anchor);
|
|
199
|
+
if (!pushLine(budget, note)) markTruncated(budget, "bytes");
|
|
200
|
+
} else {
|
|
201
|
+
markTruncated(budget, "bytes");
|
|
202
|
+
}
|
|
132
203
|
}
|
|
133
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
text: budget.output.join("\n"),
|
|
206
|
+
rows: budget.rows,
|
|
207
|
+
servedHashes: budget.servedHashes,
|
|
208
|
+
truncated: budget.truncated,
|
|
209
|
+
complete: isComplete(budget),
|
|
210
|
+
};
|
|
134
211
|
}
|
|
135
212
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (line === undefined || anchor === undefined) return true;
|
|
140
|
-
const text = clipLine(line, PREVIEW_LINE_CHARS);
|
|
141
|
-
const rendered = `${anchor}│${text}`;
|
|
142
|
-
const size = Buffer.byteLength(rendered, "utf-8") + 1;
|
|
143
|
-
if (bytes + size > maxBytes) {
|
|
144
|
-
truncated = true;
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
bytes += size;
|
|
148
|
-
output.push(rendered);
|
|
149
|
-
rows.push({ line: index + 1, anchor, text });
|
|
150
|
-
return true;
|
|
151
|
-
};
|
|
213
|
+
const head = Math.min(input.headLines ?? PREVIEW_HEAD_LINES, input.lines.length);
|
|
214
|
+
const tail = Math.min(input.tailLines ?? PREVIEW_TAIL_LINES, input.lines.length - head);
|
|
215
|
+
const attempt = (index: number): EmitOutcome => emitRow(budget, input.hashes, input.lines, index + 1, clipLine(input.lines[index] ?? "", PREVIEW_LINE_CHARS));
|
|
152
216
|
|
|
153
|
-
const head = Math.min(headCount, input.lines.length);
|
|
154
|
-
const tail = Math.min(tailCount, input.lines.length - head);
|
|
155
217
|
for (let index = 0; index < head; index += 1) {
|
|
156
|
-
if (
|
|
218
|
+
if (attempt(index) === "stopped") break;
|
|
157
219
|
}
|
|
158
|
-
|
|
159
|
-
|
|
220
|
+
let middleIndex: number | undefined;
|
|
221
|
+
if (!budget.truncated && head + tail < input.lines.length) {
|
|
222
|
+
if (pushLine(budget, formatMoreLines(input.lines.length - head - tail))) middleIndex = budget.output.length - 1;
|
|
223
|
+
else markTruncated(budget, "bytes");
|
|
160
224
|
}
|
|
161
|
-
if (!truncated) {
|
|
225
|
+
if (!budget.truncated) {
|
|
162
226
|
for (let index = input.lines.length - tail; index < input.lines.length; index += 1) {
|
|
163
|
-
if (
|
|
227
|
+
if (attempt(index) === "stopped") break;
|
|
164
228
|
}
|
|
165
229
|
}
|
|
166
|
-
if (truncated) {
|
|
167
|
-
|
|
230
|
+
if (budget.truncated) {
|
|
231
|
+
const remaining = input.lines.length - budget.rows.length;
|
|
232
|
+
if (middleIndex !== undefined) budget.output.splice(middleIndex, 1);
|
|
233
|
+
if (remaining > 0) budget.output.push(formatMoreLines(remaining));
|
|
168
234
|
}
|
|
169
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
text: budget.output.join("\n"),
|
|
237
|
+
rows: budget.rows,
|
|
238
|
+
servedHashes: budget.servedHashes,
|
|
239
|
+
truncated: budget.truncated,
|
|
240
|
+
complete: head + tail >= input.lines.length && isComplete(budget),
|
|
241
|
+
};
|
|
170
242
|
}
|
|
@@ -9,6 +9,10 @@ These files are copied from [pi-codebase-reader](https://github.com/HanzCEO/pi-c
|
|
|
9
9
|
- `parsers/manager.ts` extracts anonymous default exports (`export default function`/`class`), function expressions, top-level variable declarators, and nested `describe`/`test`/`it` calls, so entry-point, config, data, and test files outline instead of falling back to a preview.
|
|
10
10
|
- `parsers/manager.ts` adds PHP support: `parsers/php.ts` outlines classes, interfaces, traits, enums, methods, properties, constants, top-level functions, and assigned closures.
|
|
11
11
|
- `parsers/manager.ts` adds Blade support: `parsers/blade.ts` outlines `@section`/`@push`/`@once`/`@php` blocks, named directives such as `@extends` and `@include`, and `<x-…>`/`<livewire:…>` components.
|
|
12
|
+
- `parsers/java.ts` reads the field-based tree-sitter-java grammar shipped in `wasm/` (method, constructor, field, constant, enum, record, and annotation-element nodes), so annotated members keep their names; every member anchors on its declaration line rather than on its annotation.
|
|
13
|
+
- `parsers/php.ts` anchors a declaration on its first line after its attributes, so a member with `#[Test]` starts on the declaration instead of the attribute.
|
|
14
|
+
- `types.ts` and every parser drop the upstream `detail` string: outlines render the first line of each symbol's declaration, so the parameters, inheritance, and qualifier text was computed and discarded.
|
|
15
|
+
- `types.ts` keeps `SymbolInfo.name` and `SymbolInfo.type`: the extractors use them while building symbol trees and the upstream contract carries them, even though outlines render a real file line for each symbol.
|
|
12
16
|
|
|
13
17
|
The `wasm/` grammars come from the MIT-licensed `tree-sitter-javascript`, `tree-sitter-typescript`, `tree-sitter-python`, `tree-sitter-go`, `tree-sitter-rust`, `tree-sitter-solidity`, and `tree-sitter-java` packages, plus pi-codebase-reader's vendored Sass and SCSS grammars, the VS Code WebAssembly build of `tree-sitter-php`, and `tree-sitter-blade` built from v0.12.3; see `wasm/LICENSES.md` for the per-grammar copyright notices. Comments in these files are upstream prose and are exempt from this repository's no-comments rule.
|
|
14
18
|
|
|
@@ -76,7 +76,7 @@ function directiveSymbol(node: Node, parameter: Node | null, source: string): Sy
|
|
|
76
76
|
const raw = textOf(parameter, source);
|
|
77
77
|
const quoted = unquote(raw);
|
|
78
78
|
return quoted === undefined
|
|
79
|
-
? { name: "", type: label, startLine, endLine
|
|
79
|
+
? { name: "", type: label, startLine, endLine }
|
|
80
80
|
: { name: quoted, type: label, startLine, endLine };
|
|
81
81
|
}
|
|
82
82
|
|