How it looks: https://github.com/user-attachments/assets/9a355807-5461-4e8d-b7a8-9efb98cea67a Idea behind this is to reduce flickering in areas where nothing is happening - whenever these hide, the user is specifically not interacting with them, hence it can be distracting to have something flicker in the side of your eye. This PR tackles this. Release Notes: - Added graceful autohiding to scrollbars outside of the editor --------- Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
463 lines
18 KiB
Rust
463 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_for(self.scroll_handle.clone(), 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()
|
|
}
|
|
}
|