From f829bdc9b5bd81d17089b19c46af1646cd99fee6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 27 Jun 2026 18:21:51 +0300 Subject: [PATCH 1/2] Refresh code lens after dynamic registration --- crates/editor/src/code_lens.rs | 167 ++++++++++++++++++++++ crates/project/src/lsp_store.rs | 20 +-- crates/project/src/lsp_store/code_lens.rs | 16 ++- 3 files changed, 186 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/code_lens.rs b/crates/editor/src/code_lens.rs index bd88f4ceda6287..3d262788af050e 100644 --- a/crates/editor/src/code_lens.rs +++ b/crates/editor/src/code_lens.rs @@ -769,6 +769,173 @@ mod tests { }); } + #[gpui::test] + async fn test_code_lens_refresh_requeries_open_document(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + update_test_editor_settings(cx, &|settings| { + settings.code_lens = Some(CodeLens::On); + }); + + let mut cx = EditorLspTestContext::new_typescript( + lsp::ServerCapabilities { + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: None, + }), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec!["lens_cmd".to_string()], + ..lsp::ExecuteCommandOptions::default() + }), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + let lens_title = Arc::new(Mutex::new("Initial lens".to_string())); + let mut code_lens_request = + cx.set_request_handler::({ + let lens_title = lens_title.clone(); + move |_, _, _| { + let lens_title = lens_title.clone(); + async move { + let title = lens_title.lock().unwrap().clone(); + Ok(Some(vec![lsp::CodeLens { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 19), + ), + command: Some(lsp::Command { + title, + command: "lens_cmd".to_owned(), + arguments: None, + }), + data: None, + }])) + } + } + }); + + cx.set_state("ˇfunction hello() {}\nfunction world() {}"); + assert!( + code_lens_request.next().await.is_some(), + "should have received the initial code lens request" + ); + cx.run_until_parked(); + cx.editor(|editor, _, cx| { + assert_eq!( + code_lens_assertion_text(editor, cx), + indoc! {r#" + Lenses: Initial lens + Line 1: function hello() {} + "#}, + "initial fetch should render the server title" + ); + }); + + *lens_title.lock().unwrap() = "Refreshed lens".to_string(); + cx.lsp + .request::((), lsp::DEFAULT_LSP_REQUEST_TIMEOUT) + .await + .into_response() + .expect("code lens refresh request failed"); + cx.executor() + .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT * 2); + cx.run_until_parked(); + cx.editor(|editor, _, cx| { + assert_eq!( + code_lens_assertion_text(editor, cx), + indoc! {r#" + Lenses: Refreshed lens + Line 1: function hello() {} + "#}, + "refresh should update the displayed lens to the new server title" + ); + }); + } + + #[gpui::test] + async fn test_code_lens_dynamic_registration_requeries_open_document(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + update_test_editor_settings(cx, &|settings| { + settings.code_lens = Some(CodeLens::On); + }); + + // The server advertises no code lens capability up front; it registers + // `textDocument/codeLens` dynamically only after the document is open. + let mut cx = EditorLspTestContext::new_typescript( + lsp::ServerCapabilities { + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec!["lens_cmd".to_string()], + ..lsp::ExecuteCommandOptions::default() + }), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + let _code_lens_request = + cx.set_request_handler::(move |_, _, _| async { + Ok(Some(vec![lsp::CodeLens { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)), + command: Some(lsp::Command { + title: "Dynamic lens".to_owned(), + command: "lens_cmd".to_owned(), + arguments: None, + }), + data: None, + }])) + }); + + cx.set_state("ˇfunction hello() {}\nfunction world() {}"); + // Drain any debounced refresh scheduled before the capability exists, so + // the post-registration re-query can only come from the dynamic + // registration handling itself. + cx.executor() + .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT * 2); + cx.run_until_parked(); + cx.editor(|editor, _, cx| { + assert_eq!( + code_lens_assertion_text(editor, cx), + "\n", + "no lenses should render before the capability is registered" + ); + }); + + cx.lsp + .request::( + lsp::RegistrationParams { + registrations: vec![lsp::Registration { + id: "code-lens".to_string(), + method: "textDocument/codeLens".to_string(), + register_options: Some( + serde_json::to_value(lsp::CodeLensOptions { + resolve_provider: None, + }) + .unwrap(), + ), + }], + }, + lsp::DEFAULT_LSP_REQUEST_TIMEOUT, + ) + .await + .into_response() + .expect("register capability request failed"); + cx.executor() + .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT * 2); + cx.run_until_parked(); + cx.editor(|editor, _, cx| { + assert_eq!( + code_lens_assertion_text(editor, cx), + indoc! {r#" + Lenses: Dynamic lens + Line 1: function hello() {} + "#}, + "dynamic textDocument/codeLens registration should re-query and display lenses for the open document" + ); + }); + } + #[gpui::test] async fn test_code_lens_blocks_kept_across_refresh(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 454c4d18d87ba1..eea31f53b14b79 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1134,22 +1134,13 @@ impl LocalLspStore { language_server .on_request::({ - let this = lsp_store.clone(); + let lsp_store = lsp_store.clone(); move |(), cx| { - let this = this.clone(); - let mut cx = cx.clone(); + let result = lsp_store.update(cx, |lsp_store, cx| { + lsp_store.refresh_code_lens(cx); + }); async move { - this.update(&mut cx, |this, cx| { - this.invalidate_code_lens(); - cx.emit(LspStoreEvent::RefreshCodeLens); - this.downstream_client.as_ref().map(|(client, project_id)| { - client.send(proto::RefreshCodeLens { - project_id: *project_id, - }) - }) - })? - .transpose()?; - Ok(()) + result } } }) @@ -13128,6 +13119,7 @@ impl LspStore { capabilities.code_lens_provider = Some(caps); }); notify_server_capabilities_updated(&server, cx); + self.refresh_code_lens(cx); } } "textDocument/diagnostic" => { diff --git a/crates/project/src/lsp_store/code_lens.rs b/crates/project/src/lsp_store/code_lens.rs index 0f1eae82aa3915..f21fb2ebb6b384 100644 --- a/crates/project/src/lsp_store/code_lens.rs +++ b/crates/project/src/lsp_store/code_lens.rs @@ -15,6 +15,7 @@ use rpc::{TypedEnvelope, proto}; use settings::Settings as _; use std::time::Duration; use text::OffsetRangeExt as _; +use util::ResultExt as _; use crate::{ CodeAction, LspAction, LspStore, LspStoreEvent, Project, @@ -71,10 +72,20 @@ fn flatten_cache(lens: &HashMap) -> CodeLensA } impl LspStore { - pub(super) fn invalidate_code_lens(&mut self) { + pub(super) fn refresh_code_lens(&mut self, cx: &mut Context) { for lsp_data in self.lsp_data.values_mut() { lsp_data.code_lens = None; } + + cx.emit(LspStoreEvent::RefreshCodeLens); + if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { + downstream_client + .send(proto::RefreshCodeLens { + project_id: *project_id, + }) + .context("sending refresh code lens downstream") + .log_err(); + } } /// Fetches all code lenses for the buffer, each tagged with the @@ -403,8 +414,7 @@ impl LspStore { mut cx: AsyncApp, ) -> Result { lsp_store.update(&mut cx, |lsp_store, cx| { - lsp_store.invalidate_code_lens(); - cx.emit(LspStoreEvent::RefreshCodeLens); + lsp_store.refresh_code_lens(cx); }); Ok(proto::Ack {}) } From 20e446abfff758cb9a95144466f7d67425dfa19f Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:40:50 +0000 Subject: [PATCH 2/2] Autofix --- crates/project/src/lsp_store.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index eea31f53b14b79..2d3f83097c4f0a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1139,9 +1139,7 @@ impl LocalLspStore { let result = lsp_store.update(cx, |lsp_store, cx| { lsp_store.refresh_code_lens(cx); }); - async move { - result - } + async move { result } } }) .detach();