Files
oak-gpui/crates/editor/src/signature_help.rs
T
5dcb90858e Stop waiting for part of LSP responses on remote Collab clients' part (#36557)
Instead of holding a connection for potentially long LSP queries (e.g.
rust-analyzer might take minutes to look up a definition), disconnect
right after sending the initial request and handle the follow-up
responses later.

As a bonus, this allows to cancel previously sent request on the local
Collab clients' side due to this, as instead of holding and serving the
old connection, local clients now can stop previous requests, if needed.

Current PR does not convert all LSP requests to the new paradigm, but
the problematic ones, deprecating `MultiLspQuery` and moving all its
requests to the new paradigm.

Release Notes:

- Improved resource usage when querying LSP over Collab

---------

Co-authored-by: David Kleingeld <git@davidsk.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-21 09:24:34 +03:00

486 lines
18 KiB
Rust

use crate::actions::ShowSignatureHelp;
use crate::hover_popover::open_markdown_url;
use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
use gpui::{
App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful,
StyledText, Task, TextStyle, Window, combine_highlights,
};
use language::BufferSnapshot;
use markdown::{Markdown, MarkdownElement};
use multi_buffer::{Anchor, ToOffset};
use settings::Settings;
use std::ops::Range;
use text::Rope;
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, StyledExt, div, px, relative,
};
// Language-specific settings may define quotes as "brackets", so filter them out separately.
const QUOTE_PAIRS: [(&str, &str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SignatureHelpHiddenBy {
AutoClose,
Escape,
Selection,
}
impl Editor {
pub fn toggle_auto_signature_help_menu(
&mut self,
_: &ToggleAutoSignatureHelp,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.auto_signature_help = self
.auto_signature_help
.map(|auto_signature_help| !auto_signature_help)
.or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
match self.auto_signature_help {
Some(true) => {
self.show_signature_help(&ShowSignatureHelp, window, cx);
}
Some(false) => {
self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
}
None => {}
}
}
pub(super) fn hide_signature_help(
&mut self,
cx: &mut Context<Self>,
signature_help_hidden_by: SignatureHelpHiddenBy,
) -> bool {
if self.signature_help_state.is_shown() {
self.signature_help_state.task = None;
self.signature_help_state.hide(signature_help_hidden_by);
cx.notify();
true
} else {
false
}
}
pub fn auto_signature_help_enabled(&self, cx: &App) -> bool {
if let Some(auto_signature_help) = self.auto_signature_help {
auto_signature_help
} else {
EditorSettings::get_global(cx).auto_signature_help
}
}
pub(super) fn should_open_signature_help_automatically(
&mut self,
old_cursor_position: &Anchor,
cx: &mut Context<Self>,
) -> bool {
if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
return false;
}
let newest_selection = self.selections.newest::<usize>(cx);
let head = newest_selection.head();
if !newest_selection.is_empty() && head != newest_selection.tail() {
self.signature_help_state
.hide(SignatureHelpHiddenBy::Selection);
return false;
}
let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let bracket_range = |position: usize| match (position, position + 1) {
(0, b) if b <= buffer_snapshot.len() => 0..b,
(0, b) => 0..b - 1,
(a, b) if b <= buffer_snapshot.len() => a - 1..b,
(a, b) => a - 1..b - 1,
};
let not_quote_like_brackets =
|buffer: &BufferSnapshot, start: Range<usize>, end: Range<usize>| {
let text_start = buffer.text_for_range(start).collect::<String>();
let text_end = buffer.text_for_range(end).collect::<String>();
QUOTE_PAIRS
.into_iter()
.all(|(start, end)| text_start != start && text_end != end)
};
let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
let previous_brackets_range = bracket_range(previous_position);
let previous_brackets_surround = buffer_snapshot
.innermost_enclosing_bracket_ranges(
previous_brackets_range,
Some(&not_quote_like_brackets),
)
.filter(|(start_bracket_range, end_bracket_range)| {
start_bracket_range.start != previous_position
&& end_bracket_range.end != previous_position
});
let current_brackets_range = bracket_range(head);
let current_brackets_surround = buffer_snapshot
.innermost_enclosing_bracket_ranges(
current_brackets_range,
Some(&not_quote_like_brackets),
)
.filter(|(start_bracket_range, end_bracket_range)| {
start_bracket_range.start != head && end_bracket_range.end != head
});
match (previous_brackets_surround, current_brackets_surround) {
(None, None) => {
self.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
false
}
(Some(_), None) => {
self.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
false
}
(None, Some(_)) => true,
(Some(previous), Some(current)) => {
let condition = self.signature_help_state.hidden_by_selection()
|| previous != current
|| (previous == current && self.signature_help_state.is_shown());
if !condition {
self.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
}
condition
}
}
}
pub fn show_signature_help(
&mut self,
_: &ShowSignatureHelp,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.pending_rename.is_some() || self.has_visible_completions_menu() {
return;
}
let position = self.selections.newest_anchor().head();
let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(position, cx)
else {
return;
};
let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
return;
};
let task = lsp_store.update(cx, |lsp_store, cx| {
lsp_store.signature_help(&buffer, buffer_position, cx)
});
let language = self.language_at(position, cx);
self.signature_help_state
.set_task(cx.spawn_in(window, async move |editor, cx| {
let signature_help = task.await;
editor
.update(cx, |editor, cx| {
let Some(mut signature_help) =
signature_help.unwrap_or_default().into_iter().next()
else {
editor
.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
return;
};
if let Some(language) = language {
for signature in &mut signature_help.signatures {
let text = Rope::from(signature.label.as_ref());
let highlights = language
.highlight_text(&text, 0..signature.label.len())
.into_iter()
.flat_map(|(range, highlight_id)| {
Some((range, highlight_id.style(cx.theme().syntax())?))
});
signature.highlights =
combine_highlights(signature.highlights.clone(), highlights)
.collect();
}
}
let settings = ThemeSettings::get_global(cx);
let style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..TextStyle::default()
};
let scroll_handle = ScrollHandle::new();
let signatures = signature_help
.signatures
.into_iter()
.map(|s| SignatureHelp {
label: s.label,
documentation: s.documentation,
highlights: s.highlights,
active_parameter: s.active_parameter,
parameter_documentation: s
.active_parameter
.and_then(|idx| s.parameters.get(idx))
.and_then(|param| param.documentation.clone()),
})
.collect::<Vec<_>>();
if signatures.is_empty() {
editor
.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
return;
}
let current_signature = signature_help
.active_signature
.min(signatures.len().saturating_sub(1));
let signature_help_popover = SignatureHelpPopover {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
style,
signatures,
current_signature,
scroll_handle,
};
editor
.signature_help_state
.set_popover(signature_help_popover);
cx.notify();
})
.ok();
}));
}
}
#[derive(Default, Debug)]
pub struct SignatureHelpState {
task: Option<Task<()>>,
popover: Option<SignatureHelpPopover>,
hidden_by: Option<SignatureHelpHiddenBy>,
}
impl SignatureHelpState {
fn set_task(&mut self, task: Task<()>) {
self.task = Some(task);
self.hidden_by = None;
}
#[cfg(test)]
pub fn popover(&self) -> Option<&SignatureHelpPopover> {
self.popover.as_ref()
}
pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
self.popover.as_mut()
}
fn set_popover(&mut self, popover: SignatureHelpPopover) {
self.popover = Some(popover);
self.hidden_by = None;
}
fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
if self.hidden_by.is_none() {
self.popover = None;
self.hidden_by = Some(hidden_by);
}
}
fn hidden_by_selection(&self) -> bool {
self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
}
pub fn is_shown(&self) -> bool {
self.popover.is_some()
}
pub fn has_multiple_signatures(&self) -> bool {
self.popover
.as_ref()
.is_some_and(|popover| popover.signatures.len() > 1)
}
}
#[cfg(test)]
impl SignatureHelpState {
pub fn task(&self) -> Option<&Task<()>> {
self.task.as_ref()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SignatureHelp {
pub(crate) label: SharedString,
documentation: Option<Entity<Markdown>>,
highlights: Vec<(Range<usize>, HighlightStyle)>,
active_parameter: Option<usize>,
parameter_documentation: Option<Entity<Markdown>>,
}
#[derive(Clone, Debug)]
pub struct SignatureHelpPopover {
pub style: TextStyle,
pub signatures: Vec<SignatureHelp>,
pub current_signature: usize,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
}
impl SignatureHelpPopover {
pub fn render(
&mut self,
max_size: Size<Pixels>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> AnyElement {
let Some(signature) = self.signatures.get(self.current_signature) else {
return div().into_any_element();
};
let main_content = div()
.occlude()
.p_2()
.child(
div()
.id("signature_help_container")
.overflow_y_scroll()
.max_w(max_size.width)
.max_h(max_size.height)
.track_scroll(&self.scroll_handle)
.child(
StyledText::new(signature.label.clone()).with_default_highlights(
&self.style,
signature.highlights.iter().cloned(),
),
)
.when_some(
signature.parameter_documentation.clone(),
|this, param_doc| {
this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
.child(
MarkdownElement::new(
param_doc,
hover_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
border: false,
copy_button_on_hover: false,
})
.on_url_click(open_markdown_url),
)
},
)
.when_some(signature.documentation.clone(), |this, description| {
this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
.child(
MarkdownElement::new(description, hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
border: false,
copy_button_on_hover: false,
})
.on_url_click(open_markdown_url),
)
}),
)
.child(self.render_vertical_scrollbar(cx));
let controls = if self.signatures.len() > 1 {
let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
.shape(IconButtonShape::Square)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(move |window, cx| {
ui::Tooltip::for_action(
"Previous Signature",
&crate::SignatureHelpPrevious,
window,
cx,
)
})
.on_click(cx.listener(|editor, _, window, cx| {
editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
}));
let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
.shape(IconButtonShape::Square)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(move |window, cx| {
ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx)
})
.on_click(cx.listener(|editor, _, window, cx| {
editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
}));
let page = Label::new(format!(
"{}/{}",
self.current_signature + 1,
self.signatures.len()
))
.size(LabelSize::Small);
Some(
div()
.flex()
.flex_col()
.items_center()
.gap_0p5()
.px_0p5()
.py_0p5()
.children([
prev_button.into_any_element(),
div().child(page).into_any_element(),
next_button.into_any_element(),
])
.into_any_element(),
)
} else {
None
};
div()
.elevation_2(cx)
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.flex()
.flex_row()
.when_some(controls, |this, controls| {
this.children(vec![
div().flex().items_end().child(controls),
div().w_px().bg(cx.theme().colors().border_variant),
])
})
.child(main_content)
.into_any_element()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("signature_help_scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| cx.stop_propagation())
.on_any_mouse_down(|_, _, cx| cx.stop_propagation())
.on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}