Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lsp signature help #1755

Merged
merged 21 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cd27422
Add lsp signature help
sudormrfbin Mar 1, 2022
0ff483a
Do not move signature help popup on multiple triggers
sudormrfbin Mar 1, 2022
f23eb9d
Highlight current parameter in signature help
sudormrfbin Mar 2, 2022
bfdde32
Auto close signature help
sudormrfbin Mar 3, 2022
f4e9bd3
Position signature help above to not block completion
sudormrfbin Mar 12, 2022
576c3d1
Update signature help on backspace/insert mode delete
sudormrfbin Mar 22, 2022
f62bf95
Add lsp.auto-signature-help config option
sudormrfbin Mar 27, 2022
a13b762
Add serde default annotation for LspConfig
sudormrfbin Jun 1, 2022
f1de34c
Show LSP inactive message only if signature help is invoked manually
sudormrfbin Jun 2, 2022
0a4b5b2
Do not assume valid signature help response from LSP
sudormrfbin Jun 12, 2022
9a1e5d5
Check signature help capability before sending request
sudormrfbin Jun 12, 2022
70a8525
Merge branch 'master' into signature-help-popup
sudormrfbin Jun 16, 2022
b0b13be
Reuse Open enum for PositionBias in popup
sudormrfbin Jun 25, 2022
d84e18a
Close signature popup and exit insert mode on escape
sudormrfbin Jun 27, 2022
2df1b74
Add config to control signature help docs display
sudormrfbin Jun 27, 2022
9bee76f
Merge branch 'master' into signature-help-popup
sudormrfbin Jun 27, 2022
61098ba
Use new Margin api in signature help
sudormrfbin Jun 27, 2022
3b26143
Merge branch 'master' into signature-help-popup
sudormrfbin Jul 2, 2022
b675243
Invoke signature help on changing to insert mode
sudormrfbin Jul 2, 2022
d80dce7
Merge branch 'master' into signature-help-popup
sudormrfbin Jul 2, 2022
91e58aa
Merge branch 'master' into signature-help-popup
sudormrfbin Jul 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ hidden = false

### `[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]: A progress spinner is always 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 @@ -321,6 +321,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 @@ -645,7 +655,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 @@ -656,7 +671,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 @@ -711,6 +711,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 @@ -730,6 +732,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 @@ -2356,7 +2360,8 @@ async fn make_format_callback(
Ok(call)
}

enum Open {
#[derive(PartialEq)]
pub enum Open {
Below,
Above,
}
Expand Down Expand Up @@ -2754,6 +2759,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 @@ -2773,26 +2781,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 = &[')', ';', '.'];
sudormrfbin marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -2827,7 +2824,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 @@ -2999,6 +2995,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 @@ -3015,6 +3013,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 @@ -3028,6 +3028,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 @@ -3040,6 +3042,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
123 changes: 105 additions & 18 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ use helix_lsp::{
OffsetEncoding,
};

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

use helix_core::Selection;
use helix_view::editor::Action;

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::borrow::Cow;
use std::{borrow::Cow, 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 @@ -627,31 +629,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)
{
sudormrfbin marked this conversation as resolved.
Show resolved Hide resolved
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("");
sudormrfbin marked this conversation as resolved.
Show resolved Hide resolved

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()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look closely on this. Does this also work with double width characters like CJK? This seemed to be characters length after looking at the code (wasn't mentioned in the comment whether it is character length or terminal width length) and we don't seemed to be checking the width of required_size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should if the intent is to use byte length, or do we need to use string.width() ?

Copy link
Member Author

@sudormrfbin sudormrfbin Jun 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This actually turned out to be a bug, see #3154.)


Syntax highlighting uses char byte offsets so that is what we need as the final (start, end) value. Now, LSP sends the label offsets in utf-16 according to the configured OffsetEncoding (but we have it hardcoded to utf8) and the same is used by the LSP for ParameterLabel::LabelOffsets, so we would have to convert this to char byte offsets:

                    lsp::ParameterLabel::LabelOffsets([start, end]) => {
                        Some((*start as usize, *end as usize))
                    }

str.find() reports the byte index of the start of the pattern, so that would have to be changed too.

But strangely, with a naive test on a bit of CJK and non ASCII text, the parameter highlighting seems to work okay:

helix-signature-help-cjk

Details
fn main() {}

/// Documentation with doc tests:
///
/// ```
/// test
/// ```
fn func(_par老am0: char, _lö老虎wy̆: usize, _paröam1: &str) {
    return;
}

}
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);
sudormrfbin marked this conversation as resolved.
Show resolved Hide resolved
},
);
}
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 @@ -1227,11 +1227,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))?;
sudormrfbin marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -78,6 +78,8 @@ pub struct Completion {
}

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

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