From 5d12274a22e123011d04c8d1d28c630d5c9c394f Mon Sep 17 00:00:00 2001 From: ammann <161159040+agammann@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:03:11 -0700 Subject: [PATCH] Fix line offsets when incremental edits join CRLF --- textDocument/src/main.ts | 8 +++++ textDocument/src/test/textdocument.test.ts | 39 +++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/textDocument/src/main.ts b/textDocument/src/main.ts index 06409de72..14dd59dbe 100644 --- a/textDocument/src/main.ts +++ b/textDocument/src/main.ts @@ -260,6 +260,14 @@ class FullTextDocument implements TextDocument { const startOffset = this.offsetAt(range.start); const endOffset = this.offsetAt(range.end); this._content = this._content.substring(0, startOffset) + change.text + this._content.substring(endOffset, this._content.length); + const insertedEndOffset = startOffset + change.text.length; + const crlfAtStart = this._content.charCodeAt(startOffset - 1) === CharCode.CarriageReturn && this._content.charCodeAt(startOffset) === CharCode.LineFeed; + const crlfAtEnd = this._content.charCodeAt(insertedEndOffset - 1) === CharCode.CarriageReturn && this._content.charCodeAt(insertedEndOffset) === CharCode.LineFeed; + if (crlfAtStart || crlfAtEnd) { + // A CRLF pair across an edit boundary is not included in the inserted text's line offsets. + this._lineOffsets = undefined; + continue; + } // update the offsets const startLine = Math.max(range.start.line, 0); diff --git a/textDocument/src/test/textdocument.test.ts b/textDocument/src/test/textdocument.test.ts index 8aca2cac2..caeddfd67 100644 --- a/textDocument/src/test/textdocument.test.ts +++ b/textDocument/src/test/textdocument.test.ts @@ -339,6 +339,43 @@ suite('Text Document Full Updates', () => { }); suite('Text Document Incremental Updates', () => { + test('Incremental edits that join CRLF across an edit boundary', () => { + const changes = [ + { original: 'a\rb', range: Ranges.create(1, 0, 1, 0), text: '\n' }, + { original: 'a\nb', range: Ranges.create(0, 1, 0, 1), text: '\r' }, + { original: 'a\rX\nb', range: Ranges.create(1, 0, 1, 1), text: '' } + ]; + const expected = newDocument('a\r\nb'); + + for (const change of changes) { + const document = newDocument(change.original); + TextDocument.update(document, [{ text: change.text, range: change.range }], 1); + assert.strictEqual(document.getText(), expected.getText()); + assert.strictEqual(document.lineCount, expected.lineCount, change.original); + for (let offset = 0; offset <= document.getText().length; offset++) { + assert.deepStrictEqual(document.positionAt(offset), expected.positionAt(offset), change.original); + } + for (let line = 0; line < expected.lineCount; line++) { + assert.deepStrictEqual(document.getLineRange(line), expected.getLineRange(line), change.original); + assert.strictEqual(document.getEOLCharacters(line), expected.getEOLCharacters(line), change.original); + for (let character = 0; character <= 3; character++) { + assert.strictEqual(document.offsetAt(Positions.create(line, character)), expected.offsetAt(Positions.create(line, character)), change.original); + } + } + } + + const batched = newDocument('a\rb'); + TextDocument.update(batched, [ + { text: '\n', range: Ranges.create(1, 0, 1, 0) }, + { text: 'X', range: Ranges.create(1, 0, 1, 0) } + ], 1); + const expectedBatch = newDocument('a\r\nXb'); + assert.strictEqual(batched.getText(), expectedBatch.getText()); + assert.strictEqual(batched.lineCount, expectedBatch.lineCount); + for (let offset = 0; offset <= batched.getText().length; offset++) { + assert.deepStrictEqual(batched.positionAt(offset), expectedBatch.positionAt(offset)); + } + }); // assumes that only '\n' is used function assertValidLineNumbers(doc: TextDocument) { @@ -622,4 +659,4 @@ suite('Text Document Incremental Updates', () => { assert.equal(document.lineCount, 1); assertValidLineNumbers(document); }); -}); \ No newline at end of file +});