Detailed changes
@@ -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`
@@ -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.
@@ -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"
+ );
+ });
+ }
+}
@@ -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);
}
@@ -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
@@ -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 {
@@ -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() {
@@ -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)
@@ -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())
@@ -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, _| {
@@ -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 {})
@@ -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));
}
@@ -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,
@@ -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,
@@ -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(),
)
@@ -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)
@@ -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();
@@ -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.