462 lines
18 KiB
Rust
462 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, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, 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, SharedString, StatefulInteractiveElement, Styled, StyledExt,
|
|
WithScrollbar, div, 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(¬_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(¬_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 {
|
|
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,
|
|
}
|
|
|
|
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),
|
|
)
|
|
}),
|
|
)
|
|
.vertical_scrollbar(window, 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()
|
|
}
|
|
}
|