2025-01-27 01:24:50 +08:00
|
|
|
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 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 {
|
|
|
|
active_index: usize,
|
2025-02-01 23:48:25 +08:00
|
|
|
contents: Vec<(Option<Markdown>, Markdown)>,
|
2025-01-27 01:24:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Hover {
|
|
|
|
pub const ID: &'static str = "hover";
|
|
|
|
|
|
|
|
pub fn new(
|
|
|
|
hovers: Vec<(String, lsp::Hover)>,
|
|
|
|
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
|
|
|
) -> Self {
|
2025-02-01 03:08:42 +08:00
|
|
|
let n_hovers = hovers.len();
|
|
|
|
let contents = hovers
|
|
|
|
.into_iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(idx, (server_name, hover))| {
|
2025-02-01 23:22:43 +08:00
|
|
|
let header = (n_hovers > 1)
|
2025-02-01 23:48:25 +08:00
|
|
|
.then(|| format!("**[{}/{}] {}**\n", idx + 1, n_hovers, server_name))
|
|
|
|
.map(|h| Markdown::new(h, Arc::clone(&config_loader)));
|
|
|
|
|
|
|
|
let body = Markdown::new(
|
|
|
|
hover_contents_to_string(hover.contents),
|
|
|
|
Arc::clone(&config_loader),
|
|
|
|
);
|
2025-02-01 23:22:43 +08:00
|
|
|
|
2025-02-01 03:08:42 +08:00
|
|
|
(header, body)
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
2025-01-27 01:24:50 +08:00
|
|
|
Self {
|
|
|
|
active_index: usize::default(),
|
2025-02-01 03:08:42 +08:00
|
|
|
contents,
|
2025-01-27 01:24:50 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-01 03:08:42 +08:00
|
|
|
fn has_header(&self) -> bool {
|
|
|
|
self.contents.len() > 1
|
|
|
|
}
|
|
|
|
|
2025-02-01 23:48:25 +08:00
|
|
|
fn content_markdown(&self) -> &(Option<Markdown>, Markdown) {
|
|
|
|
&self.contents[self.active_index]
|
2025-02-01 23:22:43 +08:00
|
|
|
}
|
|
|
|
|
2025-02-01 23:48:25 +08:00
|
|
|
pub fn content_string(&self) -> String {
|
2025-02-01 23:22:43 +08:00
|
|
|
self.contents
|
|
|
|
.iter()
|
|
|
|
.map(|(header, body)| {
|
2025-02-01 23:48:47 +08:00
|
|
|
let header: String = header
|
|
|
|
.iter()
|
|
|
|
.map(|header| header.contents.clone())
|
|
|
|
.collect();
|
2025-02-01 23:22:43 +08:00
|
|
|
|
2025-02-01 23:48:25 +08:00
|
|
|
format!("{}{}", header, body.contents)
|
2025-02-01 23:22:43 +08:00
|
|
|
})
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join("\n\n---\n\n")
|
2025-02-01 23:31:51 +08:00
|
|
|
+ "\n"
|
2025-01-27 01:24:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
fn set_index(&mut self, index: usize) {
|
2025-02-01 03:08:42 +08:00
|
|
|
assert!((0..self.contents.len()).contains(&index));
|
2025-01-27 01:24:50 +08:00
|
|
|
self.active_index = index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2025-02-01 23:48:25 +08:00
|
|
|
let (header, contents) = self.content_markdown();
|
2025-01-27 01:24:50 +08:00
|
|
|
|
|
|
|
// 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));
|
2025-02-01 03:08:42 +08:00
|
|
|
let contents_area = area.clip_top(if self.has_header() {
|
2025-02-01 00:58:53 +08:00
|
|
|
HEADER_HEIGHT + SEPARATOR_HEIGHT
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
});
|
2025-01-27 01:24:50 +08:00
|
|
|
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);
|
|
|
|
|
2025-02-01 23:48:25 +08:00
|
|
|
let (header, contents) = self.content_markdown();
|
2025-01-27 01:24:50 +08:00
|
|
|
|
|
|
|
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);
|
2025-02-01 03:08:42 +08:00
|
|
|
let height = if self.has_header() {
|
2025-01-27 01:24:50 +08:00
|
|
|
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)
|
2025-02-01 03:08:42 +08:00
|
|
|
.unwrap_or(self.contents.len() - 1);
|
2025-01-27 01:24:50 +08:00
|
|
|
self.set_index(index);
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
|
|
|
alt!('n') => {
|
2025-02-01 03:08:42 +08:00
|
|
|
self.set_index((self.active_index + 1) % self.contents.len());
|
2025-01-27 01:24:50 +08:00
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
|
|
|
_ => EventResult::Ignored(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-01 03:08:42 +08:00
|
|
|
fn hover_contents_to_string(contents: lsp::HoverContents) -> String {
|
|
|
|
fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
|
2025-01-27 01:24:50 +08:00
|
|
|
match contents {
|
2025-02-01 03:08:42 +08:00
|
|
|
lsp::MarkedString::String(contents) => contents,
|
2025-01-27 01:24:50 +08:00
|
|
|
lsp::MarkedString::LanguageString(string) => {
|
|
|
|
if string.language == "markdown" {
|
2025-02-01 03:08:42 +08:00
|
|
|
string.value
|
2025-01-27 01:24:50 +08:00
|
|
|
} else {
|
|
|
|
format!("```{}\n{}\n```", string.language, string.value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
match contents {
|
|
|
|
lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
|
|
|
|
lsp::HoverContents::Array(contents) => contents
|
2025-02-01 03:08:42 +08:00
|
|
|
.into_iter()
|
2025-01-27 01:24:50 +08:00
|
|
|
.map(marked_string_to_markdown)
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.join("\n\n"),
|
2025-02-01 03:08:42 +08:00
|
|
|
lsp::HoverContents::Markup(contents) => contents.value,
|
2025-01-27 01:24:50 +08:00
|
|
|
}
|
|
|
|
}
|