@oh-my-pi/pi-coding-agent 14.5.3 → 14.5.6
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/CHANGELOG.md +49 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +103 -8
- package/src/config/settings-schema.ts +14 -13
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +4 -4
- package/src/edit/index.ts +111 -109
- package/src/edit/line-hash.ts +33 -3
- package/src/edit/modes/apply-patch.ts +6 -4
- package/src/edit/modes/atom.lark +27 -0
- package/src/edit/modes/atom.ts +1039 -841
- package/src/edit/modes/hashline.ts +9 -10
- package/src/edit/modes/patch.ts +23 -19
- package/src/edit/modes/replace.ts +19 -15
- package/src/edit/renderer.ts +65 -8
- package/src/edit/streaming.ts +47 -77
- package/src/extensibility/extensions/types.ts +11 -11
- package/src/extensibility/hooks/types.ts +6 -6
- package/src/lsp/edits.ts +8 -5
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +7 -7
- package/src/mcp/discoverable-tool-metadata.ts +1 -1
- package/src/mcp/manager.ts +3 -3
- package/src/mcp/tool-bridge.ts +4 -4
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-defs.ts +3 -3
- package/src/modes/components/tree-selector.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +31 -7
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/system-prompt.md +116 -60
- package/src/prompts/tools/apply-patch.md +0 -2
- package/src/prompts/tools/atom.md +81 -63
- package/src/prompts/tools/bash.md +7 -4
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/find.md +6 -1
- package/src/prompts/tools/hashline.md +10 -11
- package/src/prompts/tools/patch.md +13 -13
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/replace.md +3 -3
- package/src/prompts/tools/{grep.md → search.md} +4 -4
- package/src/sdk.ts +19 -9
- package/src/session/agent-session.ts +65 -0
- package/src/system-prompt.ts +15 -5
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +10 -1
- package/src/tools/ast-edit.ts +4 -6
- package/src/tools/ast-grep.ts +4 -6
- package/src/tools/bash.ts +1 -1
- package/src/tools/file-recorder.ts +6 -6
- package/src/tools/find.ts +11 -13
- package/src/tools/index.ts +7 -7
- package/src/tools/path-utils.ts +31 -4
- package/src/tools/read.ts +12 -6
- package/src/tools/renderers.ts +2 -2
- package/src/tools/{grep.ts → search.ts} +32 -40
- package/src/tools/write.ts +8 -4
- package/src/web/search/index.ts +1 -1
- package/src/edit/block.ts +0 -308
- package/src/edit/indent.ts +0 -150
package/src/edit/block.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Block-balanced delimiter finder used by the `splice_block` verb.
|
|
3
|
-
*
|
|
4
|
-
* Tokenizes source text to skip strings and comments, then walks a stack of
|
|
5
|
-
* open delimiters to identify the enclosing balanced block for a target line.
|
|
6
|
-
*
|
|
7
|
-
* This is intentionally language-agnostic over the C-family (C, C++, Rust,
|
|
8
|
-
* Go, Java, JS/TS, C#, Swift, Kotlin, Scala, …): it understands `// line`,
|
|
9
|
-
* `/* block * /` comments, double-quoted, single-quoted, and backtick strings
|
|
10
|
-
* with backslash escapes. It does NOT attempt to parse raw string literals,
|
|
11
|
-
* Python triple-quoted strings, or YAML/Python indent-significant blocks —
|
|
12
|
-
* those are out of scope for v1.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export type DelimiterKind = "{" | "(" | "[";
|
|
16
|
-
|
|
17
|
-
export interface BlockRange {
|
|
18
|
-
/** Byte/character offset of the opening delimiter. */
|
|
19
|
-
openOffset: number;
|
|
20
|
-
/** Byte/character offset just after the closing delimiter. */
|
|
21
|
-
closeOffsetExclusive: number;
|
|
22
|
-
/** Offset of first character after the opener (start of body). */
|
|
23
|
-
bodyStart: number;
|
|
24
|
-
/** Offset of the closing delimiter character. */
|
|
25
|
-
bodyEnd: number;
|
|
26
|
-
/** 1-indexed line number of the opener. */
|
|
27
|
-
openLine: number;
|
|
28
|
-
/** Byte/character offset of the opener line start. */
|
|
29
|
-
openLineStart: number;
|
|
30
|
-
/** 1-indexed line number of the closer. */
|
|
31
|
-
closeLine: number;
|
|
32
|
-
/** True when opener and closer are on the same line. */
|
|
33
|
-
sameLine: boolean;
|
|
34
|
-
/** Whitespace prefix of the opener's line. */
|
|
35
|
-
openerLineIndent: string;
|
|
36
|
-
/**
|
|
37
|
-
* Whitespace prefix of the first non-blank body line, or `null` when the
|
|
38
|
-
* body has no non-blank line.
|
|
39
|
-
*/
|
|
40
|
-
bodyLineIndent: string | null;
|
|
41
|
-
/** Body text exactly as it appears between the delimiters. */
|
|
42
|
-
bodyText: string;
|
|
43
|
-
/** Total enclosing blocks of the requested kind before depth selection. */
|
|
44
|
-
enclosingCount: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface BraceEvent {
|
|
48
|
-
kind: DelimiterKind | ")" | "]" | "}";
|
|
49
|
-
offset: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const OPENERS: Record<DelimiterKind, string> = { "{": "{", "(": "(", "[": "[" };
|
|
53
|
-
const CLOSERS: Record<DelimiterKind, string> = { "{": "}", "(": ")", "[": "]" };
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Walk `text` and emit positions of opening and closing delimiters that lie
|
|
57
|
-
* outside strings and comments.
|
|
58
|
-
*/
|
|
59
|
-
export function scanDelimiters(text: string): BraceEvent[] {
|
|
60
|
-
const out: BraceEvent[] = [];
|
|
61
|
-
const len = text.length;
|
|
62
|
-
let i = 0;
|
|
63
|
-
while (i < len) {
|
|
64
|
-
const ch = text[i]!;
|
|
65
|
-
// Line comment `// …` to end of line.
|
|
66
|
-
if (ch === "/" && text[i + 1] === "/") {
|
|
67
|
-
i += 2;
|
|
68
|
-
while (i < len && text[i] !== "\n") i++;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
// Hash line comment `# …` for shell/Python-like — but only when at start
|
|
72
|
-
// of a token, to avoid mangling C preprocessor lines (`#include`). We
|
|
73
|
-
// treat any `#` at column 0 or after whitespace as a line comment, which
|
|
74
|
-
// is a heuristic that's also fine for `#include` (no braces follow on
|
|
75
|
-
// the same line in practice for our use case).
|
|
76
|
-
if (ch === "#" && (i === 0 || text[i - 1] === "\n" || text[i - 1] === " " || text[i - 1] === "\t")) {
|
|
77
|
-
// Not enabled: too aggressive for C/C++/Rust files. Skip.
|
|
78
|
-
}
|
|
79
|
-
// Block comment `/* … */`.
|
|
80
|
-
if (ch === "/" && text[i + 1] === "*") {
|
|
81
|
-
i += 2;
|
|
82
|
-
while (i < len && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
83
|
-
if (i < len) i += 2;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
// String literals: ", ', `. Backslash-escape aware.
|
|
87
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
88
|
-
const quote = ch;
|
|
89
|
-
i++;
|
|
90
|
-
while (i < len) {
|
|
91
|
-
const c = text[i]!;
|
|
92
|
-
if (c === "\\") {
|
|
93
|
-
i += 2;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (c === quote) {
|
|
97
|
-
i++;
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
if (c === "\n" && (quote === '"' || quote === "'")) {
|
|
101
|
-
// Unterminated string; stop scanning this literal so we
|
|
102
|
-
// don't swallow the rest of the file.
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
i++;
|
|
106
|
-
}
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (ch === "{" || ch === "(" || ch === "[") {
|
|
110
|
-
out.push({ kind: ch, offset: i });
|
|
111
|
-
} else if (ch === "}" || ch === ")" || ch === "]") {
|
|
112
|
-
out.push({ kind: ch, offset: i });
|
|
113
|
-
}
|
|
114
|
-
i++;
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface OpenFrame {
|
|
120
|
-
kind: DelimiterKind;
|
|
121
|
-
offset: number;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Build a list of balanced (open, close) ranges by walking the events from
|
|
126
|
-
* `scanDelimiters`. Mismatched closers are skipped (the file may be partially
|
|
127
|
-
* malformed), and unclosed openers at EOF are dropped.
|
|
128
|
-
*/
|
|
129
|
-
function pairBlocks(events: BraceEvent[]): { open: OpenFrame; closeOffset: number }[] {
|
|
130
|
-
const stack: OpenFrame[] = [];
|
|
131
|
-
const pairs: { open: OpenFrame; closeOffset: number }[] = [];
|
|
132
|
-
for (const ev of events) {
|
|
133
|
-
if (ev.kind === "{" || ev.kind === "(" || ev.kind === "[") {
|
|
134
|
-
stack.push({ kind: ev.kind, offset: ev.offset });
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
// Closer.
|
|
138
|
-
const expected: DelimiterKind | null =
|
|
139
|
-
ev.kind === "}" ? "{" : ev.kind === ")" ? "(" : ev.kind === "]" ? "[" : null;
|
|
140
|
-
if (!expected) continue;
|
|
141
|
-
// Pop until we find the matching opener, but only commit pairs when
|
|
142
|
-
// kinds match. This tolerates small skews from raw strings or other
|
|
143
|
-
// unsupported constructs without exploding the search.
|
|
144
|
-
const top = stack[stack.length - 1];
|
|
145
|
-
if (top?.kind === expected) {
|
|
146
|
-
stack.pop();
|
|
147
|
-
pairs.push({ open: top, closeOffset: ev.offset });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return pairs;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function lineToOffset(text: string, line: number): number {
|
|
154
|
-
let n = 1;
|
|
155
|
-
let i = 0;
|
|
156
|
-
while (i < text.length && n < line) {
|
|
157
|
-
if (text[i] === "\n") n++;
|
|
158
|
-
i++;
|
|
159
|
-
}
|
|
160
|
-
return i;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function offsetToLine(text: string, offset: number): number {
|
|
164
|
-
let n = 1;
|
|
165
|
-
for (let i = 0; i < offset && i < text.length; i++) {
|
|
166
|
-
if (text[i] === "\n") n++;
|
|
167
|
-
}
|
|
168
|
-
return n;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function lineIndentAt(text: string, lineNumber: number): string {
|
|
172
|
-
const start = lineToOffset(text, lineNumber);
|
|
173
|
-
let i = start;
|
|
174
|
-
while (i < text.length && (text[i] === " " || text[i] === "\t")) i++;
|
|
175
|
-
return text.slice(start, i);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function extractRange(text: string, start: number, end: number): string {
|
|
179
|
-
return text.slice(start, end);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export interface FindBlockOptions {
|
|
183
|
-
kind?: DelimiterKind;
|
|
184
|
-
depth?: number;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export interface FindBlockError {
|
|
188
|
-
message: string;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Find the enclosing balanced block of `kind` containing `targetLine`
|
|
193
|
-
* (1-indexed), at the requested ancestor `depth` (0 = innermost).
|
|
194
|
-
*
|
|
195
|
-
* Returns an error object when no such block exists.
|
|
196
|
-
*/
|
|
197
|
-
export function findEnclosingBlock(
|
|
198
|
-
text: string,
|
|
199
|
-
targetLine: number,
|
|
200
|
-
options: FindBlockOptions = {},
|
|
201
|
-
): BlockRange | FindBlockError {
|
|
202
|
-
const kind: DelimiterKind = options.kind ?? "{";
|
|
203
|
-
const depth = Math.max(0, Math.floor(options.depth ?? 0));
|
|
204
|
-
|
|
205
|
-
const events = scanDelimiters(text);
|
|
206
|
-
const pairs = pairBlocks(events);
|
|
207
|
-
|
|
208
|
-
// Lines (1-indexed) that bracket the target are considered to contain it.
|
|
209
|
-
// This handles same-line `{ x }` blocks too (openLine == closeLine ==
|
|
210
|
-
// targetLine).
|
|
211
|
-
const enclosing = pairs
|
|
212
|
-
.filter(p => p.open.kind === kind)
|
|
213
|
-
.map(p => ({
|
|
214
|
-
open: p.open,
|
|
215
|
-
closeOffset: p.closeOffset,
|
|
216
|
-
openLine: offsetToLine(text, p.open.offset),
|
|
217
|
-
closeLine: offsetToLine(text, p.closeOffset),
|
|
218
|
-
}))
|
|
219
|
-
.filter(p => p.openLine <= targetLine && targetLine <= p.closeLine);
|
|
220
|
-
if (enclosing.length === 0) {
|
|
221
|
-
return {
|
|
222
|
-
message: `No enclosing \`${kind}\` block contains line ${targetLine}.`,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
// Default ordering is innermost first (largest open offset among containers).
|
|
226
|
-
// When both candidates are entirely on the target line, prefer the outermost
|
|
227
|
-
// same-line block so anchoring a call line targets the containing call before
|
|
228
|
-
// nested argument calls such as `int(port)`. Multi-line nesting keeps the
|
|
229
|
-
// existing innermost-first behavior.
|
|
230
|
-
enclosing.sort((a, b) => {
|
|
231
|
-
const aSingle = a.openLine === targetLine && a.closeLine === targetLine;
|
|
232
|
-
const bSingle = b.openLine === targetLine && b.closeLine === targetLine;
|
|
233
|
-
if (aSingle && bSingle) return a.open.offset - b.open.offset;
|
|
234
|
-
return b.open.offset - a.open.offset;
|
|
235
|
-
});
|
|
236
|
-
if (depth >= enclosing.length) {
|
|
237
|
-
return {
|
|
238
|
-
message: `Requested depth ${depth} exceeds available enclosing \`${kind}\` blocks (${enclosing.length}).`,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
const chosen = enclosing[depth]!;
|
|
242
|
-
const openOffset = chosen.open.offset;
|
|
243
|
-
const closeOffset = chosen.closeOffset;
|
|
244
|
-
const bodyStart = openOffset + 1;
|
|
245
|
-
const bodyEnd = closeOffset;
|
|
246
|
-
const openLine = chosen.openLine;
|
|
247
|
-
const closeLine = chosen.closeLine;
|
|
248
|
-
const openLineStart = lineToOffset(text, openLine);
|
|
249
|
-
const openerLineIndent = lineIndentAt(text, openLine);
|
|
250
|
-
const bodyText = extractRange(text, bodyStart, bodyEnd);
|
|
251
|
-
const bodyLineIndent = computeBodyLineIndent(text, bodyStart, bodyEnd);
|
|
252
|
-
return {
|
|
253
|
-
openOffset,
|
|
254
|
-
closeOffsetExclusive: closeOffset + 1,
|
|
255
|
-
bodyStart,
|
|
256
|
-
bodyEnd,
|
|
257
|
-
openLine,
|
|
258
|
-
openLineStart,
|
|
259
|
-
closeLine,
|
|
260
|
-
sameLine: openLine === closeLine,
|
|
261
|
-
openerLineIndent,
|
|
262
|
-
bodyLineIndent,
|
|
263
|
-
bodyText,
|
|
264
|
-
enclosingCount: enclosing.length,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function computeBodyLineIndent(text: string, bodyStart: number, bodyEnd: number): string | null {
|
|
269
|
-
// Scan body for the first line whose non-whitespace character lives within
|
|
270
|
-
// [bodyStart, bodyEnd). Return that line's leading whitespace prefix.
|
|
271
|
-
let i = bodyStart;
|
|
272
|
-
// Step over the rest of the opener's line (it may contain trailing
|
|
273
|
-
// whitespace but not body content we want to use as the indent reference).
|
|
274
|
-
while (i < bodyEnd && text[i] !== "\n") i++;
|
|
275
|
-
while (i < bodyEnd) {
|
|
276
|
-
// At line boundary; skip the newline.
|
|
277
|
-
if (text[i] === "\n") i++;
|
|
278
|
-
const lineStart = i;
|
|
279
|
-
while (i < bodyEnd && (text[i] === " " || text[i] === "\t")) i++;
|
|
280
|
-
// Skip blank lines.
|
|
281
|
-
if (i < bodyEnd && text[i] !== "\n") {
|
|
282
|
-
return text.slice(lineStart, i);
|
|
283
|
-
}
|
|
284
|
-
// Skip to end of line.
|
|
285
|
-
while (i < bodyEnd && text[i] !== "\n") i++;
|
|
286
|
-
}
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Verify that the agent's body has balanced delimiters of `kind`. Returns an
|
|
292
|
-
* error message when unbalanced, or `null` when fine.
|
|
293
|
-
*/
|
|
294
|
-
export function checkBodyBraceBalance(body: string, kind: DelimiterKind): string | null {
|
|
295
|
-
const events = scanDelimiters(body);
|
|
296
|
-
let opens = 0;
|
|
297
|
-
let closes = 0;
|
|
298
|
-
const opener = OPENERS[kind];
|
|
299
|
-
const closer = CLOSERS[kind];
|
|
300
|
-
for (const e of events) {
|
|
301
|
-
if (e.kind === opener) opens++;
|
|
302
|
-
else if (e.kind === closer) closes++;
|
|
303
|
-
}
|
|
304
|
-
if (opens !== closes) {
|
|
305
|
-
return `Replacement body has unbalanced \`${opener}\`/\`${closer}\` (open=${opens}, close=${closes}).`;
|
|
306
|
-
}
|
|
307
|
-
return null;
|
|
308
|
-
}
|
package/src/edit/indent.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Indent helpers used by `splice_block`.
|
|
3
|
-
*
|
|
4
|
-
* Pure functions: take strings/lines, return strings/lines. No I/O. Designed
|
|
5
|
-
* to be easy to unit-test in isolation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface IndentStyle {
|
|
9
|
-
kind: "tab" | "space";
|
|
10
|
-
width: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const SAMPLE_LINES_FOR_DETECTION = 256;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Detect whether the file uses tab or space indentation, and the indent width
|
|
17
|
-
* for spaces. Sampling-based; defaults to {kind: "tab", width: 1} when nothing
|
|
18
|
-
* is conclusive.
|
|
19
|
-
*/
|
|
20
|
-
export function detectIndentStyle(text: string): IndentStyle {
|
|
21
|
-
const lines = text.split("\n", SAMPLE_LINES_FOR_DETECTION + 1).slice(0, SAMPLE_LINES_FOR_DETECTION);
|
|
22
|
-
let tabIndented = 0;
|
|
23
|
-
let spaceIndented = 0;
|
|
24
|
-
const spaceWidthCounts = new Map<number, number>();
|
|
25
|
-
for (const line of lines) {
|
|
26
|
-
if (line.length === 0) continue;
|
|
27
|
-
const ch0 = line[0];
|
|
28
|
-
if (ch0 === "\t") {
|
|
29
|
-
tabIndented++;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (ch0 !== " ") continue;
|
|
33
|
-
let count = 0;
|
|
34
|
-
while (count < line.length && line[count] === " ") count++;
|
|
35
|
-
// Skip lines whose entire content is whitespace (no signal).
|
|
36
|
-
if (count === line.length) continue;
|
|
37
|
-
spaceIndented++;
|
|
38
|
-
// Record indent step. Try common values 2, 4, 8 by GCD-ish.
|
|
39
|
-
spaceWidthCounts.set(count, (spaceWidthCounts.get(count) ?? 0) + 1);
|
|
40
|
-
}
|
|
41
|
-
if (tabIndented > spaceIndented) return { kind: "tab", width: 1 };
|
|
42
|
-
if (spaceIndented === 0) return { kind: "tab", width: 1 };
|
|
43
|
-
|
|
44
|
-
// Pick the most common nonzero indent width that divides the others well.
|
|
45
|
-
const candidates = [2, 4, 8];
|
|
46
|
-
let bestCandidate = 4;
|
|
47
|
-
let bestScore = -1;
|
|
48
|
-
for (const cand of candidates) {
|
|
49
|
-
let score = 0;
|
|
50
|
-
for (const [width, count] of spaceWidthCounts) {
|
|
51
|
-
if (width % cand === 0) score += count;
|
|
52
|
-
}
|
|
53
|
-
if (score > bestScore) {
|
|
54
|
-
bestScore = score;
|
|
55
|
-
bestCandidate = cand;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return { kind: "space", width: bestCandidate };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Strip the common leading whitespace prefix from every non-empty line.
|
|
63
|
-
* Blank/whitespace-only lines pass through unchanged.
|
|
64
|
-
*
|
|
65
|
-
* Tab and space whitespace count as a single character each here; we look at
|
|
66
|
-
* raw prefix bytes. The block executor re-applies destination indent later,
|
|
67
|
-
* so tab/space mismatches in the agent's own input are normalized by
|
|
68
|
-
* `applyIndent` rather than here.
|
|
69
|
-
*/
|
|
70
|
-
export function stripCommonIndent(lines: readonly string[]): string[] {
|
|
71
|
-
let common: string | null = null;
|
|
72
|
-
for (const line of lines) {
|
|
73
|
-
if (line.trim().length === 0) continue;
|
|
74
|
-
const m = /^[\t ]*/.exec(line);
|
|
75
|
-
const prefix = m ? m[0] : "";
|
|
76
|
-
if (common === null) {
|
|
77
|
-
common = prefix;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
// Reduce to the longest shared prefix.
|
|
81
|
-
let i = 0;
|
|
82
|
-
const limit = Math.min(common.length, prefix.length);
|
|
83
|
-
while (i < limit && common[i] === prefix[i]) i++;
|
|
84
|
-
common = common.slice(0, i);
|
|
85
|
-
if (common.length === 0) break;
|
|
86
|
-
}
|
|
87
|
-
if (!common) return [...lines];
|
|
88
|
-
return lines.map(line => (line.startsWith(common!) ? line.slice(common!.length) : line));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Re-index leading whitespace of `lines` from the source style to the
|
|
93
|
-
* destination style, then prepend `prefix` to every non-empty line.
|
|
94
|
-
*
|
|
95
|
-
* - Source style is detected from the lines themselves.
|
|
96
|
-
* - Tab→space and space→tab conversion is applied to the *relative*
|
|
97
|
-
* indentation (everything past the common prefix has already been stripped
|
|
98
|
-
* by `stripCommonIndent`, so all leading whitespace here is "extra" indent).
|
|
99
|
-
* - Blank lines stay empty (no trailing whitespace).
|
|
100
|
-
*/
|
|
101
|
-
export function applyIndent(lines: readonly string[], prefix: string, destStyle: IndentStyle): string[] {
|
|
102
|
-
const sourceStyle = detectIndentStyle(lines.join("\n"));
|
|
103
|
-
return lines.map(line => {
|
|
104
|
-
if (line.trim().length === 0) return "";
|
|
105
|
-
const m = /^[\t ]*/.exec(line);
|
|
106
|
-
const leading = m ? m[0] : "";
|
|
107
|
-
const rest = line.slice(leading.length);
|
|
108
|
-
const normalized = normalizeIndent(leading, sourceStyle, destStyle);
|
|
109
|
-
return prefix + normalized + rest;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function normalizeIndent(leading: string, source: IndentStyle, dest: IndentStyle): string {
|
|
114
|
-
if (leading.length === 0) return leading;
|
|
115
|
-
// Compute total visual columns of the leading run, treating tabs in the
|
|
116
|
-
// source as `source.width` columns (or 1 column for tab-indented files,
|
|
117
|
-
// where the destination decides the visible width).
|
|
118
|
-
let columns = 0;
|
|
119
|
-
for (const ch of leading) {
|
|
120
|
-
if (ch === "\t") {
|
|
121
|
-
// Treat a source tab as one indent unit. Width = source.width or
|
|
122
|
-
// fallback 4 when source is tab style.
|
|
123
|
-
columns += source.kind === "tab" ? 1 : Math.max(1, Math.floor(source.width));
|
|
124
|
-
} else {
|
|
125
|
-
columns += 1;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (dest.kind === "tab") {
|
|
129
|
-
// Convert visual columns to whole tabs. When source was space-indented,
|
|
130
|
-
// columns is in spaces; divide by source.width to get logical levels.
|
|
131
|
-
let levels: number;
|
|
132
|
-
if (source.kind === "tab") {
|
|
133
|
-
levels = columns;
|
|
134
|
-
} else {
|
|
135
|
-
const w = Math.max(1, source.width);
|
|
136
|
-
levels = Math.round(columns / w);
|
|
137
|
-
}
|
|
138
|
-
return "\t".repeat(Math.max(0, levels));
|
|
139
|
-
}
|
|
140
|
-
// Destination is spaces. Convert to dest.width columns per level.
|
|
141
|
-
let levels: number;
|
|
142
|
-
if (source.kind === "tab") {
|
|
143
|
-
levels = columns; // tabs were 1 column each here
|
|
144
|
-
} else {
|
|
145
|
-
const w = Math.max(1, source.width);
|
|
146
|
-
levels = Math.round(columns / w);
|
|
147
|
-
}
|
|
148
|
-
const out = " ".repeat(Math.max(0, levels) * Math.max(1, dest.width));
|
|
149
|
-
return out;
|
|
150
|
-
}
|