From b4cf6549c8ebb36d3ea87f58d781f5acfb4b50cb Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Feb 2026 16:39:19 +0200 Subject: [PATCH] Support code lens in the editor # Conflicts: # crates/editor/src/editor.rs --- 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 +- .../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, 1073 insertions(+), 126 deletions(-) create mode 100644 crates/editor/src/code_lens.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index a32e1b27aee08bf2676922fea3790a99b7d7844b..d69289a985b1b32cf1386313df14502b58eaefd1 100644 --- a/assets/settings/default.json +++ b/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` diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index f4b4c69679ebd4ab0f9080cd7d110fd4e87259c4..e46a8e8c75df0ea4a27e9481e4e44ccd5e293aec 100644 --- a/crates/editor/src/actions.rs +++ b/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. diff --git a/crates/editor/src/code_lens.rs b/crates/editor/src/code_lens.rs new file mode 100644 index 0000000000000000000000000000000000000000..3120bff03963321d1183459a45e7472aacf13064 --- /dev/null +++ b/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, +} + +#[derive(Clone, Debug)] +struct CodeLensItem { + title: SharedString, + action: CodeAction, +} + +#[derive(Default)] +pub(super) struct CodeLensState { + pub(super) block_ids: HashMap>, +} + +impl CodeLensState { + fn all_block_ids(&self) -> HashSet { + self.block_ids.values().flatten().copied().collect() + } +} + +fn group_lenses_by_row( + lenses: Vec<(Anchor, CodeLensItem)>, + snapshot: &MultiBufferSnapshot, +) -> Vec { + let mut grouped: HashMap)> = 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 = 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, +) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement { + move |cx| { + let mut children: Vec = 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, + window: &mut Window, + cx: &mut Context, +) -> 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, + window: &mut Window, + cx: &mut Context, +) -> bool { + let Some(first_arg) = arguments.first() else { + return false; + }; + let Ok(runnable) = serde_json::from_value::(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, + window: &mut Window, + cx: &mut Context, +) -> bool { + if arguments.len() < 3 { + return false; + } + let Ok(locations) = serde_json::from_value::>(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, Vec>> = + 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 { + 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, + _window: &Window, + cx: &mut Context, + ) { + 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::>(); + + 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::>() + }) + }) + .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> = + 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 = 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>)> = + Vec::new(); + for (buffer_id, lenses) in new_lenses_per_buffer { + if lenses.is_empty() { + continue; + } + let blocks: Vec> = 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, + ) { + 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, + ) { + 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) { + 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::(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::(|_, _| 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::(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::(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::(|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" + ); + }); + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ae852b1055b33f151b402ee999ce50ba064788a4..00cf8bb260ffe7c8ca827916d59ca28672ba4d37 100644 --- a/crates/editor/src/editor.rs +++ b/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, + code_lens: Option, 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, @@ -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); } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index e4a20476419578ff646952c84b399e2333f0a411..4f30477ad3807e232918e982c1c4207ab930747a 100644 --- a/crates/editor/src/editor_settings.rs +++ b/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, 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 diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c29df272d35af5a69ba07c76cb7da3866786bd2b..f5f6ca677012aadc69c0a96e0a6e2d41b7bb675e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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 { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 512fbb8855aa11d8c540065a55eb296919012821..e830a6da5ade307540f3cd66a782d887476972c8 100644 --- a/crates/editor/src/element.rs +++ b/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() { diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index d92c1392c128ed72b6e2972bc54dcf7dfc152b1e..bd32ce708f66b8df1223223519b5583467ed1152 100644 --- a/crates/languages/src/rust.rs +++ b/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) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d4a4f9b04968413c51607f71047752a9b779b79a..b70f97fddfb13efaae2d7a777341ffc28857b801 100644 --- a/crates/project/src/lsp_command.rs +++ b/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::(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()) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 9ea50fdc8f12b68147c1073219625c4fd257afd3..0ed479e6a190d2aad30a2a773568ae0174f51a7e 100644 --- a/crates/project/src/lsp_store.rs +++ b/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::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..lsp::ExecuteCommandParams::default() - }, request_timeout) - .await.into_response() + .request::( + 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, _| { diff --git a/crates/project/src/lsp_store/code_lens.rs b/crates/project/src/lsp_store/code_lens.rs index 756c2dec06ea9d60c164f177ab26e6497b1bb5d3..9d55da01cf4a36c56aac2d51d919f2246249bd70 100644 --- a/crates/project/src/lsp_store/code_lens.rs +++ b/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, @@ -220,7 +226,8 @@ impl LspStore { _: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - this.update(&mut cx, |_, cx| { + this.update(&mut cx, |this, cx| { + this.invalidate_code_lens(); cx.emit(LspStoreEvent::RefreshCodeLens); }); Ok(proto::Ack {}) diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index 9c284a143613c47aa3a5fcc9af5afac9d6dbbf4d..55395bd066326fbf0da1af878b8b77eb83ac118d 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -584,6 +584,56 @@ pub struct LspRunnables { pub runnables: Vec<(Option, 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::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::new", - // "--show-output", - // ], - // ``` - // and `X::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)); } diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 1211cbd8a4519ea295773eb0d979b48258908311..643bd2d2cf4d0e9ca9ad294880f65b1a412db8b4 100644 --- a/crates/settings/src/vscode_import.rs +++ b/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, diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index b37192882694f999a5e7f3180e5a7899a8732393..6c4c20ceff67a8f1cd2dc17297769f8831882eaf 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -199,6 +199,11 @@ pub struct EditorSettingsContent { /// Drag and drop related settings pub drag_and_drop_selection: Option, + /// Whether to display code lenses from language servers above code elements. + /// + /// Default: "off" + pub code_lens: Option, + /// How to render LSP `textDocument/documentColor` colors in the editor. /// /// Default: [`DocumentColorsRenderMode::Inlay`] @@ -441,7 +446,7 @@ pub struct GutterContent { pub folds: Option, } -/// 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, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 259ee2cf261f9e435a5431ddf3c470640daf41f9..04ef02b92c23384c76137370bca272133195dee9 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/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(), ) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 4c7a98f6c0fa94e659a6db4e00aa28e2b4516e13..84afa6049125ee0877c35d5d0ea5559fe0c9b0de 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -532,6 +532,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index e35bd2aad5d08739e1d8fb51968ddfae746939a7..e2b33b88592855ab8bef1d30d83c33d274b069ec 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/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(); diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 3c944e0807ff1a6b0cda46c3416ad4e2dbc5a279..5fbb7d1c5bf81c8d42917a7a181118fbde295ce0 100644 --- a/docs/src/reference/all-settings.md +++ b/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.