Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/lsp/hover.rs')
| -rw-r--r-- | helix-term/src/ui/lsp/hover.rs | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/helix-term/src/ui/lsp/hover.rs b/helix-term/src/ui/lsp/hover.rs new file mode 100644 index 00000000..bc50037b --- /dev/null +++ b/helix-term/src/ui/lsp/hover.rs @@ -0,0 +1,189 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; +use helix_core::syntax; +use helix_lsp::lsp; +use helix_view::graphics::{Margin, Rect, Style}; +use helix_view::input::Event; +use once_cell::sync::OnceCell; +use tui::buffer::Buffer; +use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; + +use crate::compositor::{Component, Context, EventResult}; + +use crate::alt; +use crate::ui::Markdown; + +pub struct Hover { + hovers: Vec<(String, lsp::Hover)>, + active_index: usize, + config_loader: Arc<ArcSwap<syntax::Loader>>, + + content: OnceCell<(Option<Markdown>, Markdown)>, +} + +impl Hover { + pub const ID: &'static str = "hover"; + + pub fn new( + hovers: Vec<(String, lsp::Hover)>, + config_loader: Arc<ArcSwap<syntax::Loader>>, + ) -> Self { + Self { + hovers, + active_index: usize::default(), + config_loader, + content: OnceCell::new(), + } + } + + fn content(&self) -> &(Option<Markdown>, Markdown) { + self.content.get_or_init(|| { + let (server_name, hover) = &self.hovers[self.active_index]; + // Only render the header when there is more than one hover response. + let header = (self.hovers.len() > 1).then(|| { + Markdown::new( + format!( + "**[{}/{}] {}**", + self.active_index + 1, + self.hovers.len(), + server_name + ), + self.config_loader.clone(), + ) + }); + let body = Markdown::new( + hover_contents_to_string(&hover.contents), + self.config_loader.clone(), + ); + (header, body) + }) + } + + fn set_index(&mut self, index: usize) { + assert!((0..self.hovers.len()).contains(&index)); + self.active_index = index; + // Reset the cached markdown: + self.content.take(); + } +} + +const PADDING_HORIZONTAL: u16 = 2; +const PADDING_TOP: u16 = 1; +const PADDING_BOTTOM: u16 = 1; +const HEADER_HEIGHT: u16 = 1; +const SEPARATOR_HEIGHT: u16 = 1; + +impl Component for Hover { + fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { + let margin = Margin::all(1); + let area = area.inner(margin); + + let (header, contents) = self.content(); + + // show header and border only when more than one results + if let Some(header) = header { + // header LSP Name + let header = header.parse(Some(&cx.editor.theme)); + let header = Paragraph::new(&header); + header.render(area.with_height(HEADER_HEIGHT), surface); + + // border + let sep_style = Style::default(); + let borders = BorderType::line_symbols(BorderType::Plain); + for x in area.left()..area.right() { + if let Some(cell) = surface.get_mut(x, area.top() + HEADER_HEIGHT) { + cell.set_symbol(borders.horizontal).set_style(sep_style); + } + } + } + + // hover content + let contents = contents.parse(Some(&cx.editor.theme)); + let contents_area = area + .clip_top(if self.hovers.len() > 1 { + HEADER_HEIGHT + SEPARATOR_HEIGHT + } else { + 0 + }) + .clip_bottom(u16::from(cx.editor.popup_border())); + let contents_para = Paragraph::new(&contents) + .wrap(Wrap { trim: false }) + .scroll((cx.scroll.unwrap_or_default() as u16, 0)); + contents_para.render(contents_area, surface); + } + + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let max_text_width = viewport.0.saturating_sub(PADDING_HORIZONTAL).clamp(10, 120); + + let (header, contents) = self.content(); + + let header_width = header + .as_ref() + .map(|header| { + let header = header.parse(None); + let (width, _height) = crate::ui::text::required_size(&header, max_text_width); + width + }) + .unwrap_or_default(); + + let contents = contents.parse(None); + let (content_width, content_height) = + crate::ui::text::required_size(&contents, max_text_width); + + let width = PADDING_HORIZONTAL + header_width.max(content_width); + let height = if self.hovers.len() > 1 { + PADDING_TOP + HEADER_HEIGHT + SEPARATOR_HEIGHT + content_height + PADDING_BOTTOM + } else { + PADDING_TOP + content_height + PADDING_BOTTOM + }; + + Some((width, height)) + } + + fn handle_event(&mut self, event: &Event, _ctx: &mut Context) -> EventResult { + let Event::Key(event) = event else { + return EventResult::Ignored(None); + }; + + match event { + alt!('p') => { + let index = self + .active_index + .checked_sub(1) + .unwrap_or(self.hovers.len() - 1); + self.set_index(index); + EventResult::Consumed(None) + } + alt!('n') => { + self.set_index((self.active_index + 1) % self.hovers.len()); + EventResult::Consumed(None) + } + _ => EventResult::Ignored(None), + } + } +} + +fn hover_contents_to_string(contents: &lsp::HoverContents) -> String { + fn marked_string_to_markdown(contents: &lsp::MarkedString) -> String { + match contents { + lsp::MarkedString::String(contents) => contents.clone(), + lsp::MarkedString::LanguageString(string) => { + if string.language == "markdown" { + string.value.clone() + } else { + format!("```{}\n{}\n```", string.language, string.value) + } + } + } + } + match contents { + lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents), + lsp::HoverContents::Array(contents) => contents + .iter() + .map(marked_string_to_markdown) + .collect::<Vec<_>>() + .join("\n\n"), + lsp::HoverContents::Markup(contents) => contents.value.clone(), + } +} |