Support code lens in the editor (#54100)

Kirill Bulatov created

Change summary

assets/settings/default.json                     |   11 
crates/collab/tests/integration/editor_tests.rs  |    7 
crates/editor/src/actions.rs                     |    2 
crates/editor/src/code_lens.rs                   | 1066 ++++++++++++++++++
crates/editor/src/editor.rs                      |   52 
crates/editor/src/editor_settings.rs             |    4 
crates/editor/src/editor_tests.rs                |   12 
crates/editor/src/element.rs                     |    1 
crates/language/src/language.rs                  |   19 
crates/languages/src/go.rs                       |   84 +
crates/languages/src/rust.rs                     |   61 
crates/languages/src/vtsls.rs                    |    9 
crates/project/src/lsp_command.rs                |   31 
crates/project/src/lsp_store.rs                  |   73 
crates/project/src/lsp_store/code_lens.rs        |  209 +++
crates/project/src/lsp_store/lsp_ext_command.rs  |  115 -
crates/project/src/project.rs                    |   45 
crates/remote_server/src/remote_editing_tests.rs |  142 ++
crates/settings/src/vscode_import.rs             |    1 
crates/settings_content/src/editor.rs            |   47 
crates/settings_ui/src/page_data.rs              |   16 
crates/settings_ui/src/settings_ui.rs            |    1 
crates/zed/src/zed/quick_action_bar.rs           |   25 
docs/src/languages/go.md                         |   33 
docs/src/languages/typescript.md                 |   45 
docs/src/reference/all-settings.md               |   18 
26 files changed, 1,926 insertions(+), 203 deletions(-)

Detailed changes

assets/settings/default.json πŸ”—

@@ -339,6 +339,17 @@
     // The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
     "delay": 300,
   },
+  // Whether and how to display code lenses from language servers.
+  //
+  // Possible values:
+  //
+  // 1. Do not display code lenses.
+  //      "code_lens": "off",
+  // 2. Display code lenses from language servers above code elements.
+  //      "code_lens": "on",
+  // 3. Display code lenses in the code action menu.
+  //      "code_lens": "menu",
+  "code_lens": "off",
   // What to do when go to definition yields no results.
   //
   // 1. Do nothing: `none`

crates/collab/tests/integration/editor_tests.rs πŸ”—

@@ -1203,6 +1203,13 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
     cx_b.update(editor::init);
+    cx_b.update(|cx| {
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings(cx, |settings| {
+                settings.editor.code_lens = Some(settings::CodeLens::Menu);
+            });
+        });
+    });
 
     let command_name = "test_command";
     let capabilities = lsp::ServerCapabilities {

crates/editor/src/actions.rs πŸ”—

@@ -849,6 +849,8 @@ actions!(
         ToggleIndentGuides,
         /// Toggles inlay hints display.
         ToggleInlayHints,
+        /// Toggles code lens display.
+        ToggleCodeLens,
         /// Toggles semantic highlights display.
         ToggleSemanticHighlights,
         /// Toggles inline values display.

crates/editor/src/code_lens.rs πŸ”—

@@ -0,0 +1,1066 @@
+use std::{iter, ops::Range, sync::Arc};
+
+use collections::{HashMap, HashSet};
+use futures::future::join_all;
+use gpui::{MouseButton, SharedString, Task, WeakEntity};
+use itertools::Itertools;
+use language::{BufferId, ClientCommand};
+use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
+use project::{CodeAction, TaskSourceKind};
+use settings::Settings as _;
+use task::TaskContext;
+use text::Point;
+
+use ui::{Context, Window, div, prelude::*};
+use workspace::PreviewTabsSettings;
+
+use crate::{
+    Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
+    actions::ToggleCodeLens,
+    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
+};
+
+#[derive(Clone, Debug)]
+struct CodeLensLine {
+    position: Anchor,
+    indent_column: u32,
+    items: Vec<CodeLensItem>,
+}
+
+#[derive(Clone, Debug)]
+struct CodeLensItem {
+    title: SharedString,
+    action: CodeAction,
+}
+
+pub(super) struct CodeLensState {
+    pub(super) block_ids: HashMap<BufferId, Vec<CustomBlockId>>,
+    resolve_task: Task<()>,
+}
+
+impl Default for CodeLensState {
+    fn default() -> Self {
+        Self {
+            block_ids: HashMap::default(),
+            resolve_task: Task::ready(()),
+        }
+    }
+}
+
+impl CodeLensState {
+    fn all_block_ids(&self) -> HashSet<CustomBlockId> {
+        self.block_ids.values().flatten().copied().collect()
+    }
+}
+
+fn group_lenses_by_row(
+    lenses: Vec<(Anchor, CodeLensItem)>,
+    snapshot: &MultiBufferSnapshot,
+) -> impl Iterator<Item = CodeLensLine> {
+    lenses
+        .into_iter()
+        .into_group_map_by(|(position, _)| {
+            let row = position.to_point(snapshot).row;
+            MultiBufferRow(row)
+        })
+        .into_iter()
+        .sorted_by_key(|(row, _)| *row)
+        .filter_map(|(row, entries)| {
+            let position = entries.first()?.0;
+            let items = entries.into_iter().map(|(_, item)| item).collect();
+            let indent_column = snapshot.indent_size_for_line(row).len;
+            Some(CodeLensLine {
+                position,
+                indent_column,
+                items,
+            })
+        })
+}
+
+fn render_code_lens_line(
+    line_number: usize,
+    lens: CodeLensLine,
+    editor: WeakEntity<Editor>,
+) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement {
+    move |cx| {
+        let mut children = Vec::with_capacity((2 * lens.items.len()).saturating_sub(1));
+        let text_style = &cx.editor_style.text;
+        let font = text_style.font();
+        let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
+
+        for (i, item) in lens.items.iter().enumerate() {
+            if i > 0 {
+                children.push(
+                    div()
+                        .font(font.clone())
+                        .text_size(font_size)
+                        .text_color(cx.app.theme().colors().text_muted)
+                        .child(" | ")
+                        .into_any_element(),
+                );
+            }
+
+            let title = item.title.clone();
+            let action = item.action.clone();
+            let editor_handle = editor.clone();
+            let position = lens.position;
+            let id = (line_number as u64) << 32 | (i as u64);
+
+            children.push(
+                div()
+                    .id(ElementId::Integer(id))
+                    .font(font.clone())
+                    .text_size(font_size)
+                    .text_color(cx.app.theme().colors().text_muted)
+                    .cursor_pointer()
+                    .hover(|style| style.text_color(cx.app.theme().colors().text))
+                    .child(title.clone())
+                    .on_mouse_down(MouseButton::Left, |_, _, cx| {
+                        cx.stop_propagation();
+                    })
+                    .on_mouse_down(MouseButton::Right, |_, _, cx| {
+                        cx.stop_propagation();
+                    })
+                    .on_click({
+                        move |_event, window, cx| {
+                            if let Some(editor) = editor_handle.upgrade() {
+                                editor.update(cx, |editor, cx| {
+                                    editor.change_selections(
+                                        SelectionEffects::default(),
+                                        window,
+                                        cx,
+                                        |s| {
+                                            s.select_anchor_ranges([position..position]);
+                                        },
+                                    );
+
+                                    let action = action.clone();
+                                    if let Some(workspace) = editor.workspace() {
+                                        if try_handle_client_command(
+                                            &action, editor, &workspace, window, cx,
+                                        ) {
+                                            return;
+                                        }
+
+                                        let project = workspace.read(cx).project().clone();
+                                        if let Some(buffer) = editor
+                                            .buffer()
+                                            .read(cx)
+                                            .buffer(action.range.start.buffer_id)
+                                        {
+                                            project
+                                                .update(cx, |project, cx| {
+                                                    project
+                                                        .apply_code_action(buffer, action, true, cx)
+                                                })
+                                                .detach_and_log_err(cx);
+                                        }
+                                    }
+                                });
+                            }
+                        }
+                    })
+                    .into_any_element(),
+            );
+        }
+
+        div()
+            .pl(cx.margins.gutter.full_width() + cx.em_width * (lens.indent_column as f32 + 0.5))
+            .h_full()
+            .flex()
+            .flex_row()
+            .items_end()
+            .children(children)
+            .into_any_element()
+    }
+}
+
+pub(super) fn try_handle_client_command(
+    action: &CodeAction,
+    editor: &mut Editor,
+    workspace: &gpui::Entity<workspace::Workspace>,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) -> bool {
+    let Some(command) = action.lsp_action.command() else {
+        return false;
+    };
+
+    let arguments = command.arguments.as_deref().unwrap_or_default();
+    let project = workspace.read(cx).project().clone();
+    let client_command = project
+        .read(cx)
+        .lsp_store()
+        .read(cx)
+        .language_server_adapter_for_id(action.server_id)
+        .and_then(|adapter| adapter.adapter.client_command(&command.command, arguments))
+        .or_else(|| match command.command.as_str() {
+            "editor.action.showReferences"
+            | "editor.action.goToLocations"
+            | "editor.action.peekLocations" => Some(ClientCommand::ShowLocations),
+            _ => None,
+        });
+
+    match client_command {
+        Some(ClientCommand::ScheduleTask(task_template)) => {
+            schedule_task(task_template, action, editor, workspace, window, cx)
+        }
+        Some(ClientCommand::ShowLocations) => {
+            try_show_references(arguments, action, workspace, window, cx)
+        }
+        None => false,
+    }
+}
+
+fn schedule_task(
+    task_template: task::TaskTemplate,
+    action: &CodeAction,
+    editor: &Editor,
+    workspace: &gpui::Entity<workspace::Workspace>,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) -> bool {
+    let task_context = TaskContext {
+        cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
+        ..TaskContext::default()
+    };
+    let language_name = editor
+        .buffer()
+        .read(cx)
+        .buffer(action.range.start.buffer_id)
+        .and_then(|buffer| buffer.read(cx).language())
+        .map(|language| language.name());
+    let task_source_kind = match language_name {
+        Some(language_name) => TaskSourceKind::Lsp {
+            server: action.server_id,
+            language_name: SharedString::from(language_name),
+        },
+        None => TaskSourceKind::AbsPath {
+            id_base: "code-lens".into(),
+            abs_path: task_template
+                .cwd
+                .as_ref()
+                .map(std::path::PathBuf::from)
+                .unwrap_or_default(),
+        },
+    };
+
+    workspace.update(cx, |workspace, cx| {
+        workspace.schedule_task(
+            task_source_kind,
+            &task_template,
+            &task_context,
+            false,
+            window,
+            cx,
+        );
+    });
+    true
+}
+
+fn try_show_references(
+    arguments: &[serde_json::Value],
+    action: &CodeAction,
+    workspace: &gpui::Entity<workspace::Workspace>,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) -> bool {
+    if arguments.len() < 3 {
+        return false;
+    }
+    let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
+        return false;
+    };
+    if locations.is_empty() {
+        return false;
+    }
+
+    let server_id = action.server_id;
+    let project = workspace.read(cx).project().clone();
+    let workspace = workspace.clone();
+
+    cx.spawn_in(window, async move |_editor, cx| {
+        let mut buffer_locations = std::collections::HashMap::default();
+
+        for location in &locations {
+            let open_task = cx.update(|_, cx| {
+                project.update(cx, |project, cx| {
+                    let uri: lsp::Uri = location.uri.clone();
+                    project.open_local_buffer_via_lsp(uri, server_id, cx)
+                })
+            })?;
+            let buffer = open_task.await?;
+
+            let range = range_from_lsp(location.range);
+            buffer_locations
+                .entry(buffer)
+                .or_insert_with(Vec::new)
+                .push(range);
+        }
+
+        workspace.update_in(cx, |workspace, window, cx| {
+            let target = buffer_locations
+                .iter()
+                .flat_map(|(k, v)| iter::repeat(k.clone()).zip(v))
+                .map(|(buffer, location)| {
+                    buffer
+                        .read(cx)
+                        .text_for_range(location.clone())
+                        .collect::<String>()
+                })
+                .filter(|text| !text.contains('\n'))
+                .unique()
+                .take(3)
+                .join(", ");
+            let title = if target.is_empty() {
+                "References".to_owned()
+            } else {
+                format!("References to {target}")
+            };
+            let allow_preview =
+                PreviewTabsSettings::get_global(cx).enable_preview_multibuffer_from_code_navigation;
+            Editor::open_locations_in_multibuffer(
+                workspace,
+                buffer_locations,
+                title,
+                false,
+                allow_preview,
+                MultibufferSelectionMode::First,
+                window,
+                cx,
+            );
+        })?;
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
+
+    true
+}
+
+fn range_from_lsp(range: lsp::Range) -> Range<Point> {
+    let start = Point::new(range.start.line, range.start.character);
+    let end = Point::new(range.end.line, range.end.character);
+    start..end
+}
+
+impl Editor {
+    pub(super) fn refresh_code_lenses(
+        &mut self,
+        for_buffer: Option<BufferId>,
+        _window: &Window,
+        cx: &mut Context<Self>,
+    ) {
+        if !self.lsp_data_enabled() || self.code_lens.is_none() {
+            return;
+        }
+        let Some(project) = self.project.clone() else {
+            return;
+        };
+
+        let buffers_to_query = self
+            .visible_buffers(cx)
+            .into_iter()
+            .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
+            .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
+            .filter(|editor_buffer| {
+                let editor_buffer_id = editor_buffer.read(cx).remote_id();
+                for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
+                    && self.registered_buffers.contains_key(&editor_buffer_id)
+            })
+            .unique_by(|buffer| buffer.read(cx).remote_id())
+            .collect::<Vec<_>>();
+
+        if buffers_to_query.is_empty() {
+            return;
+        }
+
+        let project = project.downgrade();
+        self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
+            cx.background_executor()
+                .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
+                .await;
+
+            let Some(tasks) = project
+                .update(cx, |project, cx| {
+                    project.lsp_store().update(cx, |lsp_store, cx| {
+                        buffers_to_query
+                            .into_iter()
+                            .map(|buffer| {
+                                let buffer_id = buffer.read(cx).remote_id();
+                                let task = lsp_store.code_lens_actions(&buffer, cx);
+                                async move { (buffer_id, task.await) }
+                            })
+                            .collect::<Vec<_>>()
+                    })
+                })
+                .ok()
+            else {
+                return;
+            };
+
+            let results = join_all(tasks).await;
+            if results.is_empty() {
+                return;
+            }
+
+            let Ok(multi_buffer_snapshot) =
+                editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
+            else {
+                return;
+            };
+
+            let mut new_lenses_per_buffer = HashMap::default();
+            for (buffer_id, result) in results {
+                let actions = match result {
+                    Ok(Some(actions)) => actions,
+                    Ok(None) => continue,
+                    Err(e) => {
+                        log::error!("Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}");
+                        continue;
+                    }
+                };
+                let individual_lenses = actions
+                    .into_iter()
+                    .filter_map(|action| {
+                        let title = match &action.lsp_action {
+                            project::LspAction::CodeLens(lens) => lens
+                                .command
+                                .as_ref()
+                                .map(|cmd| SharedString::from(&cmd.title)),
+                            _ => None,
+                        }?;
+                        let position =
+                            multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
+                        Some((position, CodeLensItem { title, action }))
+                    })
+                    .collect();
+                new_lenses_per_buffer.insert(
+                    buffer_id,
+                    group_lenses_by_row(individual_lenses, &multi_buffer_snapshot)
+                        .collect::<Vec<_>>(),
+                );
+            }
+
+            editor
+                .update(cx, |editor, cx| {
+                    let code_lens = editor.code_lens.get_or_insert_with(CodeLensState::default);
+                    let mut blocks_to_remove = HashSet::default();
+                    for buffer_id in new_lenses_per_buffer.keys() {
+                        if let Some(old_ids) = code_lens.block_ids.remove(buffer_id) {
+                            blocks_to_remove.extend(old_ids);
+                        }
+                    }
+                    if !blocks_to_remove.is_empty() {
+                        editor.remove_blocks(blocks_to_remove, None, cx);
+                    }
+
+                    let editor_handle = cx.entity().downgrade();
+                    for (buffer_id, lens_lines) in new_lenses_per_buffer {
+                        if lens_lines.is_empty() {
+                            continue;
+                        }
+                        let blocks = lens_lines
+                            .into_iter()
+                            .enumerate()
+                            .map(|(line_number, lens_line)| {
+                                let position = lens_line.position;
+                                BlockProperties {
+                                    placement: BlockPlacement::Above(position),
+                                    height: Some(1),
+                                    style: BlockStyle::Flex,
+                                    render: Arc::new(render_code_lens_line(
+                                        line_number,
+                                        lens_line,
+                                        editor_handle.clone(),
+                                    )),
+                                    priority: 0,
+                                }
+                            })
+                            .collect::<Vec<_>>();
+                        let block_ids = editor.insert_blocks(blocks, None, cx);
+                        editor
+                            .code_lens
+                            .get_or_insert_with(CodeLensState::default)
+                            .block_ids
+                            .entry(buffer_id)
+                            .or_default()
+                            .extend(block_ids);
+                    }
+
+                    editor.resolve_visible_code_lenses(cx);
+                })
+                .ok();
+        });
+    }
+
+    pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
+        let Some(project) = self.project.as_ref() else {
+            return false;
+        };
+        let lsp_store = project.read(cx).lsp_store().read(cx);
+        lsp_store
+            .lsp_server_capabilities
+            .values()
+            .any(|caps| caps.code_lens_provider.is_some())
+    }
+
+    pub fn code_lens_enabled(&self) -> bool {
+        self.code_lens.is_some()
+    }
+
+    pub fn toggle_code_lens_action(
+        &mut self,
+        _: &ToggleCodeLens,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let currently_enabled = self.code_lens.is_some();
+        self.toggle_code_lens(!currently_enabled, window, cx);
+    }
+
+    pub(super) fn toggle_code_lens(
+        &mut self,
+        enabled: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if enabled {
+            self.code_lens.get_or_insert_with(CodeLensState::default);
+            self.refresh_code_lenses(None, window, cx);
+        } else {
+            self.clear_code_lenses(cx);
+        }
+    }
+
+    pub(super) fn resolve_visible_code_lenses(&mut self, cx: &mut Context<Self>) {
+        if !self.lsp_data_enabled() || self.code_lens.is_none() {
+            return;
+        }
+        let Some(project) = self.project.clone() else {
+            return;
+        };
+
+        let resolve_tasks = self
+            .visible_buffer_ranges(cx)
+            .into_iter()
+            .filter_map(|(snapshot, visible_range, _)| {
+                let buffer_id = snapshot.remote_id();
+                let buffer = self.buffer.read(cx).buffer(buffer_id)?;
+                let visible_anchor_range = snapshot.anchor_before(visible_range.start)
+                    ..snapshot.anchor_after(visible_range.end);
+                let task = project.update(cx, |project, cx| {
+                    project.lsp_store().update(cx, |lsp_store, cx| {
+                        lsp_store.resolve_visible_code_lenses(&buffer, visible_anchor_range, cx)
+                    })
+                });
+                Some((buffer_id, task))
+            })
+            .collect::<Vec<_>>();
+        if resolve_tasks.is_empty() {
+            return;
+        }
+
+        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
+        code_lens.resolve_task = cx.spawn(async move |editor, cx| {
+            let resolved_code_lens = join_all(
+                resolve_tasks
+                    .into_iter()
+                    .map(|(buffer_id, task)| async move { (buffer_id, task.await) }),
+            )
+            .await;
+            editor
+                .update(cx, |editor, cx| {
+                    editor.insert_resolved_code_lens_blocks(resolved_code_lens, cx);
+                })
+                .ok();
+        });
+    }
+
+    fn insert_resolved_code_lens_blocks(
+        &mut self,
+        resolved_code_lens: Vec<(BufferId, Vec<CodeAction>)>,
+        cx: &mut Context<Self>,
+    ) {
+        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+        let editor_handle = cx.entity().downgrade();
+
+        for (buffer_id, actions) in resolved_code_lens {
+            let lenses = actions
+                .into_iter()
+                .filter_map(|action| {
+                    let title = match &action.lsp_action {
+                        project::LspAction::CodeLens(lens) => lens
+                            .command
+                            .as_ref()
+                            .map(|cmd| SharedString::from(&cmd.title)),
+                        _ => None,
+                    }?;
+                    let position = multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
+                    Some((position, CodeLensItem { title, action }))
+                })
+                .collect();
+
+            let blocks = group_lenses_by_row(lenses, &multi_buffer_snapshot)
+                .enumerate()
+                .map(|(line_number, lens_line)| {
+                    let position = lens_line.position;
+                    BlockProperties {
+                        placement: BlockPlacement::Above(position),
+                        height: Some(1),
+                        style: BlockStyle::Flex,
+                        render: Arc::new(render_code_lens_line(
+                            line_number,
+                            lens_line,
+                            editor_handle.clone(),
+                        )),
+                        priority: 0,
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            if !blocks.is_empty() {
+                let block_ids = self.insert_blocks(blocks, None, cx);
+                self.code_lens
+                    .get_or_insert_with(CodeLensState::default)
+                    .block_ids
+                    .entry(buffer_id)
+                    .or_default()
+                    .extend(block_ids);
+            }
+        }
+        cx.notify();
+    }
+
+    pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
+        if let Some(code_lens) = self.code_lens.take() {
+            let all_blocks = code_lens.all_block_ids();
+            if !all_blocks.is_empty() {
+                self.remove_blocks(all_blocks, None, cx);
+            }
+            cx.notify();
+        }
+        self.refresh_code_lens_task = Task::ready(());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::{
+        sync::{Arc, Mutex},
+        time::Duration,
+    };
+
+    use collections::HashSet;
+    use futures::StreamExt;
+    use gpui::TestAppContext;
+    use settings::CodeLens;
+    use util::path;
+
+    use crate::{
+        Editor,
+        editor_tests::{init_test, update_test_editor_settings},
+        test::editor_lsp_test_context::EditorLspTestContext,
+    };
+
+    #[gpui::test]
+    async fn test_code_lens_blocks(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 mut code_lens_request =
+            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(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: "2 references".to_owned(),
+                            command: "lens_cmd".to_owned(),
+                            arguments: None,
+                        }),
+                        data: None,
+                    },
+                    lsp::CodeLens {
+                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
+                        command: Some(lsp::Command {
+                            title: "0 references".to_owned(),
+                            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 a code lens request"
+        );
+        cx.run_until_parked();
+
+        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+            assert_eq!(
+                editor.code_lens_enabled(),
+                true,
+                "code lens should be enabled"
+            );
+            let total_blocks: usize = editor
+                .code_lens
+                .as_ref()
+                .map(|s| s.block_ids.values().map(|v| v.len()).sum())
+                .unwrap_or(0);
+            assert_eq!(total_blocks, 2, "Should have inserted two code lens blocks");
+        });
+    }
+
+    #[gpui::test]
+    async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+
+        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;
+
+        cx.lsp
+            .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
+                panic!("Should not request code lenses when disabled");
+            });
+
+        cx.set_state("Λ‡function hello() {}");
+        cx.run_until_parked();
+
+        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+            assert_eq!(
+                editor.code_lens_enabled(),
+                false,
+                "code lens should not be enabled when setting is off"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_code_lens_toggling(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 mut code_lens_request =
+            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(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: "1 reference".to_owned(),
+                        command: "lens_cmd".to_owned(),
+                        arguments: None,
+                    }),
+                    data: None,
+                }]))
+            });
+
+        cx.set_state("Λ‡function hello() {}");
+
+        assert!(
+            code_lens_request.next().await.is_some(),
+            "should have received a code lens request"
+        );
+        cx.run_until_parked();
+
+        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+            assert_eq!(
+                editor.code_lens_enabled(),
+                true,
+                "code lens should be enabled"
+            );
+            let total_blocks: usize = editor
+                .code_lens
+                .as_ref()
+                .map(|s| s.block_ids.values().map(|v| v.len()).sum())
+                .unwrap_or(0);
+            assert_eq!(total_blocks, 1, "Should have one code lens block");
+        });
+
+        cx.update_editor(|editor, _window, cx| {
+            editor.clear_code_lenses(cx);
+        });
+
+        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+            assert_eq!(
+                editor.code_lens_enabled(),
+                false,
+                "code lens should be disabled after clearing"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_code_lens_resolve(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: Some(true),
+                }),
+                ..lsp::ServerCapabilities::default()
+            },
+            cx,
+        )
+        .await;
+
+        let mut code_lens_request =
+            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
+                Ok(Some(vec![
+                    lsp::CodeLens {
+                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
+                        command: None,
+                        data: Some(serde_json::json!({"id": "lens_1"})),
+                    },
+                    lsp::CodeLens {
+                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
+                        command: None,
+                        data: Some(serde_json::json!({"id": "lens_2"})),
+                    },
+                ]))
+            });
+
+        cx.lsp
+            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
+                let id = lens
+                    .data
+                    .as_ref()
+                    .and_then(|d| d.get("id"))
+                    .and_then(|v| v.as_str())
+                    .unwrap_or("unknown");
+                let title = match id {
+                    "lens_1" => "3 references",
+                    "lens_2" => "1 implementation",
+                    _ => "unknown",
+                };
+                Ok(lsp::CodeLens {
+                    command: Some(lsp::Command {
+                        title: title.to_owned(),
+                        command: format!("resolved_{id}"),
+                        arguments: None,
+                    }),
+                    ..lens
+                })
+            });
+
+        cx.set_state("Λ‡function hello() {}\nfunction world() {}");
+
+        assert!(
+            code_lens_request.next().await.is_some(),
+            "should have received a code lens request"
+        );
+        cx.run_until_parked();
+
+        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+            let total_blocks: usize = editor
+                .code_lens
+                .as_ref()
+                .map(|s| s.block_ids.values().map(|v| v.len()).sum())
+                .unwrap_or(0);
+            assert_eq!(
+                total_blocks, 2,
+                "Unresolved lenses should have been resolved and displayed"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_code_lens_resolve_only_visible(cx: &mut TestAppContext) {
+        init_test(cx, |_| {});
+        update_test_editor_settings(cx, &|settings| {
+            settings.code_lens = Some(CodeLens::On);
+        });
+
+        let line_count: u32 = 100;
+        let lens_every: u32 = 10;
+        let lines = (0..line_count)
+            .map(|i| format!("function func_{i}() {{}}"))
+            .collect::<Vec<_>>()
+            .join("\n");
+
+        let lens_lines = (0..line_count)
+            .filter(|i| i % lens_every == 0)
+            .collect::<Vec<_>>();
+
+        let resolved_lines = Arc::new(Mutex::new(Vec::<u32>::new()));
+
+        let fs = project::FakeFs::new(cx.executor());
+        fs.insert_tree(path!("/dir"), serde_json::json!({ "main.ts": lines }))
+            .await;
+
+        let project = project::Project::test(fs, [path!("/dir").as_ref()], cx).await;
+        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
+            workspace::MultiWorkspace::test_new(project.clone(), window, cx)
+        });
+        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "TypeScript".into(),
+                matcher: language::LanguageMatcher {
+                    path_suffixes: vec!["ts".to_string()],
+                    ..language::LanguageMatcher::default()
+                },
+                ..language::LanguageConfig::default()
+            },
+            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+        )));
+
+        let mut fake_servers = language_registry.register_fake_lsp(
+            "TypeScript",
+            language::FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    code_lens_provider: Some(lsp::CodeLensOptions {
+                        resolve_provider: Some(true),
+                    }),
+                    ..lsp::ServerCapabilities::default()
+                },
+                ..language::FakeLspAdapter::default()
+            },
+        );
+
+        let editor = workspace
+            .update_in(cx, |workspace, window, cx| {
+                workspace.open_abs_path(
+                    std::path::PathBuf::from(path!("/dir/main.ts")),
+                    workspace::OpenOptions::default(),
+                    window,
+                    cx,
+                )
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let fake_server = fake_servers.next().await.unwrap();
+
+        let lens_lines_for_handler = lens_lines.clone();
+        fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
+            let lens_lines = lens_lines_for_handler.clone();
+            async move {
+                Ok(Some(
+                    lens_lines
+                        .iter()
+                        .map(|&line| lsp::CodeLens {
+                            range: lsp::Range::new(
+                                lsp::Position::new(line, 0),
+                                lsp::Position::new(line, 10),
+                            ),
+                            command: None,
+                            data: Some(serde_json::json!({ "line": line })),
+                        })
+                        .collect(),
+                ))
+            }
+        });
+
+        {
+            let resolved_lines = resolved_lines.clone();
+            fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
+                move |lens, _| {
+                    let resolved_lines = resolved_lines.clone();
+                    async move {
+                        let line = lens
+                            .data
+                            .as_ref()
+                            .and_then(|d| d.get("line"))
+                            .and_then(|v| v.as_u64())
+                            .unwrap() as u32;
+                        resolved_lines.lock().unwrap().push(line);
+                        Ok(lsp::CodeLens {
+                            command: Some(lsp::Command {
+                                title: format!("{line} references"),
+                                command: format!("show_refs_{line}"),
+                                arguments: None,
+                            }),
+                            ..lens
+                        })
+                    }
+                },
+            );
+        }
+
+        cx.executor().advance_clock(Duration::from_millis(500));
+        cx.run_until_parked();
+
+        let initial_resolved = resolved_lines
+            .lock()
+            .unwrap()
+            .drain(..)
+            .collect::<HashSet<_>>();
+        assert_eq!(
+            initial_resolved,
+            HashSet::from_iter([0, 10, 20, 30, 40]),
+            "Only lenses visible at the top should be resolved"
+        );
+
+        editor.update_in(cx, |editor, window, cx| {
+            editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
+        });
+        cx.executor().advance_clock(Duration::from_millis(500));
+        cx.run_until_parked();
+
+        let after_scroll_resolved = resolved_lines
+            .lock()
+            .unwrap()
+            .drain(..)
+            .collect::<HashSet<_>>();
+        assert_eq!(
+            after_scroll_resolved,
+            HashSet::from_iter([60, 70, 80, 90]),
+            "Only newly visible lenses at the bottom should be resolved, not middle ones"
+        );
+    }
+}

crates/editor/src/editor.rs πŸ”—

@@ -16,6 +16,7 @@ pub mod blink_manager;
 mod bracket_colorization;
 mod clangd_ext;
 pub mod code_context_menus;
+mod code_lens;
 pub mod display_map;
 mod document_colors;
 mod document_symbols;
@@ -98,6 +99,7 @@ use code_context_menus::{
     AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
     CompletionsMenu, ContextMenuOrigin,
 };
+use code_lens::CodeLensState;
 use collections::{BTreeMap, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
 use dap::TelemetrySpawnLocation;
@@ -111,7 +113,7 @@ use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
 use element::{LineWithInvisibles, PositionMap, layout_line};
 use futures::{
     FutureExt,
-    future::{self, Shared, join},
+    future::{self, Shared},
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::blame::{GitBlame, GlobalBlameRenderer};
@@ -1338,8 +1340,10 @@ pub struct Editor {
 
     selection_drag_state: SelectionDragState,
     colors: Option<LspColorData>,
+    code_lens: Option<CodeLensState>,
     post_scroll_update: Task<()>,
     refresh_colors_task: Task<()>,
+    refresh_code_lens_task: Task<()>,
     use_document_folding_ranges: bool,
     refresh_folding_ranges_task: Task<()>,
     inlay_hints: Option<LspInlayHintData>,
@@ -2162,7 +2166,7 @@ impl Editor {
                 window,
                 |editor, _, event, window, cx| match event {
                     project::Event::RefreshCodeLens => {
-                        // we always query lens with actions, without storing them, always refreshing them
+                        editor.refresh_code_lenses(None, window, cx);
                     }
                     project::Event::RefreshInlayHints {
                         server_id,
@@ -2591,7 +2595,9 @@ impl Editor {
             runnables: RunnableData::new(),
             pull_diagnostics_task: Task::ready(()),
             colors: None,
+            code_lens: None,
             refresh_colors_task: Task::ready(()),
+            refresh_code_lens_task: Task::ready(()),
             use_document_folding_ranges: false,
             refresh_folding_ranges_task: Task::ready(()),
             inlay_hints: None,
@@ -2763,6 +2769,9 @@ impl Editor {
             editor.colors = Some(LspColorData::new(cx));
             editor.use_document_folding_ranges = true;
             editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings));
+            if EditorSettings::get_global(cx).code_lens.inline() {
+                editor.code_lens = Some(CodeLensState::default());
+            }
 
             if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
                 editor.register_buffer(buffer.read(cx).remote_id(), cx);
@@ -7207,6 +7216,10 @@ impl Editor {
                 })
             }
             CodeActionsItem::CodeAction { action, provider } => {
+                if code_lens::try_handle_client_command(&action, self, &workspace, window, cx) {
+                    return Some(Task::ready(Ok(())));
+                }
+
                 let apply_code_action =
                     provider.apply_code_action(buffer, action, true, window, cx);
                 let workspace = workspace.downgrade();
@@ -25162,6 +25175,12 @@ impl Editor {
                 self.refresh_document_colors(None, window, cx);
             }
 
+            let code_lens_inline = EditorSettings::get_global(cx).code_lens.inline();
+            let was_inline = self.code_lens.is_some();
+            if code_lens_inline != was_inline {
+                self.toggle_code_lens(code_lens_inline, window, cx);
+            }
+
             self.refresh_inlay_hints(
                 InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
                     self.selections.newest_anchor().head(),
@@ -26334,6 +26353,7 @@ impl Editor {
         self.refresh_semantic_tokens(for_buffer, None, cx);
         self.refresh_document_colors(for_buffer, window, cx);
         self.refresh_folding_ranges(for_buffer, window, cx);
+        self.refresh_code_lenses(for_buffer, window, cx);
         self.refresh_document_symbols(for_buffer, cx);
     }
 
@@ -26504,6 +26524,7 @@ impl Editor {
         self.register_visible_buffers(cx);
         self.colorize_brackets(false, cx);
         self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+        self.resolve_visible_code_lenses(cx);
         if !self.buffer().read(cx).is_singleton() || self.needs_initial_data_update {
             self.needs_initial_data_update = false;
             self.update_lsp_data(None, window, cx);
@@ -27760,21 +27781,22 @@ impl CodeActionProvider for Entity<Project> {
         cx: &mut App,
     ) -> Task<Result<Vec<CodeAction>>> {
         self.update(cx, |project, cx| {
-            let code_lens_actions = project.code_lens_actions(buffer, range.clone(), cx);
+            let code_lens_actions = if EditorSettings::get_global(cx).code_lens.show_in_menu() {
+                Some(project.code_lens_actions(buffer, range.clone(), cx))
+            } else {
+                None
+            };
             let code_actions = project.code_actions(buffer, range, None, cx);
             cx.background_spawn(async move {
-                let (code_lens_actions, code_actions) = join(code_lens_actions, code_actions).await;
-                Ok(code_lens_actions
-                    .context("code lens fetch")?
-                    .into_iter()
-                    .flatten()
-                    .chain(
-                        code_actions
-                            .context("code action fetch")?
-                            .into_iter()
-                            .flatten(),
-                    )
-                    .collect())
+                let code_lens_actions = match code_lens_actions {
+                    Some(task) => task.await.context("code lens fetch")?.unwrap_or_default(),
+                    None => Vec::new(),
+                };
+                let code_actions = code_actions
+                    .await
+                    .context("code action fetch")?
+                    .unwrap_or_default();
+                Ok(code_lens_actions.into_iter().chain(code_actions).collect())
             })
         })
     }

crates/editor/src/editor_settings.rs πŸ”—

@@ -4,7 +4,7 @@ use gpui::App;
 use language::CursorShape;
 use project::project_settings::DiagnosticSeverity;
 pub use settings::{
-    CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DiffViewStyle, DisplayIn,
+    CodeLens, CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DiffViewStyle, DisplayIn,
     DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, HideMouseMode,
     MinimapThumb, MinimapThumbBorder, MultiCursorModifier, ScrollBeyondLastLine,
     ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
@@ -58,6 +58,7 @@ pub struct EditorSettings {
     pub diagnostics_max_severity: Option<DiagnosticSeverity>,
     pub inline_code_actions: bool,
     pub drag_and_drop_selection: DragAndDropSelection,
+    pub code_lens: CodeLens,
     pub lsp_document_colors: DocumentColorsRenderMode,
     pub minimum_contrast_for_highlights: f32,
     pub completion_menu_scrollbar: ShowScrollbar,
@@ -295,6 +296,7 @@ impl Settings for EditorSettings {
                 enabled: drag_and_drop_selection.enabled.unwrap(),
                 delay: drag_and_drop_selection.delay.unwrap(),
             },
+            code_lens: editor.code_lens.unwrap(),
             lsp_document_colors: editor.lsp_document_colors.unwrap(),
             minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
             completion_menu_scrollbar: editor

crates/editor/src/editor_tests.rs πŸ”—

@@ -28396,6 +28396,9 @@ 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, |_| {});
+    update_test_editor_settings(cx, &|settings| {
+        settings.code_lens = Some(settings::CodeLens::Menu);
+    });
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
@@ -28488,15 +28491,6 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
                     }),
                     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 {

crates/editor/src/element.rs πŸ”—

@@ -502,6 +502,7 @@ impl EditorElement {
         register_action(editor, window, Editor::toggle_relative_line_numbers);
         register_action(editor, window, Editor::toggle_indent_guides);
         register_action(editor, window, Editor::toggle_inlay_hints);
+        register_action(editor, window, Editor::toggle_code_lens_action);
         register_action(editor, window, Editor::toggle_semantic_highlights);
         register_action(editor, window, Editor::toggle_edit_predictions);
         if editor.read(cx).diagnostics_enabled() {

crates/language/src/language.rs πŸ”—

@@ -202,6 +202,17 @@ pub static PLAIN_TEXT: LazyLock<Arc<Language>> = LazyLock::new(|| {
     ))
 });
 
+/// Commands that the client (editor) handles locally rather than forwarding
+/// to the language server. Servers embed these in code lens and code action
+/// responses when they want the editor to perform a well-known UI action.
+#[derive(Debug, Clone)]
+pub enum ClientCommand {
+    /// Open a location list (references panel / peek view).
+    ShowLocations,
+    /// Schedule a task from an LSP command's arguments.
+    ScheduleTask(task::TaskTemplate),
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Location {
     pub buffer: Entity<Buffer>,
@@ -555,6 +566,14 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
         Ok(original)
     }
 
+    fn client_command(
+        &self,
+        _command_name: &str,
+        _arguments: &[serde_json::Value],
+    ) -> Option<ClientCommand> {
+        None
+    }
+
     /// Method only implemented by the default JSON language server adapter.
     /// Used to provide dynamic reloading of the JSON schemas used to
     /// provide autocompletion and diagnostics in Zed setting and keybind

crates/languages/src/go.rs πŸ”—

@@ -225,6 +225,9 @@ impl LspAdapter for GoLspAdapter {
                 "parameterNames": true,
                 "rangeVariableTypes": true
             },
+            "codelenses": {
+                "test": true
+            },
             "semanticTokens": semantic_tokens_enabled
         });
 
@@ -438,6 +441,19 @@ impl LspAdapter for GoLspAdapter {
         ))
     }
 
+    fn client_command(
+        &self,
+        command_name: &str,
+        arguments: &[serde_json::Value],
+    ) -> Option<ClientCommand> {
+        if let "gopls.run_tests" = command_name {
+            let template = go_test_task_template(arguments.first()?)?;
+            Some(ClientCommand::ScheduleTask(template))
+        } else {
+            None
+        }
+    }
+
     fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
         static REGEX: LazyLock<Regex> =
             LazyLock::new(|| Regex::new(r"(?m)\n\s*").expect("Failed to create REGEX"));
@@ -445,6 +461,74 @@ impl LspAdapter for GoLspAdapter {
     }
 }
 
+fn json_string_array(value: &serde_json::Value, key: &str) -> Vec<String> {
+    value
+        .get(key)
+        .and_then(|v| v.as_array())
+        .map(|arr| {
+            arr.iter()
+                .filter_map(|v| v.as_str().map(String::from))
+                .collect()
+        })
+        .unwrap_or_default()
+}
+
+fn go_test_task_template(arg: &serde_json::Value) -> Option<task::TaskTemplate> {
+    let tests = json_string_array(arg, "Tests");
+    let benchmarks = json_string_array(arg, "Benchmarks");
+    if tests.is_empty() && benchmarks.is_empty() {
+        return None;
+    }
+
+    let mut go_args = vec!["test".to_string(), "-test.fullpath=true".to_string()];
+
+    if tests.is_empty() {
+        go_args.push("-benchmem".to_string());
+        go_args.push("-run=^$".to_string());
+    } else {
+        go_args.push("-timeout".to_string());
+        go_args.push("30s".to_string());
+        go_args.push("-run".to_string());
+        if tests.len() == 1 {
+            go_args.push(format!("^{}$", tests[0]));
+        } else {
+            go_args.push(format!("^({})$", tests.join("|")));
+        }
+    }
+
+    if !benchmarks.is_empty() {
+        go_args.push("-bench".to_string());
+        if benchmarks.len() == 1 {
+            go_args.push(format!("^{}$", benchmarks[0]));
+        } else {
+            go_args.push(format!("^({})$", benchmarks.join("|")));
+        }
+    }
+
+    go_args.push(".".to_string());
+
+    let label = if !tests.is_empty() {
+        format!("go test {}", tests.join(", "))
+    } else {
+        format!("go bench {}", benchmarks.join(", "))
+    };
+
+    let cwd = arg
+        .get("URI")
+        .and_then(|v| v.as_str())
+        .and_then(|uri| uri.strip_prefix("file://"))
+        .and_then(|path| std::path::Path::new(path).parent())
+        .map(|p| p.to_string_lossy().into_owned());
+
+    Some(task::TaskTemplate {
+        label,
+        command: "go".to_string(),
+        args: go_args,
+        cwd,
+        ..task::TaskTemplate::default()
+    })
+}
+
 fn parse_version_output(output: &Output) -> Result<&str> {
     let version_stdout =
         str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;

crates/languages/src/rust.rs πŸ”—

@@ -9,6 +9,7 @@ use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
 use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
 pub use language::*;
 use lsp::{InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
+use project::lsp_store::lsp_ext_command;
 use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
 use project::project_settings::ProjectSettings;
 use regex::Regex;
@@ -608,21 +609,61 @@ impl LspAdapter for RustLspAdapter {
             .lsp
             .get(&SERVER_NAME)
             .is_some_and(|s| s.enable_lsp_tasks);
-        if enable_lsp_tasks {
-            let experimental = json!({
-                "runnables": {
-                    "kinds": [ "cargo", "shell" ],
-                },
-            });
-            if let Some(original_experimental) = &mut original.capabilities.experimental {
-                merge_json_value_into(experimental, original_experimental);
-            } else {
-                original.capabilities.experimental = Some(experimental);
+
+        let mut experimental = json!({
+            "commands": {
+                "commands": [
+                    "rust-analyzer.showReferences",
+                    "rust-analyzer.gotoLocation",
+                    "rust-analyzer.triggerParameterHints",
+                    "rust-analyzer.rename",
+                ]
             }
+        });
+
+        if enable_lsp_tasks {
+            merge_json_value_into(
+                json!({
+                    "runnables": {
+                        "kinds": [ "cargo", "shell" ],
+                    },
+                    "commands": {
+                        "commands": [
+                            "rust-analyzer.runSingle",
+                        ]
+                    }
+                }),
+                &mut experimental,
+            );
+        }
+
+        if let Some(original_experimental) = &mut original.capabilities.experimental {
+            merge_json_value_into(experimental, original_experimental);
+        } else {
+            original.capabilities.experimental = Some(experimental);
         }
 
         Ok(original)
     }
+
+    fn client_command(
+        &self,
+        command_name: &str,
+        arguments: &[serde_json::Value],
+    ) -> Option<ClientCommand> {
+        match command_name {
+            "rust-analyzer.showReferences" => Some(ClientCommand::ShowLocations),
+            "rust-analyzer.runSingle" => {
+                let first_arg = arguments.first()?;
+                let runnable =
+                    serde_json::from_value::<lsp_ext_command::Runnable>(first_arg.clone()).ok()?;
+                let template =
+                    lsp_ext_command::runnable_to_task_template(runnable.label, runnable.args);
+                Some(ClientCommand::ScheduleTask(template))
+            }
+            _ => None,
+        }
+    }
 }
 
 impl LspInstaller for RustLspAdapter {

crates/languages/src/vtsls.rs πŸ”—

@@ -269,6 +269,15 @@ impl LspAdapter for VtslsLspAdapter {
                     "enabled": true
                 }
             },
+            "implementationsCodeLens": {
+                "enabled": true,
+                "showOnAllClassMethods": true,
+                "showOnInterfaceMethods": true
+            },
+            "referencesCodeLens": {
+                "enabled": true,
+                "showOnAllFunctions": true
+            },
             "tsserver": {
                 "maxTsServerMemory": 8092
             },

crates/project/src/lsp_command.rs πŸ”—

@@ -33,6 +33,7 @@ use lsp::{
     OneOf, RenameOptions, ServerCapabilities,
 };
 use serde_json::Value;
+
 use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
 use std::{
     cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
@@ -3851,45 +3852,27 @@ impl LspCommand for GetCodeLens {
     async fn response_from_lsp(
         self,
         message: Option<Vec<lsp::CodeLens>>,
-        lsp_store: Entity<LspStore>,
+        _lsp_store: Entity<LspStore>,
         buffer: Entity<Buffer>,
         server_id: LanguageServerId,
         cx: AsyncApp,
     ) -> anyhow::Result<Vec<CodeAction>> {
         let snapshot = buffer.read_with(&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()
+        let code_lenses = message.unwrap_or_default();
+
+        Ok(code_lenses
             .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);
+                let resolved = code_lens.command.is_some();
                 CodeAction {
                     server_id,
                     range,
                     lsp_action: LspAction::CodeLens(code_lens),
-                    resolved: false,
+                    resolved,
                 }
             })
             .collect())

crates/project/src/lsp_store.rs πŸ”—

@@ -1087,6 +1087,7 @@ impl LocalLspStore {
                     let mut cx = cx.clone();
                     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 {
@@ -5572,20 +5573,20 @@ impl LspStore {
                     .await
                     .context("resolving a code action")?;
                 if let Some(edit) = action.lsp_action.edit()
-                    && (edit.changes.is_some() || edit.document_changes.is_some()) {
-                        return LocalLspStore::deserialize_workspace_edit(
-                            this.upgrade().context("no app present")?,
-                            edit.clone(),
-                            push_to_history,
-
-                            lang_server.clone(),
-                            cx,
-                        )
-                        .await;
-                    }
+                    && (edit.changes.is_some() || edit.document_changes.is_some())
+                {
+                    return LocalLspStore::deserialize_workspace_edit(
+                        this.upgrade().context("no app present")?,
+                        edit.clone(),
+                        push_to_history,
+                        lang_server.clone(),
+                        cx,
+                    )
+                    .await;
+                }
 
                 let Some(command) = action.lsp_action.command() else {
-                    return Ok(ProjectTransaction::default())
+                    return Ok(ProjectTransaction::default());
                 };
 
                 let server_capabilities = lang_server.capabilities();
@@ -5596,15 +5597,18 @@ impl LspStore {
                     .unwrap_or_default();
 
                 if !available_commands.contains(&command.command) {
-                    log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command);
-                    return Ok(ProjectTransaction::default())
+                    log::warn!(
+                        "Skipping executeCommand for {}, not listed in language server capabilities",
+                        command.command
+                    );
+                    return Ok(ProjectTransaction::default());
                 }
 
-                let request_timeout = cx.update(|app|
+                let request_timeout = cx.update(|app| {
                     ProjectSettings::get_global(app)
-                    .global_lsp_settings
-                    .get_request_timeout()
-                );
+                        .global_lsp_settings
+                        .get_request_timeout()
+                });
 
                 this.update(cx, |this, _| {
                     this.as_local_mut()
@@ -5614,12 +5618,16 @@ impl LspStore {
                 })?;
 
                 let _result = lang_server
-                    .request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
-                        command: command.command.clone(),
-                        arguments: command.arguments.clone().unwrap_or_default(),
-                        ..lsp::ExecuteCommandParams::default()
-                    }, request_timeout)
-                    .await.into_response()
+                    .request::<lsp::request::ExecuteCommand>(
+                        lsp::ExecuteCommandParams {
+                            command: command.command.clone(),
+                            arguments: command.arguments.clone().unwrap_or_default(),
+                            ..lsp::ExecuteCommandParams::default()
+                        },
+                        request_timeout,
+                    )
+                    .await
+                    .into_response()
                     .context("execute command")?;
 
                 return this.update(cx, |this, _| {
@@ -11946,12 +11954,17 @@ impl LspStore {
         &self,
         id: LanguageServerId,
     ) -> Option<Arc<CachedLspAdapter>> {
-        self.as_local()
-            .and_then(|local| local.language_servers.get(&id))
-            .and_then(|language_server_state| match language_server_state {
-                LanguageServerState::Running { adapter, .. } => Some(adapter.clone()),
-                _ => None,
-            })
+        if let Some(local) = self.as_local()
+            && let Some(LanguageServerState::Running { adapter, .. }) =
+                local.language_servers.get(&id)
+        {
+            return Some(adapter.clone());
+        }
+        // In remote (SSH/collab) mode there are no local `language_servers`, but
+        // `language_server_statuses` is kept in sync with the upstream and carries each
+        // server's registered name, which is enough to look the adapter up in the registry.
+        let name = &self.language_server_statuses.get(&id)?.name;
+        self.languages.adapter_for_name(name)
     }
 
     pub(super) fn update_local_worktree_language_servers(

crates/project/src/lsp_store/code_lens.rs πŸ”—

@@ -1,3 +1,4 @@
+use std::ops::Range;
 use std::sync::Arc;
 
 use anyhow::{Context as _, Result};
@@ -8,14 +9,15 @@ use futures::{
     future::{Shared, join_all},
 };
 use gpui::{AppContext as _, AsyncApp, Context, Entity, Task};
-use language::Buffer;
+use language::{Anchor, Buffer, ToOffset as _};
 use lsp::LanguageServerId;
 use rpc::{TypedEnvelope, proto};
 use settings::Settings as _;
 use std::time::Duration;
+use text::OffsetRangeExt as _;
 
 use crate::{
-    CodeAction, LspStore, LspStoreEvent,
+    CodeAction, LspAction, LspStore, LspStoreEvent, Project,
     lsp_command::{GetCodeLens, LspCommand as _},
     project_settings::ProjectSettings,
 };
@@ -36,10 +38,44 @@ impl CodeLensData {
 }
 
 impl LspStore {
+    pub(super) fn invalidate_code_lens(&mut self) {
+        for lsp_data in self.lsp_data.values_mut() {
+            lsp_data.code_lens = None;
+        }
+    }
+
+    /// Fetches and returns all code lenses for the buffer.
+    ///
+    /// Resolution of individual lenses is the caller's responsibility; see
+    /// [`LspStore::resolve_visible_code_lenses`].
     pub fn code_lens_actions(
         &mut self,
         buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
+    ) -> Task<Result<Option<Vec<CodeAction>>>> {
+        let buffer_id = buffer.read(cx).remote_id();
+        let fetch_task = self.fetch_code_lenses(buffer, cx);
+
+        cx.spawn(async move |lsp_store, cx| {
+            fetch_task
+                .await
+                .map_err(|e| anyhow::anyhow!("code lens fetch failed: {e:#}"))?;
+
+            let actions = lsp_store.read_with(cx, |lsp_store, _| {
+                lsp_store
+                    .lsp_data
+                    .get(&buffer_id)
+                    .and_then(|data| data.code_lens.as_ref())
+                    .map(|code_lens| code_lens.lens.values().flatten().cloned().collect())
+            })?;
+            Ok(actions)
+        })
+    }
+
+    fn fetch_code_lenses(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
     ) -> CodeLensTask {
         let version_queried_for = buffer.read(cx).version();
         let buffer_id = buffer.read(cx).remote_id();
@@ -83,7 +119,9 @@ impl LspStore {
                     .timer(Duration::from_millis(30))
                     .await;
                 let fetched_lens = lsp_store
-                    .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx))
+                    .update(cx, |lsp_store, cx| {
+                        lsp_store.fetch_code_lens_for_buffer(&buffer, cx)
+                    })
                     .map_err(Arc::new)?
                     .await
                     .context("fetching code lens")
@@ -107,7 +145,7 @@ impl LspStore {
                 };
 
                 lsp_store
-                    .update(cx, |lsp_store, _| {
+                    .update(cx, |lsp_store, cx| {
                         let lsp_data = lsp_store.current_lsp_data(buffer_id)?;
                         let code_lens = lsp_data.code_lens.as_mut()?;
                         if let Some(fetched_lens) = fetched_lens {
@@ -120,6 +158,11 @@ impl LspStore {
                                 lsp_data.buffer_version = query_version_queried_for;
                                 code_lens.lens = fetched_lens;
                             }
+                            let snapshot = buffer.read(cx).snapshot();
+                            for actions in code_lens.lens.values_mut() {
+                                actions
+                                    .sort_by(|a, b| a.range.start.cmp(&b.range.start, &snapshot));
+                            }
                         }
                         code_lens.update = None;
                         Some(code_lens.lens.values().flatten().cloned().collect())
@@ -131,7 +174,7 @@ impl LspStore {
         new_task
     }
 
-    pub(super) fn fetch_code_lens(
+    fn fetch_code_lens_for_buffer(
         &mut self,
         buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
@@ -202,6 +245,112 @@ impl LspStore {
         }
     }
 
+    pub fn resolve_visible_code_lenses(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        visible_range: Range<Anchor>,
+        cx: &mut Context<Self>,
+    ) -> Task<Vec<CodeAction>> {
+        let buffer_id = buffer.read(cx).remote_id();
+        let snapshot = buffer.read(cx).snapshot();
+        let visible_start = visible_range.start.to_offset(&snapshot);
+        let visible_end = visible_range.end.to_offset(&snapshot);
+
+        let Some(code_lens) = self
+            .lsp_data
+            .get(&buffer_id)
+            .and_then(|data| data.code_lens.as_ref())
+        else {
+            return Task::ready(Vec::new());
+        };
+
+        let capable_servers = code_lens
+            .lens
+            .keys()
+            .filter_map(|server_id| {
+                let server = self.language_server_for_id(*server_id)?;
+                GetCodeLens::can_resolve_lens(&server.capabilities())
+                    .then_some((*server_id, server))
+            })
+            .collect::<HashMap<_, _>>();
+        if capable_servers.is_empty() {
+            return Task::ready(Vec::new());
+        }
+
+        let to_resolve = code_lens
+            .lens
+            .iter()
+            .flat_map(|(server_id, actions)| {
+                let start_idx =
+                    actions.partition_point(|a| a.range.start.to_offset(&snapshot) < visible_start);
+                let end_idx = start_idx
+                    + actions[start_idx..]
+                        .partition_point(|a| a.range.start.to_offset(&snapshot) <= visible_end);
+                actions[start_idx..end_idx].iter().enumerate().filter_map(
+                    move |(local_idx, action)| {
+                        let LspAction::CodeLens(lens) = &action.lsp_action else {
+                            return None;
+                        };
+                        if lens.command.is_some() {
+                            return None;
+                        }
+                        Some((*server_id, start_idx + local_idx, lens.clone()))
+                    },
+                )
+            })
+            .collect::<Vec<_>>();
+        if to_resolve.is_empty() {
+            return Task::ready(Vec::new());
+        }
+
+        let request_timeout = ProjectSettings::get_global(cx)
+            .global_lsp_settings
+            .get_request_timeout();
+
+        cx.spawn(async move |lsp_store, cx| {
+            let mut resolved = Vec::new();
+            for (server_id, index, lens) in to_resolve {
+                let Some(server) = capable_servers.get(&server_id) else {
+                    continue;
+                };
+                match server
+                    .request::<lsp::request::CodeLensResolve>(lens, request_timeout)
+                    .await
+                    .into_response()
+                {
+                    Ok(resolved_lens) => resolved.push((server_id, index, resolved_lens)),
+                    Err(e) => log::warn!("Failed to resolve code lens: {e:#}"),
+                }
+            }
+            if resolved.is_empty() {
+                return Vec::new();
+            }
+
+            lsp_store
+                .update(cx, |lsp_store, _| {
+                    let Some(code_lens) = lsp_store
+                        .lsp_data
+                        .get_mut(&buffer_id)
+                        .and_then(|data| data.code_lens.as_mut())
+                    else {
+                        return Vec::new();
+                    };
+                    let mut newly_resolved = Vec::new();
+                    for (server_id, index, resolved_lens) in resolved {
+                        if let Some(actions) = code_lens.lens.get_mut(&server_id) {
+                            if let Some(action) = actions.get_mut(index) {
+                                action.resolved = true;
+                                action.lsp_action = LspAction::CodeLens(resolved_lens);
+                                newly_resolved.push(action.clone());
+                            }
+                        }
+                    }
+                    newly_resolved
+                })
+                .unwrap_or_default()
+        })
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn forget_code_lens_task(&mut self, buffer_id: text::BufferId) -> Option<CodeLensTask> {
         Some(
@@ -216,13 +365,59 @@ impl LspStore {
     }
 
     pub(super) async fn handle_refresh_code_lens(
-        this: Entity<Self>,
+        lsp_store: Entity<Self>,
         _: TypedEnvelope<proto::RefreshCodeLens>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        this.update(&mut cx, |_, cx| {
+        lsp_store.update(&mut cx, |lsp_store, cx| {
+            lsp_store.invalidate_code_lens();
             cx.emit(LspStoreEvent::RefreshCodeLens);
         });
         Ok(proto::Ack {})
     }
 }
+
+impl Project {
+    pub fn code_lens_actions(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        range: Range<Anchor>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Option<Vec<CodeAction>>>> {
+        let snapshot = buffer.read(cx).snapshot();
+        let range = range.to_point(&snapshot);
+        let range_start = snapshot.anchor_before(range.start);
+        let range_end = if range.start == range.end {
+            range_start
+        } else {
+            snapshot.anchor_after(range.end)
+        };
+        let range = range_start..range_end;
+        let lsp_store = self.lsp_store();
+        let fetch_task =
+            lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
+        let buffer = buffer.clone();
+        cx.spawn(async move |_, cx| {
+            let mut actions = fetch_task.await?;
+            if let Some(actions) = &mut actions {
+                let resolve_task = lsp_store.update(cx, |lsp_store, cx| {
+                    lsp_store.resolve_visible_code_lenses(&buffer, range.clone(), cx)
+                });
+                let resolved = resolve_task.await;
+                for resolved_action in resolved {
+                    if let Some(action) = actions.iter_mut().find(|a| {
+                        a.server_id == resolved_action.server_id && a.range == resolved_action.range
+                    }) {
+                        *action = resolved_action;
+                    }
+                }
+                let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+                actions.retain(|action| {
+                    range.start.cmp(&action.range.start, &snapshot).is_ge()
+                        && range.end.cmp(&action.range.end, &snapshot).is_le()
+                });
+            }
+            Ok(actions)
+        })
+    }
+}

crates/project/src/lsp_store/lsp_ext_command.rs πŸ”—

@@ -584,6 +584,56 @@ pub struct LspRunnables {
     pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
 }
 
+pub fn runnable_to_task_template(label: String, args: RunnableArgs) -> TaskTemplate {
+    let mut task_template = TaskTemplate::default();
+    task_template.label = label;
+    match args {
+        RunnableArgs::Cargo(cargo) => {
+            match cargo.override_cargo {
+                Some(override_cargo) => {
+                    let mut override_parts = override_cargo.split(" ").map(|s| s.to_string());
+                    task_template.command = override_parts
+                        .next()
+                        .unwrap_or_else(|| override_cargo.clone());
+                    task_template.args.extend(override_parts);
+                }
+                None => task_template.command = "cargo".to_string(),
+            };
+            task_template.env = cargo.environment;
+            task_template.cwd = Some(
+                cargo
+                    .workspace_root
+                    .unwrap_or(cargo.cwd)
+                    .to_string_lossy()
+                    .to_string(),
+            );
+            task_template.args.extend(cargo.cargo_args);
+            if !cargo.executable_args.is_empty() {
+                let shell_kind = task_template.shell.shell_kind(cfg!(windows));
+                task_template.args.push("--".to_string());
+                task_template.args.extend(
+                    cargo
+                        .executable_args
+                        .into_iter()
+                        // rust-analyzer's doctest data may contain things like `X<T>::new`
+                        // which cause shell issues when run as `$SHELL -i -c "cargo test ..."`.
+                        // Escape extra cargo args unconditionally as those are unlikely to contain `~`.
+                        .flat_map(|extra_arg| {
+                            shell_kind.try_quote(&extra_arg).map(|s| s.to_string())
+                        }),
+                );
+            }
+        }
+        RunnableArgs::Shell(shell) => {
+            task_template.command = shell.program;
+            task_template.args = shell.args;
+            task_template.env = shell.environment;
+            task_template.cwd = Some(shell.cwd.to_string_lossy().into_owned());
+        }
+    }
+    task_template
+}
+
 #[async_trait(?Send)]
 impl LspCommand for GetLspRunnables {
     type Response = LspRunnables;
@@ -632,70 +682,7 @@ impl LspCommand for GetLspRunnables {
                 ),
                 None => None,
             };
-            let mut task_template = TaskTemplate::default();
-            task_template.label = runnable.label;
-            match runnable.args {
-                RunnableArgs::Cargo(cargo) => {
-                    match cargo.override_cargo {
-                        Some(override_cargo) => {
-                            let mut override_parts =
-                                override_cargo.split(" ").map(|s| s.to_string());
-                            task_template.command = override_parts
-                                .next()
-                                .unwrap_or_else(|| override_cargo.clone());
-                            task_template.args.extend(override_parts);
-                        }
-                        None => task_template.command = "cargo".to_string(),
-                    };
-                    task_template.env = cargo.environment;
-                    task_template.cwd = Some(
-                        cargo
-                            .workspace_root
-                            .unwrap_or(cargo.cwd)
-                            .to_string_lossy()
-                            .to_string(),
-                    );
-                    task_template.args.extend(cargo.cargo_args);
-                    if !cargo.executable_args.is_empty() {
-                        let shell_kind = task_template.shell.shell_kind(cfg!(windows));
-                        task_template.args.push("--".to_string());
-                        task_template.args.extend(
-                            cargo
-                                .executable_args
-                                .into_iter()
-                                // rust-analyzer's doctest data may be smth. like
-                                // ```
-                                // command: "cargo",
-                                // args: [
-                                //     "test",
-                                //     "--doc",
-                                //     "--package",
-                                //     "cargo-output-parser",
-                                //     "--",
-                                //     "X<T>::new",
-                                //     "--show-output",
-                                // ],
-                                // ```
-                                // and `X<T>::new` will cause troubles if not escaped properly, as later
-                                // the task runs as `$SHELL -i -c "cargo test ..."`.
-                                //
-                                // We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`.
-                                // That bit is not auto-expanded when using single quotes.
-                                // Escape extra cargo args unconditionally as those are unlikely to contain `~`.
-                                .flat_map(|extra_arg| {
-                                    shell_kind.try_quote(&extra_arg).map(|s| s.to_string())
-                                }),
-                        );
-                    }
-                }
-                RunnableArgs::Shell(shell) => {
-                    task_template.command = shell.program;
-                    task_template.args = shell.args;
-                    task_template.env = shell.environment;
-                    task_template.cwd = Some(shell.cwd.to_string_lossy().into_owned());
-                }
-            }
-
+            let task_template = runnable_to_task_template(runnable.label, runnable.args);
             runnables.push((location, task_template));
         }
 

crates/project/src/project.rs πŸ”—

@@ -134,7 +134,7 @@ use std::{
 
 use task_store::TaskStore;
 use terminals::Terminals;
-use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
+use text::{Anchor, BufferId, Point, Rope};
 use toolchain_store::EmptyToolchainStore;
 use util::{
     ResultExt as _, maybe,
@@ -761,7 +761,7 @@ impl LspAction {
         }
     }
 
-    fn edit(&self) -> Option<&lsp::WorkspaceEdit> {
+    pub fn edit(&self) -> Option<&lsp::WorkspaceEdit> {
         match self {
             Self::Action(action) => action.edit.as_ref(),
             Self::Command(_) => None,
@@ -769,7 +769,7 @@ impl LspAction {
         }
     }
 
-    fn command(&self) -> Option<&lsp::Command> {
+    pub fn command(&self) -> Option<&lsp::Command> {
         match self {
             Self::Action(action) => action.command.as_ref(),
             Self::Command(command) => Some(command),
@@ -4385,45 +4385,6 @@ impl Project {
         })
     }
 
-    pub fn code_lens_actions<T: Clone + ToOffset>(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        range: Range<T>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Option<Vec<CodeAction>>>> {
-        let snapshot = buffer.read(cx).snapshot();
-        let range = range.to_point(&snapshot);
-        let range_start = snapshot.anchor_before(range.start);
-        let range_end = if range.start == range.end {
-            range_start
-        } else {
-            snapshot.anchor_after(range.end)
-        };
-        let range = range_start..range_end;
-        let code_lens_actions = self
-            .lsp_store
-            .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
-
-        cx.background_spawn(async move {
-            let mut code_lens_actions = code_lens_actions
-                .await
-                .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?;
-            if let Some(code_lens_actions) = &mut code_lens_actions {
-                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<Buffer>,

crates/remote_server/src/remote_editing_tests.rs πŸ”—

@@ -2371,6 +2371,148 @@ async fn test_remote_external_agent_server(
     );
 }
 
+#[gpui::test]
+async fn test_remote_apply_code_action_skips_unadvertised_command(
+    cx: &mut TestAppContext,
+    server_cx: &mut TestAppContext,
+) {
+    let fs = FakeFs::new(server_cx.executor());
+    fs.insert_tree(
+        path!("/code"),
+        json!({
+            "project1": {
+                ".git": {},
+                "README.md": "# project 1",
+                "src": {
+                    "lib.rs": "fn one() -> usize { 1 }"
+                }
+            },
+        }),
+    )
+    .await;
+
+    let (project, headless) = init_test(&fs, cx, server_cx).await;
+
+    fs.insert_tree(
+        path!("/code/project1/.zed"),
+        json!({
+            "settings.json": r#"
+          {
+            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
+            "lsp": {
+              "rust-analyzer": {
+                "binary": {
+                  "path": "~/.cargo/bin/rust-analyzer"
+                }
+              }
+            }
+          }"#
+        }),
+    )
+    .await;
+
+    cx.update_entity(&project, |project, _| {
+        project.languages().register_test_language(LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".into()],
+                ..Default::default()
+            },
+            ..Default::default()
+        });
+        project.languages().register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
+                name: "rust-analyzer",
+                ..Default::default()
+            },
+        )
+    });
+
+    // Register the fake LSP with an empty execute_command_provider and a handler that panics
+    // if it is ever reached: commands not advertised by the server must be rejected by
+    // `apply_code_action` before dispatching to the language server.
+    let mut fake_lsp = server_cx.update(|cx| {
+        headless.read(cx).languages.register_fake_lsp_server(
+            LanguageServerName("rust-analyzer".into()),
+            lsp::ServerCapabilities {
+                execute_command_provider: Some(lsp::ExecuteCommandOptions {
+                    commands: Vec::new(),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            Some(Box::new(|fake| {
+                fake.set_request_handler::<lsp::request::ExecuteCommand, _, _>(
+                    |params, _| async move {
+                        panic!(
+                            "Unadvertised command {} must not reach the language server",
+                            params.command
+                        );
+                    },
+                );
+            })),
+        )
+    });
+
+    cx.run_until_parked();
+
+    let worktree_id = project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree(path!("/code/project1"), true, cx)
+        })
+        .await
+        .unwrap()
+        .0
+        .read_with(cx, |worktree, _| worktree.id());
+
+    cx.run_until_parked();
+
+    let (buffer, _handle) = project
+        .update(cx, |project, cx| {
+            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
+        })
+        .await
+        .unwrap();
+
+    cx.run_until_parked();
+
+    let _fake_lsp = fake_lsp.next().await.unwrap();
+
+    let server_id = server_cx.read(|cx| {
+        *headless
+            .read(cx)
+            .lsp_store
+            .read(cx)
+            .as_local()
+            .unwrap()
+            .language_servers
+            .keys()
+            .next()
+            .unwrap()
+    });
+    let buffer_id = cx.read(|cx| buffer.read(cx).remote_id());
+
+    let action = project::CodeAction {
+        server_id,
+        range: language::Anchor::min_min_range_for_buffer(buffer_id),
+        lsp_action: project::LspAction::Command(lsp::Command {
+            title: "\u{25b6}\u{fe0e} Run Tests".into(),
+            command: "rust-analyzer.runSingle".into(),
+            arguments: Some(vec![json!({"label": "test-mod tests"})]),
+        }),
+        resolved: true,
+    };
+
+    let transaction = project
+        .update(cx, |project, cx| {
+            project.apply_code_action(buffer.clone(), action, true, cx)
+        })
+        .await
+        .expect("Unadvertised command must not be forwarded to executeCommand");
+    assert_eq!(transaction.0.len(), 0);
+}
+
 pub async fn init_test(
     server_fs: &Arc<FakeFs>,
     cx: &mut TestAppContext,

crates/settings/src/vscode_import.rs πŸ”—

@@ -271,6 +271,7 @@ impl VsCodeSettings {
             hover_popover_sticky: self.read_bool("editor.hover.sticky"),
             hover_popover_hiding_delay: self.read_u64("editor.hover.hidingDelay").map(Into::into),
             inline_code_actions: None,
+            code_lens: None,
             jupyter: None,
             lsp_document_colors: None,
             lsp_highlight_debounce: None,

crates/settings_content/src/editor.rs πŸ”—

@@ -215,6 +215,11 @@ pub struct EditorSettingsContent {
     /// Drag and drop related settings
     pub drag_and_drop_selection: Option<DragAndDropSelectionContent>,
 
+    /// Whether and how to display code lenses from language servers.
+    ///
+    /// Default: "off"
+    pub code_lens: Option<CodeLens>,
+
     /// How to render LSP `textDocument/documentColor` colors in the editor.
     ///
     /// Default: [`DocumentColorsRenderMode::Inlay`]
@@ -461,7 +466,7 @@ pub struct GutterContent {
     pub folds: Option<bool>,
 }
 
-/// How to render LSP `textDocument/documentColor` colors in the editor.
+/// Whether to display code lenses from language servers above code elements.
 #[derive(
     Copy,
     Clone,
@@ -477,6 +482,46 @@ pub struct GutterContent {
     strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
+pub enum CodeLens {
+    /// Do not query and display code lenses.
+    #[default]
+    Off,
+    /// Display code lenses from language servers above code elements.
+    On,
+    /// Display code lenses in the code action menu.
+    Menu,
+}
+
+impl CodeLens {
+    pub fn enabled(&self) -> bool {
+        self != &Self::Off
+    }
+
+    pub fn inline(&self) -> bool {
+        *self == Self::On
+    }
+
+    pub fn show_in_menu(&self) -> bool {
+        *self == Self::Menu
+    }
+}
+
+/// How to render LSP `textDocument/documentColor` colors in the editor.
+#[derive(
+    Debug,
+    Clone,
+    Copy,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
 pub enum DocumentColorsRenderMode {
     /// Do not query and render document colors.
     None,

crates/settings_ui/src/page_data.rs πŸ”—

@@ -8975,6 +8975,20 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
 
     let is_global = active_language().is_none();
 
+    let code_lens_item = [SettingsPageItem::SettingItem(SettingItem {
+        title: "Code Lens",
+        description: "Whether and how to display code lenses from language servers.",
+        field: Box::new(SettingField {
+            json_path: Some("code_lens"),
+            pick: |settings_content| settings_content.editor.code_lens.as_ref(),
+            write: |settings_content, value| {
+                settings_content.editor.code_lens = value;
+            },
+        }),
+        metadata: None,
+        files: USER,
+    })];
+
     let lsp_document_colors_item = [SettingsPageItem::SettingItem(SettingItem {
         title: "LSP Document Colors",
         description: "How to render LSP color previews in the editor.",
@@ -8999,6 +9013,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
             whitespace_section(),
             completions_section(),
             inlay_hints_section(),
+            code_lens_item,
             lsp_document_colors_item,
             tasks_section(),
             miscellaneous_section(),
@@ -9014,6 +9029,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
             whitespace_section(),
             completions_section(),
             inlay_hints_section(),
+            code_lens_item,
             tasks_section(),
             miscellaneous_section(),
         )

crates/settings_ui/src/settings_ui.rs πŸ”—

@@ -536,6 +536,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
         .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
         .add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
+        .add_basic_renderer::<settings::CodeLens>(render_dropdown)
         .add_basic_renderer::<settings::DocumentColorsRenderMode>(render_dropdown)
         .add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
         .add_basic_renderer::<settings::ThemeAppearanceMode>(render_dropdown)

crates/zed/src/zed/quick_action_bar.rs πŸ”—

@@ -112,11 +112,13 @@ impl Render for QuickActionBar {
         let supports_inlay_hints = editor.update(cx, |editor, cx| editor.supports_inlay_hints(cx));
         let supports_semantic_tokens =
             editor.update(cx, |editor, cx| editor.supports_semantic_tokens(cx));
+        let supports_code_lens = editor.update(cx, |editor, cx| editor.supports_code_lens(cx));
         let editor_value = editor.read(cx);
         let selection_menu_enabled = editor_value.selection_menu_enabled(cx);
         let inlay_hints_enabled = editor_value.inlay_hints_enabled();
         let inline_values_enabled = editor_value.inline_values_enabled();
         let semantic_highlights_enabled = editor_value.semantic_highlights_enabled();
+        let code_lens_enabled = editor_value.code_lens_enabled();
         let is_full = editor_value.mode().is_full();
         let diagnostics_enabled = editor_value.diagnostics_max_severity != DiagnosticSeverity::Off;
         let supports_inline_diagnostics = editor_value.inline_diagnostics_enabled();
@@ -404,6 +406,29 @@ impl Render for QuickActionBar {
                                 );
                             }
 
+                            if supports_code_lens {
+                                menu = menu.toggleable_entry(
+                                    "Code Lens",
+                                    code_lens_enabled,
+                                    IconPosition::Start,
+                                    Some(editor::actions::ToggleCodeLens.boxed_clone()),
+                                    {
+                                        let editor = editor.clone();
+                                        move |window, cx| {
+                                            editor
+                                                .update(cx, |editor, cx| {
+                                                    editor.toggle_code_lens_action(
+                                                        &editor::actions::ToggleCodeLens,
+                                                        window,
+                                                        cx,
+                                                    );
+                                                })
+                                                .ok();
+                                        }
+                                    },
+                                );
+                            }
+
                             if supports_minimap {
                                 menu = menu.toggleable_entry("Minimap", minimap_enabled, IconPosition::Start, Some(editor::actions::ToggleMinimap.boxed_clone()), {
                                     let editor = editor.clone();

docs/src/languages/go.md πŸ”—

@@ -78,6 +78,39 @@ to override these settings.
 
 See [gopls inlayHints documentation](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md) for more information.
 
+## Code Lens
+
+Zed enables the `test` code lens for `gopls` by default. This shows "run test" and "run benchmark" links above `Test` and `Benchmark` functions in `*_test.go` files. To use them, enable the `code_lens` setting:
+
+```json [settings]
+{
+  "code_lens": "on"
+}
+```
+
+You can override the default code lens settings in your `settings.json`:
+
+```json [settings]
+{
+  "lsp": {
+    "gopls": {
+      "initialization_options": {
+        "codelenses": {
+          "test": true,
+          "generate": true,
+          "regenerate_cgo": true,
+          "tidy": true,
+          "upgrade_dependency": true,
+          "vendor": true
+        }
+      }
+    }
+  }
+}
+```
+
+See [gopls code lenses documentation](https://go.dev/gopls/codelenses) for more information.
+
 ## Debugging
 
 Zed supports zero-configuration debugging of Go tests and entry points (`func main`) using Delve. Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these preconfigured debug tasks.

docs/src/languages/typescript.md πŸ”—

@@ -189,6 +189,51 @@ When using `vtsls`:
 }
 ```
 
+## Code Lens
+
+Zed enables references and implementations code lenses for `vtsls` by default. These show reference counts and implementation counts above functions, classes, and interfaces. To use them, enable the `code_lens` setting:
+
+```json [settings]
+{
+  "code_lens": "on"
+}
+```
+
+You can override the default code lens settings in your `settings.json`:
+
+```json [settings]
+{
+  "lsp": {
+    "vtsls": {
+      "settings": {
+        "typescript": {
+          "implementationsCodeLens": {
+            "enabled": true,
+            "showOnAllClassMethods": true,
+            "showOnInterfaceMethods": true
+          },
+          "referencesCodeLens": {
+            "enabled": true,
+            "showOnAllFunctions": true
+          }
+        },
+        "javascript": {
+          "implementationsCodeLens": {
+            "enabled": true,
+            "showOnAllClassMethods": true,
+            "showOnInterfaceMethods": true
+          },
+          "referencesCodeLens": {
+            "enabled": true,
+            "showOnAllFunctions": true
+          }
+        }
+      }
+    }
+  }
+}
+```
+
 ## Debugging
 
 Zed supports debugging TypeScript code out of the box with `vscode-js-debug`.

docs/src/reference/all-settings.md πŸ”—

@@ -450,6 +450,24 @@ When enabled, this setting will automatically close tabs for files that have bee
 
 Note: Dirty files (files with unsaved changes) will not be automatically closed even when this setting is enabled, ensuring you don't lose unsaved work.
 
+## Code Lens
+
+- Description: Whether and how to display code lenses from language servers. Code lenses show contextual information such as reference counts, implementations, and other metadata provided by the language server.
+- Setting: `code_lens`
+- Default: `off`
+
+**Options**
+
+1. `off`: Do not query and display code lenses.
+2. `on`: Display code lenses from language servers above code elements.
+3. `menu`: Display code lenses in the code action menu.
+
+```json [settings]
+{
+  "code_lens": "on"
+}
+```
+
 ## Confirm Quit
 
 - Description: Whether or not to prompt the user to confirm before closing the application.