on master help sign in
Add lsp signature help (#1755)
791bf7eGokul Soumya
book/src/configuration.md+5−3
@@ -80,9 +80,11 @@ The following elements can be configured:
80
80
81### `[editor.lsp]` Section
81### `[editor.lsp]` Section
82
82
83−| Key | Description | Default |
83+| Key | Description | Default |
84−| --- | ----------- | ------- |
84+| --- | ----------- | ------- |
85−| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
85+| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
86+| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
87+| `display-signature-help-docs` | Display docs under signature help popup | `true` |
86
88
87[^1]: By default, a progress spinner is shown in the statusline beside the file path.
89[^1]: By default, a progress spinner is shown in the statusline beside the file path.
88
90
helix-lsp/src/client.rs+17−2
@@ -322,6 +322,16 @@ impl Client {
322 content_format: Some(vec![lsp::MarkupKind::Markdown]),
322 content_format: Some(vec![lsp::MarkupKind::Markdown]),
323 ..Default::default()
323 ..Default::default()
324 }),
324 }),
325+ signature_help: Some(lsp::SignatureHelpClientCapabilities {
326+ signature_information: Some(lsp::SignatureInformationSettings {
327+ documentation_format: Some(vec![lsp::MarkupKind::Markdown]),
328+ parameter_information: Some(lsp::ParameterInformationSettings {
329+ label_offset_support: Some(true),
330+ }),
331+ active_parameter_support: Some(true),
332+ }),
333+ ..Default::default()
334+ }),
325 rename: Some(lsp::RenameClientCapabilities {
335 rename: Some(lsp::RenameClientCapabilities {
326 dynamic_registration: Some(false),
336 dynamic_registration: Some(false),
327 prepare_support: Some(false),
337 prepare_support: Some(false),
@@ -646,7 +656,12 @@ impl Client {
646 text_document: lsp::TextDocumentIdentifier,
656 text_document: lsp::TextDocumentIdentifier,
647 position: lsp::Position,
657 position: lsp::Position,
648 work_done_token: Option<lsp::ProgressToken>,
658 work_done_token: Option<lsp::ProgressToken>,
649− ) -> impl Future<Output = Result<Value>> {
659+ ) -> Option<impl Future<Output = Result<Value>>> {
660+ let capabilities = self.capabilities.get().unwrap();
661+
662+ // Return early if signature help is not supported
663+ capabilities.signature_help_provider.as_ref()?;
664+
650 let params = lsp::SignatureHelpParams {
665 let params = lsp::SignatureHelpParams {
651 text_document_position_params: lsp::TextDocumentPositionParams {
666 text_document_position_params: lsp::TextDocumentPositionParams {
652 text_document,
667 text_document,
@@ -657,7 +672,7 @@ impl Client {
657 // lsp::SignatureHelpContext
672 // lsp::SignatureHelpContext
658 };
673 };
659
674
660− self.call::<lsp::request::SignatureHelpRequest>(params)
675+ Some(self.call::<lsp::request::SignatureHelpRequest>(params))
661 }
676 }
662
677
663 pub fn text_document_hover(
678 pub fn text_document_hover(
helix-term/src/commands.rs+23−19
@@ -715,6 +715,8 @@ fn kill_to_line_start(cx: &mut Context) {
715 Range::new(head, anchor)
715 Range::new(head, anchor)
716 });
716 });
717 delete_selection_insert_mode(doc, view, &selection);
717 delete_selection_insert_mode(doc, view, &selection);
718+
719+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
718}
720}
719
721
720fn kill_to_line_end(cx: &mut Context) {
722fn kill_to_line_end(cx: &mut Context) {
@@ -734,6 +736,8 @@ fn kill_to_line_end(cx: &mut Context) {
734 new_range
736 new_range
735 });
737 });
736 delete_selection_insert_mode(doc, view, &selection);
738 delete_selection_insert_mode(doc, view, &selection);
739+
740+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
737}
741}
738
742
739fn goto_first_nonwhitespace(cx: &mut Context) {
743fn goto_first_nonwhitespace(cx: &mut Context) {
@@ -2399,7 +2403,8 @@ async fn make_format_callback(
2399 Ok(call)
2403 Ok(call)
2400}
2404}
2401
2405
2406+#[derive(PartialEq)]
2402−enum Open {
2407+pub enum Open {
2403 Below,
2408 Below,
2404 Above,
2409 Above,
2405}
2410}
@@ -2797,6 +2802,9 @@ pub mod insert {
2797 use helix_lsp::lsp;
2802 use helix_lsp::lsp;
2798 // if ch matches signature_help char, trigger
2803 // if ch matches signature_help char, trigger
2799 let doc = doc_mut!(cx.editor);
2804 let doc = doc_mut!(cx.editor);
2805+ // The language_server!() macro is not used here since it will
2806+ // print an "LSP not active for current buffer" message on
2807+ // every keypress.
2800 let language_server = match doc.language_server() {
2808 let language_server = match doc.language_server() {
2801 Some(language_server) => language_server,
2809 Some(language_server) => language_server,
2802 None => return,
2810 None => return,
@@ -2816,26 +2824,15 @@ pub mod insert {
2816 {
2824 {
2817 // TODO: what if trigger is multiple chars long
2825 // TODO: what if trigger is multiple chars long
2818 let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
2826 let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
2827+ // lsp doesn't tell us when to close the signature help, so we request
2828+ // the help information again after common close triggers which should
2829+ // return None, which in turn closes the popup.
2830+ let close_triggers = &[')', ';', '.'];
2819
2831
2820− if is_trigger {
2832+ if is_trigger || close_triggers.contains(&ch) {
2821− super::signature_help(cx);
2833+ super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
2822 }
2834 }
2823 }
2835 }
2824−
2825− // SignatureHelp {
2826− // signatures: [
2827− // SignatureInformation {
2828− // label: "fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error>",
2829− // documentation: None,
2830− // parameters: Some(
2831− // [ParameterInformation { label: Simple("path: PathBuf"), documentation: None },
2832− // ParameterInformation { label: Simple("action: Action"), documentation: None }]
2833− // ),
2834− // active_parameter: Some(0)
2835− // }
2836− // ],
2837− // active_signature: None, active_parameter: Some(0)
2838− // }
2839 }
2836 }
2840
2837
2841 // The default insert hook: simply insert the character
2838 // The default insert hook: simply insert the character
@@ -2870,7 +2867,6 @@ pub mod insert {
2870 // this could also generically look at Transaction, but it's a bit annoying to look at
2867 // this could also generically look at Transaction, but it's a bit annoying to look at
2871 // Operation instead of Change.
2868 // Operation instead of Change.
2872 for hook in &[language_server_completion, signature_help] {
2869 for hook in &[language_server_completion, signature_help] {
2873− // for hook in &[signature_help] {
2874 hook(cx, c);
2870 hook(cx, c);
2875 }
2871 }
2876 }
2872 }
@@ -3042,6 +3038,8 @@ pub mod insert {
3042 }
3038 }
3043 });
3039 });
3044 doc.apply(&transaction, view.id);
3040 doc.apply(&transaction, view.id);
3041+
3042+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
3045 }
3043 }
3046
3044
3047 pub fn delete_char_forward(cx: &mut Context) {
3045 pub fn delete_char_forward(cx: &mut Context) {
@@ -3058,6 +3056,8 @@ pub mod insert {
3058 )
3056 )
3059 });
3057 });
3060 doc.apply(&transaction, view.id);
3058 doc.apply(&transaction, view.id);
3059+
3060+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
3061 }
3061 }
3062
3062
3063 pub fn delete_word_backward(cx: &mut Context) {
3063 pub fn delete_word_backward(cx: &mut Context) {
@@ -3071,6 +3071,8 @@ pub mod insert {
3071 exclude_cursor(text, next, range)
3071 exclude_cursor(text, next, range)
3072 });
3072 });
3073 delete_selection_insert_mode(doc, view, &selection);
3073 delete_selection_insert_mode(doc, view, &selection);
3074+
3075+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
3074 }
3076 }
3075
3077
3076 pub fn delete_word_forward(cx: &mut Context) {
3078 pub fn delete_word_forward(cx: &mut Context) {
@@ -3083,6 +3085,8 @@ pub mod insert {
3083 .clone()
3085 .clone()
3084 .transform(|range| movement::move_next_word_start(text, range, count));
3086 .transform(|range| movement::move_next_word_start(text, range, count));
3085 delete_selection_insert_mode(doc, view, &selection);
3087 delete_selection_insert_mode(doc, view, &selection);
3088+
3089+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
3086 }
3090 }
3087}
3091}
3088
3092
helix-term/src/commands/lsp.rs+105−19
@@ -6,18 +6,19 @@ use helix_lsp::{
6};
6};
7use tui::text::{Span, Spans};
7use tui::text::{Span, Spans};
8
8
9−use super::{align_view, push_jump, Align, Context, Editor};
9+use super::{align_view, push_jump, Align, Context, Editor, Open};
10
10
11use helix_core::{path, Selection};
11use helix_core::{path, Selection};
12use helix_view::{editor::Action, theme::Style};
12use helix_view::{editor::Action, theme::Style};
13
13
14use crate::{
14use crate::{
15 compositor::{self, Compositor},
15 compositor::{self, Compositor},
16+ ui::{
16− ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
17+ self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
18+ },
17};
19};
18
20
19−use std::collections::BTreeMap;
20−use std::{borrow::Cow, path::PathBuf};
21+use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
21
22
22/// Gets the language server that is attached to a document, and
23/// Gets the language server that is attached to a document, and
23/// if it's not active displays a status message. Using this macro
24/// if it's not active displays a status message. Using this macro
@@ -805,31 +806,116 @@ pub fn goto_reference(cx: &mut Context) {
805 );
806 );
806}
807}
807
808
809+#[derive(PartialEq)]
810+pub enum SignatureHelpInvoked {
811+ Manual,
812+ Automatic,
813+}
814+
808pub fn signature_help(cx: &mut Context) {
815pub fn signature_help(cx: &mut Context) {
816+ signature_help_impl(cx, SignatureHelpInvoked::Manual)
817+}
818+
819+pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
809 let (view, doc) = current!(cx.editor);
820 let (view, doc) = current!(cx.editor);
821+ let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
822+
810− let language_server = language_server!(cx.editor, doc);
823+ let language_server = match doc.language_server() {
824+ Some(language_server) => language_server,
825+ None => {
826+ // Do not show the message if signature help was invoked
827+ // automatically on backspace, trigger characters, etc.
828+ if was_manually_invoked {
829+ cx.editor
830+ .set_status("Language server not active for current buffer");
831+ }
832+ return;
833+ }
834+ };
811 let offset_encoding = language_server.offset_encoding();
835 let offset_encoding = language_server.offset_encoding();
812
836
813 let pos = doc.position(view.id, offset_encoding);
837 let pos = doc.position(view.id, offset_encoding);
814
838
815− let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
839+ let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
840+ Some(f) => f,
841+ None => return,
842+ };
816
843
817 cx.callback(
844 cx.callback(
818 future,
845 future,
819− move |_editor, _compositor, response: Option<lsp::SignatureHelp>| {
846+ move |editor, compositor, response: Option<lsp::SignatureHelp>| {
820− if let Some(signature_help) = response {
847+ let config = &editor.config();
821− log::info!("{:?}", signature_help);
848+
822− // signatures
849+ if !(config.lsp.auto_signature_help
823− // active_signature
850+ || SignatureHelp::visible_popup(compositor).is_some()
824− // active_parameter
851+ || was_manually_invoked)
825− // render as:
852+ {
826−
853+ return;
827− // signature
828− // ----------
829− // doc
830−
831− // with active param highlighted
832 }
854 }
855+
856+ let response = match response {
857+ // According to the spec the response should be None if there
858+ // are no signatures, but some servers don't follow this.
859+ Some(s) if !s.signatures.is_empty() => s,
860+ _ => {
861+ compositor.remove(SignatureHelp::ID);
862+ return;
863+ }
864+ };
865+ let doc = doc!(editor);
866+ let language = doc
867+ .language()
868+ .and_then(|scope| scope.strip_prefix("source."))
869+ .unwrap_or("");
870+
871+ let signature = match response
872+ .signatures
873+ .get(response.active_signature.unwrap_or(0) as usize)
874+ {
875+ Some(s) => s,
876+ None => return,
877+ };
878+ let mut contents = SignatureHelp::new(
879+ signature.label.clone(),
880+ language.to_string(),
881+ Arc::clone(&editor.syn_loader),
882+ );
883+
884+ let signature_doc = if config.lsp.display_signature_help_docs {
885+ signature.documentation.as_ref().map(|doc| match doc {
886+ lsp::Documentation::String(s) => s.clone(),
887+ lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
888+ })
889+ } else {
890+ None
891+ };
892+
893+ contents.set_signature_doc(signature_doc);
894+
895+ let active_param_range = || -> Option<(usize, usize)> {
896+ let param_idx = signature
897+ .active_parameter
898+ .or(response.active_parameter)
899+ .unwrap_or(0) as usize;
900+ let param = signature.parameters.as_ref()?.get(param_idx)?;
901+ match &param.label {
902+ lsp::ParameterLabel::Simple(string) => {
903+ let start = signature.label.find(string.as_str())?;
904+ Some((start, start + string.len()))
905+ }
906+ lsp::ParameterLabel::LabelOffsets([start, end]) => {
907+ Some((*start as usize, *end as usize))
908+ }
909+ }
910+ };
911+ contents.set_active_param_range(active_param_range());
912+
913+ let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
914+ let popup = Popup::new(SignatureHelp::ID, contents)
915+ .position(old_popup.and_then(|p| p.get_position()))
916+ .position_bias(Open::Above)
917+ .ignore_escape_key(true);
918+ compositor.replace_or_push(SignatureHelp::ID, popup);
833 },
919 },
834 );
920 );
835}
921}
helix-term/src/commands/typed.rs+3−5
@@ -1501,11 +1501,9 @@ fn run_shell_command(
1501 format!("```sh\n{}\n```", output),
1501 format!("```sh\n{}\n```", output),
1502 editor.syn_loader.clone(),
1502 editor.syn_loader.clone(),
1503 );
1503 );
1504− let mut popup = Popup::new("shell", contents);
1504+ let popup = Popup::new("shell", contents).position(Some(
1505− popup.set_position(Some(helix_core::Position::new(
1506− editor.cursor().0.unwrap_or_default().row,
1505+ helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
1507− 2,
1508− )));
1506+ ));
1509 compositor.replace_or_push("shell", popup);
1507 compositor.replace_or_push("shell", popup);
1510 });
1508 });
1511 Ok(call)
1509 Ok(call)
helix-term/src/compositor.rs+8
@@ -150,6 +150,14 @@ impl Compositor {
150 self.layers.pop()
150 self.layers.pop()
151 }
151 }
152
152
153+ pub fn remove(&mut self, id: &'static str) -> Option<Box<dyn Component>> {
154+ let idx = self
155+ .layers
156+ .iter()
157+ .position(|layer| layer.id() == Some(id))?;
158+ Some(self.layers.remove(idx))
159+ }
160+
153 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
161 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
154 // If it is a key event and a macro is being recorded, push the key event to the recording.
162 // If it is a key event and a macro is being recorded, push the key event to the recording.
155 if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
163 if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
helix-term/src/ui/completion.rs+3−1
@@ -85,6 +85,8 @@ pub struct Completion {
85}
85}
86
86
87impl Completion {
87impl Completion {
88+ pub const ID: &'static str = "completion";
89+
88 pub fn new(
90 pub fn new(
89 editor: &Editor,
91 editor: &Editor,
90 items: Vec<CompletionItem>,
92 items: Vec<CompletionItem>,
@@ -214,7 +216,7 @@ impl Completion {
214 }
216 }
215 };
217 };
216 });
218 });
217− let popup = Popup::new("completion", menu);
219+ let popup = Popup::new(Self::ID, menu);
218 let mut completion = Self {
220 let mut completion = Self {
219 popup,
221 popup,
220 start_offset,
222 start_offset,
helix-term/src/ui/editor.rs+13−1
@@ -1,7 +1,7 @@
1use crate::{
1use crate::{
2 commands,
2 commands,
3 compositor::{Component, Context, EventResult},
3 compositor::{Component, Context, EventResult},
4− key,
4+ job, key,
5 keymap::{KeymapResult, Keymaps},
5 keymap::{KeymapResult, Keymaps},
6 ui::{Completion, ProgressSpinners},
6 ui::{Completion, ProgressSpinners},
7};
7};
@@ -28,6 +28,7 @@ use std::borrow::Cow;
28use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
28use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
29use tui::buffer::Buffer as Surface;
29use tui::buffer::Buffer as Surface;
30
30
31+use super::lsp::SignatureHelp;
31use super::statusline;
32use super::statusline;
32
33
33pub struct EditorView {
34pub struct EditorView {
@@ -1205,10 +1206,21 @@ impl Component for EditorView {
1205 _ => unimplemented!(),
1206 _ => unimplemented!(),
1206 };
1207 };
1207 self.last_insert.1.clear();
1208 self.last_insert.1.clear();
1209+ commands::signature_help_impl(
1210+ &mut cx,
1211+ commands::SignatureHelpInvoked::Automatic,
1212+ );
1208 }
1213 }
1209 (Mode::Insert, Mode::Normal) => {
1214 (Mode::Insert, Mode::Normal) => {
1210 // if exiting insert mode, remove completion
1215 // if exiting insert mode, remove completion
1211 self.completion = None;
1216 self.completion = None;
1217+ // TODO: Use an on_mode_change hook to remove signature help
1218+ context.jobs.callback(async {
1219+ let call: job::Callback = Box::new(|_editor, compositor| {
1220+ compositor.remove(SignatureHelp::ID);
1221+ });
1222+ Ok(call)
1223+ });
1212 }
1224 }
1213 _ => (),
1225 _ => (),
1214 }
1226 }
helix-term/src/ui/lsp.rsnew file+133
@@ -0,0 +1,133 @@
1+use std::sync::Arc;
2+
3+use helix_core::syntax;
4+use helix_view::graphics::{Margin, Rect, Style};
5+use tui::buffer::Buffer;
6+use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
7+
8+use crate::compositor::{Component, Compositor, Context};
9+
10+use crate::ui::Markdown;
11+
12+use super::Popup;
13+
14+pub struct SignatureHelp {
15+ signature: String,
16+ signature_doc: Option<String>,
17+ /// Part of signature text
18+ active_param_range: Option<(usize, usize)>,
19+
20+ language: String,
21+ config_loader: Arc<syntax::Loader>,
22+}
23+
24+impl SignatureHelp {
25+ pub const ID: &'static str = "signature-help";
26+
27+ pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
28+ Self {
29+ signature,
30+ signature_doc: None,
31+ active_param_range: None,
32+ language,
33+ config_loader,
34+ }
35+ }
36+
37+ pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
38+ self.signature_doc = signature_doc;
39+ }
40+
41+ pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
42+ self.active_param_range = offset;
43+ }
44+
45+ pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
46+ compositor.find_id::<Popup<Self>>(Self::ID)
47+ }
48+}
49+
50+impl Component for SignatureHelp {
51+ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
52+ let margin = Margin::horizontal(1);
53+
54+ let active_param_span = self.active_param_range.map(|(start, end)| {
55+ vec![(
56+ cx.editor.theme.find_scope_index("ui.selection").unwrap(),
57+ start..end,
58+ )]
59+ });
60+
61+ let sig_text = crate::ui::markdown::highlighted_code_block(
62+ self.signature.clone(),
63+ &self.language,
64+ Some(&cx.editor.theme),
65+ Arc::clone(&self.config_loader),
66+ active_param_span,
67+ );
68+
69+ let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
70+ let sig_text_area = area.clip_top(1).with_height(sig_text_height);
71+ let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
72+ sig_text_para.render(sig_text_area.inner(&margin), surface);
73+
74+ if self.signature_doc.is_none() {
75+ return;
76+ }
77+
78+ let sep_style = Style::default();
79+ let borders = BorderType::line_symbols(BorderType::Plain);
80+ for x in sig_text_area.left()..sig_text_area.right() {
81+ if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) {
82+ cell.set_symbol(borders.horizontal).set_style(sep_style);
83+ }
84+ }
85+
86+ let sig_doc = match &self.signature_doc {
87+ None => return,
88+ Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
89+ };
90+ let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
91+ let sig_doc_area = area.clip_top(sig_text_area.height + 2);
92+ let sig_doc_para = Paragraph::new(sig_doc)
93+ .wrap(Wrap { trim: false })
94+ .scroll((cx.scroll.unwrap_or_default() as u16, 0));
95+ sig_doc_para.render(sig_doc_area.inner(&margin), surface);
96+ }
97+
98+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
99+ const PADDING: u16 = 2;
100+ const SEPARATOR_HEIGHT: u16 = 1;
101+
102+ if PADDING >= viewport.1 || PADDING >= viewport.0 {
103+ return None;
104+ }
105+ let max_text_width = (viewport.0 - PADDING).min(120);
106+
107+ let signature_text = crate::ui::markdown::highlighted_code_block(
108+ self.signature.clone(),
109+ &self.language,
110+ None,
111+ Arc::clone(&self.config_loader),
112+ None,
113+ );
114+ let (sig_width, sig_height) =
115+ crate::ui::text::required_size(&signature_text, max_text_width);
116+
117+ let (width, height) = match self.signature_doc {
118+ Some(ref doc) => {
119+ let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
120+ let doc_text = doc_md.parse(None);
121+ let (doc_width, doc_height) =
122+ crate::ui::text::required_size(&doc_text, max_text_width);
123+ (
124+ sig_width.max(doc_width),
125+ sig_height + SEPARATOR_HEIGHT + doc_height,
126+ )
127+ }
128+ None => (sig_width, sig_height),
129+ };
130+
131+ Some((width + PADDING, height + PADDING))
132+ }
133+}
helix-term/src/ui/markdown.rs+1−1
@@ -144,7 +144,7 @@ impl Markdown {
144 }
144 }
145 }
145 }
146
146
147− fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
147+ pub fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
148 fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
148 fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
149 let spans = std::mem::take(spans);
149 let spans = std::mem::take(spans);
150 if !spans.is_empty() {
150 if !spans.is_empty() {
helix-term/src/ui/mod.rs+2−1
@@ -1,11 +1,12 @@
1mod completion;
1mod completion;
2pub(crate) mod editor;
2pub(crate) mod editor;
3mod info;
3mod info;
4+pub mod lsp;
4mod markdown;
5mod markdown;
5pub mod menu;
6pub mod menu;
6pub mod overlay;
7pub mod overlay;
7mod picker;
8mod picker;
8−mod popup;
9+pub mod popup;
9mod prompt;
10mod prompt;
10mod spinner;
11mod spinner;
11mod statusline;
12mod statusline;
helix-term/src/ui/popup.rs+50−9
@@ -1,4 +1,5 @@
1use crate::{
1use crate::{
2+ commands::Open,
2 compositor::{Callback, Component, Context, EventResult},
3 compositor::{Callback, Component, Context, EventResult},
3 ctrl, key,
4 ctrl, key,
4};
5};
@@ -17,8 +18,10 @@ pub struct Popup<T: Component> {
17 margin: Margin,
18 margin: Margin,
18 size: (u16, u16),
19 size: (u16, u16),
19 child_size: (u16, u16),
20 child_size: (u16, u16),
21+ position_bias: Open,
20 scroll: usize,
22 scroll: usize,
21 auto_close: bool,
23 auto_close: bool,
24+ ignore_escape_key: bool,
22 id: &'static str,
25 id: &'static str,
23}
26}
24
27
@@ -29,15 +32,27 @@ impl<T: Component> Popup<T> {
29 position: None,
32 position: None,
30 margin: Margin::none(),
33 margin: Margin::none(),
31 size: (0, 0),
34 size: (0, 0),
35+ position_bias: Open::Below,
32 child_size: (0, 0),
36 child_size: (0, 0),
33 scroll: 0,
37 scroll: 0,
34 auto_close: false,
38 auto_close: false,
39+ ignore_escape_key: false,
35 id,
40 id,
36 }
41 }
37 }
42 }
38
43
39− pub fn set_position(&mut self, pos: Option<Position>) {
44+ pub fn position(mut self, pos: Option<Position>) -> Self {
40 self.position = pos;
45 self.position = pos;
46+ self
47+ }
48+
49+ pub fn get_position(&self) -> Option<Position> {
50+ self.position
51+ }
52+
53+ pub fn position_bias(mut self, bias: Open) -> Self {
54+ self.position_bias = bias;
55+ self
41 }
56 }
42
57
43 pub fn margin(mut self, margin: Margin) -> Self {
58 pub fn margin(mut self, margin: Margin) -> Self {
@@ -50,6 +65,18 @@ impl<T: Component> Popup<T> {
50 self
65 self
51 }
66 }
52
67
68+ /// Ignores an escape keypress event, letting the outer layer
69+ /// (usually the editor) handle it. This is useful for popups
70+ /// in insert mode like completion and signature help where
71+ /// the popup is closed on the mode change from insert to normal
72+ /// which is done with the escape key. Otherwise the popup consumes
73+ /// the escape key event and closes it, and an additional escape
74+ /// would be required to exit insert mode.
75+ pub fn ignore_escape_key(mut self, ignore: bool) -> Self {
76+ self.ignore_escape_key = ignore;
77+ self
78+ }
79+
53 pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
80 pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
54 let position = self
81 let position = self
55 .position
82 .position
@@ -68,13 +95,23 @@ impl<T: Component> Popup<T> {
68 rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
95 rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
69 }
96 }
70
97
71− // TODO: be able to specify orientation preference. We want above for most popups, below
72− // for menus/autocomplete.
73− if viewport.height > rel_y + height {
98+ let can_put_below = viewport.height > rel_y + height;
74− rel_y += 1 // position below point
99+ let can_put_above = rel_y.checked_sub(height).is_some();
75− } else {
100+ let final_pos = match self.position_bias {
76− rel_y = rel_y.saturating_sub(height) // position above point
101+ Open::Below => match can_put_below {
102+ true => Open::Below,
103+ false => Open::Above,
104+ },
105+ Open::Above => match can_put_above {
106+ true => Open::Above,
107+ false => Open::Below,
108+ },
109+ };
110+
111+ rel_y = match final_pos {
112+ Open::Above => rel_y.saturating_sub(height),
113+ Open::Below => rel_y + 1,
77− }
114+ };
78
115
79 (rel_x, rel_y)
116 (rel_x, rel_y)
80 }
117 }
@@ -112,9 +149,13 @@ impl<T: Component> Component for Popup<T> {
112 _ => return EventResult::Ignored(None),
149 _ => return EventResult::Ignored(None),
113 };
150 };
114
151
152+ if key!(Esc) == key.into() && self.ignore_escape_key {
153+ return EventResult::Ignored(None);
154+ }
155+
115 let close_fn: Callback = Box::new(|compositor, _| {
156 let close_fn: Callback = Box::new(|compositor, _| {
116 // remove the layer
157 // remove the layer
117− compositor.pop();
158+ compositor.remove(self.id.as_ref());
118 });
159 });
119
160
120 match key.into() {
161 match key.into() {
helix-view/src/editor.rs+17−2
@@ -167,10 +167,25 @@ pub struct Config {
167 pub color_modes: bool,
167 pub color_modes: bool,
168}
168}
169
169
170−#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
170+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171−#[serde(rename_all = "kebab-case", deny_unknown_fields)]
171+#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
172pub struct LspConfig {
172pub struct LspConfig {
173+ /// Display LSP progress messages below statusline
173 pub display_messages: bool,
174 pub display_messages: bool,
175+ /// Enable automatic pop up of signature help (parameter hints)
176+ pub auto_signature_help: bool,
177+ /// Display docs under signature help popup
178+ pub display_signature_help_docs: bool,
179+}
180+
181+impl Default for LspConfig {
182+ fn default() -> Self {
183+ Self {
184+ display_messages: false,
185+ auto_signature_help: true,
186+ display_signature_help_docs: true,
187+ }
188+ }
174}
189}
175
190
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]