diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index fb7343c18b..79cf97ff88 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -3569,6 +3569,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { title: "Fix with Assistant".into(), ..Default::default() })), + resolved: true, }])) } else { Task::ready(Ok(Vec::new())) diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index e76834da73..fd42ed1a24 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -1729,6 +1729,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { title: "Fix with Assistant".into(), ..Default::default() })), + resolved: true, }])) } else { Task::ready(Ok(Vec::new())) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c820fcdfc8..94c82327d0 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -307,6 +307,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) @@ -347,6 +348,7 @@ impl Server { .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 987d9e29eb..0c863cfab8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -69,7 +69,7 @@ pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, }; use futures::{ - future::{self, Shared}, + future::{self, join, Shared}, FutureExt, }; use fuzzy::StringMatchCandidate; @@ -82,10 +82,10 @@ use code_context_menus::{ use git::blame::GitBlame; use gpui::{ div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation, - AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds, - ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, - EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, - HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, + AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background, + Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, + EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, + Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, @@ -1233,11 +1233,15 @@ impl Editor { project_subscriptions.push(cx.subscribe_in( project, window, - |editor, _, event, window, cx| { - if let project::Event::RefreshInlayHints = event { + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { editor .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } else if let project::Event::SnippetEdit(id, snippet_edits) = event { + } + project::Event::SnippetEdit(id, snippet_edits) => { if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { let focus_handle = editor.focus_handle(cx); if focus_handle.is_focused(window) { @@ -1257,6 +1261,7 @@ impl Editor { } } } + _ => {} }, )); if let Some(task_inventory) = project @@ -17027,7 +17032,16 @@ impl CodeActionProvider for Entity { cx: &mut App, ) -> Task>> { self.update(cx, |project, cx| { - project.code_actions(buffer, range, None, cx) + let code_lens = project.code_lens(buffer, range.clone(), cx); + let code_actions = project.code_actions(buffer, range, None, cx); + cx.background_spawn(async move { + let (code_lens, code_actions) = join(code_lens, code_actions).await; + Ok(code_lens + .context("code lens fetch")? + .into_iter() + .chain(code_actions.context("code action fetch")?) + .collect()) + }) }) } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a37e0efc3b..3482aefec3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -17233,6 +17233,187 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) { "}); } +#[gpui::test(iterations = 10)] +async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( + LanguageConfig { + name: "TypeScript".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), + ))); + let mut fake_language_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: Some(true), + }), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec!["_the/command".to_string()], + ..lsp::ExecuteCommandOptions::default() + }), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); + + let (buffer, _handle) = project + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left); + drop(buffer_snapshot); + let actions = cx + .update_window(*workspace, |_, window, cx| { + project.code_actions(&buffer, anchor..anchor, window, cx) + }) + .unwrap(); + + fake_server + .handle_request::(|_, _| async move { + Ok(Some(vec![ + lsp::CodeLens { + range: lsp::Range::default(), + command: Some(lsp::Command { + title: "Code lens command".to_owned(), + command: "_the/command".to_owned(), + arguments: None, + }), + data: None, + }, + lsp::CodeLens { + range: lsp::Range::default(), + command: Some(lsp::Command { + title: "Command not in capabilities".to_owned(), + command: "not in capabilities".to_owned(), + arguments: None, + }), + data: None, + }, + lsp::CodeLens { + range: lsp::Range { + start: lsp::Position { + line: 1, + character: 1, + }, + end: lsp::Position { + line: 1, + character: 1, + }, + }, + command: Some(lsp::Command { + title: "Command not in range".to_owned(), + command: "_the/command".to_owned(), + arguments: None, + }), + data: None, + }, + ])) + }) + .next() + .await; + + let actions = actions.await.unwrap(); + assert_eq!( + actions.len(), + 1, + "Should have only one valid action for the 0..0 range" + ); + let action = actions[0].clone(); + let apply = project.update(cx, |project, cx| { + project.apply_code_action(buffer.clone(), action, true, cx) + }); + + // Resolving the code action does not populate its edits. In absence of + // edits, we must execute the given command. + fake_server.handle_request::(|mut lens, _| async move { + let lens_command = lens.command.as_mut().expect("should have a command"); + assert_eq!(lens_command.title, "Code lens command"); + lens_command.arguments = Some(vec![json!("the-argument")]); + Ok(lens) + }); + + // While executing the command, the language server sends the editor + // a `workspaceEdit` request. + fake_server + .handle_request::({ + let fake = fake_server.clone(); + move |params, _| { + assert_eq!(params.command, "_the/command"); + let fake = fake.clone(); + async move { + fake.server + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "X".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, + }, + ) + .await + .unwrap(); + Ok(Some(json!(null))) + } + } + }) + .next() + .await; + + // Applying the code lens command returns a project transaction containing the edits + // sent by the language server in its `workspaceEdit` request. + let transaction = apply.await.unwrap(); + assert!(transaction.0.contains_key(&buffer)); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "Xa"); + buffer.undo(cx); + assert_eq!(buffer.text(), "a"); + }); +} + mod autoclose_tags { use super::*; use language::language_settings::JsxTagAutoCloseSettings; diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f70bc56d1b..6602cd8bad 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -7,7 +7,7 @@ use gpui::{App, AsyncApp, Task}; use http_client::github::AssetKind; use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; pub use language::*; -use lsp::{LanguageServerBinary, LanguageServerName}; +use lsp::LanguageServerBinary; use regex::Regex; use smol::fs::{self}; use std::fmt::Display; diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 132a67b5b7..de7e587164 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -632,6 +632,9 @@ impl LanguageServer { diagnostic: Some(DiagnosticWorkspaceClientCapabilities { refresh_support: None, }), + code_lens: Some(CodeLensWorkspaceClientCapabilities { + refresh_support: Some(true), + }), workspace_edit: Some(WorkspaceEditClientCapabilities { resource_operations: Some(vec![ ResourceOperationKind::Create, @@ -763,6 +766,9 @@ impl LanguageServer { did_save: Some(true), ..TextDocumentSyncClientCapabilities::default() }), + code_lens: Some(CodeLensClientCapabilities { + dynamic_registration: Some(false), + }), ..TextDocumentClientCapabilities::default() }), experimental: Some(json!({ diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 5eb16bc74c..fac968be78 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -234,6 +234,19 @@ pub(crate) struct InlayHints { pub range: Range, } +#[derive(Debug, Copy, Clone)] +pub(crate) struct GetCodeLens; + +impl GetCodeLens { + pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool { + capabilities + .code_lens_provider + .as_ref() + .and_then(|code_lens_options| code_lens_options.resolve_provider) + .unwrap_or(false) + } +} + #[derive(Debug)] pub(crate) struct LinkedEditingRange { pub position: Anchor, @@ -2229,18 +2242,18 @@ impl LspCommand for GetCodeActions { .unwrap_or_default() .into_iter() .filter_map(|entry| { - let lsp_action = match entry { + let (lsp_action, resolved) = match entry { lsp::CodeActionOrCommand::CodeAction(lsp_action) => { if let Some(command) = lsp_action.command.as_ref() { if !available_commands.contains(&command.command) { return None; } } - LspAction::Action(Box::new(lsp_action)) + (LspAction::Action(Box::new(lsp_action)), false) } lsp::CodeActionOrCommand::Command(command) => { if available_commands.contains(&command.command) { - LspAction::Command(command) + (LspAction::Command(command), true) } else { return None; } @@ -2259,6 +2272,7 @@ impl LspCommand for GetCodeActions { server_id, range: self.range.clone(), lsp_action, + resolved, }) }) .collect()) @@ -3037,6 +3051,152 @@ impl LspCommand for InlayHints { } } +#[async_trait(?Send)] +impl LspCommand for GetCodeLens { + type Response = Vec; + type LspRequest = lsp::CodeLensRequest; + type ProtoRequest = proto::GetCodeLens; + + fn display_name(&self) -> &str { + "Code Lens" + } + + fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { + capabilities + .server_capabilities + .code_lens_provider + .as_ref() + .map_or(false, |code_lens_options| { + code_lens_options.resolve_provider.unwrap_or(false) + }) + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &App, + ) -> Result { + Ok(lsp::CodeLensParams { + text_document: lsp::TextDocumentIdentifier { + uri: file_path_to_lsp_url(path)?, + }, + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }) + } + + async fn response_from_lsp( + self, + message: Option>, + lsp_store: Entity, + buffer: Entity, + server_id: LanguageServerId, + mut cx: AsyncApp, + ) -> anyhow::Result> { + let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; + let language_server = cx.update(|cx| { + lsp_store + .read(cx) + .language_server_for_id(server_id) + .with_context(|| { + format!("Missing the language server that just returned a response {server_id}") + }) + })??; + let server_capabilities = language_server.capabilities(); + let available_commands = server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + .unwrap_or_default(); + Ok(message + .unwrap_or_default() + .into_iter() + .filter(|code_lens| { + code_lens + .command + .as_ref() + .is_none_or(|command| available_commands.contains(&command.command)) + }) + .map(|code_lens| { + let code_lens_range = range_from_lsp(code_lens.range); + let start = snapshot.clip_point_utf16(code_lens_range.start, Bias::Left); + let end = snapshot.clip_point_utf16(code_lens_range.end, Bias::Right); + let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); + CodeAction { + server_id, + range, + lsp_action: LspAction::CodeLens(code_lens), + resolved: false, + } + }) + .collect()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeLens { + proto::GetCodeLens { + project_id, + buffer_id: buffer.remote_id().into(), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetCodeLens, + _: Entity, + buffer: Entity, + mut cx: AsyncApp, + ) -> Result { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + Ok(Self) + } + + fn response_to_proto( + response: Vec, + _: &mut LspStore, + _: PeerId, + buffer_version: &clock::Global, + _: &mut App, + ) -> proto::GetCodeLensResponse { + proto::GetCodeLensResponse { + lens_actions: response + .iter() + .map(LspStore::serialize_code_action) + .collect(), + version: serialize_version(buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::GetCodeLensResponse, + _: Entity, + buffer: Entity, + mut cx: AsyncApp, + ) -> anyhow::Result> { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + message + .lens_actions + .into_iter() + .map(LspStore::deserialize_code_action) + .collect::>>() + .context("deserializing proto code lens response") + } + + fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result { + BufferId::new(message.buffer_id) + } +} + #[async_trait(?Send)] impl LspCommand for LinkedEditingRange { type Response = Vec>; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 81afa08813..4300f65be0 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -807,6 +807,27 @@ impl LocalLspStore { }) .detach(); + language_server + .on_request::({ + let this = this.clone(); + move |(), mut cx| { + let this = this.clone(); + async move { + this.update(&mut cx, |this, cx| { + cx.emit(LspStoreEvent::RefreshCodeLens); + this.downstream_client.as_ref().map(|(client, project_id)| { + client.send(proto::RefreshCodeLens { + project_id: *project_id, + }) + }) + })? + .transpose()?; + Ok(()) + } + } + }) + .detach(); + language_server .on_request::({ let this = this.clone(); @@ -1628,9 +1649,8 @@ impl LocalLspStore { ) -> anyhow::Result<()> { match &mut action.lsp_action { LspAction::Action(lsp_action) => { - if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) - && lsp_action.data.is_some() - && (lsp_action.command.is_none() || lsp_action.edit.is_none()) + if !action.resolved + && GetCodeActions::can_resolve_actions(&lang_server.capabilities()) { *lsp_action = Box::new( lang_server @@ -1639,8 +1659,17 @@ impl LocalLspStore { ); } } + LspAction::CodeLens(lens) => { + if !action.resolved && GetCodeLens::can_resolve_lens(&lang_server.capabilities()) { + *lens = lang_server + .request::(lens.clone()) + .await?; + } + } LspAction::Command(_) => {} } + + action.resolved = true; anyhow::Ok(()) } @@ -2887,6 +2916,7 @@ pub enum LspStoreEvent { }, Notification(String), RefreshInlayHints, + RefreshCodeLens, DiagnosticsUpdated { language_server_id: LanguageServerId, path: ProjectPath, @@ -2942,6 +2972,7 @@ impl LspStore { client.add_entity_request_handler(Self::handle_resolve_inlay_hint); client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); client.add_entity_request_handler(Self::handle_refresh_inlay_hints); + client.add_entity_request_handler(Self::handle_refresh_code_lens); client.add_entity_request_handler(Self::handle_on_type_formatting); client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers); @@ -4316,6 +4347,7 @@ impl LspStore { cx, ) } + pub fn code_actions( &mut self, buffer_handle: &Entity, @@ -4395,6 +4427,66 @@ impl LspStore { } } + pub fn code_lens( + &mut self, + buffer_handle: &Entity, + cx: &mut Context, + ) -> Task>> { + if let Some((upstream_client, project_id)) = self.upstream_client() { + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), + project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeLens( + GetCodeLens.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); + cx.spawn(|weak_project, cx| async move { + let Some(project) = weak_project.upgrade() else { + return Ok(Vec::new()); + }; + let responses = request_task.await?.responses; + let code_lens = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetCodeLensResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|code_lens_response| { + GetCodeLens.response_from_proto( + code_lens_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) + .await; + + Ok(code_lens + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect()) + }) + } else { + let code_lens_task = + self.request_multiple_lsp_locally(buffer_handle, None::, GetCodeLens, cx); + cx.spawn(|_, _| async move { Ok(code_lens_task.await.into_iter().flatten().collect()) }) + } + } + #[inline(never)] pub fn completions( &self, @@ -6308,6 +6400,43 @@ impl LspStore { .collect(), }) } + Some(proto::multi_lsp_query::Request::GetCodeLens(get_code_lens)) => { + let get_code_lens = GetCodeLens::from_proto( + get_code_lens, + this.clone(), + buffer.clone(), + cx.clone(), + ) + .await?; + + let code_lens_actions = this + .update(&mut cx, |project, cx| { + project.request_multiple_lsp_locally( + &buffer, + None::, + get_code_lens, + cx, + ) + })? + .await + .into_iter(); + + this.update(&mut cx, |project, cx| proto::MultiLspQueryResponse { + responses: code_lens_actions + .map(|actions| proto::LspResponse { + response: Some(proto::lsp_response::Response::GetCodeLensResponse( + GetCodeLens::response_to_proto( + actions, + project, + sender_id, + &buffer_version, + cx, + ), + )), + }) + .collect(), + }) + } None => anyhow::bail!("empty multi lsp query request"), } } @@ -7211,6 +7340,17 @@ impl LspStore { }) } + async fn handle_refresh_code_lens( + this: Entity, + _: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + this.update(&mut cx, |_, cx| { + cx.emit(LspStoreEvent::RefreshCodeLens); + })?; + Ok(proto::Ack {}) + } + async fn handle_open_buffer_for_symbol( this: Entity, envelope: TypedEnvelope, @@ -8434,6 +8574,10 @@ impl LspStore { proto::code_action::Kind::Command as i32, serde_json::to_vec(command).unwrap(), ), + LspAction::CodeLens(code_lens) => ( + proto::code_action::Kind::CodeLens as i32, + serde_json::to_vec(code_lens).unwrap(), + ), }; proto::CodeAction { @@ -8442,6 +8586,7 @@ impl LspStore { end: Some(serialize_anchor(&action.range.end)), lsp_action, kind, + resolved: action.resolved, } } @@ -8449,11 +8594,11 @@ impl LspStore { let start = action .start .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid start"))?; + .context("invalid start")?; let end = action .end .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid end"))?; + .context("invalid end")?; let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { Some(proto::code_action::Kind::Action) => { LspAction::Action(serde_json::from_slice(&action.lsp_action)?) @@ -8461,11 +8606,15 @@ impl LspStore { Some(proto::code_action::Kind::Command) => { LspAction::Command(serde_json::from_slice(&action.lsp_action)?) } + Some(proto::code_action::Kind::CodeLens) => { + LspAction::CodeLens(serde_json::from_slice(&action.lsp_action)?) + } None => anyhow::bail!("Unknown action kind {}", action.kind), }; Ok(CodeAction { server_id: LanguageServerId(action.server_id as usize), range: start..end, + resolved: action.resolved, lsp_action, }) } diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index f69a213fa1..d049ac2c4e 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -6,6 +6,14 @@ use crate::{LanguageServerPromptRequest, LspStore, LspStoreEvent}; pub const RUST_ANALYZER_NAME: &str = "rust-analyzer"; +pub const EXTRA_SUPPORTED_COMMANDS: &[&str] = &[ + "rust-analyzer.runSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "rust-analyzer.triggerParameterHints", + "rust-analyzer.rename", +]; + /// Experimental: Informs the end user about the state of the server /// /// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6593b7875d..d29518b281 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -280,6 +280,7 @@ pub enum Event { Reshared, Rejoined, RefreshInlayHints, + RefreshCodeLens, RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), ExpandedAllForEntry(WorktreeId, ProjectEntryId), @@ -509,6 +510,8 @@ pub struct CodeAction { /// The raw code action provided by the language server. /// Can be either an action or a command. pub lsp_action: LspAction, + /// Whether the action needs to be resolved using the language server. + pub resolved: bool, } /// An action sent back by a language server. @@ -519,6 +522,8 @@ pub enum LspAction { Action(Box), /// A command data to run as an action. Command(lsp::Command), + /// A code lens data to run as an action. + CodeLens(lsp::CodeLens), } impl LspAction { @@ -526,6 +531,11 @@ impl LspAction { match self { Self::Action(action) => &action.title, Self::Command(command) => &command.title, + Self::CodeLens(lens) => lens + .command + .as_ref() + .map(|command| command.title.as_str()) + .unwrap_or("Unknown command"), } } @@ -533,6 +543,7 @@ impl LspAction { match self { Self::Action(action) => action.kind.clone(), Self::Command(_) => Some(lsp::CodeActionKind::new("command")), + Self::CodeLens(_) => Some(lsp::CodeActionKind::new("code lens")), } } @@ -540,6 +551,7 @@ impl LspAction { match self { Self::Action(action) => action.edit.as_ref(), Self::Command(_) => None, + Self::CodeLens(_) => None, } } @@ -547,6 +559,7 @@ impl LspAction { match self { Self::Action(action) => action.command.as_ref(), Self::Command(command) => Some(command), + Self::CodeLens(lens) => lens.command.as_ref(), } } } @@ -2483,6 +2496,7 @@ impl Project { }; } LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints), + LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens), LspStoreEvent::LanguageServerPrompt(prompt) => { cx.emit(Event::LanguageServerPrompt(prompt.clone())) } @@ -3163,6 +3177,34 @@ impl Project { }) } + pub fn code_lens( + &mut self, + buffer_handle: &Entity, + range: Range, + cx: &mut Context, + ) -> Task>> { + let snapshot = buffer_handle.read(cx).snapshot(); + let range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end); + let code_lens_actions = self + .lsp_store + .update(cx, |lsp_store, cx| lsp_store.code_lens(buffer_handle, cx)); + + cx.background_spawn(async move { + let mut code_lens_actions = code_lens_actions.await?; + code_lens_actions.retain(|code_lens_action| { + range + .start + .cmp(&code_lens_action.range.start, &snapshot) + .is_ge() + && range + .end + .cmp(&code_lens_action.range.end, &snapshot) + .is_le() + }); + Ok(code_lens_actions) + }) + } + pub fn apply_code_action( &self, buffer_handle: Entity, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index c8a1507a9a..f4b8366cd0 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -346,7 +346,12 @@ message Envelope { GitDiff git_diff = 319; GitDiffResponse git_diff_response = 320; - GitInit git_init = 321; // current max + GitInit git_init = 321; + + CodeLens code_lens = 322; + GetCodeLens get_code_lens = 323; + GetCodeLensResponse get_code_lens_response = 324; + RefreshCodeLens refresh_code_lens = 325; // current max } reserved 87 to 88; @@ -1263,6 +1268,25 @@ message RefreshInlayHints { uint64 project_id = 1; } +message CodeLens { + bytes lsp_lens = 1; +} + +message GetCodeLens { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; +} + +message GetCodeLensResponse { + repeated CodeAction lens_actions = 1; + repeated VectorClockEntry version = 2; +} + +message RefreshCodeLens { + uint64 project_id = 1; +} + message MarkupContent { bool is_markdown = 1; string value = 2; @@ -1298,9 +1322,11 @@ message CodeAction { Anchor end = 3; bytes lsp_action = 4; Kind kind = 5; + bool resolved = 6; enum Kind { Action = 0; Command = 1; + CodeLens = 2; } } @@ -2346,6 +2372,7 @@ message MultiLspQuery { GetHover get_hover = 5; GetCodeActions get_code_actions = 6; GetSignatureHelp get_signature_help = 7; + GetCodeLens get_code_lens = 8; } } @@ -2365,6 +2392,7 @@ message LspResponse { GetHoverResponse get_hover_response = 1; GetCodeActionsResponse get_code_actions_response = 2; GetSignatureHelpResponse get_signature_help_response = 3; + GetCodeLensResponse get_code_lens_response = 4; } } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 16a08cd42f..53e1a8f653 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -340,6 +340,9 @@ messages!( (ResolveCompletionDocumentationResponse, Background), (ResolveInlayHint, Background), (ResolveInlayHintResponse, Background), + (RefreshCodeLens, Background), + (GetCodeLens, Background), + (GetCodeLensResponse, Background), (RespondToChannelInvite, Foreground), (RespondToContactRequest, Foreground), (RoomUpdated, Foreground), @@ -513,6 +516,7 @@ request_messages!( (GetUsers, UsersResponse), (IncomingCall, Ack), (InlayHints, InlayHintsResponse), + (GetCodeLens, GetCodeLensResponse), (InviteChannelMember, Ack), (JoinChannel, JoinRoomResponse), (JoinChannelBuffer, JoinChannelBufferResponse), @@ -534,6 +538,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (CountLanguageModelTokens, CountLanguageModelTokensResponse), (RefreshInlayHints, Ack), + (RefreshCodeLens, Ack), (RejoinChannelBuffers, RejoinChannelBuffersResponse), (RejoinRoom, RejoinRoomResponse), (ReloadBuffers, ReloadBuffersResponse), @@ -632,6 +637,7 @@ entity_messages!( ApplyCodeActionKind, FormatBuffers, GetCodeActions, + GetCodeLens, GetCompletions, GetDefinition, GetDeclaration, @@ -659,6 +665,7 @@ entity_messages!( PerformRename, PrepareRename, RefreshInlayHints, + RefreshCodeLens, ReloadBuffers, RemoveProjectCollaborator, RenameProjectEntry,