editor: Treat blank rename as no-op (#62630)

Confirming an inline rename after deleting the entire symbol name or
entering only whitespace currently submits an invalid rename request to
the language server. This can remove the symbol text instead of leaving
the source unchanged.

Treat empty and whitespace-only replacements as successful no-ops after
dismissing the inline rename UI. Returning a completed task also
consumes the confirmation action, preventing Enter from propagating back
into the editor. Non-blank rename behavior remains unchanged.

The regression test covers both empty and whitespace-only rename fields,
verifying that no LSP rename request is sent and the original buffer
remains intact.

Release Notes:

- editor: Fixed confirming a blank symbol LSP-rename modifying the
source code
This commit is contained in:
Bennet Bo Fenner authored and GitHub committed 2026-08-14 13:53:21 +00:00
1 parent f7356481e7
commit a5234566b4
2 files changed
+87 -1

No files matched your search

+5 -1
View File
@@ -8026,6 +8026,11 @@ impl Editor {
return None;
}
let rename = self.take_rename(false, window, cx)?;
let new_name = rename.editor.read(cx).text(cx);
if new_name.trim().is_empty() {
return Some(Task::ready(Ok(())));
}
let workspace = self.workspace()?.downgrade();
let (buffer, start) = self
.buffer
@@ -8040,7 +8045,6 @@ impl Editor {
}
let old_name = rename.old_name;
let new_name = rename.editor.read(cx).text(cx);
let rename = self.semantics_provider.as_ref()?.perform_rename(
&buffer,
+82
View File
@@ -33205,6 +33205,88 @@ async fn test_go_to_bookmark_with_out_of_order_bookmarks(cx: &mut TestAppContext
assert_eq!(ctx.cursor_row(), 1, "Prev from row 5 should go to row 1");
}
#[gpui::test]
async fn test_empty_rename_is_no_op(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let capabilities = lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
};
let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
cx.set_state("let aˇbc = 1;");
let rename_request_count = Arc::new(AtomicUsize::new(0));
let _rename_handler = cx.set_request_handler::<lsp::request::Rename, _, _>({
let rename_request_count = rename_request_count.clone();
move |_, _, _| {
rename_request_count.fetch_add(1, atomic::Ordering::SeqCst);
async { Ok(None) }
}
});
let mut prepare_rename_handler = cx
.set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
move |_, _, _| async move {
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
start: lsp::Position {
line: 0,
character: 4,
},
end: lsp::Position {
line: 0,
character: 7,
},
})))
},
);
for new_name in ["", " "] {
let prepare_rename_task = cx
.update_editor(|editor, window, cx| editor.rename(&Rename, window, cx))
.expect("Prepare rename was not started");
prepare_rename_handler.next().await.unwrap();
prepare_rename_task.await.expect("Prepare rename failed");
let rename_editor = cx.editor(|editor, _, _| {
editor
.pending_rename()
.expect("Rename should still be pending")
.editor
.clone()
});
rename_editor.update_in(&mut cx.cx.cx, |editor, window, cx| {
editor.backspace(&Backspace, window, cx);
editor.insert(new_name, window, cx);
});
assert_eq!(
cx.editor(|editor, _, cx| {
editor
.pending_rename()
.expect("Rename should still be pending")
.editor
.read(cx)
.text(cx)
}),
new_name
);
cx.update_editor(|editor, window, cx| {
editor
.confirm_rename(&ConfirmRename, window, cx)
.expect("Confirm rename should consume the action")
})
.await
.expect("Confirm rename failed");
assert_eq!(rename_request_count.load(atomic::Ordering::SeqCst), 0);
assert!(cx.editor(|editor, _, _| editor.pending_rename().is_none()));
assert_eq!(cx.buffer_text(), "let abc = 1;");
}
}
#[gpui::test]
async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
init_test(cx, |_| {});