Support code lens in the editor

Kirill Bulatov created

# Conflicts:
#	crates/editor/src/editor.rs

Change summary

assets/settings/default.json                    |   9 
crates/editor/src/actions.rs                    |   2 
crates/editor/src/code_lens.rs                  | 793 +++++++++++++++++++
crates/editor/src/editor.rs                     |  18 
crates/editor/src/editor_settings.rs            |   4 
crates/editor/src/editor_tests.rs               |   9 
crates/editor/src/element.rs                    |   1 
crates/languages/src/rust.rs                    |  42 
crates/project/src/lsp_command.rs               |  45 
crates/project/src/lsp_store.rs                 |  55 
crates/project/src/lsp_store/code_lens.rs       |   9 
crates/project/src/lsp_store/lsp_ext_command.rs | 115 +-
crates/settings/src/vscode_import.rs            |   1 
crates/settings_content/src/editor.rs           |  37 
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/reference/all-settings.md              |  17 
18 files changed, 1,073 insertions(+), 126 deletions(-)

Detailed changes

assets/settings/default.json πŸ”—

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

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

@@ -833,6 +833,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,793 @@
+use std::{collections::HashMap as StdHashMap, 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;
+use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint as _};
+use project::{CodeAction, LspAction, TaskSourceKind, lsp_store::lsp_ext_command};
+use task::TaskContext;
+use text::Point;
+
+use ui::{Context, Window, div, prelude::*};
+
+use crate::{
+    Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
+    actions::ToggleCodeLens,
+    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
+};
+
+#[derive(Clone, Debug)]
+struct CodeLensLine {
+    position: Anchor,
+    items: Vec<CodeLensItem>,
+}
+
+#[derive(Clone, Debug)]
+struct CodeLensItem {
+    title: SharedString,
+    action: CodeAction,
+}
+
+#[derive(Default)]
+pub(super) struct CodeLensState {
+    pub(super) block_ids: HashMap<BufferId, Vec<CustomBlockId>>,
+}
+
+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,
+) -> Vec<CodeLensLine> {
+    let mut grouped: HashMap<u32, (Anchor, Vec<CodeLensItem>)> = HashMap::default();
+
+    for (position, item) in lenses {
+        let row = position.to_point(snapshot).row;
+        grouped
+            .entry(row)
+            .or_insert_with(|| (position, Vec::new()))
+            .1
+            .push(item);
+    }
+
+    let mut result: Vec<CodeLensLine> = grouped
+        .into_iter()
+        .map(|(_, (position, items))| CodeLensLine { position, items })
+        .collect();
+
+    result.sort_by_key(|lens| lens.position.to_point(snapshot).row);
+    result
+}
+
+fn render_code_lens_line(
+    lens: CodeLensLine,
+    editor: WeakEntity<Editor>,
+) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement {
+    move |cx| {
+        let mut children: Vec<gpui::AnyElement> = Vec::new();
+        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;
+
+            children.push(
+                div()
+                    .id(SharedString::from(format!(
+                        "code-lens-{}-{}-{}",
+                        position.text_anchor.offset, i, title
+                    )))
+                    .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();
+                                        let buffer = editor.buffer().clone();
+                                        if let Some(excerpt_buffer) = buffer.read(cx).as_singleton()
+                                        {
+                                            project
+                                                .update(cx, |project, cx| {
+                                                    project.apply_code_action(
+                                                        excerpt_buffer.clone(),
+                                                        action,
+                                                        true,
+                                                        cx,
+                                                    )
+                                                })
+                                                .detach_and_log_err(cx);
+                                        }
+                                    }
+                                });
+                            }
+                        }
+                    })
+                    .into_any_element(),
+            );
+        }
+
+        div()
+            .pl(cx.margins.gutter.full_width())
+            .h_full()
+            .flex()
+            .flex_row()
+            .items_end()
+            .children(children)
+            .into_any_element()
+    }
+}
+
+fn try_handle_client_command(
+    action: &CodeAction,
+    editor: &mut Editor,
+    workspace: &gpui::Entity<workspace::Workspace>,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) -> bool {
+    let command = match &action.lsp_action {
+        LspAction::CodeLens(lens) => lens.command.as_ref(),
+        _ => None,
+    };
+    let Some(command) = command else {
+        return false;
+    };
+    let arguments = command.arguments.as_deref().unwrap_or_default();
+
+    match command.command.as_str() {
+        "rust-analyzer.runSingle" | "rust-analyzer.debugSingle" => {
+            try_schedule_runnable(arguments, action, editor, workspace, window, cx)
+        }
+        "rust-analyzer.showReferences" => {
+            try_show_references(arguments, action, editor, workspace, window, cx)
+        }
+        _ => false,
+    }
+}
+
+fn try_schedule_runnable(
+    arguments: &[serde_json::Value],
+    action: &CodeAction,
+    editor: &Editor,
+    workspace: &gpui::Entity<workspace::Workspace>,
+    window: &mut Window,
+    cx: &mut Context<Editor>,
+) -> bool {
+    let Some(first_arg) = arguments.first() else {
+        return false;
+    };
+    let Ok(runnable) = serde_json::from_value::<lsp_ext_command::Runnable>(first_arg.clone())
+    else {
+        return false;
+    };
+
+    let task_template = lsp_ext_command::runnable_to_task_template(runnable.label, runnable.args);
+    let task_context = TaskContext {
+        cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
+        ..TaskContext::default()
+    };
+    let language_name = editor
+        .buffer()
+        .read(cx)
+        .as_singleton()
+        .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,
+    _editor: &mut Editor,
+    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: StdHashMap<gpui::Entity<language::Buffer>, Vec<Range<Point>>> =
+            StdHashMap::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_default().push(range);
+        }
+
+        workspace.update_in(cx, |workspace, window, cx| {
+            Editor::open_locations_in_multibuffer(
+                workspace,
+                buffer_locations,
+                "References".to_owned(),
+                false,
+                true,
+                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_excerpts(true, cx)
+            .into_values()
+            .map(|(buffer, ..)| buffer)
+            .chain(for_buffer.and_then(|id| self.buffer.read(cx).buffer(id)))
+            .filter(|buffer| {
+                let id = buffer.read(cx).remote_id();
+                for_buffer.is_none_or(|target| target == id)
+                    && self.registered_buffers.contains_key(&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<BufferId, Vec<CodeLensLine>> =
+                HashMap::default();
+
+            for (buffer_id, result) in results {
+                match result {
+                    Ok(Some(actions)) => {
+                        let individual_lenses: Vec<(Anchor, CodeLensItem)> = actions
+                            .into_iter()
+                            .filter_map(|action| {
+                                let title = match &action.lsp_action {
+                                    project::LspAction::CodeLens(lens) => {
+                                        lens.command.as_ref().map(|cmd| cmd.title.clone())
+                                    }
+                                    _ => None,
+                                }?;
+
+                                let position = multi_buffer_snapshot.anchor_in_excerpt(
+                                    multi_buffer_snapshot.excerpts().next()?.0,
+                                    action.range.start,
+                                )?;
+
+                                Some((
+                                    position,
+                                    CodeLensItem {
+                                        title: title.into(),
+                                        action,
+                                    },
+                                ))
+                            })
+                            .collect();
+
+                        let grouped =
+                            group_lenses_by_row(individual_lenses, &multi_buffer_snapshot);
+                        new_lenses_per_buffer.insert(buffer_id, grouped);
+                    }
+                    Ok(None) => {}
+                    Err(e) => {
+                        log::error!("Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}");
+                    }
+                }
+            }
+
+            editor
+                .update(cx, |editor, cx| {
+                    let code_lens = editor.code_lens.get_or_insert_with(CodeLensState::default);
+
+                    let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
+                    for (buffer_id, _) in &new_lenses_per_buffer {
+                        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();
+
+                    let mut all_new_blocks: Vec<(BufferId, Vec<BlockProperties<Anchor>>)> =
+                        Vec::new();
+                    for (buffer_id, lenses) in new_lenses_per_buffer {
+                        if lenses.is_empty() {
+                            continue;
+                        }
+                        let blocks: Vec<BlockProperties<Anchor>> = lenses
+                            .into_iter()
+                            .map(|lens| {
+                                let position = lens.position;
+                                let render_fn = render_code_lens_line(lens, editor_handle.clone());
+                                BlockProperties {
+                                    placement: BlockPlacement::Above(position),
+                                    height: Some(1),
+                                    style: BlockStyle::Flex,
+                                    render: Arc::new(render_fn),
+                                    priority: 0,
+                                }
+                            })
+                            .collect();
+                        all_new_blocks.push((buffer_id, blocks));
+                    }
+
+                    for (buffer_id, blocks) in all_new_blocks {
+                        let block_ids = editor.insert_blocks(blocks, None, cx);
+                        editor
+                            .code_lens
+                            .get_or_insert_with(CodeLensState::default)
+                            .block_ids
+                            .insert(buffer_id, block_ids);
+                    }
+
+                    cx.notify();
+                })
+                .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 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 futures::StreamExt;
+    use gpui::TestAppContext;
+
+    use settings::CodeLens;
+
+    use crate::{
+        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"
+            );
+        });
+    }
+}

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;
@@ -95,6 +96,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;
@@ -1330,8 +1332,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>,
@@ -2140,7 +2144,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,
@@ -2558,7 +2562,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,
@@ -2739,6 +2745,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.enabled() {
+                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);
@@ -24573,6 +24582,12 @@ impl Editor {
                 self.refresh_document_colors(None, window, cx);
             }
 
+            let code_lens_enabled = EditorSettings::get_global(cx).code_lens.enabled();
+            let was_enabled = self.code_lens.is_some();
+            if code_lens_enabled != was_enabled {
+                self.toggle_code_lens(code_lens_enabled, window, cx);
+            }
+
             self.refresh_inlay_hints(
                 InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
                     self.selections.newest_anchor().head(),
@@ -25744,6 +25759,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);
     }
 

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,
@@ -55,6 +55,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,
@@ -287,6 +288,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 πŸ”—

@@ -27828,15 +27828,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 πŸ”—

@@ -501,6 +501,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/languages/src/rust.rs πŸ”—

@@ -608,17 +608,39 @@ 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",
+                            "rust-analyzer.debugSingle",
+                        ]
+                    }
+                }),
+                &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)

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

@@ -7,6 +7,7 @@ use crate::{
     LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse,
     ProjectTransaction, PulledDiagnostics, ResolveState,
     lsp_store::{LocalLspStore, LspFoldingRange, LspStore},
+    project_settings::ProjectSettings,
 };
 use anyhow::{Context as _, Result};
 use async_trait::async_trait;
@@ -33,6 +34,7 @@ use lsp::{
     OneOf, RenameOptions, ServerCapabilities,
 };
 use serde_json::Value;
+use settings::Settings as _;
 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,
@@ -3865,31 +3867,44 @@ impl LspCommand for GetCodeLens {
                     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 can_resolve = Self::can_resolve_lens(&language_server.capabilities());
+        let mut code_lenses = message.unwrap_or_default();
+
+        if can_resolve {
+            let request_timeout = cx.update(|cx| {
+                ProjectSettings::get_global(cx)
+                    .global_lsp_settings
+                    .get_request_timeout()
+            });
+
+            for lens in &mut code_lenses {
+                if lens.command.is_none() {
+                    match language_server
+                        .request::<lsp::request::CodeLensResolve>(lens.clone(), request_timeout)
+                        .await
+                        .into_response()
+                    {
+                        Ok(resolved) => *lens = resolved,
+                        Err(e) => log::warn!("Failed to resolve code lens: {e:#}"),
+                    }
+                }
+            }
+        }
+
+        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 πŸ”—

@@ -1086,6 +1086,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 {
@@ -5460,20 +5461,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();
@@ -5484,15 +5485,17 @@ 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!(
+                        "Executing command {} not listed in the language server capabilities",
+                        command.command
+                    );
                 }
 
-                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()
@@ -5502,12 +5505,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, _| {

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

@@ -36,6 +36,12 @@ 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;
+        }
+    }
+
     pub fn code_lens_actions(
         &mut self,
         buffer: &Entity<Buffer>,
@@ -220,7 +226,8 @@ impl LspStore {
         _: TypedEnvelope<proto::RefreshCodeLens>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        this.update(&mut cx, |_, cx| {
+        this.update(&mut cx, |this, cx| {
+            this.invalidate_code_lens();
             cx.emit(LspStoreEvent::RefreshCodeLens);
         });
         Ok(proto::Ack {})

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/settings/src/vscode_import.rs πŸ”—

@@ -268,6 +268,7 @@ impl VsCodeSettings {
             hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
             hover_popover_enabled: self.read_bool("editor.hover.enabled"),
             inline_code_actions: None,
+            code_lens: None,
             jupyter: None,
             lsp_document_colors: None,
             lsp_highlight_debounce: None,

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

@@ -199,6 +199,11 @@ pub struct EditorSettingsContent {
     /// Drag and drop related settings
     pub drag_and_drop_selection: Option<DragAndDropSelectionContent>,
 
+    /// Whether to display code lenses from language servers above code elements.
+    ///
+    /// Default: "off"
+    pub code_lens: Option<CodeLens>,
+
     /// How to render LSP `textDocument/documentColor` colors in the editor.
     ///
     /// Default: [`DocumentColorsRenderMode::Inlay`]
@@ -441,7 +446,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,
@@ -457,6 +462,36 @@ pub struct GutterContent {
     strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
+pub enum CodeLens {
+    /// Do not display code lenses.
+    #[default]
+    Off,
+    /// Display code lenses from language servers above code elements.
+    On,
+}
+
+impl CodeLens {
+    pub fn enabled(&self) -> bool {
+        self != &Self::Off
+    }
+}
+
+/// 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 πŸ”—

@@ -8892,6 +8892,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 to display code lenses from language servers above code elements.",
+        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.",
@@ -8916,6 +8930,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(),
@@ -8931,6 +8946,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 πŸ”—

@@ -532,6 +532,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/reference/all-settings.md πŸ”—

@@ -450,6 +450,23 @@ 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 to display code lenses from language servers above code elements. 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 display code lenses.
+2. `on`: Display code lenses from language servers above code elements.
+
+```json [settings]
+{
+  "code_lens": "on"
+}
+```
+
 ## Confirm Quit
 
 - Description: Whether or not to prompt the user to confirm before closing the application.