From 69e08d9e91341570c0750b0c963fb31b36cbad33 Mon Sep 17 00:00:00 2001 From: Hichem Date: Tue, 16 Apr 2024 20:57:22 +0200 Subject: [PATCH] allow cycling through function signatures/overloads (#9974) implement handle_event to cycle through the function signatures. To change the signature press alt+p/n . Signed-off-by: Ben Fekih, Hichem --- helix-term/src/handlers/signature_help.rs | 112 +++++++++++++--------- helix-term/src/ui/lsp.rs | 95 +++++++++++++----- 2 files changed, 140 insertions(+), 67 deletions(-) diff --git a/helix-term/src/handlers/signature_help.rs b/helix-term/src/handlers/signature_help.rs index 3c746548a..0bb1d3d16 100644 --- a/helix-term/src/handlers/signature_help.rs +++ b/helix-term/src/handlers/signature_help.rs @@ -5,7 +5,7 @@ use helix_core::syntax::LanguageServerFeature; use helix_event::{ cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx, }; -use helix_lsp::lsp; +use helix_lsp::lsp::{self, SignatureInformation}; use helix_stdx::rope::RopeSliceExt; use helix_view::document::Mode; use helix_view::events::{DocumentDidChange, SelectionDidChange}; @@ -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::SignatureHelp; +use crate::ui::lsp::{Signature, SignatureHelp}; use crate::ui::Popup; use crate::{job, ui}; @@ -82,6 +82,7 @@ impl helix_event::AsyncHook for SignatureHelpHandler { } } self.state = if open { State::Open } else { State::Closed }; + return timeout; } } @@ -138,6 +139,31 @@ pub fn request_signature_help( }); } +fn active_param_range( + signature: &SignatureInformation, + response_active_parameter: Option, +) -> Option<(usize, usize)> { + let param_idx = signature + .active_parameter + .or(response_active_parameter) + .unwrap_or(0) as usize; + let param = signature.parameters.as_ref()?.get(param_idx)?; + match ¶m.label { + lsp::ParameterLabel::Simple(string) => { + let start = signature.label.find(string.as_str())?; + Some((start, start + string.len())) + } + lsp::ParameterLabel::LabelOffsets([start, end]) => { + // LS sends offsets based on utf-16 based string representation + // but highlighting in helix is done using byte offset. + use helix_core::str_utils::char_to_byte_idx; + let from = char_to_byte_idx(&signature.label, *start as usize); + let to = char_to_byte_idx(&signature.label, *end as usize); + Some((from, to)) + } + } +} + pub fn show_signature_help( editor: &mut Editor, compositor: &mut Compositor, @@ -184,54 +210,50 @@ pub fn show_signature_help( let doc = doc!(editor); let language = doc.language_name().unwrap_or(""); - let signature = match response + if response.signatures.is_empty() { + return; + } + + let signatures: Vec = response .signatures - .get(response.active_signature.unwrap_or(0) as usize) - { - Some(s) => s, - None => return, - }; - let mut contents = SignatureHelp::new( - signature.label.clone(), + .into_iter() + .map(|s| { + let active_param_range = active_param_range(&s, response.active_parameter); + + let signature_doc = if config.lsp.display_signature_help_docs { + s.documentation.map(|doc| match doc { + lsp::Documentation::String(s) => s, + lsp::Documentation::MarkupContent(markup) => markup.value, + }) + } else { + None + }; + + Signature { + signature: s.label, + signature_doc, + active_param_range, + } + }) + .collect(); + + let old_popup = compositor.find_id::>(SignatureHelp::ID); + let mut active_signature = old_popup + .as_ref() + .map(|popup| popup.contents().active_signature()) + .unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize); + + if active_signature >= signatures.len() { + active_signature = signatures.len() - 1; + } + + let contents = SignatureHelp::new( language.to_string(), Arc::clone(&editor.syn_loader), + active_signature, + signatures, ); - let signature_doc = if config.lsp.display_signature_help_docs { - signature.documentation.as_ref().map(|doc| match doc { - lsp::Documentation::String(s) => s.clone(), - lsp::Documentation::MarkupContent(markup) => markup.value.clone(), - }) - } else { - None - }; - - contents.set_signature_doc(signature_doc); - - let active_param_range = || -> Option<(usize, usize)> { - let param_idx = signature - .active_parameter - .or(response.active_parameter) - .unwrap_or(0) as usize; - let param = signature.parameters.as_ref()?.get(param_idx)?; - match ¶m.label { - lsp::ParameterLabel::Simple(string) => { - let start = signature.label.find(string.as_str())?; - Some((start, start + string.len())) - } - lsp::ParameterLabel::LabelOffsets([start, end]) => { - // LS sends offsets based on utf-16 based string representation - // but highlighting in helix is done using byte offset. - use helix_core::str_utils::char_to_byte_idx; - let from = char_to_byte_idx(&signature.label, *start as usize); - let to = char_to_byte_idx(&signature.label, *end as usize); - Some((from, to)) - } - } - }; - contents.set_active_param_range(active_param_range()); - - let old_popup = compositor.find_id::>(SignatureHelp::ID); let mut popup = Popup::new(SignatureHelp::ID, contents) .position(old_popup.and_then(|p| p.get_position())) .position_bias(Open::Above) diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index a3698e38d..d53437ecd 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -3,60 +3,95 @@ 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}; +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crate::alt; use crate::ui::Markdown; use super::Popup; -pub struct SignatureHelp { - signature: String, - signature_doc: Option, +pub struct Signature { + pub signature: String, + pub signature_doc: Option, /// Part of signature text - active_param_range: Option<(usize, usize)>, + pub active_param_range: Option<(usize, usize)>, +} +pub struct SignatureHelp { language: String, config_loader: Arc>, + active_signature: usize, + signatures: Vec, } impl SignatureHelp { pub const ID: &'static str = "signature-help"; pub fn new( - signature: String, language: String, config_loader: Arc>, + active_signature: usize, + signatures: Vec, ) -> Self { Self { - signature, - signature_doc: None, - active_param_range: None, language, config_loader, + active_signature, + signatures, } } - pub fn set_signature_doc(&mut self, signature_doc: Option) { - self.signature_doc = signature_doc; - } - - pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) { - self.active_param_range = offset; + pub fn active_signature(&self) -> usize { + self.active_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 active_param_span = self.active_param_range.map(|(start, end)| { + let signature = &self.signatures[self.active_signature]; + + let active_param_span = signature.active_param_range.map(|(start, end)| { vec![( cx.editor .theme @@ -66,21 +101,29 @@ impl Component for SignatureHelp { )] }); + let sig = &self.signatures[self.active_signature]; let sig_text = crate::ui::markdown::highlighted_code_block( - &self.signature, + sig.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 self.signature_doc.is_none() { + if sig.signature_doc.is_none() { return; } @@ -92,7 +135,7 @@ impl Component for SignatureHelp { } } - let sig_doc = match &self.signature_doc { + let sig_doc = match &sig.signature_doc { None => return, Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)), }; @@ -110,13 +153,15 @@ impl Component for SignatureHelp { const PADDING: u16 = 2; const SEPARATOR_HEIGHT: u16 = 1; + let sig = &self.signatures[self.active_signature]; + 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, + sig.signature.as_str(), &self.language, None, Arc::clone(&self.config_loader), @@ -125,7 +170,7 @@ impl Component for SignatureHelp { let (sig_width, sig_height) = crate::ui::text::required_size(&signature_text, max_text_width); - let (width, height) = match self.signature_doc { + let (width, height) = match sig.signature_doc { Some(ref doc) => { let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader)); let doc_text = doc_md.parse(None); @@ -139,6 +184,12 @@ impl Component for SignatureHelp { None => (sig_width, sig_height), }; - Some((width + PADDING, height + PADDING)) + 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)) } }