Skip to content

Commit

Permalink
Add lsp signature help (helix-editor#1755)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sudormrfbin authored Jul 19, 2022
1 parent 02f0099 commit 791bf7e
Show file tree
Hide file tree
Showing 13 changed files with 380 additions and 63 deletions.
8 changes: 5 additions & 3 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ The following elements can be configured:

### `[editor.lsp]` Section

| Key | Description | Default |
| --- | ----------- | ------- |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| Key | Description | Default |
| --- | ----------- | ------- |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |

[^1]: By default, a progress spinner is shown in the statusline beside the file path.

Expand Down
19 changes: 17 additions & 2 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]),
..Default::default()
}),
signature_help: Some(lsp::SignatureHelpClientCapabilities {
signature_information: Some(lsp::SignatureInformationSettings {
documentation_format: Some(vec![lsp::MarkupKind::Markdown]),
parameter_information: Some(lsp::ParameterInformationSettings {
label_offset_support: Some(true),
}),
active_parameter_support: Some(true),
}),
..Default::default()
}),
rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false),
prepare_support: Some(false),
Expand Down Expand Up @@ -646,7 +656,12 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();

// Return early if signature help is not supported
capabilities.signature_help_provider.as_ref()?;

let params = lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
Expand All @@ -657,7 +672,7 @@ impl Client {
// lsp::SignatureHelpContext
};

self.call::<lsp::request::SignatureHelpRequest>(params)
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}

pub fn text_document_hover(
Expand Down
42 changes: 23 additions & 19 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,8 @@ fn kill_to_line_start(cx: &mut Context) {
Range::new(head, anchor)
});
delete_selection_insert_mode(doc, view, &selection);

lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}

fn kill_to_line_end(cx: &mut Context) {
Expand All @@ -734,6 +736,8 @@ fn kill_to_line_end(cx: &mut Context) {
new_range
});
delete_selection_insert_mode(doc, view, &selection);

lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}

fn goto_first_nonwhitespace(cx: &mut Context) {
Expand Down Expand Up @@ -2399,7 +2403,8 @@ async fn make_format_callback(
Ok(call)
}

enum Open {
#[derive(PartialEq)]
pub enum Open {
Below,
Above,
}
Expand Down Expand Up @@ -2797,6 +2802,9 @@ pub mod insert {
use helix_lsp::lsp;
// if ch matches signature_help char, trigger
let doc = doc_mut!(cx.editor);
// The language_server!() macro is not used here since it will
// print an "LSP not active for current buffer" message on
// every keypress.
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
Expand All @@ -2816,26 +2824,15 @@ pub mod insert {
{
// TODO: what if trigger is multiple chars long
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
// lsp doesn't tell us when to close the signature help, so we request
// the help information again after common close triggers which should
// return None, which in turn closes the popup.
let close_triggers = &[')', ';', '.'];

if is_trigger {
super::signature_help(cx);
if is_trigger || close_triggers.contains(&ch) {
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
}

// SignatureHelp {
// signatures: [
// SignatureInformation {
// label: "fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error>",
// documentation: None,
// parameters: Some(
// [ParameterInformation { label: Simple("path: PathBuf"), documentation: None },
// ParameterInformation { label: Simple("action: Action"), documentation: None }]
// ),
// active_parameter: Some(0)
// }
// ],
// active_signature: None, active_parameter: Some(0)
// }
}

// The default insert hook: simply insert the character
Expand Down Expand Up @@ -2870,7 +2867,6 @@ pub mod insert {
// this could also generically look at Transaction, but it's a bit annoying to look at
// Operation instead of Change.
for hook in &[language_server_completion, signature_help] {
// for hook in &[signature_help] {
hook(cx, c);
}
}
Expand Down Expand Up @@ -3042,6 +3038,8 @@ pub mod insert {
}
});
doc.apply(&transaction, view.id);

lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}

pub fn delete_char_forward(cx: &mut Context) {
Expand All @@ -3058,6 +3056,8 @@ pub mod insert {
)
});
doc.apply(&transaction, view.id);

lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}

pub fn delete_word_backward(cx: &mut Context) {
Expand All @@ -3071,6 +3071,8 @@ pub mod insert {
exclude_cursor(text, next, range)
});
delete_selection_insert_mode(doc, view, &selection);

lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}

pub fn delete_word_forward(cx: &mut Context) {
Expand All @@ -3083,6 +3085,8 @@ pub mod insert {
.clone()
.transform(|range| movement::move_next_word_start(text, range, count));
delete_selection_insert_mode(doc, view, &selection);

lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
}

Expand Down
124 changes: 105 additions & 19 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ use helix_lsp::{
};
use tui::text::{Span, Spans};

use super::{align_view, push_jump, Align, Context, Editor};
use super::{align_view, push_jump, Align, Context, Editor, Open};

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

use crate::{
compositor::{self, Compositor},
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
ui::{
self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
},
};

use std::collections::BTreeMap;
use std::{borrow::Cow, path::PathBuf};
use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};

/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
Expand Down Expand Up @@ -805,31 +806,116 @@ pub fn goto_reference(cx: &mut Context) {
);
}

#[derive(PartialEq)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
}

pub fn signature_help(cx: &mut Context) {
signature_help_impl(cx, SignatureHelpInvoked::Manual)
}

pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;

let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if was_manually_invoked {
cx.editor
.set_status("Language server not active for current buffer");
}
return;
}
};
let offset_encoding = language_server.offset_encoding();

let pos = doc.position(view.id, offset_encoding);

let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
Some(f) => f,
None => return,
};

cx.callback(
future,
move |_editor, _compositor, response: Option<lsp::SignatureHelp>| {
if let Some(signature_help) = response {
log::info!("{:?}", signature_help);
// signatures
// active_signature
// active_parameter
// render as:

// signature
// ----------
// doc

// with active param highlighted
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
let config = &editor.config();

if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| was_manually_invoked)
{
return;
}

let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
compositor.remove(SignatureHelp::ID);
return;
}
};
let doc = doc!(editor);
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");

let signature = match response
.signatures
.get(response.active_signature.unwrap_or(0) as usize)
{
Some(s) => s,
None => return,
};
let mut contents = SignatureHelp::new(
signature.label.clone(),
language.to_string(),
Arc::clone(&editor.syn_loader),
);

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 &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
Some((*start as usize, *end as usize))
}
}
};
contents.set_active_param_range(active_param_range());

let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
compositor.replace_or_push(SignatureHelp::ID, popup);
},
);
}
Expand Down
8 changes: 3 additions & 5 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1501,11 +1501,9 @@ fn run_shell_command(
format!("```sh\n{}\n```", output),
editor.syn_loader.clone(),
);
let mut popup = Popup::new("shell", contents);
popup.set_position(Some(helix_core::Position::new(
editor.cursor().0.unwrap_or_default().row,
2,
)));
let popup = Popup::new("shell", contents).position(Some(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
));
compositor.replace_or_push("shell", popup);
});
Ok(call)
Expand Down
8 changes: 8 additions & 0 deletions helix-term/src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ impl Compositor {
self.layers.pop()
}

pub fn remove(&mut self, id: &'static str) -> Option<Box<dyn Component>> {
let idx = self
.layers
.iter()
.position(|layer| layer.id() == Some(id))?;
Some(self.layers.remove(idx))
}

pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
// If it is a key event and a macro is being recorded, push the key event to the recording.
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
Expand Down
4 changes: 3 additions & 1 deletion helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ pub struct Completion {
}

impl Completion {
pub const ID: &'static str = "completion";

pub fn new(
editor: &Editor,
items: Vec<CompletionItem>,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 791bf7e

Please sign in to comment.