@jerryan/pi-hashline-edit 0.7.1 → 0.7.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 +127 -114
- package/package.json +53 -53
- package/prompts/edit.md +4 -4
- package/prompts/read.md +1 -1
- package/src/edit-diff.ts +39 -77
- package/src/edit-response.ts +5 -67
- package/src/edit.ts +520 -642
- package/src/hashline.ts +1071 -1058
package/README.md
CHANGED
|
@@ -1,114 +1,127 @@
|
|
|
1
|
-

|
|
2
|
-
|
|
3
|
-
# pi-hashline-edit
|
|
4
|
-
|
|
5
|
-
A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-editing workflow.
|
|
6
|
-
|
|
7
|
-
Every line returned by `read` carries a short content hash. Edits reference these hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
|
|
8
|
-
|
|
9
|
-
Inspired by [oh-my-pi](https://github.com/can1357/oh-my-pi).
|
|
10
|
-
|
|
11
|
-
## Differences from upstream
|
|
12
|
-
|
|
13
|
-
This is a fork of the original [pi-hashline-edit](https://github.com/earendil-works/pi-hashline-edit). The core protocol (hash-anchored reads, stale-anchor rejection, atomic writes) is unchanged from upstream. Key differences:
|
|
14
|
-
|
|
15
|
-
- **Single edit shape.** One entry type: `{ range: [start, end], lines: [...] }`. No `op` field, no `append`/`prepend`/`replace_text` ops, no `after`/`before`. The tuple enforces explicit endpoint anchors, eliminating the common "forgot `end`" failure mode.
|
|
16
|
-
- **Standard hex hash alphabet.** `0-9 A-F` instead of `ZPMQVRWSNKTXJBYH`. Hex pairs are more likely to be single tokens.
|
|
17
|
-
- **Symmetric boundary-duplication detection.** Runtime warnings catch duplicated boundary lines on both sides of a replacement, not just trailing.
|
|
18
|
-
- **`read` raw mode.** `raw: true` returns plain text without `LINE#HASH:` anchors, for reads that don't plan to edit.
|
|
19
|
-
- **Inline FNV-1a hashing.** Replaces `xxhashjs` dependency. Always incorporates line index.
|
|
20
|
-
- **Minimal prompt surface.** Prompt text describes what the model needs to use the tool; return-format documentation and error catalogues are omitted.
|
|
21
|
-
- **No legacy compatibility.** The `{ oldText, newText }` substring-replace format is not accepted. The schema is hashline-only.
|
|
22
|
-
|
|
23
|
-
## Installation
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# From npm
|
|
27
|
-
pi install npm:@jerryan/pi-hashline-edit
|
|
28
|
-
|
|
29
|
-
# From a local checkout
|
|
30
|
-
pi install /path/to/pi-hashline-edit
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## How It Works
|
|
34
|
-
|
|
35
|
-
### `read` — tagged line output
|
|
36
|
-
|
|
37
|
-
Text files are returned with a `LINE#HASH
|
|
38
|
-
|
|
39
|
-
```text
|
|
40
|
-
8#A4
|
|
41
|
-
9#3F
|
|
42
|
-
10#B2
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
- `LINE` — 1-indexed line number.
|
|
46
|
-
- `HASH` — 2-character content hash (hex digits `0-9 A-F`).
|
|
47
|
-
|
|
48
|
-
Optional parameters:
|
|
49
|
-
- `offset` — start reading from this line number (1-indexed).
|
|
50
|
-
- `limit` — maximum number of lines to return.
|
|
51
|
-
- `raw` — when `true`, returns plain text without LINE#HASH anchors. Saves tokens when you don't plan to edit this file.
|
|
52
|
-
|
|
53
|
-
Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not participate in the hashline protocol. Binary and directory paths are rejected with a descriptive error.
|
|
54
|
-
|
|
55
|
-
### `edit` — hash-anchored modifications
|
|
56
|
-
|
|
57
|
-
Each edit entry replaces an inclusive anchor range:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"path": "src/main.ts",
|
|
62
|
-
"edits": [
|
|
63
|
-
{ "range": ["11#3F", "11#3F"], "lines": [" console.log('hashline');"] },
|
|
64
|
-
{ "range": ["42#B2", "45#C7"], "lines": ["function foo() {", " return 42;", "}"] }
|
|
65
|
-
]
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
- `range` — `[start, end]` pair of LINE#HASH anchors. Use the same anchor twice for single-line.
|
|
70
|
-
- `lines` — new content replacing the range (string array). Use `[]` to delete.
|
|
71
|
-
|
|
72
|
-
All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so line numbers stay consistent across operations.
|
|
73
|
-
|
|
74
|
-
### Chained edits
|
|
75
|
-
|
|
76
|
-
After a successful edit, the
|
|
77
|
-
|
|
78
|
-
### Diff
|
|
79
|
-
|
|
80
|
-
Each edit result
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# pi-hashline-edit
|
|
4
|
+
|
|
5
|
+
A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-editing workflow.
|
|
6
|
+
|
|
7
|
+
Every line returned by `read` carries a short content hash. Edits reference these hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
|
|
8
|
+
|
|
9
|
+
Inspired by [oh-my-pi](https://github.com/can1357/oh-my-pi).
|
|
10
|
+
|
|
11
|
+
## Differences from upstream
|
|
12
|
+
|
|
13
|
+
This is a fork of the original [pi-hashline-edit](https://github.com/earendil-works/pi-hashline-edit). The core protocol (hash-anchored reads, stale-anchor rejection, atomic writes) is unchanged from upstream. Key differences:
|
|
14
|
+
|
|
15
|
+
- **Single edit shape.** One entry type: `{ range: [start, end], lines: [...] }`. No `op` field, no `append`/`prepend`/`replace_text` ops, no `after`/`before`. The tuple enforces explicit endpoint anchors, eliminating the common "forgot `end`" failure mode.
|
|
16
|
+
- **Standard hex hash alphabet.** `0-9 A-F` instead of `ZPMQVRWSNKTXJBYH`. Hex pairs are more likely to be single tokens.
|
|
17
|
+
- **Symmetric boundary-duplication detection.** Runtime warnings catch duplicated boundary lines on both sides of a replacement, not just trailing.
|
|
18
|
+
- **`read` raw mode.** `raw: true` returns plain text without `LINE#HASH:` anchors, for reads that don't plan to edit.
|
|
19
|
+
- **Inline FNV-1a hashing.** Replaces `xxhashjs` dependency. Always incorporates line index.
|
|
20
|
+
- **Minimal prompt surface.** Prompt text describes what the model needs to use the tool; return-format documentation and error catalogues are omitted.
|
|
21
|
+
- **No legacy compatibility.** The `{ oldText, newText }` substring-replace format is not accepted. The schema is hashline-only.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# From npm
|
|
27
|
+
pi install npm:@jerryan/pi-hashline-edit
|
|
28
|
+
|
|
29
|
+
# From a local checkout
|
|
30
|
+
pi install /path/to/pi-hashline-edit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
### `read` — tagged line output
|
|
36
|
+
|
|
37
|
+
Text files are returned with a `LINE#HASH│` prefix on every line. Line numbers may be left-padded within each returned block so the `#HASH│` columns align:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
8#A4│function hello() {
|
|
41
|
+
9#3F│ console.log("world");
|
|
42
|
+
10#B2│}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `LINE` — 1-indexed line number.
|
|
46
|
+
- `HASH` — 2-character content hash (hex digits `0-9 A-F`).
|
|
47
|
+
|
|
48
|
+
Optional parameters:
|
|
49
|
+
- `offset` — start reading from this line number (1-indexed).
|
|
50
|
+
- `limit` — maximum number of lines to return.
|
|
51
|
+
- `raw` — when `true`, returns plain text without LINE#HASH anchors. Saves tokens when you don't plan to edit this file.
|
|
52
|
+
|
|
53
|
+
Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not participate in the hashline protocol. Binary and directory paths are rejected with a descriptive error.
|
|
54
|
+
|
|
55
|
+
### `edit` — hash-anchored modifications
|
|
56
|
+
|
|
57
|
+
Each edit entry replaces an inclusive anchor range:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"path": "src/main.ts",
|
|
62
|
+
"edits": [
|
|
63
|
+
{ "range": ["11#3F", "11#3F"], "lines": [" console.log('hashline');"] },
|
|
64
|
+
{ "range": ["42#B2", "45#C7"], "lines": ["function foo() {", " return 42;", "}"] }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `range` — `[start, end]` pair of LINE#HASH anchors. Use the same anchor twice for single-line.
|
|
70
|
+
- `lines` — new content replacing the range (string array). Use `[]` to delete.
|
|
71
|
+
|
|
72
|
+
All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so line numbers stay consistent across operations.
|
|
73
|
+
|
|
74
|
+
### Chained edits
|
|
75
|
+
|
|
76
|
+
After a successful edit, the response contains a unified diff where context and added lines carry fresh `LINE#HASH` anchors. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
|
|
77
|
+
|
|
78
|
+
### Diff output
|
|
79
|
+
|
|
80
|
+
Each edit result shows a unified diff with hashline-formatted lines:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
8#A4│function hello() {
|
|
84
|
+
-9 │ console.log("world");
|
|
85
|
+
+9#B1│ console.log("hashline");
|
|
86
|
+
10#B2│}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- Context lines: ` NN#HH│content` (space prefix)
|
|
90
|
+
- Removed lines: `-NN │content` (no hash, aligned separator)
|
|
91
|
+
- Added lines: `+NN#HH│content` (hash for new anchors)
|
|
92
|
+
- Multiple hunks are shown when edits are far apart.
|
|
93
|
+
|
|
94
|
+
## Design Decisions
|
|
95
|
+
|
|
96
|
+
- **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes a snippet with fresh `LINE#HASH` references for the affected lines for immediate retry.
|
|
97
|
+
- **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
|
|
98
|
+
- **Strict patch content.** If `lines` contains `LINE#HASH│` display prefixes or diff `+`/`-` markers, the edit is rejected with `[E_INVALID_PATCH]`. The model must send literal file content; the runtime does not silently strip accidental prefixes.
|
|
99
|
+
- **Full-file deletion guardrail.** Edits that would empty a file with more than 50 lines are rejected with `[E_WOULD_EMPTY]`. Small files show the full diff normally; large deletions are almost always mistakes.
|
|
100
|
+
- **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
|
|
101
|
+
- **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
|
|
102
|
+
- **Schema-delegated validation.** Field-type and schema validation are the responsibility of pi's AJV layer. The extension's runtime guard only prevents crashes from missing required top-level fields.
|
|
103
|
+
|
|
104
|
+
## Hashing
|
|
105
|
+
|
|
106
|
+
Hashes are computed with inline FNV-1a (32-bit, mask-reduced to 8 bits), then mapped to a 2-character hex string from `0-9 A-F`.
|
|
107
|
+
|
|
108
|
+
The line index is always incorporated into the hash, so identical content on different lines produces different hashes.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
Requires [Node.js](https://nodejs.org) and npm.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm install
|
|
116
|
+
npm test
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
|
|
120
|
+
|
|
121
|
+
## Credits
|
|
122
|
+
|
|
123
|
+
Thanks to [can1357](https://github.com/can1357) for the original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@jerryan/pi-hashline-edit",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Hashline read/edit tool override for pi-coding-agent",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/JerryAZR/pi-hashline-edit.git"
|
|
8
|
-
},
|
|
9
|
-
"author": "JerryAZR",
|
|
10
|
-
"publishConfig": {
|
|
11
|
-
"registry": "https://registry.npmjs.org/"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"pi-package",
|
|
15
|
-
"pi",
|
|
16
|
-
"coding-agent",
|
|
17
|
-
"extension",
|
|
18
|
-
"hashline"
|
|
19
|
-
],
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"files": [
|
|
22
|
-
"index.ts",
|
|
23
|
-
"src",
|
|
24
|
-
"prompts",
|
|
25
|
-
"README.md",
|
|
26
|
-
"LICENSE"
|
|
27
|
-
],
|
|
28
|
-
"pi": {
|
|
29
|
-
"extensions": [
|
|
30
|
-
"./index.ts"
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"diff": "^8.0.2",
|
|
35
|
-
"file-type": "^21.3.0"
|
|
36
|
-
},
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"@earendil-works/pi-ai": ">=0.74.0",
|
|
39
|
-
"@earendil-works/pi-coding-agent": ">=0.74.0",
|
|
40
|
-
"@earendil-works/pi-tui": "*",
|
|
41
|
-
"@sinclair/typebox": "*"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"test": "vitest run",
|
|
45
|
-
"test:watch": "vitest"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
49
|
-
"@types/node": "^22.0.0",
|
|
50
|
-
"ajv": "^8.20.0",
|
|
51
|
-
"vitest": "^3.0.0"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@jerryan/pi-hashline-edit",
|
|
3
|
+
"version": "0.7.3",
|
|
4
|
+
"description": "Hashline read/edit tool override for pi-coding-agent",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/JerryAZR/pi-hashline-edit.git"
|
|
8
|
+
},
|
|
9
|
+
"author": "JerryAZR",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org/"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi",
|
|
16
|
+
"coding-agent",
|
|
17
|
+
"extension",
|
|
18
|
+
"hashline"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"index.ts",
|
|
23
|
+
"src",
|
|
24
|
+
"prompts",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"diff": "^8.0.2",
|
|
35
|
+
"file-type": "^21.3.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@earendil-works/pi-ai": ">=0.74.0",
|
|
39
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0",
|
|
40
|
+
"@earendil-works/pi-tui": "*",
|
|
41
|
+
"@sinclair/typebox": "*"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"ajv": "^8.20.0",
|
|
51
|
+
"vitest": "^3.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/prompts/edit.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Patch a UTF-8 text file using `LINE#HASH` anchors copied verbatim from `read`.
|
|
2
2
|
|
|
3
|
-
Submit one `edit` call per file. All operations go in a single `edits` array; anchors must come from the same fresh source — the most recent `read` or
|
|
3
|
+
Submit one `edit` call per file. All operations go in a single `edits` array; anchors must come from the same fresh source — the most recent `read` or diff output of a successful `edit` on this file.
|
|
4
4
|
|
|
5
5
|
Each edit entry replaces an inclusive anchor range:
|
|
6
6
|
```json
|
|
7
7
|
{ "range": [startAnchor, endAnchor], "lines": [...] }
|
|
8
8
|
```
|
|
9
|
-
- `range` — `[start, end]` pair of LINE#HASH anchors from the most recent `read
|
|
9
|
+
- `range` — `[start, end]` pair of LINE#HASH anchors from the most recent `read` or diff output.
|
|
10
10
|
Use the same anchor twice for single-line: `["42#A4", "42#A4"]`.
|
|
11
11
|
- `lines` — new content replacing the range (string array). Use `[]` to delete.
|
|
12
|
-
Must be literal file content, not LINE#HASH
|
|
12
|
+
Must be literal file content, not LINE#HASH│-prefixed output. Match indentation exactly.
|
|
13
13
|
|
|
14
14
|
Example:
|
|
15
15
|
```json
|
|
@@ -19,5 +19,5 @@ Example:
|
|
|
19
19
|
] }
|
|
20
20
|
|
|
21
21
|
Rules:
|
|
22
|
-
- Do not guess, shift, or construct anchors. Copy them from the most recent `read` of this file.
|
|
22
|
+
- Do not guess, shift, or construct anchors. Copy them from the most recent `read` or diff output of this file.
|
|
23
23
|
- Do not emit overlapping or adjacent edits — merge them into one.
|
package/prompts/read.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Read a UTF-8 text file or a supported image. Text lines are prefixed `LINE#HASH
|
|
1
|
+
Read a UTF-8 text file or a supported image. Text lines are prefixed `LINE#HASH│content` — copy those anchors verbatim into `edit`.
|
|
2
2
|
|
|
3
3
|
Use `offset` and `limit` to page through. Default cap: {{DEFAULT_MAX_LINES}} lines or {{DEFAULT_MAX_BYTES}}; when truncated, the tail of the output tells you the next `offset`.
|
|
4
4
|
|
package/src/edit-diff.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
FUZZY_DOUBLE_QUOTES_RE,
|
|
6
6
|
FUZZY_SINGLE_QUOTES_RE,
|
|
7
7
|
FUZZY_UNICODE_SPACES_RE,
|
|
8
|
+
ANCHOR_SEP,
|
|
9
|
+
CONTENT_SEP,
|
|
8
10
|
} from "./hashline";
|
|
9
11
|
|
|
10
12
|
// ─── Line ending normalization ──────────────────────────────────────────
|
|
@@ -223,102 +225,62 @@ export function replaceText(
|
|
|
223
225
|
|
|
224
226
|
// ─── Diff generation ────────────────────────────────────────────────────
|
|
225
227
|
|
|
226
|
-
function formatDiffPreviewLine(
|
|
227
|
-
prefix: " " | "+" | "-",
|
|
228
|
-
lineNum: number,
|
|
229
|
-
lineNumWidth: number,
|
|
230
|
-
line: string,
|
|
231
|
-
includeHash: boolean,
|
|
232
|
-
): string {
|
|
233
|
-
const paddedLineNum = String(lineNum).padStart(lineNumWidth, " ");
|
|
234
|
-
if (!includeHash) {
|
|
235
|
-
return `${prefix}${paddedLineNum} ${line}`;
|
|
236
|
-
}
|
|
237
|
-
return `${prefix}${paddedLineNum}#${computeLineHash(lineNum, line)}:${line}`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
228
|
export function generateDiffString(
|
|
241
229
|
oldContent: string,
|
|
242
230
|
newContent: string,
|
|
243
231
|
contextLines = 4,
|
|
244
|
-
): { diff: string
|
|
245
|
-
const
|
|
246
|
-
|
|
232
|
+
): { diff: string } {
|
|
233
|
+
const patch = Diff.structuredPatch("a", "b", oldContent, newContent, undefined, undefined, {
|
|
234
|
+
context: contextLines,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!patch.hunks.length) {
|
|
238
|
+
return { diff: "" };
|
|
239
|
+
}
|
|
240
|
+
|
|
247
241
|
const maxLineNum = Math.max(
|
|
248
242
|
oldContent.split("\n").length,
|
|
249
243
|
newContent.split("\n").length,
|
|
250
244
|
);
|
|
251
245
|
const lineNumWidth = String(maxLineNum).length;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const raw = part.value.split("\n");
|
|
260
|
-
if (raw[raw.length - 1] === "") raw.pop();
|
|
261
|
-
|
|
262
|
-
if (part.added || part.removed) {
|
|
263
|
-
if (firstChangedLine === undefined) firstChangedLine = newLineNum;
|
|
264
|
-
for (const line of raw) {
|
|
265
|
-
if (part.added) {
|
|
266
|
-
output.push(
|
|
267
|
-
formatDiffPreviewLine("+", newLineNum, lineNumWidth, line, true),
|
|
268
|
-
);
|
|
269
|
-
newLineNum++;
|
|
270
|
-
} else {
|
|
271
|
-
output.push(
|
|
272
|
-
formatDiffPreviewLine("-", oldLineNum, lineNumWidth, line, false),
|
|
273
|
-
);
|
|
274
|
-
oldLineNum++;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
lastWasChange = true;
|
|
278
|
-
continue;
|
|
246
|
+
const hashPad = " ".repeat(ANCHOR_SEP.length + 2); // align with `${ANCHOR_SEP}HH${CONTENT_SEP}`
|
|
247
|
+
const output: string[] = [];
|
|
248
|
+
|
|
249
|
+
for (let h = 0; h < patch.hunks.length; h++) {
|
|
250
|
+
const hunk = patch.hunks[h]!;
|
|
251
|
+
if (h > 0) {
|
|
252
|
+
output.push(" ...");
|
|
279
253
|
}
|
|
280
254
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (lastWasChange || nextPartIsChange) {
|
|
284
|
-
let linesToShow = raw;
|
|
285
|
-
let skipStart = 0;
|
|
286
|
-
let skipEnd = 0;
|
|
255
|
+
let oldLineNum = hunk.oldStart;
|
|
256
|
+
let newLineNum = hunk.newStart;
|
|
287
257
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
linesToShow = raw.slice(skipStart);
|
|
291
|
-
}
|
|
292
|
-
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
293
|
-
skipEnd = linesToShow.length - contextLines;
|
|
294
|
-
linesToShow = linesToShow.slice(0, contextLines);
|
|
295
|
-
}
|
|
258
|
+
for (const line of hunk.lines) {
|
|
259
|
+
if (line === "\") continue;
|
|
296
260
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
261
|
+
const prefix = line[0] as " " | "+" | "-";
|
|
262
|
+
const text = line.slice(1);
|
|
263
|
+
|
|
264
|
+
if (prefix === "-") {
|
|
265
|
+
const padded = String(oldLineNum).padStart(lineNumWidth, " ");
|
|
266
|
+
output.push(`-${padded}${hashPad}${CONTENT_SEP}${text}`);
|
|
267
|
+
oldLineNum++;
|
|
268
|
+
} else if (prefix === "+") {
|
|
269
|
+
const padded = String(newLineNum).padStart(lineNumWidth, " ");
|
|
270
|
+
const hash = computeLineHash(newLineNum, text);
|
|
271
|
+
output.push(`+${padded}${ANCHOR_SEP}${hash}${CONTENT_SEP}${text}`);
|
|
272
|
+
newLineNum++;
|
|
273
|
+
} else {
|
|
274
|
+
const padded = String(newLineNum).padStart(lineNumWidth, " ");
|
|
275
|
+
const hash = computeLineHash(newLineNum, text);
|
|
276
|
+
output.push(` ${padded}${ANCHOR_SEP}${hash}${CONTENT_SEP}${text}`);
|
|
306
277
|
oldLineNum++;
|
|
307
278
|
newLineNum++;
|
|
308
279
|
}
|
|
309
|
-
if (skipEnd > 0) {
|
|
310
|
-
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
311
|
-
oldLineNum += skipEnd;
|
|
312
|
-
newLineNum += skipEnd;
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
oldLineNum += raw.length;
|
|
316
|
-
newLineNum += raw.length;
|
|
317
280
|
}
|
|
318
|
-
lastWasChange = false;
|
|
319
281
|
}
|
|
320
282
|
|
|
321
|
-
return { diff: output.join("\n")
|
|
283
|
+
return { diff: output.join("\n") };
|
|
322
284
|
}
|
|
323
285
|
|
|
324
286
|
export interface CompactHashlineDiffPreview {
|