mirror of
https://github.com/zed-industries/zed.git
synced 2026-04-18 07:47:53 +00:00
Support code lens in the editor
# Conflicts: # crates/editor/src/editor.rs
This commit is contained in:
@@ -342,6 +342,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`
|
||||
|
||||
@@ -841,6 +841,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,913 @@
|
||||
use std::{collections::HashMap as StdHashMap, 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> {
|
||||
let mut grouped: HashMap<MultiBufferRow, (Anchor, Vec<CodeLensItem>)> = HashMap::default();
|
||||
|
||||
for (position, item) in lenses {
|
||||
let row = position.to_point(snapshot).row;
|
||||
grouped
|
||||
.entry(MultiBufferRow(row))
|
||||
.or_insert_with(|| (position, Vec::new()))
|
||||
.1
|
||||
.push(item);
|
||||
}
|
||||
|
||||
grouped
|
||||
.into_iter()
|
||||
.map(|(_, (position, items))| {
|
||||
let row = position.to_point(snapshot).row;
|
||||
let indent_column = snapshot
|
||||
.indent_size_for_line(multi_buffer::MultiBufferRow(row))
|
||||
.len;
|
||||
CodeLensLine {
|
||||
position,
|
||||
indent_column,
|
||||
items,
|
||||
}
|
||||
})
|
||||
.sorted_by_key(|lens| lens.position.to_point(snapshot).row)
|
||||
}
|
||||
|
||||
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<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;
|
||||
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();
|
||||
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() + 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)
|
||||
.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,
|
||||
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| {
|
||||
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();
|
||||
let grouped = group_lenses_by_row(individual_lenses, &multi_buffer_snapshot);
|
||||
new_lenses_per_buffer.insert(buffer_id, grouped.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);
|
||||
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 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, _, excerpt_range)| {
|
||||
let buffer_id = snapshot.remote_id();
|
||||
let buffer = self.buffer.read(cx).buffer(buffer_id)?;
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
lsp_store.resolve_visible_code_lenses(&buffer, excerpt_range.context, 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 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;
|
||||
@@ -97,6 +98,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;
|
||||
@@ -1336,8 +1338,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>,
|
||||
@@ -2159,7 +2163,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,
|
||||
@@ -2581,7 +2585,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,
|
||||
@@ -2753,6 +2759,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);
|
||||
@@ -7131,6 +7140,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();
|
||||
@@ -24822,6 +24835,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(),
|
||||
@@ -25994,6 +26013,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);
|
||||
}
|
||||
|
||||
@@ -26164,6 +26184,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);
|
||||
|
||||
@@ -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,
|
||||
@@ -293,6 +294,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
|
||||
|
||||
@@ -27965,15 +27965,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() {
|
||||
|
||||
@@ -203,6 +203,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>,
|
||||
@@ -556,6 +567,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())
|
||||
|
||||
@@ -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 {
|
||||
@@ -5544,20 +5545,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();
|
||||
@@ -5568,15 +5569,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()
|
||||
@@ -5586,12 +5589,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, _| {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -8,14 +9,14 @@ 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 crate::{
|
||||
CodeAction, LspStore, LspStoreEvent,
|
||||
CodeAction, LspAction, LspStore, LspStoreEvent,
|
||||
lsp_command::{GetCodeLens, LspCommand as _},
|
||||
project_settings::ProjectSettings,
|
||||
};
|
||||
@@ -36,6 +37,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>,
|
||||
@@ -107,7 +114,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 +127,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())
|
||||
@@ -202,6 +214,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(
|
||||
@@ -220,7 +338,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -758,7 +758,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,
|
||||
@@ -766,7 +766,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),
|
||||
@@ -4382,11 +4382,19 @@ impl Project {
|
||||
let code_lens_actions = self
|
||||
.lsp_store
|
||||
.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx));
|
||||
let lsp_store = self.lsp_store.clone();
|
||||
let buffer = buffer.clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
cx.spawn(async move |_, cx| {
|
||||
let mut code_lens_actions = code_lens_actions
|
||||
.await
|
||||
.map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?;
|
||||
let resolved = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.resolve_visible_code_lenses(&buffer, range.clone(), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(code_lens_actions) = &mut code_lens_actions {
|
||||
code_lens_actions.retain(|code_lens_action| {
|
||||
range
|
||||
@@ -4398,6 +4406,14 @@ impl Project {
|
||||
.cmp(&code_lens_action.range.end, &snapshot)
|
||||
.is_le()
|
||||
});
|
||||
for resolved_action in resolved {
|
||||
if let Some(existing) = code_lens_actions.iter_mut().find(|existing_action| {
|
||||
existing_action.server_id == resolved_action.server_id
|
||||
&& existing_action.range == resolved_action.range
|
||||
}) {
|
||||
*existing = resolved_action;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(code_lens_actions)
|
||||
})
|
||||
|
||||
@@ -270,6 +270,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 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`]
|
||||
@@ -457,7 +462,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,
|
||||
@@ -473,6 +478,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,
|
||||
|
||||
@@ -8903,6 +8903,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.",
|
||||
@@ -8927,6 +8941,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(),
|
||||
@@ -8942,6 +8957,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
|
||||
whitespace_section(),
|
||||
completions_section(),
|
||||
inlay_hints_section(),
|
||||
code_lens_item,
|
||||
tasks_section(),
|
||||
miscellaneous_section(),
|
||||
)
|
||||
|
||||
@@ -535,6 +535,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,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.
|
||||
|
||||
Reference in New Issue
Block a user