@khanhicetea/pi-better-tool 0.2.1 → 0.2.3
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 +116 -69
- package/package.json +34 -5
- package/src/apply.ts +22 -12
- package/src/diagnostics.ts +159 -26
- package/src/index.ts +15 -5
- package/src/paths.ts +19 -0
- package/src/read-evidence.ts +74 -33
- package/src/read-symbol.ts +214 -0
- package/src/similarity.ts +140 -63
- package/src/source-file.ts +60 -0
- package/src/symbols.ts +228 -0
- package/src/text.ts +36 -7
- package/src/tool.ts +178 -106
package/README.md
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
# pi-better-tool
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Context-aware tools for the [Pi coding agent](https://github.com/earendil-works/pi-mono):
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
Could not find edits[1] in /code/app.go. The oldText must match exactly including all whitespace and newlines.
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
That failure wastes a whole loop: the model has to `read` the file again, guess a larger context, and retry — sometimes failing again. `pi-better-tool` resolves only ambiguity supported by verified evidence and otherwise returns **recovery context** so the next call succeeds without re-reading:
|
|
14
|
-
|
|
15
|
-
- **Ambiguous literal match after a bounded read** — when exactly one occurrence was fully visible in the latest successful, byte-verified `read` of the same file, edit selects that occurrence. Its success message reports the selected/read ranges and up to four remaining occurrences with effective prefix/suffix context and retryable snippets.
|
|
16
|
-
- **Other ambiguous matches (2+ occurrences)** — bounded occurrence line numbers plus the **minimum prefix/suffix context** that makes the first few occurrences unique, rendered as ready-to-use `oldText` snippets when they fit safely.
|
|
17
|
-
- **Text not found** — the closest matching region (fuzzy line similarity), a line-by-line comparison against your `oldText`, the exact file bytes to retry with, and likely causes (tabs vs spaces, indentation, case).
|
|
5
|
+
- **`edit`** replaces the built-in edit tool. Failures explain what failed, what was not written, and what to do next.
|
|
6
|
+
- **`read_symbol`** is a new tool. Read a whole function, method, class, or type by containing line or exact name. With only a path, get a symbol outline.
|
|
7
|
+
- **`read` stays unchanged** for ordinary text, images, and explicit line ranges.
|
|
18
8
|
|
|
19
9
|
## Install
|
|
20
10
|
|
|
@@ -22,100 +12,157 @@ That failure wastes a whole loop: the model has to `read` the file again, guess
|
|
|
22
12
|
pi install npm:@khanhicetea/pi-better-tool
|
|
23
13
|
```
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
From this monorepo:
|
|
26
16
|
|
|
27
17
|
```bash
|
|
28
18
|
pi install /absolute/path/to/pi-kit/packages/pi-better-tool
|
|
29
19
|
```
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
The root `package.json` also registers the extension. After local changes, use `/reload` in Pi to load the new tool definition. After upgrading to a release that adds grammar packages, restart Pi once: ast-grep’s native language registry cannot be extended by `/reload` in an already-running process.
|
|
32
22
|
|
|
33
|
-
##
|
|
23
|
+
## Read the symbol, not guessed line windows
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
Found 2 occurrences of the text in dup.go. The text must be unique. Please provide more context to make it unique.
|
|
25
|
+
After grep identifies a location:
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
```json
|
|
28
|
+
{"path":"src/server.ts","line":142}
|
|
29
|
+
```
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
Call `read_symbol` with an exact name instead:
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
```json
|
|
34
|
+
{"path":"src/server.ts","symbol":"Server.handleRequest"}
|
|
45
35
|
```
|
|
46
|
-
log()
|
|
47
|
-
}
|
|
48
36
|
|
|
49
|
-
|
|
50
|
-
```
|
|
37
|
+
Get the enclosing class or function:
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
log()
|
|
55
|
-
}
|
|
39
|
+
```json
|
|
40
|
+
{"path":"src/server.ts","line":142,"parent":1}
|
|
56
41
|
```
|
|
57
42
|
|
|
58
|
-
|
|
43
|
+
Discover names and ranges without reading every body:
|
|
59
44
|
|
|
60
|
-
|
|
45
|
+
```json
|
|
46
|
+
{"path":"src/server.ts"}
|
|
61
47
|
```
|
|
62
48
|
|
|
63
|
-
|
|
49
|
+
### Arguments
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
| Argument | Meaning |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `path` | Local relative/absolute path; supports `@path`, `~/path`, and file URLs. |
|
|
54
|
+
| `line` | 1-based file line. Select the innermost declaration containing it. |
|
|
55
|
+
| `column` | Optional 1-based UTF-16 column with `line`, to distinguish same-line symbols. |
|
|
56
|
+
| `symbol` | Exact, case-sensitive name or qualified name such as `Server.run`. Combine with `line` for duplicate names. |
|
|
57
|
+
| `parent` | Move outward through enclosing declarations; default 0, maximum 20. |
|
|
58
|
+
| `context` | Extra whole lines before/after the declaration; default 0, maximum 20. |
|
|
59
|
+
| `offset` | 1-based position **within the selection**, not a file line. For an outline, the entry position. |
|
|
60
|
+
| `limit` | Maximum source lines (default 1000) or outline entries (default 50); maximum 1800. |
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
Differences vs your oldText (2 of 3 compared lines match):
|
|
70
|
-
file line 4 differs from your oldText line 2:
|
|
71
|
-
file: →tab→fmt.Println("hi")
|
|
72
|
-
oldText: fmt.Println("hi")
|
|
62
|
+
Omit both `line` and `symbol` for an outline. Named selection never silently chooses the first duplicate. Line selection never silently chooses between same-line siblings. Candidate lists include concrete calls with names and positions.
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
### Languages and boundaries
|
|
65
|
+
|
|
66
|
+
Syntax parsing uses Tree-sitter through `@ast-grep/napi`, not indentation or brace-counting guesses:
|
|
67
|
+
|
|
68
|
+
- JavaScript, JSX, TypeScript, TSX, and their module extensions
|
|
69
|
+
- Bash-compatible shell scripts (`.sh`, `.bash`, `.zsh`, and related extensions)
|
|
70
|
+
- C and C++ (including common header and CUDA/Arduino extensions)
|
|
71
|
+
- C# and Java
|
|
72
|
+
- Kotlin
|
|
73
|
+
- PHP
|
|
74
|
+
- Python and `.pyi`
|
|
75
|
+
- Ruby (including `Gemfile` and `Rakefile`)
|
|
76
|
+
- Rust
|
|
77
|
+
- Swift
|
|
78
|
+
|
|
79
|
+
The reader handles named declarations, nested functions, JS/TS arrow functions and methods, Python decorators, Rust attributes, and containing classes/types. It includes export/declaration wrappers when applicable. It returns **whole source lines**, so a line can also contain adjacent code. It does not resolve imports, references, overload implementations, macros, or runtime bindings. Leading standalone comments are not automatically attached to a declaration.
|
|
80
|
+
|
|
81
|
+
Incomplete syntax, unsupported languages, unavailable parsers, and locations without declarations do not produce guessed symbol boundaries. Failures give a bounded source preview or candidate list and concrete next-call arguments. Missing paths include a bounded list of nearby files when the parent directory is accessible.
|
|
82
|
+
|
|
83
|
+
### Output and pagination
|
|
84
|
+
|
|
85
|
+
Results identify the selected symbol, enclosing names, full declaration range, displayed file range, and a SHA-256 source snapshot. Source appears in an unnumbered fenced block, so line-number prefixes cannot accidentally enter `oldText`.
|
|
86
|
+
|
|
87
|
+
Large selections return a whole-line page, explicitly marked **partial**, with the complete next `read_symbol` arguments. Follow that continuation instead of calculating file offsets. Each call reads a fresh snapshot; do not combine pages whose snapshot hashes differ.
|
|
88
|
+
|
|
89
|
+
Limits:
|
|
90
|
+
|
|
91
|
+
- Source analysis: 2 MiB UTF-8, 100,000 syntax nodes, 10,000 declarations, 1,024 characters per qualified symbol name.
|
|
92
|
+
- Complete read output: 48 KiB / 1,950 lines, including metadata and fences.
|
|
93
|
+
- No clipped copyable source lines or broken fences. A line too large to display gets preview/read guidance instead.
|
|
94
|
+
- Invalid UTF-8, NUL-containing input, and non-regular files are rejected.
|
|
95
|
+
|
|
96
|
+
Parser packages are runtime dependencies. Common platforms use prebuilt native binaries. If a grammar is unavailable on a platform, the tool gives read guidance; it never runs repository code, installs a compiler, or builds a grammar during a tool call. The edit tool can still work without loading symbol parsers.
|
|
97
|
+
|
|
98
|
+
## Recover from an edit failure in the next call
|
|
99
|
+
|
|
100
|
+
The `edit` input remains:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"path": "src/server.ts",
|
|
105
|
+
"edits": [
|
|
106
|
+
{ "oldText": "exact current text", "newText": "replacement text" }
|
|
107
|
+
]
|
|
78
108
|
}
|
|
79
109
|
```
|
|
80
110
|
|
|
81
|
-
|
|
82
|
-
|
|
111
|
+
All entries match the **original file**, not the output of earlier entries. Matching/overlap failures apply none of the batch. Fix the reported entries and resubmit the **complete batch**.
|
|
112
|
+
|
|
113
|
+
| Failure | Returned context |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| Repeated `oldText` | Occurrence ranges, bounded unique anchor expansions, and concrete context-read calls. |
|
|
116
|
+
| Text not found | Closest-region comparison with original whitespace, likely causes, and exact retry text only when unique and clearly better than competing candidates. |
|
|
117
|
+
| Overlapping entries | Both ranges plus a unique merged source anchor when it fits. Apply both intended changes to one `newText`. |
|
|
118
|
+
| Empty anchor | Explain insertion anchoring and give a context-read call. |
|
|
119
|
+
| No change | Explain that the output is identical; do not repeat the same call. |
|
|
120
|
+
| Replacement already appears | Report its locations as a clue, not proof that the intended change is complete. |
|
|
83
121
|
|
|
84
|
-
|
|
122
|
+
A failed batch starts with, for example:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
[edit failure: not-found]
|
|
126
|
+
Batch status: 0/2 replacements written. Fix edits[1] and resubmit the complete batch against the original file; no earlier replacement was applied.
|
|
85
127
|
```
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
Only snippets explicitly presented as retryable may be copied directly into `oldText`. Low-confidence, competing, stale, oversized, or omitted candidates require verification. Suggested `read_symbol` calls get the enclosing source declaration without another search for its boundary. Similarity scores are heuristics, not probabilities.
|
|
88
130
|
|
|
89
|
-
|
|
131
|
+
### Verified read evidence
|
|
90
132
|
|
|
91
|
-
|
|
92
|
-
- same matching engine ported from pi's `edit-diff.ts`: exact match first, fuzzy fallback (trailing whitespace, smart quotes, dashes, unicode spaces), uniqueness checked in fuzzy-normalized space, all edits matched against the original content, overlap/empty/no-change detection
|
|
93
|
-
- same BOM and CRLF handling
|
|
94
|
-
- same success result shape (`details.diff` / `details.patch` / `details.firstChangedLine`), and no custom renderers — the built-in diff renderer is inherited
|
|
95
|
-
- read-based selection fails closed unless the newest same-file read is still in active context, its stored output exactly matches the current bytes and built-in read formatting, and exactly one complete literal occurrence lies inside the visible range; fuzzy/Unicode-equivalent ambiguity, same-line ambiguity, stale reads, and broad reads containing multiple occurrences still refuse
|
|
133
|
+
For repeated **literal** text, edit can select an occurrence only when exactly one tracked occurrence is fully contained in the newest verified same-file read result in Pi's active, compaction-aware **stored session context**.
|
|
96
134
|
|
|
97
|
-
|
|
135
|
+
Both readers participate:
|
|
98
136
|
|
|
99
|
-
|
|
137
|
+
- Built-in `read`: reproduce its current LF-normalized output and truncation format.
|
|
138
|
+
- `read_symbol`: regenerate the result from the original arguments and current source. Snapshot, selection, envelope, and displayed source must all agree. Result `details` alone are never trusted.
|
|
100
139
|
|
|
101
|
-
|
|
140
|
+
Only displayed source is evidence. Outline entries, unseen parts of partial symbols, and omitted final newline separators are not. The newest same-file failed, missing, malformed, or stale result blocks fallback to an older read. Retained-tail compaction messages are supported when the host exposes them. Canonical paths support symlink aliases.
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
142
|
+
This verifies stored context, not the final provider payload: other extensions can remove messages or rewrite requests. Built-in BOM-bearing read output remains conservatively rejected; `read_symbol` deliberately strips a UTF-8 BOM and normalizes CRLF before parsing and snapshotting. Fuzzy-equivalent ambiguity and highly repetitive files fail closed.
|
|
143
|
+
|
|
144
|
+
## Edit safety and compatibility
|
|
145
|
+
|
|
146
|
+
The matching engine preserves exact-first/fuzzy-fallback behavior, fuzzy-space uniqueness, original-file batch matching, overlap/no-change checks, CRLF restoration, and UTF-8 BOM preservation. Success details keep Pi's `diff`, `patch`, and `firstChangedLine` shape; the built-in edit renderer is inherited.
|
|
107
147
|
|
|
108
|
-
|
|
148
|
+
Intentional safeguards include 1–100 replacements per call, rejection of empty/fuzzy-empty anchors, and rejection of invalid UTF-8 or NUL-containing files. Self-overlapping strings retain Pi's non-overlapping occurrence-counting policy. The pure compatibility shim accepts array, JSON-string, single-object, and legacy top-level edit arguments.
|
|
149
|
+
|
|
150
|
+
Both tools use the **local filesystem**. They do not inherit an SSH, container, or sandbox backend. File mutations use Pi's shared mutation queue. Immediately before writing, edit rechecks file identity and content to catch many external changes.
|
|
151
|
+
|
|
152
|
+
The final write remains an **in-place overwrite**, not a cross-process lock, race-free compare-and-swap, or crash-safe atomic transaction. Another process can change a file after the check. A rejected write may leave the file unchanged, partially written, or fully written: inspect it before retrying. A resolved write is the commit boundary; later cancellation cannot roll it back. In-place writes preserve existing symlink/hard-link semantics.
|
|
153
|
+
|
|
154
|
+
Edit diagnostics stay below Pi's 50 KiB / 2,000-line limits. Snippets are omitted atomically when needed, and similarity work is bounded. Renderer `details.diff` and `details.patch` remain complete rather than being blindly truncated.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
109
157
|
|
|
110
|
-
|
|
158
|
+
Tested against `@earendil-works/pi-coding-agent` 0.82.1. Pi/typebox peer dependencies remain wildcard ranges, as Pi's packaging guidance requires. Re-run the read-format, session-evidence, and renderer compatibility tests on host upgrades.
|
|
111
159
|
|
|
112
160
|
```bash
|
|
113
|
-
npm run check --workspace
|
|
114
|
-
npm pack --dry-run --workspace
|
|
115
|
-
npm publish --workspace=@khanhicetea/pi-better-tool
|
|
161
|
+
npm run check --workspace @khanhicetea/pi-better-tool
|
|
162
|
+
npm pack --dry-run --workspace @khanhicetea/pi-better-tool
|
|
116
163
|
```
|
|
117
164
|
|
|
118
|
-
|
|
165
|
+
Tests cover existing edit behavior, recovery round-trips, syntax boundaries across languages, nested/duplicate symbols, pagination, encoding/size failures, cancellation, and read-to-edit evidence.
|
|
119
166
|
|
|
120
167
|
## License
|
|
121
168
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanhicetea/pi-better-tool",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Context-aware tools for pi: safe edit recovery and syntax-aware read_symbol for whole functions, methods, and classes by line or name",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -13,8 +13,20 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/khanhicetea/pi-kit/issues"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"edit",
|
|
20
|
+
"read",
|
|
21
|
+
"symbols",
|
|
22
|
+
"tree-sitter",
|
|
23
|
+
"tools"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
18
30
|
"scripts": {
|
|
19
31
|
"test": "vitest run",
|
|
20
32
|
"typecheck": "tsc --noEmit",
|
|
@@ -33,12 +45,29 @@
|
|
|
33
45
|
"vitest": "^3.2.0"
|
|
34
46
|
},
|
|
35
47
|
"pi": {
|
|
36
|
-
"extensions": [
|
|
48
|
+
"extensions": [
|
|
49
|
+
"./src/index.ts"
|
|
50
|
+
]
|
|
37
51
|
},
|
|
38
52
|
"engines": {
|
|
39
53
|
"node": ">=20"
|
|
40
54
|
},
|
|
41
55
|
"publishConfig": {
|
|
42
56
|
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@ast-grep/lang-bash": "0.0.8",
|
|
60
|
+
"@ast-grep/lang-c": "0.0.6",
|
|
61
|
+
"@ast-grep/lang-cpp": "0.0.6",
|
|
62
|
+
"@ast-grep/lang-csharp": "0.0.6",
|
|
63
|
+
"@ast-grep/lang-go": "0.0.6",
|
|
64
|
+
"@ast-grep/lang-java": "0.0.7",
|
|
65
|
+
"@ast-grep/lang-kotlin": "0.0.7",
|
|
66
|
+
"@ast-grep/lang-php": "0.0.7",
|
|
67
|
+
"@ast-grep/lang-python": "0.0.6",
|
|
68
|
+
"@ast-grep/lang-ruby": "0.0.7",
|
|
69
|
+
"@ast-grep/lang-rust": "0.0.7",
|
|
70
|
+
"@ast-grep/lang-swift": "0.0.8",
|
|
71
|
+
"@ast-grep/napi": "0.45.3"
|
|
43
72
|
}
|
|
44
73
|
}
|
package/src/apply.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Edit matching and application engine.
|
|
3
3
|
*
|
|
4
|
-
* A port of pi's built-in `applyEditsToNormalizedContent
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A port of pi's built-in `applyEditsToNormalizedContent`. Instead of throwing
|
|
5
|
+
* opaque errors, `analyzeEdits` returns a structured failure describing *why*
|
|
6
|
+
* an edit failed. It also rejects fuzzy-normalized-empty needles and can accept
|
|
7
|
+
* a conservatively verified literal selection as intentional safety extensions.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Otherwise matching semantics follow the built-in tool:
|
|
10
10
|
* - exact match first, then fuzzy-normalized fallback (trailing whitespace,
|
|
11
11
|
* smart quotes, dashes, unicode spaces)
|
|
12
12
|
* - uniqueness is always checked in fully fuzzy-normalized space
|
|
@@ -18,10 +18,11 @@ import {
|
|
|
18
18
|
countFuzzyOccurrences,
|
|
19
19
|
findAllOccurrences,
|
|
20
20
|
getLineSpans,
|
|
21
|
+
getLogicalLineSpans,
|
|
21
22
|
lineAt,
|
|
22
23
|
normalizeForFuzzyMatch,
|
|
23
24
|
normalizeToLF,
|
|
24
|
-
|
|
25
|
+
splitLogicalLinesWithEndings,
|
|
25
26
|
type LineSpan,
|
|
26
27
|
} from "./text.ts";
|
|
27
28
|
|
|
@@ -42,7 +43,9 @@ export type EditFailure =
|
|
|
42
43
|
| {
|
|
43
44
|
kind: "ambiguous";
|
|
44
45
|
editIndex: number;
|
|
45
|
-
/**
|
|
46
|
+
/** Total non-overlapping occurrences in fully fuzzy-normalized space. */
|
|
47
|
+
occurrenceCount: number;
|
|
48
|
+
/** A bounded prefix of occurrence offsets for diagnostics/selection. */
|
|
46
49
|
occurrenceOffsets: number[];
|
|
47
50
|
}
|
|
48
51
|
| {
|
|
@@ -85,6 +88,9 @@ export interface AnalyzeOptions {
|
|
|
85
88
|
ambiguousSelections?: ReadonlyMap<number, number>;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
/** Avoid materializing unbounded offset arrays for highly repetitive files. */
|
|
92
|
+
const MAX_TRACKED_OCCURRENCE_OFFSETS = 256;
|
|
93
|
+
|
|
88
94
|
export function normalizeEdits(edits: EditOp[]): EditOp[] {
|
|
89
95
|
return edits.map((edit) => ({
|
|
90
96
|
oldText: normalizeToLF(edit.oldText),
|
|
@@ -159,7 +165,11 @@ export function analyzeEdits(normalizedContent: string, rawEdits: EditOp[], opti
|
|
|
159
165
|
const occurrences = countFuzzyOccurrences(fuzzyBase, edit.oldText);
|
|
160
166
|
let selectedMatch = matchResult;
|
|
161
167
|
if (occurrences > 1) {
|
|
162
|
-
const occurrenceOffsets = findAllOccurrences(
|
|
168
|
+
const occurrenceOffsets = findAllOccurrences(
|
|
169
|
+
fuzzyBase,
|
|
170
|
+
normalizeForFuzzyMatch(edit.oldText),
|
|
171
|
+
MAX_TRACKED_OCCURRENCE_OFFSETS,
|
|
172
|
+
);
|
|
163
173
|
const selectedOffset = options.ambiguousSelections?.get(i);
|
|
164
174
|
if (
|
|
165
175
|
selectedOffset === undefined ||
|
|
@@ -167,7 +177,7 @@ export function analyzeEdits(normalizedContent: string, rawEdits: EditOp[], opti
|
|
|
167
177
|
) {
|
|
168
178
|
return {
|
|
169
179
|
ok: false,
|
|
170
|
-
failure: { kind: "ambiguous", editIndex: i, occurrenceOffsets },
|
|
180
|
+
failure: { kind: "ambiguous", editIndex: i, occurrenceCount: occurrences, occurrenceOffsets },
|
|
171
181
|
};
|
|
172
182
|
}
|
|
173
183
|
selectedMatch = {
|
|
@@ -257,10 +267,10 @@ function applyReplacementsPreservingUnchangedLines(
|
|
|
257
267
|
baseContent: string,
|
|
258
268
|
replacements: Replacement[],
|
|
259
269
|
): string {
|
|
260
|
-
const originalLines =
|
|
261
|
-
const baseLines =
|
|
270
|
+
const originalLines = splitLogicalLinesWithEndings(originalContent);
|
|
271
|
+
const baseLines = getLogicalLineSpans(baseContent);
|
|
262
272
|
if (originalLines.length !== baseLines.length) {
|
|
263
|
-
throw new Error("Cannot preserve unchanged lines because the base content has a different line count.");
|
|
273
|
+
throw new Error("Cannot preserve unchanged lines because the base content has a different logical line count.");
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
const groups: Array<InternalLineWindow & { replacements: Replacement[] }> = [];
|