Detailed changes
@@ -339,6 +339,17 @@
// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
"delay": 300,
},
+ // Whether and how to display code lenses from language servers.
+ //
+ // Possible values:
+ //
+ // 1. Do not display code lenses.
+ // "code_lens": "off",
+ // 2. Display code lenses from language servers above code elements.
+ // "code_lens": "on",
+ // 3. Display code lenses in the code action menu.
+ // "code_lens": "menu",
+ "code_lens": "off",
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
@@ -1203,6 +1203,13 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.await;
let active_call_a = cx_a.read(ActiveCall::global);
cx_b.update(editor::init);
+ cx_b.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ settings.editor.code_lens = Some(settings::CodeLens::Menu);
+ });
+ });
+ });
let command_name = "test_command";
let capabilities = lsp::ServerCapabilities {
@@ -849,6 +849,8 @@ actions!(
ToggleIndentGuides,
/// Toggles inlay hints display.
ToggleInlayHints,
+ /// Toggles code lens display.
+ ToggleCodeLens,
/// Toggles semantic highlights display.
ToggleSemanticHighlights,
/// Toggles inline values display.
@@ -0,0 +1,1066 @@
+use std::{iter, ops::Range, sync::Arc};
+
+use collections::{HashMap, HashSet};
+use futures::future::join_all;
+use gpui::{MouseButton, SharedString, Task, WeakEntity};
+use itertools::Itertools;
+use language::{BufferId, ClientCommand};
+use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
+use project::{CodeAction, TaskSourceKind};
+use settings::Settings as _;
+use task::TaskContext;
+use text::Point;
+
+use ui::{Context, Window, div, prelude::*};
+use workspace::PreviewTabsSettings;
+
+use crate::{
+ Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
+ actions::ToggleCodeLens,
+ display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
+};
+
+#[derive(Clone, Debug)]
+struct CodeLensLine {
+ position: Anchor,
+ indent_column: u32,
+ items: Vec<CodeLensItem>,
+}
+
+#[derive(Clone, Debug)]
+struct CodeLensItem {
+ title: SharedString,
+ action: CodeAction,
+}
+
+pub(super) struct CodeLensState {
+ pub(super) block_ids: HashMap<BufferId, Vec<CustomBlockId>>,
+ resolve_task: Task<()>,
+}
+
+impl Default for CodeLensState {
+ fn default() -> Self {
+ Self {
+ block_ids: HashMap::default(),
+ resolve_task: Task::ready(()),
+ }
+ }
+}
+
+impl CodeLensState {
+ fn all_block_ids(&self) -> HashSet<CustomBlockId> {
+ self.block_ids.values().flatten().copied().collect()
+ }
+}
+
+fn group_lenses_by_row(
+ lenses: Vec<(Anchor, CodeLensItem)>,
+ snapshot: &MultiBufferSnapshot,
+) -> impl Iterator<Item = CodeLensLine> {
+ lenses
+ .into_iter()
+ .into_group_map_by(|(position, _)| {
+ let row = position.to_point(snapshot).row;
+ MultiBufferRow(row)
+ })
+ .into_iter()
+ .sorted_by_key(|(row, _)| *row)
+ .filter_map(|(row, entries)| {
+ let position = entries.first()?.0;
+ let items = entries.into_iter().map(|(_, item)| item).collect();
+ let indent_column = snapshot.indent_size_for_line(row).len;
+ Some(CodeLensLine {
+ position,
+ indent_column,
+ items,
+ })
+ })
+}
+
+fn render_code_lens_line(
+ line_number: usize,
+ lens: CodeLensLine,
+ editor: WeakEntity<Editor>,
+) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement {
+ move |cx| {
+ let mut children = Vec::with_capacity((2 * lens.items.len()).saturating_sub(1));
+ let text_style = &cx.editor_style.text;
+ let font = text_style.font();
+ let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
+
+ for (i, item) in lens.items.iter().enumerate() {
+ if i > 0 {
+ children.push(
+ div()
+ .font(font.clone())
+ .text_size(font_size)
+ .text_color(cx.app.theme().colors().text_muted)
+ .child(" | ")
+ .into_any_element(),
+ );
+ }
+
+ let title = item.title.clone();
+ let action = item.action.clone();
+ let editor_handle = editor.clone();
+ let position = lens.position;
+ let id = (line_number as u64) << 32 | (i as u64);
+
+ children.push(
+ div()
+ .id(ElementId::Integer(id))
+ .font(font.clone())
+ .text_size(font_size)
+ .text_color(cx.app.theme().colors().text_muted)
+ .cursor_pointer()
+ .hover(|style| style.text_color(cx.app.theme().colors().text))
+ .child(title.clone())
+ .on_mouse_down(MouseButton::Left, |_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_down(MouseButton::Right, |_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_click({
+ move |_event, window, cx| {
+ if let Some(editor) = editor_handle.upgrade() {
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(
+ SelectionEffects::default(),
+ window,
+ cx,
+ |s| {
+ s.select_anchor_ranges([position..position]);
+ },
+ );
+
+ let action = action.clone();
+ if let Some(workspace) = editor.workspace() {
+ if try_handle_client_command(
+ &action, editor, &workspace, window, cx,
+ ) {
+ return;
+ }
+
+ let project = workspace.read(cx).project().clone();
+ if let Some(buffer) = editor
+ .buffer()
+ .read(cx)
+ .buffer(action.range.start.buffer_id)
+ {
+ project
+ .update(cx, |project, cx| {
+ project
+ .apply_code_action(buffer, action, true, cx)
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ });
+ }
+ }
+ })
+ .into_any_element(),
+ );
+ }
+
+ div()
+ .pl(cx.margins.gutter.full_width() + cx.em_width * (lens.indent_column as f32 + 0.5))
+ .h_full()
+ .flex()
+ .flex_row()
+ .items_end()
+ .children(children)
+ .into_any_element()
+ }
+}
+
+pub(super) fn try_handle_client_command(
+ action: &CodeAction,
+ editor: &mut Editor,
+ workspace: &gpui::Entity<workspace::Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+) -> bool {
+ let Some(command) = action.lsp_action.command() else {
+ return false;
+ };
+
+ let arguments = command.arguments.as_deref().unwrap_or_default();
+ let project = workspace.read(cx).project().clone();
+ let client_command = project
+ .read(cx)
+ .lsp_store()
+ .read(cx)
+ .language_server_adapter_for_id(action.server_id)
+ .and_then(|adapter| adapter.adapter.client_command(&command.command, arguments))
+ .or_else(|| match command.command.as_str() {
+ "editor.action.showReferences"
+ | "editor.action.goToLocations"
+ | "editor.action.peekLocations" => Some(ClientCommand::ShowLocations),
+ _ => None,
+ });
+
+ match client_command {
+ Some(ClientCommand::ScheduleTask(task_template)) => {
+ schedule_task(task_template, action, editor, workspace, window, cx)
+ }
+ Some(ClientCommand::ShowLocations) => {
+ try_show_references(arguments, action, workspace, window, cx)
+ }
+ None => false,
+ }
+}
+
+fn schedule_task(
+ task_template: task::TaskTemplate,
+ action: &CodeAction,
+ editor: &Editor,
+ workspace: &gpui::Entity<workspace::Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+) -> bool {
+ let task_context = TaskContext {
+ cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
+ ..TaskContext::default()
+ };
+ let language_name = editor
+ .buffer()
+ .read(cx)
+ .buffer(action.range.start.buffer_id)
+ .and_then(|buffer| buffer.read(cx).language())
+ .map(|language| language.name());
+ let task_source_kind = match language_name {
+ Some(language_name) => TaskSourceKind::Lsp {
+ server: action.server_id,
+ language_name: SharedString::from(language_name),
+ },
+ None => TaskSourceKind::AbsPath {
+ id_base: "code-lens".into(),
+ abs_path: task_template
+ .cwd
+ .as_ref()
+ .map(std::path::PathBuf::from)
+ .unwrap_or_default(),
+ },
+ };
+
+ workspace.update(cx, |workspace, cx| {
+ workspace.schedule_task(
+ task_source_kind,
+ &task_template,
+ &task_context,
+ false,
+ window,
+ cx,
+ );
+ });
+ true
+}
+
+fn try_show_references(
+ arguments: &[serde_json::Value],
+ action: &CodeAction,
+ workspace: &gpui::Entity<workspace::Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+) -> bool {
+ if arguments.len() < 3 {
+ return false;
+ }
+ let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
+ return false;
+ };
+ if locations.is_empty() {
+ return false;
+ }
+
+ let server_id = action.server_id;
+ let project = workspace.read(cx).project().clone();
+ let workspace = workspace.clone();
+
+ cx.spawn_in(window, async move |_editor, cx| {
+ let mut buffer_locations = std::collections::HashMap::default();
+
+ for location in &locations {
+ let open_task = cx.update(|_, cx| {
+ project.update(cx, |project, cx| {
+ let uri: lsp::Uri = location.uri.clone();
+ project.open_local_buffer_via_lsp(uri, server_id, cx)
+ })
+ })?;
+ let buffer = open_task.await?;
+
+ let range = range_from_lsp(location.range);
+ buffer_locations
+ .entry(buffer)
+ .or_insert_with(Vec::new)
+ .push(range);
+ }
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ let target = buffer_locations
+ .iter()
+ .flat_map(|(k, v)| iter::repeat(k.clone()).zip(v))
+ .map(|(buffer, location)| {
+ buffer
+ .read(cx)
+ .text_for_range(location.clone())
+ .collect::<String>()
+ })
+ .filter(|text| !text.contains('\n'))
+ .unique()
+ .take(3)
+ .join(", ");
+ let title = if target.is_empty() {
+ "References".to_owned()
+ } else {
+ format!("References to {target}")
+ };
+ let allow_preview =
+ PreviewTabsSettings::get_global(cx).enable_preview_multibuffer_from_code_navigation;
+ Editor::open_locations_in_multibuffer(
+ workspace,
+ buffer_locations,
+ title,
+ false,
+ allow_preview,
+ MultibufferSelectionMode::First,
+ window,
+ cx,
+ );
+ })?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+
+ true
+}
+
+fn range_from_lsp(range: lsp::Range) -> Range<Point> {
+ let start = Point::new(range.start.line, range.start.character);
+ let end = Point::new(range.end.line, range.end.character);
+ start..end
+}
+
+impl Editor {
+ pub(super) fn refresh_code_lenses(
+ &mut self,
+ for_buffer: Option<BufferId>,
+ _window: &Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.lsp_data_enabled() || self.code_lens.is_none() {
+ return;
+ }
+ let Some(project) = self.project.clone() else {
+ return;
+ };
+
+ let buffers_to_query = self
+ .visible_buffers(cx)
+ .into_iter()
+ .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
+ .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
+ .filter(|editor_buffer| {
+ let editor_buffer_id = editor_buffer.read(cx).remote_id();
+ for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
+ && self.registered_buffers.contains_key(&editor_buffer_id)
+ })
+ .unique_by(|buffer| buffer.read(cx).remote_id())
+ .collect::<Vec<_>>();
+
+ if buffers_to_query.is_empty() {
+ return;
+ }
+
+ let project = project.downgrade();
+ self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
+ cx.background_executor()
+ .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
+ .await;
+
+ let Some(tasks) = project
+ .update(cx, |project, cx| {
+ project.lsp_store().update(cx, |lsp_store, cx| {
+ buffers_to_query
+ .into_iter()
+ .map(|buffer| {
+ let buffer_id = buffer.read(cx).remote_id();
+ let task = lsp_store.code_lens_actions(&buffer, cx);
+ async move { (buffer_id, task.await) }
+ })
+ .collect::<Vec<_>>()
+ })
+ })
+ .ok()
+ else {
+ return;
+ };
+
+ let results = join_all(tasks).await;
+ if results.is_empty() {
+ return;
+ }
+
+ let Ok(multi_buffer_snapshot) =
+ editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
+ else {
+ return;
+ };
+
+ let mut new_lenses_per_buffer = HashMap::default();
+ for (buffer_id, result) in results {
+ let actions = match result {
+ Ok(Some(actions)) => actions,
+ Ok(None) => continue,
+ Err(e) => {
+ log::error!("Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}");
+ continue;
+ }
+ };
+ let individual_lenses = actions
+ .into_iter()
+ .filter_map(|action| {
+ let title = match &action.lsp_action {
+ project::LspAction::CodeLens(lens) => lens
+ .command
+ .as_ref()
+ .map(|cmd| SharedString::from(&cmd.title)),
+ _ => None,
+ }?;
+ let position =
+ multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
+ Some((position, CodeLensItem { title, action }))
+ })
+ .collect();
+ new_lenses_per_buffer.insert(
+ buffer_id,
+ group_lenses_by_row(individual_lenses, &multi_buffer_snapshot)
+ .collect::<Vec<_>>(),
+ );
+ }
+
+ editor
+ .update(cx, |editor, cx| {
+ let code_lens = editor.code_lens.get_or_insert_with(CodeLensState::default);
+ let mut blocks_to_remove = HashSet::default();
+ for buffer_id in new_lenses_per_buffer.keys() {
+ if let Some(old_ids) = code_lens.block_ids.remove(buffer_id) {
+ blocks_to_remove.extend(old_ids);
+ }
+ }
+ if !blocks_to_remove.is_empty() {
+ editor.remove_blocks(blocks_to_remove, None, cx);
+ }
+
+ let editor_handle = cx.entity().downgrade();
+ for (buffer_id, lens_lines) in new_lenses_per_buffer {
+ if lens_lines.is_empty() {
+ continue;
+ }
+ let blocks = lens_lines
+ .into_iter()
+ .enumerate()
+ .map(|(line_number, lens_line)| {
+ let position = lens_line.position;
+ BlockProperties {
+ placement: BlockPlacement::Above(position),
+ height: Some(1),
+ style: BlockStyle::Flex,
+ render: Arc::new(render_code_lens_line(
+ line_number,
+ lens_line,
+ editor_handle.clone(),
+ )),
+ priority: 0,
+ }
+ })
+ .collect::<Vec<_>>();
+ let block_ids = editor.insert_blocks(blocks, None, cx);
+ editor
+ .code_lens
+ .get_or_insert_with(CodeLensState::default)
+ .block_ids
+ .entry(buffer_id)
+ .or_default()
+ .extend(block_ids);
+ }
+
+ editor.resolve_visible_code_lenses(cx);
+ })
+ .ok();
+ });
+ }
+
+ pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
+ let Some(project) = self.project.as_ref() else {
+ return false;
+ };
+ let lsp_store = project.read(cx).lsp_store().read(cx);
+ lsp_store
+ .lsp_server_capabilities
+ .values()
+ .any(|caps| caps.code_lens_provider.is_some())
+ }
+
+ pub fn code_lens_enabled(&self) -> bool {
+ self.code_lens.is_some()
+ }
+
+ pub fn toggle_code_lens_action(
+ &mut self,
+ _: &ToggleCodeLens,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let currently_enabled = self.code_lens.is_some();
+ self.toggle_code_lens(!currently_enabled, window, cx);
+ }
+
+ pub(super) fn toggle_code_lens(
+ &mut self,
+ enabled: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if enabled {
+ self.code_lens.get_or_insert_with(CodeLensState::default);
+ self.refresh_code_lenses(None, window, cx);
+ } else {
+ self.clear_code_lenses(cx);
+ }
+ }
+
+ pub(super) fn resolve_visible_code_lenses(&mut self, cx: &mut Context<Self>) {
+ if !self.lsp_data_enabled() || self.code_lens.is_none() {
+ return;
+ }
+ let Some(project) = self.project.clone() else {
+ return;
+ };
+
+ let resolve_tasks = self
+ .visible_buffer_ranges(cx)
+ .into_iter()
+ .filter_map(|(snapshot, visible_range, _)| {
+ let buffer_id = snapshot.remote_id();
+ let buffer = self.buffer.read(cx).buffer(buffer_id)?;
+ let visible_anchor_range = snapshot.anchor_before(visible_range.start)
+ ..snapshot.anchor_after(visible_range.end);
+ let task = project.update(cx, |project, cx| {
+ project.lsp_store().update(cx, |lsp_store, cx| {
+ lsp_store.resolve_visible_code_lenses(&buffer, visible_anchor_range, cx)
+ })
+ });
+ Some((buffer_id, task))
+ })
+ .collect::<Vec<_>>();
+ if resolve_tasks.is_empty() {
+ return;
+ }
+
+ let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
+ code_lens.resolve_task = cx.spawn(async move |editor, cx| {
+ let resolved_code_lens = join_all(
+ resolve_tasks
+ .into_iter()
+ .map(|(buffer_id, task)| async move { (buffer_id, task.await) }),
+ )
+ .await;
+ editor
+ .update(cx, |editor, cx| {
+ editor.insert_resolved_code_lens_blocks(resolved_code_lens, cx);
+ })
+ .ok();
+ });
+ }
+
+ fn insert_resolved_code_lens_blocks(
+ &mut self,
+ resolved_code_lens: Vec<(BufferId, Vec<CodeAction>)>,
+ cx: &mut Context<Self>,
+ ) {
+ let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+ let editor_handle = cx.entity().downgrade();
+
+ for (buffer_id, actions) in resolved_code_lens {
+ let lenses = actions
+ .into_iter()
+ .filter_map(|action| {
+ let title = match &action.lsp_action {
+ project::LspAction::CodeLens(lens) => lens
+ .command
+ .as_ref()
+ .map(|cmd| SharedString::from(&cmd.title)),
+ _ => None,
+ }?;
+ let position = multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
+ Some((position, CodeLensItem { title, action }))
+ })
+ .collect();
+
+ let blocks = group_lenses_by_row(lenses, &multi_buffer_snapshot)
+ .enumerate()
+ .map(|(line_number, lens_line)| {
+ let position = lens_line.position;
+ BlockProperties {
+ placement: BlockPlacement::Above(position),
+ height: Some(1),
+ style: BlockStyle::Flex,
+ render: Arc::new(render_code_lens_line(
+ line_number,
+ lens_line,
+ editor_handle.clone(),
+ )),
+ priority: 0,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ if !blocks.is_empty() {
+ let block_ids = self.insert_blocks(blocks, None, cx);
+ self.code_lens
+ .get_or_insert_with(CodeLensState::default)
+ .block_ids
+ .entry(buffer_id)
+ .or_default()
+ .extend(block_ids);
+ }
+ }
+ cx.notify();
+ }
+
+ pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
+ if let Some(code_lens) = self.code_lens.take() {
+ let all_blocks = code_lens.all_block_ids();
+ if !all_blocks.is_empty() {
+ self.remove_blocks(all_blocks, None, cx);
+ }
+ cx.notify();
+ }
+ self.refresh_code_lens_task = Task::ready(());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{
+ sync::{Arc, Mutex},
+ time::Duration,
+ };
+
+ use collections::HashSet;
+ use futures::StreamExt;
+ use gpui::TestAppContext;
+ use settings::CodeLens;
+ use util::path;
+
+ use crate::{
+ Editor,
+ editor_tests::{init_test, update_test_editor_settings},
+ test::editor_lsp_test_context::EditorLspTestContext,
+ };
+
+ #[gpui::test]
+ async fn test_code_lens_blocks(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ update_test_editor_settings(cx, &|settings| {
+ settings.code_lens = Some(CodeLens::On);
+ });
+
+ let mut cx = EditorLspTestContext::new_typescript(
+ lsp::ServerCapabilities {
+ code_lens_provider: Some(lsp::CodeLensOptions {
+ resolve_provider: None,
+ }),
+ execute_command_provider: Some(lsp::ExecuteCommandOptions {
+ commands: vec!["lens_cmd".to_string()],
+ ..lsp::ExecuteCommandOptions::default()
+ }),
+ ..lsp::ServerCapabilities::default()
+ },
+ cx,
+ )
+ .await;
+
+ let mut code_lens_request =
+ cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
+ Ok(Some(vec![
+ lsp::CodeLens {
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
+ command: Some(lsp::Command {
+ title: "2 references".to_owned(),
+ command: "lens_cmd".to_owned(),
+ arguments: None,
+ }),
+ data: None,
+ },
+ lsp::CodeLens {
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
+ command: Some(lsp::Command {
+ title: "0 references".to_owned(),
+ command: "lens_cmd".to_owned(),
+ arguments: None,
+ }),
+ data: None,
+ },
+ ]))
+ });
+
+ cx.set_state("Λfunction hello() {}\nfunction world() {}");
+
+ assert!(
+ code_lens_request.next().await.is_some(),
+ "should have received a code lens request"
+ );
+ cx.run_until_parked();
+
+ cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+ assert_eq!(
+ editor.code_lens_enabled(),
+ true,
+ "code lens should be enabled"
+ );
+ let total_blocks: usize = editor
+ .code_lens
+ .as_ref()
+ .map(|s| s.block_ids.values().map(|v| v.len()).sum())
+ .unwrap_or(0);
+ assert_eq!(total_blocks, 2, "Should have inserted two code lens blocks");
+ });
+ }
+
+ #[gpui::test]
+ async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_typescript(
+ lsp::ServerCapabilities {
+ code_lens_provider: Some(lsp::CodeLensOptions {
+ resolve_provider: None,
+ }),
+ execute_command_provider: Some(lsp::ExecuteCommandOptions {
+ commands: vec!["lens_cmd".to_string()],
+ ..lsp::ExecuteCommandOptions::default()
+ }),
+ ..lsp::ServerCapabilities::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.lsp
+ .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
+ panic!("Should not request code lenses when disabled");
+ });
+
+ cx.set_state("Λfunction hello() {}");
+ cx.run_until_parked();
+
+ cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+ assert_eq!(
+ editor.code_lens_enabled(),
+ false,
+ "code lens should not be enabled when setting is off"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_code_lens_toggling(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ update_test_editor_settings(cx, &|settings| {
+ settings.code_lens = Some(CodeLens::On);
+ });
+
+ let mut cx = EditorLspTestContext::new_typescript(
+ lsp::ServerCapabilities {
+ code_lens_provider: Some(lsp::CodeLensOptions {
+ resolve_provider: None,
+ }),
+ execute_command_provider: Some(lsp::ExecuteCommandOptions {
+ commands: vec!["lens_cmd".to_string()],
+ ..lsp::ExecuteCommandOptions::default()
+ }),
+ ..lsp::ServerCapabilities::default()
+ },
+ cx,
+ )
+ .await;
+
+ let mut code_lens_request =
+ cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
+ Ok(Some(vec![lsp::CodeLens {
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
+ command: Some(lsp::Command {
+ title: "1 reference".to_owned(),
+ command: "lens_cmd".to_owned(),
+ arguments: None,
+ }),
+ data: None,
+ }]))
+ });
+
+ cx.set_state("Λfunction hello() {}");
+
+ assert!(
+ code_lens_request.next().await.is_some(),
+ "should have received a code lens request"
+ );
+ cx.run_until_parked();
+
+ cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+ assert_eq!(
+ editor.code_lens_enabled(),
+ true,
+ "code lens should be enabled"
+ );
+ let total_blocks: usize = editor
+ .code_lens
+ .as_ref()
+ .map(|s| s.block_ids.values().map(|v| v.len()).sum())
+ .unwrap_or(0);
+ assert_eq!(total_blocks, 1, "Should have one code lens block");
+ });
+
+ cx.update_editor(|editor, _window, cx| {
+ editor.clear_code_lenses(cx);
+ });
+
+ cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+ assert_eq!(
+ editor.code_lens_enabled(),
+ false,
+ "code lens should be disabled after clearing"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_code_lens_resolve(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ update_test_editor_settings(cx, &|settings| {
+ settings.code_lens = Some(CodeLens::On);
+ });
+
+ let mut cx = EditorLspTestContext::new_typescript(
+ lsp::ServerCapabilities {
+ code_lens_provider: Some(lsp::CodeLensOptions {
+ resolve_provider: Some(true),
+ }),
+ ..lsp::ServerCapabilities::default()
+ },
+ cx,
+ )
+ .await;
+
+ let mut code_lens_request =
+ cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
+ Ok(Some(vec![
+ lsp::CodeLens {
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
+ command: None,
+ data: Some(serde_json::json!({"id": "lens_1"})),
+ },
+ lsp::CodeLens {
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
+ command: None,
+ data: Some(serde_json::json!({"id": "lens_2"})),
+ },
+ ]))
+ });
+
+ cx.lsp
+ .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
+ let id = lens
+ .data
+ .as_ref()
+ .and_then(|d| d.get("id"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("unknown");
+ let title = match id {
+ "lens_1" => "3 references",
+ "lens_2" => "1 implementation",
+ _ => "unknown",
+ };
+ Ok(lsp::CodeLens {
+ command: Some(lsp::Command {
+ title: title.to_owned(),
+ command: format!("resolved_{id}"),
+ arguments: None,
+ }),
+ ..lens
+ })
+ });
+
+ cx.set_state("Λfunction hello() {}\nfunction world() {}");
+
+ assert!(
+ code_lens_request.next().await.is_some(),
+ "should have received a code lens request"
+ );
+ cx.run_until_parked();
+
+ cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
+ let total_blocks: usize = editor
+ .code_lens
+ .as_ref()
+ .map(|s| s.block_ids.values().map(|v| v.len()).sum())
+ .unwrap_or(0);
+ assert_eq!(
+ total_blocks, 2,
+ "Unresolved lenses should have been resolved and displayed"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_code_lens_resolve_only_visible(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ update_test_editor_settings(cx, &|settings| {
+ settings.code_lens = Some(CodeLens::On);
+ });
+
+ let line_count: u32 = 100;
+ let lens_every: u32 = 10;
+ let lines = (0..line_count)
+ .map(|i| format!("function func_{i}() {{}}"))
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let lens_lines = (0..line_count)
+ .filter(|i| i % lens_every == 0)
+ .collect::<Vec<_>>();
+
+ let resolved_lines = Arc::new(Mutex::new(Vec::<u32>::new()));
+
+ let fs = project::FakeFs::new(cx.executor());
+ fs.insert_tree(path!("/dir"), serde_json::json!({ "main.ts": lines }))
+ .await;
+
+ let project = project::Project::test(fs, [path!("/dir").as_ref()], cx).await;
+ let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
+ workspace::MultiWorkspace::test_new(project.clone(), window, cx)
+ });
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(Arc::new(language::Language::new(
+ language::LanguageConfig {
+ name: "TypeScript".into(),
+ matcher: language::LanguageMatcher {
+ path_suffixes: vec!["ts".to_string()],
+ ..language::LanguageMatcher::default()
+ },
+ ..language::LanguageConfig::default()
+ },
+ Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+ )));
+
+ let mut fake_servers = language_registry.register_fake_lsp(
+ "TypeScript",
+ language::FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ code_lens_provider: Some(lsp::CodeLensOptions {
+ resolve_provider: Some(true),
+ }),
+ ..lsp::ServerCapabilities::default()
+ },
+ ..language::FakeLspAdapter::default()
+ },
+ );
+
+ let editor = workspace
+ .update_in(cx, |workspace, window, cx| {
+ workspace.open_abs_path(
+ std::path::PathBuf::from(path!("/dir/main.ts")),
+ workspace::OpenOptions::default(),
+ window,
+ cx,
+ )
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let fake_server = fake_servers.next().await.unwrap();
+
+ let lens_lines_for_handler = lens_lines.clone();
+ fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
+ let lens_lines = lens_lines_for_handler.clone();
+ async move {
+ Ok(Some(
+ lens_lines
+ .iter()
+ .map(|&line| lsp::CodeLens {
+ range: lsp::Range::new(
+ lsp::Position::new(line, 0),
+ lsp::Position::new(line, 10),
+ ),
+ command: None,
+ data: Some(serde_json::json!({ "line": line })),
+ })
+ .collect(),
+ ))
+ }
+ });
+
+ {
+ let resolved_lines = resolved_lines.clone();
+ fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
+ move |lens, _| {
+ let resolved_lines = resolved_lines.clone();
+ async move {
+ let line = lens
+ .data
+ .as_ref()
+ .and_then(|d| d.get("line"))
+ .and_then(|v| v.as_u64())
+ .unwrap() as u32;
+ resolved_lines.lock().unwrap().push(line);
+ Ok(lsp::CodeLens {
+ command: Some(lsp::Command {
+ title: format!("{line} references"),
+ command: format!("show_refs_{line}"),
+ arguments: None,
+ }),
+ ..lens
+ })
+ }
+ },
+ );
+ }
+
+ cx.executor().advance_clock(Duration::from_millis(500));
+ cx.run_until_parked();
+
+ let initial_resolved = resolved_lines
+ .lock()
+ .unwrap()
+ .drain(..)
+ .collect::<HashSet<_>>();
+ assert_eq!(
+ initial_resolved,
+ HashSet::from_iter([0, 10, 20, 30, 40]),
+ "Only lenses visible at the top should be resolved"
+ );
+
+ editor.update_in(cx, |editor, window, cx| {
+ editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(500));
+ cx.run_until_parked();
+
+ let after_scroll_resolved = resolved_lines
+ .lock()
+ .unwrap()
+ .drain(..)
+ .collect::<HashSet<_>>();
+ assert_eq!(
+ after_scroll_resolved,
+ HashSet::from_iter([60, 70, 80, 90]),
+ "Only newly visible lenses at the bottom should be resolved, not middle ones"
+ );
+ }
+}
@@ -16,6 +16,7 @@ pub mod blink_manager;
mod bracket_colorization;
mod clangd_ext;
pub mod code_context_menus;
+mod code_lens;
pub mod display_map;
mod document_colors;
mod document_symbols;
@@ -98,6 +99,7 @@ use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
};
+use code_lens::CodeLensState;
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
use dap::TelemetrySpawnLocation;
@@ -111,7 +113,7 @@ use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
use element::{LineWithInvisibles, PositionMap, layout_line};
use futures::{
FutureExt,
- future::{self, Shared, join},
+ future::{self, Shared},
};
use fuzzy::{StringMatch, StringMatchCandidate};
use git::blame::{GitBlame, GlobalBlameRenderer};
@@ -1338,8 +1340,10 @@ pub struct Editor {
selection_drag_state: SelectionDragState,
colors: Option<LspColorData>,
+ code_lens: Option<CodeLensState>,
post_scroll_update: Task<()>,
refresh_colors_task: Task<()>,
+ refresh_code_lens_task: Task<()>,
use_document_folding_ranges: bool,
refresh_folding_ranges_task: Task<()>,
inlay_hints: Option<LspInlayHintData>,
@@ -2162,7 +2166,7 @@ impl Editor {
window,
|editor, _, event, window, cx| match event {
project::Event::RefreshCodeLens => {
- // we always query lens with actions, without storing them, always refreshing them
+ editor.refresh_code_lenses(None, window, cx);
}
project::Event::RefreshInlayHints {
server_id,
@@ -2591,7 +2595,9 @@ impl Editor {
runnables: RunnableData::new(),
pull_diagnostics_task: Task::ready(()),
colors: None,
+ code_lens: None,
refresh_colors_task: Task::ready(()),
+ refresh_code_lens_task: Task::ready(()),
use_document_folding_ranges: false,
refresh_folding_ranges_task: Task::ready(()),
inlay_hints: None,
@@ -2763,6 +2769,9 @@ impl Editor {
editor.colors = Some(LspColorData::new(cx));
editor.use_document_folding_ranges = true;
editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings));
+ if EditorSettings::get_global(cx).code_lens.inline() {
+ editor.code_lens = Some(CodeLensState::default());
+ }
if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
editor.register_buffer(buffer.read(cx).remote_id(), cx);
@@ -7207,6 +7216,10 @@ impl Editor {
})
}
CodeActionsItem::CodeAction { action, provider } => {
+ if code_lens::try_handle_client_command(&action, self, &workspace, window, cx) {
+ return Some(Task::ready(Ok(())));
+ }
+
let apply_code_action =
provider.apply_code_action(buffer, action, true, window, cx);
let workspace = workspace.downgrade();
@@ -25162,6 +25175,12 @@ impl Editor {
self.refresh_document_colors(None, window, cx);
}
+ let code_lens_inline = EditorSettings::get_global(cx).code_lens.inline();
+ let was_inline = self.code_lens.is_some();
+ if code_lens_inline != was_inline {
+ self.toggle_code_lens(code_lens_inline, window, cx);
+ }
+
self.refresh_inlay_hints(
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
self.selections.newest_anchor().head(),
@@ -26334,6 +26353,7 @@ impl Editor {
self.refresh_semantic_tokens(for_buffer, None, cx);
self.refresh_document_colors(for_buffer, window, cx);
self.refresh_folding_ranges(for_buffer, window, cx);
+ self.refresh_code_lenses(for_buffer, window, cx);
self.refresh_document_symbols(for_buffer, cx);
}
@@ -26504,6 +26524,7 @@ impl Editor {
self.register_visible_buffers(cx);
self.colorize_brackets(false, cx);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ self.resolve_visible_code_lenses(cx);
if !self.buffer().read(cx).is_singleton() || self.needs_initial_data_update {
self.needs_initial_data_update = false;
self.update_lsp_data(None, window, cx);
@@ -27760,21 +27781,22 @@ impl CodeActionProvider for Entity<Project> {
cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> {
self.update(cx, |project, cx| {
- let code_lens_actions = project.code_lens_actions(buffer, range.clone(), cx);
+ let code_lens_actions = if EditorSettings::get_global(cx).code_lens.show_in_menu() {
+ Some(project.code_lens_actions(buffer, range.clone(), cx))
+ } else {
+ None
+ };
let code_actions = project.code_actions(buffer, range, None, cx);
cx.background_spawn(async move {
- let (code_lens_actions, code_actions) = join(code_lens_actions, code_actions).await;
- Ok(code_lens_actions
- .context("code lens fetch")?
- .into_iter()
- .flatten()
- .chain(
- code_actions
- .context("code action fetch")?
- .into_iter()
- .flatten(),
- )
- .collect())
+ let code_lens_actions = match code_lens_actions {
+ Some(task) => task.await.context("code lens fetch")?.unwrap_or_default(),
+ None => Vec::new(),
+ };
+ let code_actions = code_actions
+ .await
+ .context("code action fetch")?
+ .unwrap_or_default();
+ Ok(code_lens_actions.into_iter().chain(code_actions).collect())
})
})
}
@@ -4,7 +4,7 @@ use gpui::App;
use language::CursorShape;
use project::project_settings::DiagnosticSeverity;
pub use settings::{
- CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DiffViewStyle, DisplayIn,
+ CodeLens, CompletionDetailAlignment, CurrentLineHighlight, DelayMs, DiffViewStyle, DisplayIn,
DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, HideMouseMode,
MinimapThumb, MinimapThumbBorder, MultiCursorModifier, ScrollBeyondLastLine,
ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
@@ -58,6 +58,7 @@ pub struct EditorSettings {
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
pub inline_code_actions: bool,
pub drag_and_drop_selection: DragAndDropSelection,
+ pub code_lens: CodeLens,
pub lsp_document_colors: DocumentColorsRenderMode,
pub minimum_contrast_for_highlights: f32,
pub completion_menu_scrollbar: ShowScrollbar,
@@ -295,6 +296,7 @@ impl Settings for EditorSettings {
enabled: drag_and_drop_selection.enabled.unwrap(),
delay: drag_and_drop_selection.delay.unwrap(),
},
+ code_lens: editor.code_lens.unwrap(),
lsp_document_colors: editor.lsp_document_colors.unwrap(),
minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
completion_menu_scrollbar: editor
@@ -28396,6 +28396,9 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
+ update_test_editor_settings(cx, &|settings| {
+ settings.code_lens = Some(settings::CodeLens::Menu);
+ });
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
@@ -28488,15 +28491,6 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
}),
data: None,
},
- lsp::CodeLens {
- range: lsp::Range::default(),
- command: Some(lsp::Command {
- title: "Command not in capabilities".to_owned(),
- command: "not in capabilities".to_owned(),
- arguments: None,
- }),
- data: None,
- },
lsp::CodeLens {
range: lsp::Range {
start: lsp::Position {
@@ -502,6 +502,7 @@ impl EditorElement {
register_action(editor, window, Editor::toggle_relative_line_numbers);
register_action(editor, window, Editor::toggle_indent_guides);
register_action(editor, window, Editor::toggle_inlay_hints);
+ register_action(editor, window, Editor::toggle_code_lens_action);
register_action(editor, window, Editor::toggle_semantic_highlights);
register_action(editor, window, Editor::toggle_edit_predictions);
if editor.read(cx).diagnostics_enabled() {
@@ -202,6 +202,17 @@ pub static PLAIN_TEXT: LazyLock<Arc<Language>> = LazyLock::new(|| {
))
});
+/// Commands that the client (editor) handles locally rather than forwarding
+/// to the language server. Servers embed these in code lens and code action
+/// responses when they want the editor to perform a well-known UI action.
+#[derive(Debug, Clone)]
+pub enum ClientCommand {
+ /// Open a location list (references panel / peek view).
+ ShowLocations,
+ /// Schedule a task from an LSP command's arguments.
+ ScheduleTask(task::TaskTemplate),
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Location {
pub buffer: Entity<Buffer>,
@@ -555,6 +566,14 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
Ok(original)
}
+ fn client_command(
+ &self,
+ _command_name: &str,
+ _arguments: &[serde_json::Value],
+ ) -> Option<ClientCommand> {
+ None
+ }
+
/// Method only implemented by the default JSON language server adapter.
/// Used to provide dynamic reloading of the JSON schemas used to
/// provide autocompletion and diagnostics in Zed setting and keybind
@@ -225,6 +225,9 @@ impl LspAdapter for GoLspAdapter {
"parameterNames": true,
"rangeVariableTypes": true
},
+ "codelenses": {
+ "test": true
+ },
"semanticTokens": semantic_tokens_enabled
});
@@ -438,6 +441,19 @@ impl LspAdapter for GoLspAdapter {
))
}
+ fn client_command(
+ &self,
+ command_name: &str,
+ arguments: &[serde_json::Value],
+ ) -> Option<ClientCommand> {
+ if let "gopls.run_tests" = command_name {
+ let template = go_test_task_template(arguments.first()?)?;
+ Some(ClientCommand::ScheduleTask(template))
+ } else {
+ None
+ }
+ }
+
fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
static REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)\n\s*").expect("Failed to create REGEX"));
@@ -445,6 +461,74 @@ impl LspAdapter for GoLspAdapter {
}
}
+fn json_string_array(value: &serde_json::Value, key: &str) -> Vec<String> {
+ value
+ .get(key)
+ .and_then(|v| v.as_array())
+ .map(|arr| {
+ arr.iter()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect()
+ })
+ .unwrap_or_default()
+}
+
+fn go_test_task_template(arg: &serde_json::Value) -> Option<task::TaskTemplate> {
+ let tests = json_string_array(arg, "Tests");
+ let benchmarks = json_string_array(arg, "Benchmarks");
+ if tests.is_empty() && benchmarks.is_empty() {
+ return None;
+ }
+
+ let mut go_args = vec!["test".to_string(), "-test.fullpath=true".to_string()];
+
+ if tests.is_empty() {
+ go_args.push("-benchmem".to_string());
+ go_args.push("-run=^$".to_string());
+ } else {
+ go_args.push("-timeout".to_string());
+ go_args.push("30s".to_string());
+ go_args.push("-run".to_string());
+ if tests.len() == 1 {
+ go_args.push(format!("^{}$", tests[0]));
+ } else {
+ go_args.push(format!("^({})$", tests.join("|")));
+ }
+ }
+
+ if !benchmarks.is_empty() {
+ go_args.push("-bench".to_string());
+ if benchmarks.len() == 1 {
+ go_args.push(format!("^{}$", benchmarks[0]));
+ } else {
+ go_args.push(format!("^({})$", benchmarks.join("|")));
+ }
+ }
+
+ go_args.push(".".to_string());
+
+ let label = if !tests.is_empty() {
+ format!("go test {}", tests.join(", "))
+ } else {
+ format!("go bench {}", benchmarks.join(", "))
+ };
+
+ let cwd = arg
+ .get("URI")
+ .and_then(|v| v.as_str())
+ .and_then(|uri| uri.strip_prefix("file://"))
+ .and_then(|path| std::path::Path::new(path).parent())
+ .map(|p| p.to_string_lossy().into_owned());
+
+ Some(task::TaskTemplate {
+ label,
+ command: "go".to_string(),
+ args: go_args,
+ cwd,
+ ..task::TaskTemplate::default()
+ })
+}
+
fn parse_version_output(output: &Output) -> Result<&str> {
let version_stdout =
str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?;
@@ -9,6 +9,7 @@ use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
pub use language::*;
use lsp::{InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
+use project::lsp_store::lsp_ext_command;
use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
use project::project_settings::ProjectSettings;
use regex::Regex;
@@ -608,21 +609,61 @@ impl LspAdapter for RustLspAdapter {
.lsp
.get(&SERVER_NAME)
.is_some_and(|s| s.enable_lsp_tasks);
- if enable_lsp_tasks {
- let experimental = json!({
- "runnables": {
- "kinds": [ "cargo", "shell" ],
- },
- });
- if let Some(original_experimental) = &mut original.capabilities.experimental {
- merge_json_value_into(experimental, original_experimental);
- } else {
- original.capabilities.experimental = Some(experimental);
+
+ let mut experimental = json!({
+ "commands": {
+ "commands": [
+ "rust-analyzer.showReferences",
+ "rust-analyzer.gotoLocation",
+ "rust-analyzer.triggerParameterHints",
+ "rust-analyzer.rename",
+ ]
}
+ });
+
+ if enable_lsp_tasks {
+ merge_json_value_into(
+ json!({
+ "runnables": {
+ "kinds": [ "cargo", "shell" ],
+ },
+ "commands": {
+ "commands": [
+ "rust-analyzer.runSingle",
+ ]
+ }
+ }),
+ &mut experimental,
+ );
+ }
+
+ if let Some(original_experimental) = &mut original.capabilities.experimental {
+ merge_json_value_into(experimental, original_experimental);
+ } else {
+ original.capabilities.experimental = Some(experimental);
}
Ok(original)
}
+
+ fn client_command(
+ &self,
+ command_name: &str,
+ arguments: &[serde_json::Value],
+ ) -> Option<ClientCommand> {
+ match command_name {
+ "rust-analyzer.showReferences" => Some(ClientCommand::ShowLocations),
+ "rust-analyzer.runSingle" => {
+ let first_arg = arguments.first()?;
+ let runnable =
+ serde_json::from_value::<lsp_ext_command::Runnable>(first_arg.clone()).ok()?;
+ let template =
+ lsp_ext_command::runnable_to_task_template(runnable.label, runnable.args);
+ Some(ClientCommand::ScheduleTask(template))
+ }
+ _ => None,
+ }
+ }
}
impl LspInstaller for RustLspAdapter {
@@ -269,6 +269,15 @@ impl LspAdapter for VtslsLspAdapter {
"enabled": true
}
},
+ "implementationsCodeLens": {
+ "enabled": true,
+ "showOnAllClassMethods": true,
+ "showOnInterfaceMethods": true
+ },
+ "referencesCodeLens": {
+ "enabled": true,
+ "showOnAllFunctions": true
+ },
"tsserver": {
"maxTsServerMemory": 8092
},
@@ -33,6 +33,7 @@ use lsp::{
OneOf, RenameOptions, ServerCapabilities,
};
use serde_json::Value;
+
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
use std::{
cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
@@ -3851,45 +3852,27 @@ impl LspCommand for GetCodeLens {
async fn response_from_lsp(
self,
message: Option<Vec<lsp::CodeLens>>,
- lsp_store: Entity<LspStore>,
+ _lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
cx: AsyncApp,
) -> anyhow::Result<Vec<CodeAction>> {
let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
- let language_server = cx.update(|cx| {
- lsp_store
- .read(cx)
- .language_server_for_id(server_id)
- .with_context(|| {
- format!("Missing the language server that just returned a response {server_id}")
- })
- })?;
- let server_capabilities = language_server.capabilities();
- let available_commands = server_capabilities
- .execute_command_provider
- .as_ref()
- .map(|options| options.commands.as_slice())
- .unwrap_or_default();
- Ok(message
- .unwrap_or_default()
+ let code_lenses = message.unwrap_or_default();
+
+ Ok(code_lenses
.into_iter()
- .filter(|code_lens| {
- code_lens
- .command
- .as_ref()
- .is_none_or(|command| available_commands.contains(&command.command))
- })
.map(|code_lens| {
let code_lens_range = range_from_lsp(code_lens.range);
let start = snapshot.clip_point_utf16(code_lens_range.start, Bias::Left);
let end = snapshot.clip_point_utf16(code_lens_range.end, Bias::Right);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
+ let resolved = code_lens.command.is_some();
CodeAction {
server_id,
range,
lsp_action: LspAction::CodeLens(code_lens),
- resolved: false,
+ resolved,
}
})
.collect())
@@ -1087,6 +1087,7 @@ impl LocalLspStore {
let mut cx = cx.clone();
async move {
this.update(&mut cx, |this, cx| {
+ this.invalidate_code_lens();
cx.emit(LspStoreEvent::RefreshCodeLens);
this.downstream_client.as_ref().map(|(client, project_id)| {
client.send(proto::RefreshCodeLens {
@@ -5572,20 +5573,20 @@ impl LspStore {
.await
.context("resolving a code action")?;
if let Some(edit) = action.lsp_action.edit()
- && (edit.changes.is_some() || edit.document_changes.is_some()) {
- return LocalLspStore::deserialize_workspace_edit(
- this.upgrade().context("no app present")?,
- edit.clone(),
- push_to_history,
-
- lang_server.clone(),
- cx,
- )
- .await;
- }
+ && (edit.changes.is_some() || edit.document_changes.is_some())
+ {
+ return LocalLspStore::deserialize_workspace_edit(
+ this.upgrade().context("no app present")?,
+ edit.clone(),
+ push_to_history,
+ lang_server.clone(),
+ cx,
+ )
+ .await;
+ }
let Some(command) = action.lsp_action.command() else {
- return Ok(ProjectTransaction::default())
+ return Ok(ProjectTransaction::default());
};
let server_capabilities = lang_server.capabilities();
@@ -5596,15 +5597,18 @@ impl LspStore {
.unwrap_or_default();
if !available_commands.contains(&command.command) {
- log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command);
- return Ok(ProjectTransaction::default())
+ log::warn!(
+ "Skipping executeCommand for {}, not listed in language server capabilities",
+ command.command
+ );
+ return Ok(ProjectTransaction::default());
}
- let request_timeout = cx.update(|app|
+ let request_timeout = cx.update(|app| {
ProjectSettings::get_global(app)
- .global_lsp_settings
- .get_request_timeout()
- );
+ .global_lsp_settings
+ .get_request_timeout()
+ });
this.update(cx, |this, _| {
this.as_local_mut()
@@ -5614,12 +5618,16 @@ impl LspStore {
})?;
let _result = lang_server
- .request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
- command: command.command.clone(),
- arguments: command.arguments.clone().unwrap_or_default(),
- ..lsp::ExecuteCommandParams::default()
- }, request_timeout)
- .await.into_response()
+ .request::<lsp::request::ExecuteCommand>(
+ lsp::ExecuteCommandParams {
+ command: command.command.clone(),
+ arguments: command.arguments.clone().unwrap_or_default(),
+ ..lsp::ExecuteCommandParams::default()
+ },
+ request_timeout,
+ )
+ .await
+ .into_response()
.context("execute command")?;
return this.update(cx, |this, _| {
@@ -11946,12 +11954,17 @@ impl LspStore {
&self,
id: LanguageServerId,
) -> Option<Arc<CachedLspAdapter>> {
- self.as_local()
- .and_then(|local| local.language_servers.get(&id))
- .and_then(|language_server_state| match language_server_state {
- LanguageServerState::Running { adapter, .. } => Some(adapter.clone()),
- _ => None,
- })
+ if let Some(local) = self.as_local()
+ && let Some(LanguageServerState::Running { adapter, .. }) =
+ local.language_servers.get(&id)
+ {
+ return Some(adapter.clone());
+ }
+ // In remote (SSH/collab) mode there are no local `language_servers`, but
+ // `language_server_statuses` is kept in sync with the upstream and carries each
+ // server's registered name, which is enough to look the adapter up in the registry.
+ let name = &self.language_server_statuses.get(&id)?.name;
+ self.languages.adapter_for_name(name)
}
pub(super) fn update_local_worktree_language_servers(
@@ -1,3 +1,4 @@
+use std::ops::Range;
use std::sync::Arc;
use anyhow::{Context as _, Result};
@@ -8,14 +9,15 @@ use futures::{
future::{Shared, join_all},
};
use gpui::{AppContext as _, AsyncApp, Context, Entity, Task};
-use language::Buffer;
+use language::{Anchor, Buffer, ToOffset as _};
use lsp::LanguageServerId;
use rpc::{TypedEnvelope, proto};
use settings::Settings as _;
use std::time::Duration;
+use text::OffsetRangeExt as _;
use crate::{
- CodeAction, LspStore, LspStoreEvent,
+ CodeAction, LspAction, LspStore, LspStoreEvent, Project,
lsp_command::{GetCodeLens, LspCommand as _},
project_settings::ProjectSettings,
};
@@ -36,10 +38,44 @@ impl CodeLensData {
}
impl LspStore {
+ pub(super) fn invalidate_code_lens(&mut self) {
+ for lsp_data in self.lsp_data.values_mut() {
+ lsp_data.code_lens = None;
+ }
+ }
+
+ /// Fetches and returns all code lenses for the buffer.
+ ///
+ /// Resolution of individual lenses is the caller's responsibility; see
+ /// [`LspStore::resolve_visible_code_lenses`].
pub fn code_lens_actions(
&mut self,
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
+ ) -> Task<Result<Option<Vec<CodeAction>>>> {
+ let buffer_id = buffer.read(cx).remote_id();
+ let fetch_task = self.fetch_code_lenses(buffer, cx);
+
+ cx.spawn(async move |lsp_store, cx| {
+ fetch_task
+ .await
+ .map_err(|e| anyhow::anyhow!("code lens fetch failed: {e:#}"))?;
+
+ let actions = lsp_store.read_with(cx, |lsp_store, _| {
+ lsp_store
+ .lsp_data
+ .get(&buffer_id)
+ .and_then(|data| data.code_lens.as_ref())
+ .map(|code_lens| code_lens.lens.values().flatten().cloned().collect())
+ })?;
+ Ok(actions)
+ })
+ }
+
+ fn fetch_code_lenses(
+ &mut self,
+ buffer: &Entity<Buffer>,
+ cx: &mut Context<Self>,
) -> CodeLensTask {
let version_queried_for = buffer.read(cx).version();
let buffer_id = buffer.read(cx).remote_id();
@@ -83,7 +119,9 @@ impl LspStore {
.timer(Duration::from_millis(30))
.await;
let fetched_lens = lsp_store
- .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx))
+ .update(cx, |lsp_store, cx| {
+ lsp_store.fetch_code_lens_for_buffer(&buffer, cx)
+ })
.map_err(Arc::new)?
.await
.context("fetching code lens")
@@ -107,7 +145,7 @@ impl LspStore {
};
lsp_store
- .update(cx, |lsp_store, _| {
+ .update(cx, |lsp_store, cx| {
let lsp_data = lsp_store.current_lsp_data(buffer_id)?;
let code_lens = lsp_data.code_lens.as_mut()?;
if let Some(fetched_lens) = fetched_lens {
@@ -120,6 +158,11 @@ impl LspStore {
lsp_data.buffer_version = query_version_queried_for;
code_lens.lens = fetched_lens;
}
+ let snapshot = buffer.read(cx).snapshot();
+ for actions in code_lens.lens.values_mut() {
+ actions
+ .sort_by(|a, b| a.range.start.cmp(&b.range.start, &snapshot));
+ }
}
code_lens.update = None;
Some(code_lens.lens.values().flatten().cloned().collect())
@@ -131,7 +174,7 @@ impl LspStore {
new_task
}
- pub(super) fn fetch_code_lens(
+ fn fetch_code_lens_for_buffer(
&mut self,
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
@@ -202,6 +245,112 @@ impl LspStore {
}
}
+ pub fn resolve_visible_code_lenses(
+ &mut self,
+ buffer: &Entity<Buffer>,
+ visible_range: Range<Anchor>,
+ cx: &mut Context<Self>,
+ ) -> Task<Vec<CodeAction>> {
+ let buffer_id = buffer.read(cx).remote_id();
+ let snapshot = buffer.read(cx).snapshot();
+ let visible_start = visible_range.start.to_offset(&snapshot);
+ let visible_end = visible_range.end.to_offset(&snapshot);
+
+ let Some(code_lens) = self
+ .lsp_data
+ .get(&buffer_id)
+ .and_then(|data| data.code_lens.as_ref())
+ else {
+ return Task::ready(Vec::new());
+ };
+
+ let capable_servers = code_lens
+ .lens
+ .keys()
+ .filter_map(|server_id| {
+ let server = self.language_server_for_id(*server_id)?;
+ GetCodeLens::can_resolve_lens(&server.capabilities())
+ .then_some((*server_id, server))
+ })
+ .collect::<HashMap<_, _>>();
+ if capable_servers.is_empty() {
+ return Task::ready(Vec::new());
+ }
+
+ let to_resolve = code_lens
+ .lens
+ .iter()
+ .flat_map(|(server_id, actions)| {
+ let start_idx =
+ actions.partition_point(|a| a.range.start.to_offset(&snapshot) < visible_start);
+ let end_idx = start_idx
+ + actions[start_idx..]
+ .partition_point(|a| a.range.start.to_offset(&snapshot) <= visible_end);
+ actions[start_idx..end_idx].iter().enumerate().filter_map(
+ move |(local_idx, action)| {
+ let LspAction::CodeLens(lens) = &action.lsp_action else {
+ return None;
+ };
+ if lens.command.is_some() {
+ return None;
+ }
+ Some((*server_id, start_idx + local_idx, lens.clone()))
+ },
+ )
+ })
+ .collect::<Vec<_>>();
+ if to_resolve.is_empty() {
+ return Task::ready(Vec::new());
+ }
+
+ let request_timeout = ProjectSettings::get_global(cx)
+ .global_lsp_settings
+ .get_request_timeout();
+
+ cx.spawn(async move |lsp_store, cx| {
+ let mut resolved = Vec::new();
+ for (server_id, index, lens) in to_resolve {
+ let Some(server) = capable_servers.get(&server_id) else {
+ continue;
+ };
+ match server
+ .request::<lsp::request::CodeLensResolve>(lens, request_timeout)
+ .await
+ .into_response()
+ {
+ Ok(resolved_lens) => resolved.push((server_id, index, resolved_lens)),
+ Err(e) => log::warn!("Failed to resolve code lens: {e:#}"),
+ }
+ }
+ if resolved.is_empty() {
+ return Vec::new();
+ }
+
+ lsp_store
+ .update(cx, |lsp_store, _| {
+ let Some(code_lens) = lsp_store
+ .lsp_data
+ .get_mut(&buffer_id)
+ .and_then(|data| data.code_lens.as_mut())
+ else {
+ return Vec::new();
+ };
+ let mut newly_resolved = Vec::new();
+ for (server_id, index, resolved_lens) in resolved {
+ if let Some(actions) = code_lens.lens.get_mut(&server_id) {
+ if let Some(action) = actions.get_mut(index) {
+ action.resolved = true;
+ action.lsp_action = LspAction::CodeLens(resolved_lens);
+ newly_resolved.push(action.clone());
+ }
+ }
+ }
+ newly_resolved
+ })
+ .unwrap_or_default()
+ })
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn forget_code_lens_task(&mut self, buffer_id: text::BufferId) -> Option<CodeLensTask> {
Some(
@@ -216,13 +365,59 @@ impl LspStore {
}
pub(super) async fn handle_refresh_code_lens(
- this: Entity<Self>,
+ lsp_store: Entity<Self>,
_: TypedEnvelope<proto::RefreshCodeLens>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
- this.update(&mut cx, |_, cx| {
+ lsp_store.update(&mut cx, |lsp_store, cx| {
+ lsp_store.invalidate_code_lens();
cx.emit(LspStoreEvent::RefreshCodeLens);
});
Ok(proto::Ack {})
}
}
+
+impl Project {
+ pub fn code_lens_actions(
+ &mut self,
+ buffer: &Entity<Buffer>,
+ range: Range<Anchor>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Option<Vec<CodeAction>>>> {
+ let snapshot = buffer.read(cx).snapshot();
+ let range = range.to_point(&snapshot);
+ let range_start = snapshot.anchor_before(range.start);
+ let range_end = if range.start == range.end {
+ range_start
+ } else {
+ snapshot.anchor_after(range.end)
+ };
+ let range = range_start..range_end;
+ let lsp_store = self.lsp_store();
+ let fetch_task =
+ lsp_store.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
+ let buffer = buffer.clone();
+ cx.spawn(async move |_, cx| {
+ let mut actions = fetch_task.await?;
+ if let Some(actions) = &mut actions {
+ let resolve_task = lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store.resolve_visible_code_lenses(&buffer, range.clone(), cx)
+ });
+ let resolved = resolve_task.await;
+ for resolved_action in resolved {
+ if let Some(action) = actions.iter_mut().find(|a| {
+ a.server_id == resolved_action.server_id && a.range == resolved_action.range
+ }) {
+ *action = resolved_action;
+ }
+ }
+ let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+ actions.retain(|action| {
+ range.start.cmp(&action.range.start, &snapshot).is_ge()
+ && range.end.cmp(&action.range.end, &snapshot).is_le()
+ });
+ }
+ Ok(actions)
+ })
+ }
+}
@@ -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));
}
@@ -134,7 +134,7 @@ use std::{
use task_store::TaskStore;
use terminals::Terminals;
-use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
+use text::{Anchor, BufferId, Point, Rope};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _, maybe,
@@ -761,7 +761,7 @@ impl LspAction {
}
}
- fn edit(&self) -> Option<&lsp::WorkspaceEdit> {
+ pub fn edit(&self) -> Option<&lsp::WorkspaceEdit> {
match self {
Self::Action(action) => action.edit.as_ref(),
Self::Command(_) => None,
@@ -769,7 +769,7 @@ impl LspAction {
}
}
- fn command(&self) -> Option<&lsp::Command> {
+ pub fn command(&self) -> Option<&lsp::Command> {
match self {
Self::Action(action) => action.command.as_ref(),
Self::Command(command) => Some(command),
@@ -4385,45 +4385,6 @@ impl Project {
})
}
- pub fn code_lens_actions<T: Clone + ToOffset>(
- &mut self,
- buffer: &Entity<Buffer>,
- range: Range<T>,
- cx: &mut Context<Self>,
- ) -> Task<Result<Option<Vec<CodeAction>>>> {
- let snapshot = buffer.read(cx).snapshot();
- let range = range.to_point(&snapshot);
- let range_start = snapshot.anchor_before(range.start);
- let range_end = if range.start == range.end {
- range_start
- } else {
- snapshot.anchor_after(range.end)
- };
- let range = range_start..range_end;
- let code_lens_actions = self
- .lsp_store
- .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
-
- cx.background_spawn(async move {
- let mut code_lens_actions = code_lens_actions
- .await
- .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?;
- if let Some(code_lens_actions) = &mut code_lens_actions {
- code_lens_actions.retain(|code_lens_action| {
- range
- .start
- .cmp(&code_lens_action.range.start, &snapshot)
- .is_ge()
- && range
- .end
- .cmp(&code_lens_action.range.end, &snapshot)
- .is_le()
- });
- }
- Ok(code_lens_actions)
- })
- }
-
pub fn apply_code_action(
&self,
buffer_handle: Entity<Buffer>,
@@ -2371,6 +2371,148 @@ async fn test_remote_external_agent_server(
);
}
+#[gpui::test]
+async fn test_remote_apply_code_action_skips_unadvertised_command(
+ cx: &mut TestAppContext,
+ server_cx: &mut TestAppContext,
+) {
+ let fs = FakeFs::new(server_cx.executor());
+ fs.insert_tree(
+ path!("/code"),
+ json!({
+ "project1": {
+ ".git": {},
+ "README.md": "# project 1",
+ "src": {
+ "lib.rs": "fn one() -> usize { 1 }"
+ }
+ },
+ }),
+ )
+ .await;
+
+ let (project, headless) = init_test(&fs, cx, server_cx).await;
+
+ fs.insert_tree(
+ path!("/code/project1/.zed"),
+ json!({
+ "settings.json": r#"
+ {
+ "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
+ "lsp": {
+ "rust-analyzer": {
+ "binary": {
+ "path": "~/.cargo/bin/rust-analyzer"
+ }
+ }
+ }
+ }"#
+ }),
+ )
+ .await;
+
+ cx.update_entity(&project, |project, _| {
+ project.languages().register_test_language(LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".into()],
+ ..Default::default()
+ },
+ ..Default::default()
+ });
+ project.languages().register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ name: "rust-analyzer",
+ ..Default::default()
+ },
+ )
+ });
+
+ // Register the fake LSP with an empty execute_command_provider and a handler that panics
+ // if it is ever reached: commands not advertised by the server must be rejected by
+ // `apply_code_action` before dispatching to the language server.
+ let mut fake_lsp = server_cx.update(|cx| {
+ headless.read(cx).languages.register_fake_lsp_server(
+ LanguageServerName("rust-analyzer".into()),
+ lsp::ServerCapabilities {
+ execute_command_provider: Some(lsp::ExecuteCommandOptions {
+ commands: Vec::new(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ Some(Box::new(|fake| {
+ fake.set_request_handler::<lsp::request::ExecuteCommand, _, _>(
+ |params, _| async move {
+ panic!(
+ "Unadvertised command {} must not reach the language server",
+ params.command
+ );
+ },
+ );
+ })),
+ )
+ });
+
+ cx.run_until_parked();
+
+ let worktree_id = project
+ .update(cx, |project, cx| {
+ project.find_or_create_worktree(path!("/code/project1"), true, cx)
+ })
+ .await
+ .unwrap()
+ .0
+ .read_with(cx, |worktree, _| worktree.id());
+
+ cx.run_until_parked();
+
+ let (buffer, _handle) = project
+ .update(cx, |project, cx| {
+ project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
+ })
+ .await
+ .unwrap();
+
+ cx.run_until_parked();
+
+ let _fake_lsp = fake_lsp.next().await.unwrap();
+
+ let server_id = server_cx.read(|cx| {
+ *headless
+ .read(cx)
+ .lsp_store
+ .read(cx)
+ .as_local()
+ .unwrap()
+ .language_servers
+ .keys()
+ .next()
+ .unwrap()
+ });
+ let buffer_id = cx.read(|cx| buffer.read(cx).remote_id());
+
+ let action = project::CodeAction {
+ server_id,
+ range: language::Anchor::min_min_range_for_buffer(buffer_id),
+ lsp_action: project::LspAction::Command(lsp::Command {
+ title: "\u{25b6}\u{fe0e} Run Tests".into(),
+ command: "rust-analyzer.runSingle".into(),
+ arguments: Some(vec![json!({"label": "test-mod tests"})]),
+ }),
+ resolved: true,
+ };
+
+ let transaction = project
+ .update(cx, |project, cx| {
+ project.apply_code_action(buffer.clone(), action, true, cx)
+ })
+ .await
+ .expect("Unadvertised command must not be forwarded to executeCommand");
+ assert_eq!(transaction.0.len(), 0);
+}
+
pub async fn init_test(
server_fs: &Arc<FakeFs>,
cx: &mut TestAppContext,
@@ -271,6 +271,7 @@ impl VsCodeSettings {
hover_popover_sticky: self.read_bool("editor.hover.sticky"),
hover_popover_hiding_delay: self.read_u64("editor.hover.hidingDelay").map(Into::into),
inline_code_actions: None,
+ code_lens: None,
jupyter: None,
lsp_document_colors: None,
lsp_highlight_debounce: None,
@@ -215,6 +215,11 @@ pub struct EditorSettingsContent {
/// Drag and drop related settings
pub drag_and_drop_selection: Option<DragAndDropSelectionContent>,
+ /// Whether and how to display code lenses from language servers.
+ ///
+ /// Default: "off"
+ pub code_lens: Option<CodeLens>,
+
/// How to render LSP `textDocument/documentColor` colors in the editor.
///
/// Default: [`DocumentColorsRenderMode::Inlay`]
@@ -461,7 +466,7 @@ pub struct GutterContent {
pub folds: Option<bool>,
}
-/// How to render LSP `textDocument/documentColor` colors in the editor.
+/// Whether to display code lenses from language servers above code elements.
#[derive(
Copy,
Clone,
@@ -477,6 +482,46 @@ pub struct GutterContent {
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
+pub enum CodeLens {
+ /// Do not query and display code lenses.
+ #[default]
+ Off,
+ /// Display code lenses from language servers above code elements.
+ On,
+ /// Display code lenses in the code action menu.
+ Menu,
+}
+
+impl CodeLens {
+ pub fn enabled(&self) -> bool {
+ self != &Self::Off
+ }
+
+ pub fn inline(&self) -> bool {
+ *self == Self::On
+ }
+
+ pub fn show_in_menu(&self) -> bool {
+ *self == Self::Menu
+ }
+}
+
+/// How to render LSP `textDocument/documentColor` colors in the editor.
+#[derive(
+ Debug,
+ Clone,
+ Copy,
+ Default,
+ Serialize,
+ Deserialize,
+ JsonSchema,
+ MergeFrom,
+ PartialEq,
+ Eq,
+ strum::VariantArray,
+ strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
pub enum DocumentColorsRenderMode {
/// Do not query and render document colors.
None,
@@ -8975,6 +8975,20 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
let is_global = active_language().is_none();
+ let code_lens_item = [SettingsPageItem::SettingItem(SettingItem {
+ title: "Code Lens",
+ description: "Whether and how to display code lenses from language servers.",
+ field: Box::new(SettingField {
+ json_path: Some("code_lens"),
+ pick: |settings_content| settings_content.editor.code_lens.as_ref(),
+ write: |settings_content, value| {
+ settings_content.editor.code_lens = value;
+ },
+ }),
+ metadata: None,
+ files: USER,
+ })];
+
let lsp_document_colors_item = [SettingsPageItem::SettingItem(SettingItem {
title: "LSP Document Colors",
description: "How to render LSP color previews in the editor.",
@@ -8999,6 +9013,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
whitespace_section(),
completions_section(),
inlay_hints_section(),
+ code_lens_item,
lsp_document_colors_item,
tasks_section(),
miscellaneous_section(),
@@ -9014,6 +9029,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
whitespace_section(),
completions_section(),
inlay_hints_section(),
+ code_lens_item,
tasks_section(),
miscellaneous_section(),
)
@@ -536,6 +536,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
+ .add_basic_renderer::<settings::CodeLens>(render_dropdown)
.add_basic_renderer::<settings::DocumentColorsRenderMode>(render_dropdown)
.add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::ThemeAppearanceMode>(render_dropdown)
@@ -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();
@@ -78,6 +78,39 @@ to override these settings.
See [gopls inlayHints documentation](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md) for more information.
+## Code Lens
+
+Zed enables the `test` code lens for `gopls` by default. This shows "run test" and "run benchmark" links above `Test` and `Benchmark` functions in `*_test.go` files. To use them, enable the `code_lens` setting:
+
+```json [settings]
+{
+ "code_lens": "on"
+}
+```
+
+You can override the default code lens settings in your `settings.json`:
+
+```json [settings]
+{
+ "lsp": {
+ "gopls": {
+ "initialization_options": {
+ "codelenses": {
+ "test": true,
+ "generate": true,
+ "regenerate_cgo": true,
+ "tidy": true,
+ "upgrade_dependency": true,
+ "vendor": true
+ }
+ }
+ }
+ }
+}
+```
+
+See [gopls code lenses documentation](https://go.dev/gopls/codelenses) for more information.
+
## Debugging
Zed supports zero-configuration debugging of Go tests and entry points (`func main`) using Delve. Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these preconfigured debug tasks.
@@ -189,6 +189,51 @@ When using `vtsls`:
}
```
+## Code Lens
+
+Zed enables references and implementations code lenses for `vtsls` by default. These show reference counts and implementation counts above functions, classes, and interfaces. To use them, enable the `code_lens` setting:
+
+```json [settings]
+{
+ "code_lens": "on"
+}
+```
+
+You can override the default code lens settings in your `settings.json`:
+
+```json [settings]
+{
+ "lsp": {
+ "vtsls": {
+ "settings": {
+ "typescript": {
+ "implementationsCodeLens": {
+ "enabled": true,
+ "showOnAllClassMethods": true,
+ "showOnInterfaceMethods": true
+ },
+ "referencesCodeLens": {
+ "enabled": true,
+ "showOnAllFunctions": true
+ }
+ },
+ "javascript": {
+ "implementationsCodeLens": {
+ "enabled": true,
+ "showOnAllClassMethods": true,
+ "showOnInterfaceMethods": true
+ },
+ "referencesCodeLens": {
+ "enabled": true,
+ "showOnAllFunctions": true
+ }
+ }
+ }
+ }
+ }
+}
+```
+
## Debugging
Zed supports debugging TypeScript code out of the box with `vscode-js-debug`.
@@ -450,6 +450,24 @@ When enabled, this setting will automatically close tabs for files that have bee
Note: Dirty files (files with unsaved changes) will not be automatically closed even when this setting is enabled, ensuring you don't lose unsaved work.
+## Code Lens
+
+- Description: Whether and how to display code lenses from language servers. Code lenses show contextual information such as reference counts, implementations, and other metadata provided by the language server.
+- Setting: `code_lens`
+- Default: `off`
+
+**Options**
+
+1. `off`: Do not query and display code lenses.
+2. `on`: Display code lenses from language servers above code elements.
+3. `menu`: Display code lenses in the code action menu.
+
+```json [settings]
+{
+ "code_lens": "on"
+}
+```
+
## Confirm Quit
- Description: Whether or not to prompt the user to confirm before closing the application.