Skip to content

Commit 791bf7e

Browse files
authored
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
1 parent 02f0099 commit 791bf7e

File tree

13 files changed

+380
-63
lines changed

13 files changed

+380
-63
lines changed

book/src/configuration.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

720722
fn 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

739743
fn 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

Lines changed: 105 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ use helix_lsp::{
66
};
77
use 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

1111
use helix_core::{path, Selection};
1212
use helix_view::{editor::Action, theme::Style};
1313

1414
use 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+
808815
pub 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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ pub struct Completion {
8585
}
8686

8787
impl 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,

0 commit comments

Comments
 (0)