on master help sign in
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

commit791bf7e50a19bcf7612788deb7514847089cb976 authorGokul Soumya <gokulps15@gmail.com> dateTue 19 Jul 2022, 07:58 (4 years ago) parent02f0099
13 files changed+380−63
book/src/configuration.md+5−3 helix-lsp/src/client.rs+17−2 helix-term/src/commands.rs+23−19 helix-term/src/commands/lsp.rs+105−19 helix-term/src/commands/typed.rs+3−5 helix-term/src/compositor.rs+8 helix-term/src/ui/completion.rs+3−1 helix-term/src/ui/editor.rs+13−1 helix-term/src/ui/lsp.rs+133 helix-term/src/ui/markdown.rs+1−1 helix-term/src/ui/mod.rs+2−1 helix-term/src/ui/popup.rs+50−9 helix-view/src/editor.rs+17−2
book/src/configuration.md+5−3
@@ -80,9 +80,11 @@ The following elements can be configured:
8080
8181### `[editor.lsp]` Section
8282
83−| Key | Description | Default |
84−| --- | ----------- | ------- |
85−| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
83+| Key | Description | Default |
84+| --- | ----------- | ------- |
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` |
8688
8789[^1]: By default, a progress spinner is shown in the statusline beside the file path.
8890
helix-lsp/src/client.rs+17−2
@@ -322,6 +322,16 @@ impl Client {
322322 content_format: Some(vec![lsp::MarkupKind::Markdown]),
323323 ..Default::default()
324324 }),
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+ }),
325335 rename: Some(lsp::RenameClientCapabilities {
326336 dynamic_registration: Some(false),
327337 prepare_support: Some(false),
@@ -646,7 +656,12 @@ impl Client {
646656 text_document: lsp::TextDocumentIdentifier,
647657 position: lsp::Position,
648658 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+
650665 let params = lsp::SignatureHelpParams {
651666 text_document_position_params: lsp::TextDocumentPositionParams {
652667 text_document,
@@ -657,7 +672,7 @@ impl Client {
657672 // lsp::SignatureHelpContext
658673 };
659674
660− self.call::<lsp::request::SignatureHelpRequest>(params)
675+ Some(self.call::<lsp::request::SignatureHelpRequest>(params))
661676 }
662677
663678 pub fn text_document_hover(
helix-term/src/commands.rs+23−19
@@ -715,6 +715,8 @@ fn kill_to_line_start(cx: &mut Context) {
715715 Range::new(head, anchor)
716716 });
717717 delete_selection_insert_mode(doc, view, &selection);
718+
719+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
718720}
719721
720722fn kill_to_line_end(cx: &mut Context) {
@@ -734,6 +736,8 @@ fn kill_to_line_end(cx: &mut Context) {
734736 new_range
735737 });
736738 delete_selection_insert_mode(doc, view, &selection);
739+
740+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
737741}
738742
739743fn goto_first_nonwhitespace(cx: &mut Context) {
@@ -2399,7 +2403,8 @@ async fn make_format_callback(
23992403 Ok(call)
24002404}
24012405
2402−enum Open {
2406+#[derive(PartialEq)]
2407+pub enum Open {
24032408 Below,
24042409 Above,
24052410}
@@ -2797,6 +2802,9 @@ pub mod insert {
27972802 use helix_lsp::lsp;
27982803 // if ch matches signature_help char, trigger
27992804 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.
28002808 let language_server = match doc.language_server() {
28012809 Some(language_server) => language_server,
28022810 None => return,
@@ -2816,26 +2824,15 @@ pub mod insert {
28162824 {
28172825 // TODO: what if trigger is multiple chars long
28182826 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 = &[')', ';', '.'];
28192831
2820− if is_trigger {
2821− super::signature_help(cx);
2832+ if is_trigger || close_triggers.contains(&ch) {
2833+ super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
28222834 }
28232835 }
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− // }
28392836 }
28402837
28412838 // The default insert hook: simply insert the character
@@ -2870,7 +2867,6 @@ pub mod insert {
28702867 // this could also generically look at Transaction, but it's a bit annoying to look at
28712868 // Operation instead of Change.
28722869 for hook in &[language_server_completion, signature_help] {
2873− // for hook in &[signature_help] {
28742870 hook(cx, c);
28752871 }
28762872 }
@@ -3042,6 +3038,8 @@ pub mod insert {
30423038 }
30433039 });
30443040 doc.apply(&transaction, view.id);
3041+
3042+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
30453043 }
30463044
30473045 pub fn delete_char_forward(cx: &mut Context) {
@@ -3058,6 +3056,8 @@ pub mod insert {
30583056 )
30593057 });
30603058 doc.apply(&transaction, view.id);
3059+
3060+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
30613061 }
30623062
30633063 pub fn delete_word_backward(cx: &mut Context) {
@@ -3071,6 +3071,8 @@ pub mod insert {
30713071 exclude_cursor(text, next, range)
30723072 });
30733073 delete_selection_insert_mode(doc, view, &selection);
3074+
3075+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
30743076 }
30753077
30763078 pub fn delete_word_forward(cx: &mut Context) {
@@ -3083,6 +3085,8 @@ pub mod insert {
30833085 .clone()
30843086 .transform(|range| movement::move_next_word_start(text, range, count));
30853087 delete_selection_insert_mode(doc, view, &selection);
3088+
3089+ lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
30863090 }
30873091}
30883092
helix-term/src/commands/lsp.rs+105−19
@@ -6,18 +6,19 @@ use helix_lsp::{
66};
77use tui::text::{Span, Spans};
88
9−use super::{align_view, push_jump, Align, Context, Editor};
9+use super::{align_view, push_jump, Align, Context, Editor, Open};
1010
1111use helix_core::{path, Selection};
1212use helix_view::{editor::Action, theme::Style};
1313
1414use crate::{
1515 compositor::{self, Compositor},
16− ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
16+ ui::{
17+ self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
18+ },
1719};
1820
19−use std::collections::BTreeMap;
20−use std::{borrow::Cow, path::PathBuf};
21+use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
2122
2223/// Gets the language server that is attached to a document, and
2324/// if it's not active displays a status message. Using this macro
@@ -805,31 +806,116 @@ pub fn goto_reference(cx: &mut Context) {
805806 );
806807}
807808
809+#[derive(PartialEq)]
810+pub enum SignatureHelpInvoked {
811+ Manual,
812+ Automatic,
813+}
814+
808815pub 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) {
809820 let (view, doc) = current!(cx.editor);
810− let language_server = language_server!(cx.editor, doc);
821+ let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
822+
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+ };
811835 let offset_encoding = language_server.offset_encoding();
812836
813837 let pos = doc.position(view.id, offset_encoding);
814838
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+ };
816843
817844 cx.callback(
818845 future,
819− move |_editor, _compositor, response: Option<lsp::SignatureHelp>| {
820− if let Some(signature_help) = response {
821− log::info!("{:?}", signature_help);
822− // signatures
823− // active_signature
824− // active_parameter
825− // render as:
826−
827− // signature
828− // ----------
829− // doc
830−
831− // with active param highlighted
846+ move |editor, compositor, response: Option<lsp::SignatureHelp>| {
847+ let config = &editor.config();
848+
849+ if !(config.lsp.auto_signature_help
850+ || SignatureHelp::visible_popup(compositor).is_some()
851+ || was_manually_invoked)
852+ {
853+ return;
832854 }
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);
833919 },
834920 );
835921}
helix-term/src/commands/typed.rs+3−5
@@ -1501,11 +1501,9 @@ fn run_shell_command(
15011501 format!("```sh\n{}\n```", output),
15021502 editor.syn_loader.clone(),
15031503 );
1504− let mut popup = Popup::new("shell", contents);
1505− popup.set_position(Some(helix_core::Position::new(
1506− editor.cursor().0.unwrap_or_default().row,
1507− 2,
1508− )));
1504+ let popup = Popup::new("shell", contents).position(Some(
1505+ helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
1506+ ));
15091507 compositor.replace_or_push("shell", popup);
15101508 });
15111509 Ok(call)
helix-term/src/compositor.rs+8
@@ -150,6 +150,14 @@ impl Compositor {
150150 self.layers.pop()
151151 }
152152
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+
153161 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
154162 // If it is a key event and a macro is being recorded, push the key event to the recording.
155163 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 {
8585}
8686
8787impl Completion {
88+ pub const ID: &'static str = "completion";
89+
8890 pub fn new(
8991 editor: &Editor,
9092 items: Vec<CompletionItem>,
@@ -214,7 +216,7 @@ impl Completion {
214216 }
215217 };
216218 });
217− let popup = Popup::new("completion", menu);
219+ let popup = Popup::new(Self::ID, menu);
218220 let mut completion = Self {
219221 popup,
220222 start_offset,
helix-term/src/ui/editor.rs+13−1
@@ -1,7 +1,7 @@
11use crate::{
22 commands,
33 compositor::{Component, Context, EventResult},
4− key,
4+ job, key,
55 keymap::{KeymapResult, Keymaps},
66 ui::{Completion, ProgressSpinners},
77};
@@ -28,6 +28,7 @@ use std::borrow::Cow;
2828use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
2929use tui::buffer::Buffer as Surface;
3030
31+use super::lsp::SignatureHelp;
3132use super::statusline;
3233
3334pub struct EditorView {
@@ -1205,10 +1206,21 @@ impl Component for EditorView {
12051206 _ => unimplemented!(),
12061207 };
12071208 self.last_insert.1.clear();
1209+ commands::signature_help_impl(
1210+ &mut cx,
1211+ commands::SignatureHelpInvoked::Automatic,
1212+ );
12081213 }
12091214 (Mode::Insert, Mode::Normal) => {
12101215 // if exiting insert mode, remove completion
12111216 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+ });
12121224 }
12131225 _ => (),
12141226 }
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 {
144144 }
145145 }
146146
147− fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
147+ pub fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
148148 fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
149149 let spans = std::mem::take(spans);
150150 if !spans.is_empty() {
helix-term/src/ui/mod.rs+2−1
@@ -1,11 +1,12 @@
11mod completion;
22pub(crate) mod editor;
33mod info;
4+pub mod lsp;
45mod markdown;
56pub mod menu;
67pub mod overlay;
78mod picker;
8−mod popup;
9+pub mod popup;
910mod prompt;
1011mod spinner;
1112mod statusline;
helix-term/src/ui/popup.rs+50−9
@@ -1,4 +1,5 @@
11use crate::{
2+ commands::Open,
23 compositor::{Callback, Component, Context, EventResult},
34 ctrl, key,
45};
@@ -17,8 +18,10 @@ pub struct Popup<T: Component> {
1718 margin: Margin,
1819 size: (u16, u16),
1920 child_size: (u16, u16),
21+ position_bias: Open,
2022 scroll: usize,
2123 auto_close: bool,
24+ ignore_escape_key: bool,
2225 id: &'static str,
2326}
2427
@@ -29,15 +32,27 @@ impl<T: Component> Popup<T> {
2932 position: None,
3033 margin: Margin::none(),
3134 size: (0, 0),
35+ position_bias: Open::Below,
3236 child_size: (0, 0),
3337 scroll: 0,
3438 auto_close: false,
39+ ignore_escape_key: false,
3540 id,
3641 }
3742 }
3843
39− pub fn set_position(&mut self, pos: Option<Position>) {
44+ pub fn position(mut self, pos: Option<Position>) -> Self {
4045 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
4156 }
4257
4358 pub fn margin(mut self, margin: Margin) -> Self {
@@ -50,6 +65,18 @@ impl<T: Component> Popup<T> {
5065 self
5166 }
5267
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+
5380 pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
5481 let position = self
5582 .position
@@ -68,13 +95,23 @@ impl<T: Component> Popup<T> {
6895 rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
6996 }
7097
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 {
74− rel_y += 1 // position below point
75− } else {
76− rel_y = rel_y.saturating_sub(height) // position above point
77− }
98+ let can_put_below = viewport.height > rel_y + height;
99+ let can_put_above = rel_y.checked_sub(height).is_some();
100+ let final_pos = match self.position_bias {
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,
114+ };
78115
79116 (rel_x, rel_y)
80117 }
@@ -112,9 +149,13 @@ impl<T: Component> Component for Popup<T> {
112149 _ => return EventResult::Ignored(None),
113150 };
114151
152+ if key!(Esc) == key.into() && self.ignore_escape_key {
153+ return EventResult::Ignored(None);
154+ }
155+
115156 let close_fn: Callback = Box::new(|compositor, _| {
116157 // remove the layer
117− compositor.pop();
158+ compositor.remove(self.id.as_ref());
118159 });
119160
120161 match key.into() {
helix-view/src/editor.rs+17−2
@@ -167,10 +167,25 @@ pub struct Config {
167167 pub color_modes: bool,
168168}
169169
170−#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
171−#[serde(rename_all = "kebab-case", deny_unknown_fields)]
170+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171+#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
172172pub struct LspConfig {
173+ /// Display LSP progress messages below statusline
173174 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+ }
174189}
175190
176191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]