diff options
author | Gokul Soumya | 2022-07-19 02:28:24 +0000 |
---|---|---|
committer | GitHub | 2022-07-19 02:28:24 +0000 |
commit | 791bf7e50a19bcf7612788deb7514847089cb976 (patch) | |
tree | 0bac607be8b940aed8000b77a2f4dfa2e14882b8 /helix-term/src/ui | |
parent | 02f009921007301284cbb0db4bc36bc629088fbb (diff) |
Add lsp signature help (#1755)
* Add lsp signature help
* Do not move signature help popup on multiple triggers
* Highlight current parameter in signature help
* Auto close signature help
* Position signature help above to not block completion
* Update signature help on backspace/insert mode delete
* Add lsp.auto-signature-help config option
* Add serde default annotation for LspConfig
* Show LSP inactive message only if signature help is invoked manually
* Do not assume valid signature help response from LSP
Malformed LSP responses are common, and these should not crash the
editor.
* Check signature help capability before sending request
* Reuse Open enum for PositionBias in popup
* Close signature popup and exit insert mode on escape
* Add config to control signature help docs display
* Use new Margin api in signature help
* Invoke signature help on changing to insert mode
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r-- | helix-term/src/ui/completion.rs | 4 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 14 | ||||
-rw-r--r-- | helix-term/src/ui/lsp.rs | 133 | ||||
-rw-r--r-- | helix-term/src/ui/markdown.rs | 2 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 3 | ||||
-rw-r--r-- | helix-term/src/ui/popup.rs | 59 |
6 files changed, 202 insertions, 13 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index a3637415..c1816db1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -85,6 +85,8 @@ pub struct Completion { } impl Completion { + pub const ID: &'static str = "completion"; + pub fn new( editor: &Editor, items: Vec<CompletionItem>, @@ -214,7 +216,7 @@ impl Completion { } }; }); - let popup = Popup::new("completion", menu); + let popup = Popup::new(Self::ID, menu); let mut completion = Self { popup, start_offset, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 911ee0f0..849f0b0b 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,7 +1,7 @@ use crate::{ commands, compositor::{Component, Context, EventResult}, - key, + job, key, keymap::{KeymapResult, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -28,6 +28,7 @@ use std::borrow::Cow; use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; use tui::buffer::Buffer as Surface; +use super::lsp::SignatureHelp; use super::statusline; pub struct EditorView { @@ -1205,10 +1206,21 @@ impl Component for EditorView { _ => unimplemented!(), }; self.last_insert.1.clear(); + commands::signature_help_impl( + &mut cx, + commands::SignatureHelpInvoked::Automatic, + ); } (Mode::Insert, Mode::Normal) => { // if exiting insert mode, remove completion self.completion = None; + // TODO: Use an on_mode_change hook to remove signature help + context.jobs.callback(async { + let call: job::Callback = Box::new(|_editor, compositor| { + compositor.remove(SignatureHelp::ID); + }); + Ok(call) + }); } _ => (), } diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs new file mode 100644 index 00000000..f2854551 --- /dev/null +++ b/helix-term/src/ui/lsp.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use helix_core::syntax; +use helix_view::graphics::{Margin, Rect, Style}; +use tui::buffer::Buffer; +use tui::widgets::{BorderType, Paragraph, Widget, Wrap}; + +use crate::compositor::{Component, Compositor, Context}; + +use crate::ui::Markdown; + +use super::Popup; + +pub struct SignatureHelp { + signature: String, + signature_doc: Option<String>, + /// Part of signature text + active_param_range: Option<(usize, usize)>, + + language: String, + config_loader: Arc<syntax::Loader>, +} + +impl SignatureHelp { + pub const ID: &'static str = "signature-help"; + + pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self { + Self { + signature, + signature_doc: None, + active_param_range: None, + language, + config_loader, + } + } + + pub fn set_signature_doc(&mut self, signature_doc: Option<String>) { + self.signature_doc = signature_doc; + } + + pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) { + self.active_param_range = offset; + } + + pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> { + compositor.find_id::<Popup<Self>>(Self::ID) + } +} + +impl Component for SignatureHelp { + fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) { + let margin = Margin::horizontal(1); + + let active_param_span = self.active_param_range.map(|(start, end)| { + vec![( + cx.editor.theme.find_scope_index("ui.selection").unwrap(), + start..end, + )] + }); + + let sig_text = crate::ui::markdown::highlighted_code_block( + self.signature.clone(), + &self.language, + Some(&cx.editor.theme), + Arc::clone(&self.config_loader), + active_param_span, + ); + + 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_para = Paragraph::new(sig_text).wrap(Wrap { trim: false }); + sig_text_para.render(sig_text_area.inner(&margin), surface); + + if self.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 &self.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); + 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; + + if PADDING >= viewport.1 || PADDING >= viewport.0 { + return None; + } + let max_text_width = (viewport.0 - PADDING).min(120); + + let signature_text = crate::ui::markdown::highlighted_code_block( + self.signature.clone(), + &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 self.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), + }; + + Some((width + PADDING, height + PADDING)) + } +} diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index a5c78c41..c53b3b66 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -144,7 +144,7 @@ impl Markdown { } } - fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> { + pub fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> { fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) { let spans = std::mem::take(spans); if !spans.is_empty() { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 88a226e9..257608f0 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,11 +1,12 @@ mod completion; pub(crate) mod editor; mod info; +pub mod lsp; mod markdown; pub mod menu; pub mod overlay; mod picker; -mod popup; +pub mod popup; mod prompt; mod spinner; mod statusline; diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index f5b79526..77ab2462 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -1,4 +1,5 @@ use crate::{ + commands::Open, compositor::{Callback, Component, Context, EventResult}, ctrl, key, }; @@ -17,8 +18,10 @@ pub struct Popup<T: Component> { margin: Margin, size: (u16, u16), child_size: (u16, u16), + position_bias: Open, scroll: usize, auto_close: bool, + ignore_escape_key: bool, id: &'static str, } @@ -29,15 +32,27 @@ impl<T: Component> Popup<T> { position: None, margin: Margin::none(), size: (0, 0), + position_bias: Open::Below, child_size: (0, 0), scroll: 0, auto_close: false, + ignore_escape_key: false, id, } } - pub fn set_position(&mut self, pos: Option<Position>) { + pub fn position(mut self, pos: Option<Position>) -> Self { self.position = pos; + self + } + + pub fn get_position(&self) -> Option<Position> { + self.position + } + + pub fn position_bias(mut self, bias: Open) -> Self { + self.position_bias = bias; + self } pub fn margin(mut self, margin: Margin) -> Self { @@ -50,6 +65,18 @@ impl<T: Component> Popup<T> { self } + /// Ignores an escape keypress event, letting the outer layer + /// (usually the editor) handle it. This is useful for popups + /// in insert mode like completion and signature help where + /// the popup is closed on the mode change from insert to normal + /// which is done with the escape key. Otherwise the popup consumes + /// the escape key event and closes it, and an additional escape + /// would be required to exit insert mode. + pub fn ignore_escape_key(mut self, ignore: bool) -> Self { + self.ignore_escape_key = ignore; + self + } + pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { let position = self .position @@ -68,13 +95,23 @@ impl<T: Component> Popup<T> { rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); } - // TODO: be able to specify orientation preference. We want above for most popups, below - // for menus/autocomplete. - if viewport.height > rel_y + height { - rel_y += 1 // position below point - } else { - rel_y = rel_y.saturating_sub(height) // position above point - } + let can_put_below = viewport.height > rel_y + height; + let can_put_above = rel_y.checked_sub(height).is_some(); + let final_pos = match self.position_bias { + Open::Below => match can_put_below { + true => Open::Below, + false => Open::Above, + }, + Open::Above => match can_put_above { + true => Open::Above, + false => Open::Below, + }, + }; + + rel_y = match final_pos { + Open::Above => rel_y.saturating_sub(height), + Open::Below => rel_y + 1, + }; (rel_x, rel_y) } @@ -112,9 +149,13 @@ impl<T: Component> Component for Popup<T> { _ => return EventResult::Ignored(None), }; + if key!(Esc) == key.into() && self.ignore_escape_key { + return EventResult::Ignored(None); + } + let close_fn: Callback = Box::new(|compositor, _| { // remove the layer - compositor.pop(); + compositor.remove(self.id.as_ref()); }); match key.into() { |