@oh-my-pi/hashline 15.11.4 → 15.11.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/package.json +1 -1
- package/src/apply.ts +20 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/hashline",
|
|
4
|
-
"version": "15.11.
|
|
4
|
+
"version": "15.11.6",
|
|
5
5
|
"description": "Hashline: a compact, line-anchored patch language and applier. Pluggable FS/IO so it works over disk, in-memory, or any custom backend.",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
package/src/apply.ts
CHANGED
|
@@ -35,26 +35,31 @@ function getEditAnchors(edit: AppliedEdit): Anchor[] {
|
|
|
35
35
|
return getCursorAnchors(edit.cursor);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function trailingPhantomLine(fileLines: readonly string[]): number {
|
|
39
|
+
// `split("\n")` on a newline-terminated file yields a trailing "" sentinel.
|
|
40
|
+
// It is addressable for inserts (append-past-end), but it is not real
|
|
41
|
+
// content. Deleting it only strips the file's final newline, so ignore delete
|
|
42
|
+
// edits that land there; inclusive ranges ending at EOF then do the intended
|
|
43
|
+
// thing and delete through the last concrete line.
|
|
44
|
+
return fileLines.length > 1 && fileLines[fileLines.length - 1] === "" ? fileLines.length : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function dropTrailingPhantomDeletes(edits: AppliedEdit[], fileLines: readonly string[]): AppliedEdit[] {
|
|
48
|
+
const phantomLine = trailingPhantomLine(fileLines);
|
|
49
|
+
if (phantomLine === 0) return edits;
|
|
50
|
+
return edits.filter(edit => edit.kind !== "delete" || edit.anchor.line !== phantomLine);
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
/**
|
|
39
54
|
* Verify every anchored edit points at an existing line. File-version binding is
|
|
40
55
|
* checked once per section via the header hash before this function runs.
|
|
41
56
|
*/
|
|
42
|
-
function validateLineBounds(edits: AppliedEdit[], fileLines: string[]): void {
|
|
43
|
-
// `split("\n")` on a newline-terminated file yields a trailing "" sentinel.
|
|
44
|
-
// It is addressable for inserts (append-past-end), but deleting it would
|
|
45
|
-
// silently strip the file's final newline — an off-by-one that must error.
|
|
46
|
-
const phantomLine = fileLines.length > 1 && fileLines[fileLines.length - 1] === "" ? fileLines.length : 0;
|
|
57
|
+
function validateLineBounds(edits: readonly AppliedEdit[], fileLines: readonly string[]): void {
|
|
47
58
|
for (const edit of edits) {
|
|
48
59
|
for (const anchor of getEditAnchors(edit)) {
|
|
49
60
|
if (anchor.line < 1 || anchor.line > fileLines.length) {
|
|
50
61
|
throw new Error(`Line ${anchor.line} does not exist (file has ${fileLines.length} lines)`);
|
|
51
62
|
}
|
|
52
|
-
if (edit.kind === "delete" && anchor.line === phantomLine) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Line ${anchor.line} is the trailing blank sentinel of a newline-terminated file and has no content to delete. ` +
|
|
55
|
-
`End the range at line ${anchor.line - 1}, or use \`insert tail:\` to append.`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
@@ -742,7 +747,10 @@ export function applyEdits(text: string, edits: readonly Edit[]): ApplyResult {
|
|
|
742
747
|
if (firstChangedLine === undefined || line < firstChangedLine) firstChangedLine = line;
|
|
743
748
|
};
|
|
744
749
|
|
|
745
|
-
const targetEdits =
|
|
750
|
+
const targetEdits = dropTrailingPhantomDeletes(
|
|
751
|
+
appliedEdits.map((edit, index) => cloneAppliedEdit(edit, index)),
|
|
752
|
+
fileLines,
|
|
753
|
+
);
|
|
746
754
|
validateLineBounds(targetEdits, fileLines);
|
|
747
755
|
const { edits: repaired, warnings: boundaryWarnings } = repairReplacementBoundaries(targetEdits, fileLines);
|
|
748
756
|
const { edits: landed, warnings: landingWarnings } = repairAfterInsertLandings(repaired, fileLines);
|