diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index fcc0333e8cd8..1dbf54930e8a 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -2,7 +2,7 @@ use futures_util::{stream::FuturesOrdered, FutureExt}; use helix_lsp::{ block_on, lsp::{ - self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, + self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, Hover, NumberOrString, }, util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, @@ -1010,53 +1010,60 @@ pub fn signature_help(cx: &mut Context) { pub fn hover(cx: &mut Context) { let (view, doc) = current!(cx.editor); + if doc + .language_servers_with_feature(LanguageServerFeature::Hover) + .count() + == 0 + { + cx.editor.set_status(format!( + "No configured language server supports {}", + LanguageServerFeature::Hover + )); + return; + } - // TODO support multiple language servers (merge UI somehow) - let language_server = - language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover); - // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier - let pos = doc.position(view.id, language_server.offset_encoding()); - let future = language_server - .text_document_hover(doc.identifier(), pos, None) - .unwrap(); + let mut seen_language_servers = HashSet::new(); + let mut futures: FuturesOrdered<_> = doc + .language_servers_with_feature(LanguageServerFeature::Hover) + .filter(|ls| seen_language_servers.insert(ls.id())) + .map(|language_server| { + let lsp_name = language_server.name().to_string(); + // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier + let pos = doc.position(view.id, language_server.offset_encoding()); + let request = language_server + .text_document_hover(doc.identifier(), pos, None) + .unwrap(); - cx.callback( - future, - move |editor, compositor, response: Option| { - if let Some(hover) = response { - // hover.contents / .range <- used for visualizing - - fn marked_string_to_markdown(contents: lsp::MarkedString) -> String { - match contents { - lsp::MarkedString::String(contents) => contents, - lsp::MarkedString::LanguageString(string) => { - if string.language == "markdown" { - string.value - } else { - format!("```{}\n{}\n```", string.language, string.value) - } - } - } - } + async move { + let json = request.await?; + let response = serde_json::from_value::>(json)?; + anyhow::Ok((lsp_name, response)) + } + }) + .collect(); - let contents = match hover.contents { - lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents), - lsp::HoverContents::Array(contents) => contents - .into_iter() - .map(marked_string_to_markdown) - .collect::>() - .join("\n\n"), - lsp::HoverContents::Markup(contents) => contents.value, - }; + cx.jobs.callback(async move { + let mut hovers: Vec<(String, Hover)> = Vec::new(); - // skip if contents empty + while let Some((lsp_name, hover)) = futures.try_next().await? { + if let Some(hover) = hover { + hovers.push((lsp_name, hover)); + } + } - let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); - let popup = Popup::new("hover", contents).auto_close(true); - compositor.replace_or_push("hover", popup); + let call = move |editor: &mut Editor, compositor: &mut Compositor| { + if hovers.is_empty() { + editor.set_status("No hover results available."); + return; } - }, - ); + + // create new popup + let contents = ui::lsp::hover::Hover::new(hovers, editor.syn_loader.clone()); + let popup = Popup::new(ui::lsp::hover::Hover::ID, contents).auto_close(true); + compositor.replace_or_push(ui::lsp::hover::Hover::ID, popup); + }; + Ok(Callback::EditorCompositor(Box::new(call))) + }); } pub fn rename_symbol(cx: &mut Context) { diff --git a/helix-term/src/handlers/completion.rs b/helix-term/src/handlers/completion.rs index 68956c85f504..bc51c0ac47fe 100644 --- a/helix-term/src/handlers/completion.rs +++ b/helix-term/src/handlers/completion.rs @@ -26,7 +26,7 @@ use crate::events::{OnModeSwitch, PostCommand, PostInsertChar}; use crate::job::{dispatch, dispatch_blocking}; use crate::keymap::MappableCommand; use crate::ui::editor::InsertEvent; -use crate::ui::lsp::SignatureHelp; +use crate::ui::lsp::signature_help::SignatureHelp; use crate::ui::{self, CompletionItem, Popup}; use super::Handlers; diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index aaa97b9a058d..1a4e8070d1ba 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -18,7 +18,7 @@ use crate::commands::Open; use crate::compositor::Compositor; use crate::events::{OnModeSwitch, PostInsertChar}; use crate::handlers::Handlers; -use crate::ui::lsp::{Signature, SignatureHelp}; +use crate::ui::lsp::signature_help::{Signature, SignatureHelp}; use crate::ui::Popup; use crate::{job, ui}; diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index 4886263055c2..e71cf095ddc3 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -1,209 +1,2 @@ -use std::sync::Arc; - -use arc_swap::ArcSwap; -use helix_core::syntax; -use helix_view::graphics::{Margin, Rect, Style}; -use helix_view::input::Event; -use tui::buffer::Buffer; -use tui::layout::Alignment; -use tui::text::Text; -use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; - -use crate::compositor::{Component, Compositor, Context, EventResult}; - -use crate::alt; -use crate::ui::Markdown; - -use super::Popup; - -pub struct Signature { - pub signature: String, - pub signature_doc: Option, - /// Part of signature text - pub active_param_range: Option<(usize, usize)>, -} - -pub struct SignatureHelp { - language: String, - config_loader: Arc>, - active_signature: usize, - lsp_signature: Option, - signatures: Vec, -} - -impl SignatureHelp { - pub const ID: &'static str = "signature-help"; - - pub fn new( - language: String, - config_loader: Arc>, - active_signature: usize, - lsp_signature: Option, - signatures: Vec, - ) -> Self { - Self { - language, - config_loader, - active_signature, - lsp_signature, - signatures, - } - } - - pub fn active_signature(&self) -> usize { - self.active_signature - } - - pub fn lsp_signature(&self) -> Option { - self.lsp_signature - } - - pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup> { - compositor.find_id::>(Self::ID) - } - - fn signature_index(&self) -> String { - format!("({}/{})", self.active_signature + 1, self.signatures.len()) - } -} - -impl Component for SignatureHelp { - fn handle_event(&mut self, event: &Event, _cx: &mut Context) -> EventResult { - let Event::Key(event) = event else { - return EventResult::Ignored(None); - }; - - if self.signatures.len() <= 1 { - return EventResult::Ignored(None); - } - - match event { - alt!('p') => { - self.active_signature = self - .active_signature - .checked_sub(1) - .unwrap_or(self.signatures.len() - 1); - EventResult::Consumed(None) - } - alt!('n') => { - self.active_signature = (self.active_signature + 1) % self.signatures.len(); - EventResult::Consumed(None) - } - _ => EventResult::Ignored(None), - } - } - - fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { - let margin = Margin::horizontal(1); - - let signature = self - .signatures - .get(self.active_signature) - .unwrap_or_else(|| &self.signatures[0]); - - let active_param_span = signature.active_param_range.map(|(start, end)| { - vec![( - cx.editor - .theme - .find_scope_index_exact("ui.selection") - .unwrap(), - start..end, - )] - }); - - let signature = self - .signatures - .get(self.active_signature) - .unwrap_or_else(|| &self.signatures[0]); - - let sig_text = crate::ui::markdown::highlighted_code_block( - signature.signature.as_str(), - &self.language, - Some(&cx.editor.theme), - Arc::clone(&self.config_loader), - active_param_span, - ); - - if self.signatures.len() > 1 { - let signature_index = self.signature_index(); - let text = Text::from(signature_index); - let paragraph = Paragraph::new(&text).alignment(Alignment::Right); - paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface); - } - - let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); - let sig_text_area = area.clip_top(1).with_height(sig_text_height); - let sig_text_area = sig_text_area.inner(margin).intersection(surface.area); - let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false }); - sig_text_para.render(sig_text_area, surface); - - if signature.signature_doc.is_none() { - return; - } - - let sep_style = Style::default(); - let borders = BorderType::line_symbols(BorderType::Plain); - for x in sig_text_area.left()..sig_text_area.right() { - if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) { - cell.set_symbol(borders.horizontal).set_style(sep_style); - } - } - - let sig_doc = match &signature.signature_doc { - None => return, - Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), - }; - let sig_doc = sig_doc.parse(Some(&cx.editor.theme)); - let sig_doc_area = area - .clip_top(sig_text_area.height + 2) - .clip_bottom(u16::from(cx.editor.popup_border())); - let sig_doc_para = Paragraph::new(&sig_doc) - .wrap(Wrap { trim: false }) - .scroll((cx.scroll.unwrap_or_default() as u16, 0)); - sig_doc_para.render(sig_doc_area.inner(margin), surface); - } - - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - const PADDING: u16 = 2; - const SEPARATOR_HEIGHT: u16 = 1; - - let signature = self - .signatures - .get(self.active_signature) - .unwrap_or_else(|| &self.signatures[0]); - - let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120); - - let signature_text = crate::ui::markdown::highlighted_code_block( - signature.signature.as_str(), - &self.language, - None, - Arc::clone(&self.config_loader), - None, - ); - let (sig_width, sig_height) = - crate::ui::text::required_size(&signature_text, max_text_width); - - let (width, height) = match signature.signature_doc { - Some(ref doc) => { - let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader)); - let doc_text = doc_md.parse(None); - let (doc_width, doc_height) = - crate::ui::text::required_size(&doc_text, max_text_width); - ( - sig_width.max(doc_width), - sig_height + SEPARATOR_HEIGHT + doc_height, - ) - } - None => (sig_width, sig_height), - }; - - let sig_index_width = if self.signatures.len() > 1 { - self.signature_index().len() + 1 - } else { - 0 - }; - - Some((width + PADDING + sig_index_width as u16, height + PADDING)) - } -} +pub mod hover; +pub mod signature_help; diff --git a/helix-term/src/ui/lsp/hover.rs b/helix-term/src/ui/lsp/hover.rs new file mode 100644 index 000000000000..d4fdea4aa0c5 --- /dev/null +++ b/helix-term/src/ui/lsp/hover.rs @@ -0,0 +1,215 @@ +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, Compositor, Context, EventResult}; + +use crate::alt; +use crate::ui::Markdown; + +use crate::ui::Popup; + +pub struct Hover { + hovers: Vec<(String, lsp::Hover)>, + active_index: usize, + config_loader: Arc>, + + header: Option, + contents: Option, +} + +impl Hover { + pub const ID: &'static str = "hover"; + + pub fn new( + hovers: Vec<(String, lsp::Hover)>, + config_loader: Arc>, + ) -> Self { + let mut hover = Self { + hovers, + active_index: usize::default(), + config_loader, + header: None, + contents: None, + }; + hover.set_index(hover.active_index); + hover + } + + fn prepare_markdowns(&mut self) { + let Some((lsp_name, hover)) = self.hovers.get(self.active_index) else { + log::info!( + "prepare_markdowns: failed \nindex:{}\ncount:{}", + self.active_index, + self.hovers.len() + ); + return; + }; + self.header = Some(Markdown::new( + format!( + "**[{}/{}] {}**", + self.active_index + 1, + self.hovers.len(), + lsp_name + ), + self.config_loader.clone(), + )); + let contents = hover_contents_to_string(&hover.contents); + self.contents = Some(Markdown::new(contents, self.config_loader.clone())); + } + + pub fn set_hover(&mut self, hovers: Vec<(String, lsp::Hover)>) { + self.hovers = hovers; + self.set_index(usize::default()); + } + + fn set_index(&mut self, index: usize) { + self.active_index = index; + self.prepare_markdowns(); + } + + pub fn next_hover(&mut self) { + let index = if self.active_index < self.hovers.len() - 1 { + self.active_index + 1 + } else { + usize::default() + }; + self.set_index(index); + } + + pub fn previous_hover(&mut self) { + let index = if self.active_index > 0 { + self.active_index - 1 + } else { + self.hovers.len() - 1 + }; + self.set_index(index); + } + + pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup> { + compositor.find_id::>(Self::ID) + } +} + +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 (Some(header), Some(contents)) = (self.header.as_ref(), self.contents.as_ref()) else { + log::info!("markdown not ready"); + return; + }; + + // show header and border only when more than one results + if self.hovers.len() > 1 { + // 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 (Some(header), Some(contents)) = (self.header.as_ref(), self.contents.as_ref()) else { + log::info!("markdown not ready"); + return None; + }; + + let header = header.parse(None); + let (header_width, _header_height) = + crate::ui::text::required_size(&header, max_text_width); + + 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') => { + self.previous_hover(); + EventResult::Consumed(None) + } + alt!('n') => { + self.next_hover(); + 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::>() + .join("\n\n"), + lsp::HoverContents::Markup(contents) => contents.value.clone(), + } +} diff --git a/helix-term/src/ui/lsp/signature_help.rs b/helix-term/src/ui/lsp/signature_help.rs new file mode 100644 index 000000000000..2dee812417af --- /dev/null +++ b/helix-term/src/ui/lsp/signature_help.rs @@ -0,0 +1,209 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; +use helix_core::syntax; +use helix_view::graphics::{Margin, Rect, Style}; +use helix_view::input::Event; +use tui::buffer::Buffer; +use tui::layout::Alignment; +use tui::text::Text; +use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; + +use crate::compositor::{Component, Compositor, Context, EventResult}; + +use crate::alt; +use crate::ui::Markdown; + +use crate::ui::Popup; + +pub struct Signature { + pub signature: String, + pub signature_doc: Option, + /// Part of signature text + pub active_param_range: Option<(usize, usize)>, +} + +pub struct SignatureHelp { + language: String, + config_loader: Arc>, + active_signature: usize, + lsp_signature: Option, + signatures: Vec, +} + +impl SignatureHelp { + pub const ID: &'static str = "signature-help"; + + pub fn new( + language: String, + config_loader: Arc>, + active_signature: usize, + lsp_signature: Option, + signatures: Vec, + ) -> Self { + Self { + language, + config_loader, + active_signature, + lsp_signature, + signatures, + } + } + + pub fn active_signature(&self) -> usize { + self.active_signature + } + + pub fn lsp_signature(&self) -> Option { + self.lsp_signature + } + + pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup> { + compositor.find_id::>(Self::ID) + } + + fn signature_index(&self) -> String { + format!("({}/{})", self.active_signature + 1, self.signatures.len()) + } +} + +impl Component for SignatureHelp { + fn handle_event(&mut self, event: &Event, _cx: &mut Context) -> EventResult { + let Event::Key(event) = event else { + return EventResult::Ignored(None); + }; + + if self.signatures.len() <= 1 { + return EventResult::Ignored(None); + } + + match event { + alt!('p') => { + self.active_signature = self + .active_signature + .checked_sub(1) + .unwrap_or(self.signatures.len() - 1); + EventResult::Consumed(None) + } + alt!('n') => { + self.active_signature = (self.active_signature + 1) % self.signatures.len(); + EventResult::Consumed(None) + } + _ => EventResult::Ignored(None), + } + } + + fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { + let margin = Margin::horizontal(1); + + let signature = self + .signatures + .get(self.active_signature) + .unwrap_or_else(|| &self.signatures[0]); + + let active_param_span = signature.active_param_range.map(|(start, end)| { + vec![( + cx.editor + .theme + .find_scope_index_exact("ui.selection") + .unwrap(), + start..end, + )] + }); + + let signature = self + .signatures + .get(self.active_signature) + .unwrap_or_else(|| &self.signatures[0]); + + let sig_text = crate::ui::markdown::highlighted_code_block( + signature.signature.as_str(), + &self.language, + Some(&cx.editor.theme), + Arc::clone(&self.config_loader), + active_param_span, + ); + + if self.signatures.len() > 1 { + let signature_index = self.signature_index(); + let text = Text::from(signature_index); + let paragraph = Paragraph::new(&text).alignment(Alignment::Right); + paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface); + } + + let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); + let sig_text_area = area.clip_top(1).with_height(sig_text_height); + let sig_text_area = sig_text_area.inner(margin).intersection(surface.area); + let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false }); + sig_text_para.render(sig_text_area, surface); + + if signature.signature_doc.is_none() { + return; + } + + let sep_style = Style::default(); + let borders = BorderType::line_symbols(BorderType::Plain); + for x in sig_text_area.left()..sig_text_area.right() { + if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) { + cell.set_symbol(borders.horizontal).set_style(sep_style); + } + } + + let sig_doc = match &signature.signature_doc { + None => return, + Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), + }; + let sig_doc = sig_doc.parse(Some(&cx.editor.theme)); + let sig_doc_area = area + .clip_top(sig_text_area.height + 2) + .clip_bottom(u16::from(cx.editor.popup_border())); + let sig_doc_para = Paragraph::new(&sig_doc) + .wrap(Wrap { trim: false }) + .scroll((cx.scroll.unwrap_or_default() as u16, 0)); + sig_doc_para.render(sig_doc_area.inner(margin), surface); + } + + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + const PADDING: u16 = 2; + const SEPARATOR_HEIGHT: u16 = 1; + + let signature = self + .signatures + .get(self.active_signature) + .unwrap_or_else(|| &self.signatures[0]); + + let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120); + + let signature_text = crate::ui::markdown::highlighted_code_block( + signature.signature.as_str(), + &self.language, + None, + Arc::clone(&self.config_loader), + None, + ); + let (sig_width, sig_height) = + crate::ui::text::required_size(&signature_text, max_text_width); + + let (width, height) = match signature.signature_doc { + Some(ref doc) => { + let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader)); + let doc_text = doc_md.parse(None); + let (doc_width, doc_height) = + crate::ui::text::required_size(&doc_text, max_text_width); + ( + sig_width.max(doc_width), + sig_height + SEPARATOR_HEIGHT + doc_height, + ) + } + None => (sig_width, sig_height), + }; + + let sig_index_width = if self.signatures.len() > 1 { + self.signature_index().len() + 1 + } else { + 0 + }; + + Some((width + PADDING + sig_index_width as u16, height + PADDING)) + } +}