From 3181085ba2d1762635fe393b039c3eed72ac29c1 Mon Sep 17 00:00:00 2001 From: qrzychu Date: Thu, 23 Jul 2026 07:54:28 +0000 Subject: [PATCH] Fix cursor going to the end of file when LSP formatter uses CRLF line endings (#59151) I got super annoyed when using Zed with F#, so I fixed it. This also fixes other LSPs on Windows Fix is super simple, the test fails if the fix is commented out Closes #48367 (and probably others) Release Notes: - fix cursor jumping to the end of file when LSP formats the file using CLRF line endings --- crates/project/src/lsp_store.rs | 5 +- .../tests/integration/project_tests.rs | 100 ++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index f61ddbf7ad..15836796ad 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3363,7 +3363,10 @@ impl LocalLspStore { let snapshot = snapshot?; let mut lsp_edits = lsp_edits .into_iter() - .map(|edit| (range_from_lsp(edit.range), edit.new_text)) + .map(|mut edit| { + LineEnding::normalize(&mut edit.new_text); + (range_from_lsp(edit.range), edit.new_text) + }) .collect::>(); lsp_edits.sort_unstable_by_key(|(range, _)| (range.start, range.end)); diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 13051448ca..3ddc239901 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -5006,6 +5006,106 @@ fn chunks_with_diagnostics( chunks } +#[gpui::test] +async fn test_edits_from_lsp_with_crlf_line_endings(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let text = " + fn a() {} + fn b() {} + fn c() {} + " + .unindent(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "a.rs": text.clone(), + }), + ) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) + .await + .unwrap(); + + // Simulate the language server sending us a whole-document edit that uses + // CRLF line endings. FsAutoComplete does this when formatting via Fantomas + // on Windows. The buffer's text is always LF-normalized, so the differing + // line endings must not be treated as changes; otherwise the entire buffer + // is replaced and cursor positions are lost. + let edits = lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.as_local_mut().unwrap().edits_from_lsp( + &buffer, + [lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)), + new_text: "fn a() {}\r\nfn b() {}\r\nfn c() {}\r\n".into(), + }], + LanguageServerId(0), + None, + cx, + ) + }) + .await + .unwrap(); + assert!(edits.is_empty(), "expected no edits, got {edits:?}"); + + // The same whole-document CRLF edit, but with an actual change on one + // line, must produce an edit for just that change. + let edits = lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.as_local_mut().unwrap().edits_from_lsp( + &buffer, + [lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)), + new_text: "fn a() {}\r\nfn b(x: u32) {}\r\nfn c() {}\r\n".into(), + }], + LanguageServerId(0), + None, + cx, + ) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + let edits = edits + .into_iter() + .map(|(range, text)| { + ( + range.start.to_point(buffer)..range.end.to_point(buffer), + text, + ) + }) + .collect::>(); + + assert_eq!( + edits, + [(Point::new(1, 5)..Point::new(1, 5), "x: u32".into())] + ); + + for (range, new_text) in edits { + buffer.edit([(range, new_text)], None, cx); + } + assert_eq!( + buffer.text(), + " + fn a() {} + fn b(x: u32) {} + fn c() {} + " + .unindent() + ); + }); +} + #[gpui::test(iterations = 10)] async fn test_definition(cx: &mut gpui::TestAppContext) { init_test(cx);