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.
@@ -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
- function servedHashesOf(rows: OutlineRow[]): string[] {
47
- return [...new Set(rows.map((row) => row.anchor))];
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 countSymbols(symbols: OutlineSymbol[]): number {
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
- total += 1;
54
- if (symbol.children) total += countSymbols(symbol.children);
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 rows: OutlineRow[] = [];
76
- const output: string[] = [header];
77
- let bytes = Buffer.byteLength(header, "utf-8") + 1;
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
- const contentLine = symbol.endLine === symbol.startLine ? input.lines[symbol.startLine - 1] : undefined;
98
- const text = contentLine === undefined ? symbolLabel(symbol, depth) : clipLine(contentLine, PREVIEW_LINE_CHARS);
99
- if (!addRow(symbol.startLine, text)) return false;
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
- output.push(`${" ".repeat(depth + 1)}(${children.length} nested items)`);
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
- output.push(`... (${countSymbols(input.symbols) - rows.length} more symbols)`);
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: servedHashesOf(rows), truncated };
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 rows: OutlineRow[] = [];
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
- output.push(`${anchor}│`, "[File is empty. Use replace to insert content.]");
131
- rows.push({ line: 1, anchor, text: "" });
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 { text: output.join("\n"), rows, servedHashes: servedHashesOf(rows), truncated: false };
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 addRow = (index: number): boolean => {
137
- const line = input.lines[index];
138
- const anchor = input.hashes[index];
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 (!addRow(index)) break;
218
+ if (attempt(index) === "stopped") break;
157
219
  }
158
- if (!truncated && head + tail < input.lines.length) {
159
- output.push(`... (${input.lines.length - head - tail} more lines)`);
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 (!addRow(index)) break;
227
+ if (attempt(index) === "stopped") break;
164
228
  }
165
229
  }
166
- if (truncated) {
167
- output.push(`... (${input.lines.length - rows.length} more lines)`);
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 { text: output.join("\n"), rows, servedHashes: servedHashesOf(rows), truncated };
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, detail: raw.trim() }
79
+ ? { name: "", type: label, startLine, endLine }
80
80
  : { name: quoted, type: label, startLine, endLine };
81
81
  }
82
82