Skip to content

Commit 68244a9

Browse files
Fix bug in range updater when using CRLF line endings (#3263)
Fixes #3260
1 parent 3807d8a commit 68244a9

6 files changed

Lines changed: 116 additions & 11 deletions

File tree

packages/lib-engine/src/actions/Actions.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,10 @@ import GetText from "./GetText";
2222
import Highlight from "./Highlight";
2323
import { Decrement, Increment } from "./incrementDecrement";
2424
import { IndentLine, OutdentLine } from "./IndentLine";
25+
import { InsertCopyAfter, InsertCopyBefore } from "./InsertCopy";
2526
import {
26-
CopyContentAfter as InsertCopyAfter,
27-
CopyContentBefore as InsertCopyBefore,
28-
} from "./InsertCopy";
29-
import {
30-
InsertEmptyLineBelow as InsertEmptyLineAfter,
31-
InsertEmptyLineAbove as InsertEmptyLineBefore,
27+
InsertEmptyLineAfter,
28+
InsertEmptyLineBefore,
3229
InsertEmptyLinesAround,
3330
} from "./InsertEmptyLines";
3431
import InsertSnippet from "./InsertSnippet";

packages/lib-engine/src/actions/InsertCopy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class InsertCopy implements SimpleAction {
9999
}
100100
}
101101

102-
export class CopyContentBefore extends InsertCopy {
102+
export class InsertCopyBefore extends InsertCopy {
103103
constructor(
104104
ide: IDE,
105105
rangeUpdater: RangeUpdater,
@@ -109,7 +109,7 @@ export class CopyContentBefore extends InsertCopy {
109109
}
110110
}
111111

112-
export class CopyContentAfter extends InsertCopy {
112+
export class InsertCopyAfter extends InsertCopy {
113113
constructor(
114114
ide: IDE,
115115
rangeUpdater: RangeUpdater,

packages/lib-engine/src/actions/InsertEmptyLines.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,13 @@ export class InsertEmptyLinesAround extends InsertEmptyLines {
103103
}
104104
}
105105

106-
export class InsertEmptyLineAbove extends InsertEmptyLines {
106+
export class InsertEmptyLineBefore extends InsertEmptyLines {
107107
protected getEdits(targets: Target[]) {
108108
return targets.map((target) => constructChangeEdit(target, "before"));
109109
}
110110
}
111111

112-
export class InsertEmptyLineBelow extends InsertEmptyLines {
112+
export class InsertEmptyLineAfter extends InsertEmptyLines {
113113
protected getEdits(targets: Target[]) {
114114
return targets.map((target) => constructChangeEdit(target, "after"));
115115
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as assert from "node:assert/strict";
2+
import type {
3+
Disposable,
4+
TextDocumentChangeEvent,
5+
} from "@cursorless/lib-common";
6+
import {
7+
FakeIDE,
8+
InMemoryTextEditor,
9+
Range,
10+
Selection,
11+
} from "@cursorless/lib-common";
12+
import { RangeUpdater } from "./RangeUpdater";
13+
import { performEditsAndUpdateSelections } from "./updateSelections";
14+
15+
suite("performEditsAndUpdateSelections", () => {
16+
test("treats CRLF-normalized replace inserts as replace edits", async () => {
17+
const ide = new TestIDE();
18+
const editor = new InMemoryTextEditor({
19+
ide,
20+
content: "abc\r\ndef",
21+
});
22+
const rangeUpdater = new RangeUpdater(ide);
23+
const selection = new Selection(0, 3, 0, 3);
24+
25+
const { updated } = await performEditsAndUpdateSelections({
26+
rangeUpdater,
27+
editor,
28+
edits: [
29+
{
30+
range: new Range(0, 3, 0, 3),
31+
text: "\n",
32+
isReplace: true,
33+
},
34+
],
35+
preserveCursorSelections: true,
36+
selections: { updated: [selection] },
37+
});
38+
39+
assert.equal(editor.document.getText(), "abc\r\n\r\ndef");
40+
assert.equal(updated.length, 1);
41+
assert.equal(updated[0].anchor.line, 0);
42+
assert.equal(updated[0].anchor.character, 3);
43+
assert.equal(updated[0].active.line, 0);
44+
assert.equal(updated[0].active.character, 3);
45+
});
46+
});
47+
48+
class TestIDE extends FakeIDE {
49+
private textDocumentChangeListeners: ((
50+
event: TextDocumentChangeEvent,
51+
) => void)[] = [];
52+
53+
override onDidChangeTextDocument = (
54+
listener: (event: TextDocumentChangeEvent) => void,
55+
): Disposable => {
56+
this.textDocumentChangeListeners.push(listener);
57+
58+
return {
59+
dispose: () => {
60+
this.textDocumentChangeListeners =
61+
this.textDocumentChangeListeners.filter(
62+
(candidate) => candidate !== listener,
63+
);
64+
},
65+
};
66+
};
67+
68+
override emitDidChangeTextDocument = (event: TextDocumentChangeEvent) => {
69+
this.textDocumentChangeListeners.forEach((listener) => listener(event));
70+
};
71+
}

packages/lib-engine/src/util/performDocumentEdits.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ export async function performDocumentEdits(
66
editor: EditableTextEditor,
77
edits: Edit[],
88
) {
9+
const eol = editor.document.eol === "LF" ? "\n" : "\r\n";
10+
911
const deregister = rangeUpdater.registerReplaceEditList(
1012
editor.document,
11-
edits.filter((edit) => edit.isReplace),
13+
edits
14+
.filter((edit) => edit.isReplace)
15+
.map((edit) => ({
16+
...edit,
17+
text: edit.text.replaceAll(/\r?\n/g, eol),
18+
})),
1219
);
1320

1421
try {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
languageId: plaintext
2+
command:
3+
version: 7
4+
spokenForm: float line
5+
action:
6+
name: insertEmptyLineAfter
7+
target:
8+
type: primitive
9+
modifiers:
10+
- type: containingScope
11+
scopeType: {type: line}
12+
usePrePhraseSnapshot: false
13+
initialState:
14+
documentContents: "abc\r\ndef"
15+
selections:
16+
- anchor: {line: 0, character: 3}
17+
active: {line: 0, character: 3}
18+
marks: {}
19+
finalState:
20+
documentContents: "abc\r\n\r\ndef"
21+
selections:
22+
- anchor: {line: 0, character: 3}
23+
active: {line: 0, character: 3}
24+
thatMark:
25+
- type: UntypedTarget
26+
contentRange:
27+
start: {line: 0, character: 0}
28+
end: {line: 0, character: 3}
29+
isReversed: false
30+
hasExplicitRange: true

0 commit comments

Comments
 (0)