Skip to content

Commit 5adb718

Browse files
committed
fix(fmt): count lone carriage returns in LSP position mapping
1 parent 933c4bb commit 5adb718

2 files changed

Lines changed: 34 additions & 10 deletions

File tree

‎packages/rstack/src/fmt/lsp/minimalEdit.ts‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ interface MinimalTextEdit {
105105
/**
106106
* Reduces a reformat to the single LSP text edit that actually changed.
107107
*
108-
* The offset → position mapping only counts line feeds because LSP characters
109-
* and JS offsets share the UTF-16 unit, and because `computeMinimalEdit` never
110-
* places a boundary inside a surrogate pair or a `\r\n`.
108+
* Positions count UTF-16 code units, like JS offsets, and lines end at `\n`,
109+
* `\r\n`, or a lone `\r`, like the protocol's. `computeMinimalEdit` keeping
110+
* boundaries out of surrogate pairs and `\r\n` is what makes the mapping exact.
111111
*/
112112
const computeMinimalTextEdit = (source: string, formatted: string): MinimalTextEdit | undefined => {
113113
const edit = computeMinimalEdit(source, formatted);
@@ -117,13 +117,18 @@ const computeMinimalTextEdit = (source: string, formatted: string): MinimalTextE
117117

118118
let line = 0;
119119
let lineStart = 0;
120-
// Resumes from the previous call's line, so start and end share one scan.
120+
// Resumes from the previous call's line, so start and end share one scan. A
121+
// `\r` followed by `\n` defers to the `\n` so the pair counts once.
121122
const advanceTo = (offset: number): Position => {
122-
let lineFeed = source.indexOf('\n', lineStart);
123-
while (lineFeed !== -1 && lineFeed < offset) {
124-
line++;
125-
lineStart = lineFeed + 1;
126-
lineFeed = source.indexOf('\n', lineStart);
123+
for (let index = lineStart; index < offset; index++) {
124+
const code = source.charCodeAt(index);
125+
if (
126+
code === LINE_FEED ||
127+
(code === CARRIAGE_RETURN && source.charCodeAt(index + 1) !== LINE_FEED)
128+
) {
129+
line++;
130+
lineStart = index + 1;
131+
}
127132
}
128133

129134
return { line, character: offset - lineStart };

‎packages/rstack/tests/fmt/lsp/minimalEdit.test.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'rstack/test';
22
import { TextDocument } from 'vscode-languageserver-textdocument';
3-
import { computeMinimalEdit } from '../../../src/fmt/lsp/minimalEdit.ts';
3+
import { computeMinimalEdit, computeMinimalTextEdit } from '../../../src/fmt/lsp/minimalEdit.ts';
44

55
/** Applies an edit the way an editor does, to prove it rewrites the document. */
66
const applyMinimalEdit = (source: string, formatted: string): string => {
@@ -165,12 +165,31 @@ test('addresses every combination of line terminators', () => {
165165
if (applied !== formatted) {
166166
failures.push(`${JSON.stringify(source)} -> ${JSON.stringify(formatted)}`);
167167
}
168+
169+
// The hand-rolled position mapping must agree with the reference
170+
// implementation on every terminator combination.
171+
const range = computeMinimalTextEdit(source, formatted)?.range;
172+
const expected = {
173+
start: document.positionAt(edit.start),
174+
end: document.positionAt(edit.end),
175+
};
176+
if (JSON.stringify(range) !== JSON.stringify(expected)) {
177+
failures.push(`positions ${JSON.stringify(source)} -> ${JSON.stringify(formatted)}`);
178+
}
168179
}
169180
}
170181

171182
expect(failures).toEqual([]);
172183
});
173184

185+
// The protocol counts a lone `\r` as a line break, so the mapping must too.
186+
test('maps positions across lone carriage returns', () => {
187+
expect(computeMinimalTextEdit('a\rb=2;\r', 'a\rb = 2;\r')).toEqual({
188+
range: { start: { line: 1, character: 1 }, end: { line: 1, character: 2 } },
189+
newText: ' = ',
190+
});
191+
});
192+
174193
// U+2028 ends a line for JavaScript but never for the protocol.
175194
test('treats a line separator as ordinary content', () => {
176195
const source = 'const a = "\u2028";\nconst b=2;\n';

0 commit comments

Comments
 (0)