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
This commit is contained in:
parent
02f0099210
commit
791bf7e50a
13 changed files with 379 additions and 62 deletions
|
@ -80,9 +80,11 @@ The following elements can be configured:
|
||||||
|
|
||||||
### `[editor.lsp]` Section
|
### `[editor.lsp]` Section
|
||||||
|
|
||||||
| Key | Description | Default |
|
| Key | Description | Default |
|
||||||
| --- | ----------- | ------- |
|
| --- | ----------- | ------- |
|
||||||
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
|
| `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.
|
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
|
||||||
|
|
||||||
|
|
|
@ -322,6 +322,16 @@ impl Client {
|
||||||
content_format: Some(vec![lsp::MarkupKind::Markdown]),
|
content_format: Some(vec![lsp::MarkupKind::Markdown]),
|
||||||
..Default::default()
|
..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 {
|
rename: Some(lsp::RenameClientCapabilities {
|
||||||
dynamic_registration: Some(false),
|
dynamic_registration: Some(false),
|
||||||
prepare_support: Some(false),
|
prepare_support: Some(false),
|
||||||
|
@ -646,7 +656,12 @@ impl Client {
|
||||||
text_document: lsp::TextDocumentIdentifier,
|
text_document: lsp::TextDocumentIdentifier,
|
||||||
position: lsp::Position,
|
position: lsp::Position,
|
||||||
work_done_token: Option<lsp::ProgressToken>,
|
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 {
|
let params = lsp::SignatureHelpParams {
|
||||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||||
text_document,
|
text_document,
|
||||||
|
@ -657,7 +672,7 @@ impl Client {
|
||||||
// lsp::SignatureHelpContext
|
// lsp::SignatureHelpContext
|
||||||
};
|
};
|
||||||
|
|
||||||
self.call::<lsp::request::SignatureHelpRequest>(params)
|
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_document_hover(
|
pub fn text_document_hover(
|
||||||
|
|
|
@ -715,6 +715,8 @@ fn kill_to_line_start(cx: &mut Context) {
|
||||||
Range::new(head, anchor)
|
Range::new(head, anchor)
|
||||||
});
|
});
|
||||||
delete_selection_insert_mode(doc, view, &selection);
|
delete_selection_insert_mode(doc, view, &selection);
|
||||||
|
|
||||||
|
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kill_to_line_end(cx: &mut Context) {
|
fn kill_to_line_end(cx: &mut Context) {
|
||||||
|
@ -734,6 +736,8 @@ fn kill_to_line_end(cx: &mut Context) {
|
||||||
new_range
|
new_range
|
||||||
});
|
});
|
||||||
delete_selection_insert_mode(doc, view, &selection);
|
delete_selection_insert_mode(doc, view, &selection);
|
||||||
|
|
||||||
|
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn goto_first_nonwhitespace(cx: &mut Context) {
|
fn goto_first_nonwhitespace(cx: &mut Context) {
|
||||||
|
@ -2399,7 +2403,8 @@ async fn make_format_callback(
|
||||||
Ok(call)
|
Ok(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Open {
|
#[derive(PartialEq)]
|
||||||
|
pub enum Open {
|
||||||
Below,
|
Below,
|
||||||
Above,
|
Above,
|
||||||
}
|
}
|
||||||
|
@ -2797,6 +2802,9 @@ pub mod insert {
|
||||||
use helix_lsp::lsp;
|
use helix_lsp::lsp;
|
||||||
// if ch matches signature_help char, trigger
|
// if ch matches signature_help char, trigger
|
||||||
let doc = doc_mut!(cx.editor);
|
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() {
|
let language_server = match doc.language_server() {
|
||||||
Some(language_server) => language_server,
|
Some(language_server) => language_server,
|
||||||
None => return,
|
None => return,
|
||||||
|
@ -2816,26 +2824,15 @@ pub mod insert {
|
||||||
{
|
{
|
||||||
// TODO: what if trigger is multiple chars long
|
// TODO: what if trigger is multiple chars long
|
||||||
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
|
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 {
|
if is_trigger || close_triggers.contains(&ch) {
|
||||||
super::signature_help(cx);
|
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
|
// The default insert hook: simply insert the character
|
||||||
|
@ -2870,7 +2867,6 @@ pub mod insert {
|
||||||
// this could also generically look at Transaction, but it's a bit annoying to look at
|
// this could also generically look at Transaction, but it's a bit annoying to look at
|
||||||
// Operation instead of Change.
|
// Operation instead of Change.
|
||||||
for hook in &[language_server_completion, signature_help] {
|
for hook in &[language_server_completion, signature_help] {
|
||||||
// for hook in &[signature_help] {
|
|
||||||
hook(cx, c);
|
hook(cx, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3042,6 +3038,8 @@ pub mod insert {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
|
|
||||||
|
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_char_forward(cx: &mut Context) {
|
pub fn delete_char_forward(cx: &mut Context) {
|
||||||
|
@ -3058,6 +3056,8 @@ pub mod insert {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
|
|
||||||
|
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_word_backward(cx: &mut Context) {
|
pub fn delete_word_backward(cx: &mut Context) {
|
||||||
|
@ -3071,6 +3071,8 @@ pub mod insert {
|
||||||
exclude_cursor(text, next, range)
|
exclude_cursor(text, next, range)
|
||||||
});
|
});
|
||||||
delete_selection_insert_mode(doc, view, &selection);
|
delete_selection_insert_mode(doc, view, &selection);
|
||||||
|
|
||||||
|
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_word_forward(cx: &mut Context) {
|
pub fn delete_word_forward(cx: &mut Context) {
|
||||||
|
@ -3083,6 +3085,8 @@ pub mod insert {
|
||||||
.clone()
|
.clone()
|
||||||
.transform(|range| movement::move_next_word_start(text, range, count));
|
.transform(|range| movement::move_next_word_start(text, range, count));
|
||||||
delete_selection_insert_mode(doc, view, &selection);
|
delete_selection_insert_mode(doc, view, &selection);
|
||||||
|
|
||||||
|
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,19 @@ use helix_lsp::{
|
||||||
};
|
};
|
||||||
use tui::text::{Span, Spans};
|
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_core::{path, Selection};
|
||||||
use helix_view::{editor::Action, theme::Style};
|
use helix_view::{editor::Action, theme::Style};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
compositor::{self, Compositor},
|
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, collections::BTreeMap, path::PathBuf, sync::Arc};
|
||||||
use std::{borrow::Cow, path::PathBuf};
|
|
||||||
|
|
||||||
/// Gets the language server that is attached to a document, and
|
/// Gets the language server that is attached to a document, and
|
||||||
/// if it's not active displays a status message. Using this macro
|
/// if it's not active displays a status message. Using this macro
|
||||||
|
@ -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) {
|
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 (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 offset_encoding = language_server.offset_encoding();
|
||||||
|
|
||||||
let pos = doc.position(view.id, 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(
|
cx.callback(
|
||||||
future,
|
future,
|
||||||
move |_editor, _compositor, response: Option<lsp::SignatureHelp>| {
|
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
|
||||||
if let Some(signature_help) = response {
|
let config = &editor.config();
|
||||||
log::info!("{:?}", signature_help);
|
|
||||||
// signatures
|
|
||||||
// active_signature
|
|
||||||
// active_parameter
|
|
||||||
// render as:
|
|
||||||
|
|
||||||
// signature
|
if !(config.lsp.auto_signature_help
|
||||||
// ----------
|
|| SignatureHelp::visible_popup(compositor).is_some()
|
||||||
// doc
|
|| was_manually_invoked)
|
||||||
|
{
|
||||||
// with active param highlighted
|
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 ¶m.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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1501,11 +1501,9 @@ fn run_shell_command(
|
||||||
format!("```sh\n{}\n```", output),
|
format!("```sh\n{}\n```", output),
|
||||||
editor.syn_loader.clone(),
|
editor.syn_loader.clone(),
|
||||||
);
|
);
|
||||||
let mut popup = Popup::new("shell", contents);
|
let popup = Popup::new("shell", contents).position(Some(
|
||||||
popup.set_position(Some(helix_core::Position::new(
|
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
|
||||||
editor.cursor().0.unwrap_or_default().row,
|
));
|
||||||
2,
|
|
||||||
)));
|
|
||||||
compositor.replace_or_push("shell", popup);
|
compositor.replace_or_push("shell", popup);
|
||||||
});
|
});
|
||||||
Ok(call)
|
Ok(call)
|
||||||
|
|
|
@ -150,6 +150,14 @@ impl Compositor {
|
||||||
self.layers.pop()
|
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 {
|
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 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) {
|
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
|
||||||
|
|
|
@ -85,6 +85,8 @@ pub struct Completion {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completion {
|
impl Completion {
|
||||||
|
pub const ID: &'static str = "completion";
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
editor: &Editor,
|
editor: &Editor,
|
||||||
items: Vec<CompletionItem>,
|
items: Vec<CompletionItem>,
|
||||||
|
@ -214,7 +216,7 @@ impl Completion {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let popup = Popup::new("completion", menu);
|
let popup = Popup::new(Self::ID, menu);
|
||||||
let mut completion = Self {
|
let mut completion = Self {
|
||||||
popup,
|
popup,
|
||||||
start_offset,
|
start_offset,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
commands,
|
commands,
|
||||||
compositor::{Component, Context, EventResult},
|
compositor::{Component, Context, EventResult},
|
||||||
key,
|
job, key,
|
||||||
keymap::{KeymapResult, Keymaps},
|
keymap::{KeymapResult, Keymaps},
|
||||||
ui::{Completion, ProgressSpinners},
|
ui::{Completion, ProgressSpinners},
|
||||||
};
|
};
|
||||||
|
@ -28,6 +28,7 @@ use std::borrow::Cow;
|
||||||
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
|
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
|
||||||
use tui::buffer::Buffer as Surface;
|
use tui::buffer::Buffer as Surface;
|
||||||
|
|
||||||
|
use super::lsp::SignatureHelp;
|
||||||
use super::statusline;
|
use super::statusline;
|
||||||
|
|
||||||
pub struct EditorView {
|
pub struct EditorView {
|
||||||
|
@ -1205,10 +1206,21 @@ impl Component for EditorView {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
self.last_insert.1.clear();
|
self.last_insert.1.clear();
|
||||||
|
commands::signature_help_impl(
|
||||||
|
&mut cx,
|
||||||
|
commands::SignatureHelpInvoked::Automatic,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
(Mode::Insert, Mode::Normal) => {
|
(Mode::Insert, Mode::Normal) => {
|
||||||
// if exiting insert mode, remove completion
|
// if exiting insert mode, remove completion
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
|
// TODO: Use an on_mode_change hook to remove signature help
|
||||||
|
context.jobs.callback(async {
|
||||||
|
let call: job::Callback = Box::new(|_editor, compositor| {
|
||||||
|
compositor.remove(SignatureHelp::ID);
|
||||||
|
});
|
||||||
|
Ok(call)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
133
helix-term/src/ui/lsp.rs
Normal file
133
helix-term/src/ui/lsp.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use helix_core::syntax;
|
||||||
|
use helix_view::graphics::{Margin, Rect, Style};
|
||||||
|
use tui::buffer::Buffer;
|
||||||
|
use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
|
||||||
|
|
||||||
|
use crate::compositor::{Component, Compositor, Context};
|
||||||
|
|
||||||
|
use crate::ui::Markdown;
|
||||||
|
|
||||||
|
use super::Popup;
|
||||||
|
|
||||||
|
pub struct SignatureHelp {
|
||||||
|
signature: String,
|
||||||
|
signature_doc: Option<String>,
|
||||||
|
/// Part of signature text
|
||||||
|
active_param_range: Option<(usize, usize)>,
|
||||||
|
|
||||||
|
language: String,
|
||||||
|
config_loader: Arc<syntax::Loader>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignatureHelp {
|
||||||
|
pub const ID: &'static str = "signature-help";
|
||||||
|
|
||||||
|
pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||||
|
Self {
|
||||||
|
signature,
|
||||||
|
signature_doc: None,
|
||||||
|
active_param_range: None,
|
||||||
|
language,
|
||||||
|
config_loader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
|
||||||
|
self.signature_doc = signature_doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
|
||||||
|
self.active_param_range = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
|
||||||
|
compositor.find_id::<Popup<Self>>(Self::ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for SignatureHelp {
|
||||||
|
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
|
||||||
|
let margin = Margin::horizontal(1);
|
||||||
|
|
||||||
|
let active_param_span = self.active_param_range.map(|(start, end)| {
|
||||||
|
vec![(
|
||||||
|
cx.editor.theme.find_scope_index("ui.selection").unwrap(),
|
||||||
|
start..end,
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
|
||||||
|
let sig_text = crate::ui::markdown::highlighted_code_block(
|
||||||
|
self.signature.clone(),
|
||||||
|
&self.language,
|
||||||
|
Some(&cx.editor.theme),
|
||||||
|
Arc::clone(&self.config_loader),
|
||||||
|
active_param_span,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
|
||||||
|
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
|
||||||
|
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
|
||||||
|
sig_text_para.render(sig_text_area.inner(&margin), surface);
|
||||||
|
|
||||||
|
if self.signature_doc.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sep_style = Style::default();
|
||||||
|
let borders = BorderType::line_symbols(BorderType::Plain);
|
||||||
|
for x in sig_text_area.left()..sig_text_area.right() {
|
||||||
|
if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) {
|
||||||
|
cell.set_symbol(borders.horizontal).set_style(sep_style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sig_doc = match &self.signature_doc {
|
||||||
|
None => return,
|
||||||
|
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
|
||||||
|
};
|
||||||
|
let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
|
||||||
|
let sig_doc_area = area.clip_top(sig_text_area.height + 2);
|
||||||
|
let sig_doc_para = Paragraph::new(sig_doc)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
|
||||||
|
sig_doc_para.render(sig_doc_area.inner(&margin), surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||||
|
const PADDING: u16 = 2;
|
||||||
|
const SEPARATOR_HEIGHT: u16 = 1;
|
||||||
|
|
||||||
|
if PADDING >= viewport.1 || PADDING >= viewport.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let max_text_width = (viewport.0 - PADDING).min(120);
|
||||||
|
|
||||||
|
let signature_text = crate::ui::markdown::highlighted_code_block(
|
||||||
|
self.signature.clone(),
|
||||||
|
&self.language,
|
||||||
|
None,
|
||||||
|
Arc::clone(&self.config_loader),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let (sig_width, sig_height) =
|
||||||
|
crate::ui::text::required_size(&signature_text, max_text_width);
|
||||||
|
|
||||||
|
let (width, height) = match self.signature_doc {
|
||||||
|
Some(ref doc) => {
|
||||||
|
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
|
||||||
|
let doc_text = doc_md.parse(None);
|
||||||
|
let (doc_width, doc_height) =
|
||||||
|
crate::ui::text::required_size(&doc_text, max_text_width);
|
||||||
|
(
|
||||||
|
sig_width.max(doc_width),
|
||||||
|
sig_height + SEPARATOR_HEIGHT + doc_height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => (sig_width, sig_height),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((width + PADDING, height + PADDING))
|
||||||
|
}
|
||||||
|
}
|
|
@ -144,7 +144,7 @@ impl Markdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
|
pub fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
|
||||||
fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
|
fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
|
||||||
let spans = std::mem::take(spans);
|
let spans = std::mem::take(spans);
|
||||||
if !spans.is_empty() {
|
if !spans.is_empty() {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
mod completion;
|
mod completion;
|
||||||
pub(crate) mod editor;
|
pub(crate) mod editor;
|
||||||
mod info;
|
mod info;
|
||||||
|
pub mod lsp;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod overlay;
|
pub mod overlay;
|
||||||
mod picker;
|
mod picker;
|
||||||
mod popup;
|
pub mod popup;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
mod spinner;
|
mod spinner;
|
||||||
mod statusline;
|
mod statusline;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
commands::Open,
|
||||||
compositor::{Callback, Component, Context, EventResult},
|
compositor::{Callback, Component, Context, EventResult},
|
||||||
ctrl, key,
|
ctrl, key,
|
||||||
};
|
};
|
||||||
|
@ -17,8 +18,10 @@ pub struct Popup<T: Component> {
|
||||||
margin: Margin,
|
margin: Margin,
|
||||||
size: (u16, u16),
|
size: (u16, u16),
|
||||||
child_size: (u16, u16),
|
child_size: (u16, u16),
|
||||||
|
position_bias: Open,
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
auto_close: bool,
|
auto_close: bool,
|
||||||
|
ignore_escape_key: bool,
|
||||||
id: &'static str,
|
id: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,15 +32,27 @@ impl<T: Component> Popup<T> {
|
||||||
position: None,
|
position: None,
|
||||||
margin: Margin::none(),
|
margin: Margin::none(),
|
||||||
size: (0, 0),
|
size: (0, 0),
|
||||||
|
position_bias: Open::Below,
|
||||||
child_size: (0, 0),
|
child_size: (0, 0),
|
||||||
scroll: 0,
|
scroll: 0,
|
||||||
auto_close: false,
|
auto_close: false,
|
||||||
|
ignore_escape_key: false,
|
||||||
id,
|
id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_position(&mut self, pos: Option<Position>) {
|
pub fn position(mut self, pos: Option<Position>) -> Self {
|
||||||
self.position = pos;
|
self.position = pos;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_position(&self) -> Option<Position> {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn position_bias(mut self, bias: Open) -> Self {
|
||||||
|
self.position_bias = bias;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn margin(mut self, margin: Margin) -> Self {
|
pub fn margin(mut self, margin: Margin) -> Self {
|
||||||
|
@ -50,6 +65,18 @@ impl<T: Component> Popup<T> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ignores an escape keypress event, letting the outer layer
|
||||||
|
/// (usually the editor) handle it. This is useful for popups
|
||||||
|
/// in insert mode like completion and signature help where
|
||||||
|
/// the popup is closed on the mode change from insert to normal
|
||||||
|
/// which is done with the escape key. Otherwise the popup consumes
|
||||||
|
/// the escape key event and closes it, and an additional escape
|
||||||
|
/// would be required to exit insert mode.
|
||||||
|
pub fn ignore_escape_key(mut self, ignore: bool) -> Self {
|
||||||
|
self.ignore_escape_key = ignore;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
|
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
|
||||||
let position = self
|
let position = self
|
||||||
.position
|
.position
|
||||||
|
@ -68,13 +95,23 @@ impl<T: Component> Popup<T> {
|
||||||
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
|
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: be able to specify orientation preference. We want above for most popups, below
|
let can_put_below = viewport.height > rel_y + height;
|
||||||
// for menus/autocomplete.
|
let can_put_above = rel_y.checked_sub(height).is_some();
|
||||||
if viewport.height > rel_y + height {
|
let final_pos = match self.position_bias {
|
||||||
rel_y += 1 // position below point
|
Open::Below => match can_put_below {
|
||||||
} else {
|
true => Open::Below,
|
||||||
rel_y = rel_y.saturating_sub(height) // position above point
|
false => Open::Above,
|
||||||
}
|
},
|
||||||
|
Open::Above => match can_put_above {
|
||||||
|
true => Open::Above,
|
||||||
|
false => Open::Below,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
rel_y = match final_pos {
|
||||||
|
Open::Above => rel_y.saturating_sub(height),
|
||||||
|
Open::Below => rel_y + 1,
|
||||||
|
};
|
||||||
|
|
||||||
(rel_x, rel_y)
|
(rel_x, rel_y)
|
||||||
}
|
}
|
||||||
|
@ -112,9 +149,13 @@ impl<T: Component> Component for Popup<T> {
|
||||||
_ => return EventResult::Ignored(None),
|
_ => return EventResult::Ignored(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if key!(Esc) == key.into() && self.ignore_escape_key {
|
||||||
|
return EventResult::Ignored(None);
|
||||||
|
}
|
||||||
|
|
||||||
let close_fn: Callback = Box::new(|compositor, _| {
|
let close_fn: Callback = Box::new(|compositor, _| {
|
||||||
// remove the layer
|
// remove the layer
|
||||||
compositor.pop();
|
compositor.remove(self.id.as_ref());
|
||||||
});
|
});
|
||||||
|
|
||||||
match key.into() {
|
match key.into() {
|
||||||
|
|
|
@ -167,10 +167,25 @@ pub struct Config {
|
||||||
pub color_modes: bool,
|
pub color_modes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
|
||||||
pub struct LspConfig {
|
pub struct LspConfig {
|
||||||
|
/// Display LSP progress messages below statusline
|
||||||
pub display_messages: bool,
|
pub display_messages: bool,
|
||||||
|
/// Enable automatic pop up of signature help (parameter hints)
|
||||||
|
pub auto_signature_help: bool,
|
||||||
|
/// Display docs under signature help popup
|
||||||
|
pub display_signature_help_docs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LspConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
display_messages: false,
|
||||||
|
auto_signature_help: true,
|
||||||
|
display_signature_help_docs: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
Loading…
Add table
Reference in a new issue