mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 01:16:18 +04:00
refactor completion and signature help using hooks
This commit is contained in:
parent
13ed4f6c47
commit
8e592a151f
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1165,6 +1165,7 @@ version = "23.10.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dunce",
|
"dunce",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
|
"ropey",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -51,7 +51,8 @@ ### `[editor]` Section
|
|||||||
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
|
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
|
||||||
| `auto-format` | Enable automatic formatting on save | `true` |
|
| `auto-format` | Enable automatic formatting on save | `true` |
|
||||||
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
|
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
|
||||||
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
|
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` |
|
||||||
|
| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
|
||||||
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
|
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
|
||||||
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
|
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
|
||||||
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
|
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
use helix_stdx::path;
|
use helix_stdx::path;
|
||||||
use lsp::{
|
use lsp::{
|
||||||
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
|
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
|
||||||
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
|
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, WorkspaceFolder,
|
||||||
WorkspaceFoldersChangeEvent,
|
WorkspaceFoldersChangeEvent,
|
||||||
};
|
};
|
||||||
use lsp_types as lsp;
|
use lsp_types as lsp;
|
||||||
@ -999,6 +999,7 @@ pub fn completion(
|
|||||||
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>,
|
||||||
|
context: lsp::CompletionContext,
|
||||||
) -> Option<impl Future<Output = Result<Value>>> {
|
) -> Option<impl Future<Output = Result<Value>>> {
|
||||||
let capabilities = self.capabilities.get().unwrap();
|
let capabilities = self.capabilities.get().unwrap();
|
||||||
|
|
||||||
@ -1010,13 +1011,12 @@ pub fn completion(
|
|||||||
text_document,
|
text_document,
|
||||||
position,
|
position,
|
||||||
},
|
},
|
||||||
|
context: Some(context),
|
||||||
// TODO: support these tokens by async receiving and updating the choice list
|
// TODO: support these tokens by async receiving and updating the choice list
|
||||||
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
|
||||||
partial_result_params: lsp::PartialResultParams {
|
partial_result_params: lsp::PartialResultParams {
|
||||||
partial_result_token: None,
|
partial_result_token: None,
|
||||||
},
|
},
|
||||||
context: None,
|
|
||||||
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(self.call::<lsp::request::Completion>(params))
|
Some(self.call::<lsp::request::Completion>(params))
|
||||||
@ -1063,7 +1063,7 @@ pub fn text_document_signature_help(
|
|||||||
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>,
|
||||||
) -> Option<impl Future<Output = Result<Value>>> {
|
) -> Option<impl Future<Output = Result<Option<SignatureHelp>>>> {
|
||||||
let capabilities = self.capabilities.get().unwrap();
|
let capabilities = self.capabilities.get().unwrap();
|
||||||
|
|
||||||
// Return early if the server does not support signature help.
|
// Return early if the server does not support signature help.
|
||||||
@ -1079,7 +1079,8 @@ pub fn text_document_signature_help(
|
|||||||
// lsp::SignatureHelpContext
|
// lsp::SignatureHelpContext
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
|
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
|
||||||
|
Some(async move { Ok(serde_json::from_value(res.await?)?) })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_document_range_inlay_hints(
|
pub fn text_document_range_inlay_hints(
|
||||||
|
@ -14,6 +14,7 @@ homepage.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dunce = "1.0"
|
dunce = "1.0"
|
||||||
etcetera = "0.8"
|
etcetera = "0.8"
|
||||||
|
ropey = { version = "1.6.1", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.9"
|
tempfile = "3.9"
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
|
pub mod rope;
|
||||||
|
26
helix-stdx/src/rope.rs
Normal file
26
helix-stdx/src/rope.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
|
pub trait RopeSliceExt: Sized {
|
||||||
|
fn ends_with(self, text: &str) -> bool;
|
||||||
|
fn starts_with(self, text: &str) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RopeSliceExt for RopeSlice<'_> {
|
||||||
|
fn ends_with(self, text: &str) -> bool {
|
||||||
|
let len = self.len_bytes();
|
||||||
|
if len < text.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.get_byte_slice(len - text.len()..)
|
||||||
|
.map_or(false, |end| end == text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn starts_with(self, text: &str) -> bool {
|
||||||
|
let len = self.len_bytes();
|
||||||
|
if len < text.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.get_byte_slice(..len - text.len())
|
||||||
|
.map_or(false, |start| start == text)
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,6 @@
|
|||||||
use arc_swap::{access::Map, ArcSwap};
|
use arc_swap::{access::Map, ArcSwap};
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use helix_core::{
|
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
|
||||||
chars::char_is_word,
|
|
||||||
diagnostic::{DiagnosticTag, NumberOrString},
|
|
||||||
pos_at_coords, syntax, Selection,
|
|
||||||
};
|
|
||||||
use helix_lsp::{
|
use helix_lsp::{
|
||||||
lsp::{self, notification::Notification},
|
lsp::{self, notification::Notification},
|
||||||
util::lsp_range_to_range,
|
util::lsp_range_to_range,
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
pub use dap::*;
|
pub use dap::*;
|
||||||
use helix_vcs::Hunk;
|
use helix_vcs::Hunk;
|
||||||
pub use lsp::*;
|
pub use lsp::*;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tui::widgets::Row;
|
use tui::widgets::Row;
|
||||||
pub use typed::*;
|
pub use typed::*;
|
||||||
|
|
||||||
@ -33,7 +32,7 @@
|
|||||||
};
|
};
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
|
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
|
||||||
editor::{Action, CompleteAction},
|
editor::Action,
|
||||||
info::Info,
|
info::Info,
|
||||||
input::KeyEvent,
|
input::KeyEvent,
|
||||||
keyboard::KeyCode,
|
keyboard::KeyCode,
|
||||||
@ -52,14 +51,10 @@
|
|||||||
filter_picker_entry,
|
filter_picker_entry,
|
||||||
job::Callback,
|
job::Callback,
|
||||||
keymap::ReverseKeymap,
|
keymap::ReverseKeymap,
|
||||||
ui::{
|
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
||||||
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
|
|
||||||
Popup, Prompt, PromptEvent,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::job::{self, Jobs};
|
use crate::job::{self, Jobs};
|
||||||
use futures_util::{stream::FuturesUnordered, TryStreamExt};
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fmt,
|
fmt,
|
||||||
@ -2593,7 +2588,6 @@ fn delete_by_selection_insert_mode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_selection(cx: &mut Context) {
|
fn delete_selection(cx: &mut Context) {
|
||||||
@ -2667,10 +2661,6 @@ fn insert_mode(cx: &mut Context) {
|
|||||||
.transform(|range| Range::new(range.to(), range.from()));
|
.transform(|range| Range::new(range.to(), range.from()));
|
||||||
|
|
||||||
doc.set_selection(view.id, selection);
|
doc.set_selection(view.id, selection);
|
||||||
|
|
||||||
// [TODO] temporary workaround until we're not using the idle timer to
|
|
||||||
// trigger auto completions any more
|
|
||||||
cx.editor.clear_idle_timer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// inserts at the end of each selection
|
// inserts at the end of each selection
|
||||||
@ -3497,9 +3487,9 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
|
|||||||
|
|
||||||
pub mod insert {
|
pub mod insert {
|
||||||
use crate::events::PostInsertChar;
|
use crate::events::PostInsertChar;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
|
||||||
pub type PostHook = fn(&mut Context, char);
|
|
||||||
|
|
||||||
/// Exclude the cursor in range.
|
/// Exclude the cursor in range.
|
||||||
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
|
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
|
||||||
@ -3513,88 +3503,6 @@ fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// It trigger completion when idle timer reaches deadline
|
|
||||||
// Only trigger completion if the word under cursor is longer than n characters
|
|
||||||
pub fn idle_completion(cx: &mut Context) {
|
|
||||||
let config = cx.editor.config();
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
let text = doc.text().slice(..);
|
|
||||||
let cursor = doc.selection(view.id).primary().cursor(text);
|
|
||||||
|
|
||||||
use helix_core::chars::char_is_word;
|
|
||||||
let mut iter = text.chars_at(cursor);
|
|
||||||
iter.reverse();
|
|
||||||
for _ in 0..config.completion_trigger_len {
|
|
||||||
match iter.next() {
|
|
||||||
Some(c) if char_is_word(c) => {}
|
|
||||||
_ => return,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super::completion(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn language_server_completion(cx: &mut Context, ch: char) {
|
|
||||||
let config = cx.editor.config();
|
|
||||||
if !config.auto_completion {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
use helix_lsp::lsp;
|
|
||||||
// if ch matches completion char, trigger completion
|
|
||||||
let doc = doc_mut!(cx.editor);
|
|
||||||
let trigger_completion = doc
|
|
||||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
|
||||||
.any(|ls| {
|
|
||||||
// TODO: what if trigger is multiple chars long
|
|
||||||
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
|
|
||||||
trigger_characters: Some(triggers),
|
|
||||||
..
|
|
||||||
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
|
|
||||||
});
|
|
||||||
|
|
||||||
if trigger_completion {
|
|
||||||
cx.editor.clear_idle_timer();
|
|
||||||
super::completion(cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signature_help(cx: &mut Context, ch: char) {
|
|
||||||
use helix_lsp::lsp;
|
|
||||||
// if ch matches signature_help char, trigger
|
|
||||||
let doc = doc_mut!(cx.editor);
|
|
||||||
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
|
|
||||||
let Some(language_server) = doc
|
|
||||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
|
||||||
.next()
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let capabilities = language_server.capabilities();
|
|
||||||
|
|
||||||
if let lsp::ServerCapabilities {
|
|
||||||
signature_help_provider:
|
|
||||||
Some(lsp::SignatureHelpOptions {
|
|
||||||
trigger_characters: Some(triggers),
|
|
||||||
// TODO: retrigger_characters
|
|
||||||
..
|
|
||||||
}),
|
|
||||||
..
|
|
||||||
} = capabilities
|
|
||||||
{
|
|
||||||
// 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 || close_triggers.contains(&ch) {
|
|
||||||
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The default insert hook: simply insert the character
|
// The default insert hook: simply insert the character
|
||||||
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
|
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
|
||||||
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
|
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
|
||||||
@ -3624,12 +3532,6 @@ pub fn insert_char(cx: &mut Context, c: char) {
|
|||||||
doc.apply(&t, view.id);
|
doc.apply(&t, view.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
|
|
||||||
// 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] {
|
|
||||||
hook(cx, c);
|
|
||||||
}
|
|
||||||
helix_event::dispatch(PostInsertChar { c, cx });
|
helix_event::dispatch(PostInsertChar { c, cx });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3855,8 +3757,6 @@ pub fn delete_char_backward(cx: &mut Context) {
|
|||||||
});
|
});
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
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) {
|
||||||
@ -4510,151 +4410,14 @@ fn remove_primary_selection(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn completion(cx: &mut Context) {
|
pub fn completion(cx: &mut Context) {
|
||||||
use helix_lsp::{lsp, util::pos_to_lsp_pos};
|
|
||||||
|
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
let range = doc.selection(view.id).primary();
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
let cursor = range.cursor(text);
|
||||||
|
|
||||||
let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion
|
cx.editor
|
||||||
{
|
.handlers
|
||||||
savepoint.clone()
|
.trigger_completions(cursor, doc.id(), view.id);
|
||||||
} else {
|
|
||||||
doc.savepoint(view)
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = savepoint.text.clone();
|
|
||||||
let cursor = savepoint.cursor();
|
|
||||||
|
|
||||||
let mut seen_language_servers = HashSet::new();
|
|
||||||
|
|
||||||
let mut futures: FuturesUnordered<_> = doc
|
|
||||||
.language_servers_with_feature(LanguageServerFeature::Completion)
|
|
||||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
|
||||||
.map(|language_server| {
|
|
||||||
let language_server_id = language_server.id();
|
|
||||||
let offset_encoding = language_server.offset_encoding();
|
|
||||||
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
|
|
||||||
let doc_id = doc.identifier();
|
|
||||||
let completion_request = language_server.completion(doc_id, pos, None).unwrap();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let json = completion_request.await?;
|
|
||||||
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
|
||||||
|
|
||||||
let items = match response {
|
|
||||||
Some(lsp::CompletionResponse::Array(items)) => items,
|
|
||||||
// TODO: do something with is_incomplete
|
|
||||||
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
|
||||||
is_incomplete: _is_incomplete,
|
|
||||||
items,
|
|
||||||
})) => items,
|
|
||||||
None => Vec::new(),
|
|
||||||
}
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| CompletionItem {
|
|
||||||
item,
|
|
||||||
language_server_id,
|
|
||||||
resolved: false,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
anyhow::Ok(items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// setup a channel that allows the request to be canceled
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
// set completion_request so that this request can be canceled
|
|
||||||
// by setting completion_request, the old channel stored there is dropped
|
|
||||||
// and the associated request is automatically dropped
|
|
||||||
cx.editor.completion_request_handle = Some(tx);
|
|
||||||
let future = async move {
|
|
||||||
let items_future = async move {
|
|
||||||
let mut items = Vec::new();
|
|
||||||
// TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
|
|
||||||
while let Some(mut lsp_items) = futures.try_next().await? {
|
|
||||||
items.append(&mut lsp_items);
|
|
||||||
}
|
|
||||||
anyhow::Ok(items)
|
|
||||||
};
|
|
||||||
tokio::select! {
|
|
||||||
biased;
|
|
||||||
_ = rx => {
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
res = items_future => {
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let trigger_offset = cursor;
|
|
||||||
|
|
||||||
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
|
|
||||||
// completion filtering. For example logger.te| should filter the initial suggestion list with "te".
|
|
||||||
|
|
||||||
use helix_core::chars;
|
|
||||||
let mut iter = text.chars_at(cursor);
|
|
||||||
iter.reverse();
|
|
||||||
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
|
|
||||||
let start_offset = cursor.saturating_sub(offset);
|
|
||||||
|
|
||||||
let trigger_doc = doc.id();
|
|
||||||
let trigger_view = view.id;
|
|
||||||
|
|
||||||
// FIXME: The commands Context can only have a single callback
|
|
||||||
// which means it gets overwritten when executing keybindings
|
|
||||||
// with multiple commands or macros. This would mean that completion
|
|
||||||
// might be incorrectly applied when repeating the insertmode action
|
|
||||||
//
|
|
||||||
// TODO: to solve this either make cx.callback a Vec of callbacks or
|
|
||||||
// alternatively move `last_insert` to `helix_view::Editor`
|
|
||||||
cx.callback = Some(Box::new(
|
|
||||||
move |compositor: &mut Compositor, _cx: &mut compositor::Context| {
|
|
||||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
|
||||||
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
cx.jobs.callback(async move {
|
|
||||||
let items = future.await?;
|
|
||||||
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
|
|
||||||
let (view, doc) = current_ref!(editor);
|
|
||||||
// check if the completion request is stale.
|
|
||||||
//
|
|
||||||
// Completions are completed asynchronously and therefore the user could
|
|
||||||
//switch document/view or leave insert mode. In all of thoise cases the
|
|
||||||
// completion should be discarded
|
|
||||||
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if items.is_empty() {
|
|
||||||
// editor.set_error("No completion available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let size = compositor.size();
|
|
||||||
let ui = compositor.find::<ui::EditorView>().unwrap();
|
|
||||||
let completion_area = ui.set_completion(
|
|
||||||
editor,
|
|
||||||
savepoint,
|
|
||||||
items,
|
|
||||||
start_offset,
|
|
||||||
trigger_offset,
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
let size = compositor.size();
|
|
||||||
let signature_help_area = compositor
|
|
||||||
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
|
||||||
.map(|signature_help| signature_help.area(size, editor));
|
|
||||||
// Delete the signature help popup if they intersect.
|
|
||||||
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b))
|
|
||||||
{
|
|
||||||
compositor.remove(SignatureHelp::ID);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// comments
|
// comments
|
||||||
@ -4833,10 +4596,6 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
doc.set_selection(view.id, selection);
|
doc.set_selection(view.id, selection);
|
||||||
|
|
||||||
// [TODO] temporary workaround until we're not using the idle timer to
|
|
||||||
// trigger auto completions any more
|
|
||||||
editor.clear_idle_timer();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
|
use futures_util::{stream::FuturesUnordered, FutureExt};
|
||||||
use helix_lsp::{
|
use helix_lsp::{
|
||||||
block_on,
|
block_on,
|
||||||
lsp::{
|
lsp::{
|
||||||
@ -8,21 +8,21 @@
|
|||||||
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
|
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
|
||||||
Client, OffsetEncoding,
|
Client, OffsetEncoding,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tui::{
|
use tui::{
|
||||||
text::{Span, Spans},
|
text::{Span, Spans},
|
||||||
widgets::Row,
|
widgets::Row,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{align_view, push_jump, Align, Context, Editor, Open};
|
use super::{align_view, push_jump, Align, Context, Editor};
|
||||||
|
|
||||||
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
|
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
|
||||||
use helix_stdx::path;
|
use helix_stdx::path;
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
|
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||||
editor::Action,
|
editor::Action,
|
||||||
graphics::Margin,
|
graphics::Margin,
|
||||||
|
handlers::lsp::SignatureHelpInvoked,
|
||||||
theme::Style,
|
theme::Style,
|
||||||
Document, View,
|
Document, View,
|
||||||
};
|
};
|
||||||
@ -30,10 +30,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
compositor::{self, Compositor},
|
compositor::{self, Compositor},
|
||||||
job::Callback,
|
job::Callback,
|
||||||
ui::{
|
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
|
||||||
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
|
|
||||||
PromptEvent,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
@ -42,7 +39,6 @@
|
|||||||
fmt::Write,
|
fmt::Write,
|
||||||
future::Future,
|
future::Future,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gets the first language server that is attached to a document which supports a specific feature.
|
/// Gets the first language server that is attached to a document which supports a specific feature.
|
||||||
@ -1132,146 +1128,10 @@ pub fn goto_reference(cx: &mut Context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
|
||||||
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)
|
cx.editor
|
||||||
}
|
.handlers
|
||||||
|
.trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor)
|
||||||
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
|
|
||||||
let (view, doc) = current!(cx.editor);
|
|
||||||
|
|
||||||
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
|
|
||||||
let future = doc
|
|
||||||
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
|
||||||
.find_map(|language_server| {
|
|
||||||
let pos = doc.position(view.id, language_server.offset_encoding());
|
|
||||||
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
|
||||||
});
|
|
||||||
|
|
||||||
let Some(future) = future else {
|
|
||||||
// Do not show the message if signature help was invoked
|
|
||||||
// automatically on backspace, trigger characters, etc.
|
|
||||||
if invoked == SignatureHelpInvoked::Manual {
|
|
||||||
cx.editor
|
|
||||||
.set_error("No configured language server supports signature-help");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
signature_help_impl_with_future(cx, future.boxed(), invoked);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn signature_help_impl_with_future(
|
|
||||||
cx: &mut Context,
|
|
||||||
future: BoxFuture<'static, helix_lsp::Result<Value>>,
|
|
||||||
invoked: SignatureHelpInvoked,
|
|
||||||
) {
|
|
||||||
cx.callback(
|
|
||||||
future,
|
|
||||||
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
|
|
||||||
let config = &editor.config();
|
|
||||||
|
|
||||||
if !(config.lsp.auto_signature_help
|
|
||||||
|| SignatureHelp::visible_popup(compositor).is_some()
|
|
||||||
|| invoked == SignatureHelpInvoked::Manual)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
|
|
||||||
// it very probably means the server was a little slow to respond and the user has
|
|
||||||
// already moved on to something else, making a signature help popup will just be an
|
|
||||||
// annoyance, see https://github.com/helix-editor/helix/issues/3112
|
|
||||||
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
|
|
||||||
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_name().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]) => {
|
|
||||||
// LS sends offsets based on utf-16 based string representation
|
|
||||||
// but highlighting in helix is done using byte offset.
|
|
||||||
use helix_core::str_utils::char_to_byte_idx;
|
|
||||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
|
||||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
|
||||||
Some((from, to))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
contents.set_active_param_range(active_param_range());
|
|
||||||
|
|
||||||
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
|
||||||
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
|
||||||
.position(old_popup.and_then(|p| p.get_position()))
|
|
||||||
.position_bias(Open::Above)
|
|
||||||
.ignore_escape_key(true);
|
|
||||||
|
|
||||||
// Don't create a popup if it intersects the auto-complete menu.
|
|
||||||
let size = compositor.size();
|
|
||||||
if compositor
|
|
||||||
.find::<ui::EditorView>()
|
|
||||||
.unwrap()
|
|
||||||
.completion
|
|
||||||
.as_mut()
|
|
||||||
.map(|completion| completion.area(size, editor))
|
|
||||||
.filter(|area| area.intersects(popup.area(size, editor)))
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
compositor.replace_or_push(SignatureHelp::ID, popup);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hover(cx: &mut Context) {
|
pub fn hover(cx: &mut Context) {
|
||||||
|
@ -1,15 +1,30 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
|
use helix_event::AsyncHook;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::events;
|
use crate::events;
|
||||||
|
use crate::handlers::completion::CompletionHandler;
|
||||||
|
use crate::handlers::signature_help::SignatureHelpHandler;
|
||||||
|
|
||||||
|
pub use completion::trigger_auto_completion;
|
||||||
|
pub use helix_view::handlers::lsp::SignatureHelpInvoked;
|
||||||
|
pub use helix_view::handlers::Handlers;
|
||||||
|
|
||||||
|
mod completion;
|
||||||
|
mod signature_help;
|
||||||
|
|
||||||
}
|
|
||||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||||
events::register();
|
events::register();
|
||||||
|
|
||||||
|
let completions = CompletionHandler::new(config).spawn();
|
||||||
|
let signature_hints = SignatureHelpHandler::new().spawn();
|
||||||
let handlers = Handlers {
|
let handlers = Handlers {
|
||||||
|
completions,
|
||||||
|
signature_hints,
|
||||||
};
|
};
|
||||||
|
completion::register_hooks(&handlers);
|
||||||
|
signature_help::register_hooks(&handlers);
|
||||||
handlers
|
handlers
|
||||||
}
|
}
|
||||||
|
465
helix-term/src/handlers/completion.rs
Normal file
465
helix-term/src/handlers/completion.rs
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
use futures_util::stream::FuturesUnordered;
|
||||||
|
use helix_core::chars::char_is_word;
|
||||||
|
use helix_core::syntax::LanguageServerFeature;
|
||||||
|
use helix_event::{
|
||||||
|
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||||
|
};
|
||||||
|
use helix_lsp::lsp;
|
||||||
|
use helix_lsp::util::pos_to_lsp_pos;
|
||||||
|
use helix_stdx::rope::RopeSliceExt;
|
||||||
|
use helix_view::document::{Mode, SavePoint};
|
||||||
|
use helix_view::handlers::lsp::CompletionEvent;
|
||||||
|
use helix_view::{DocumentId, Editor, ViewId};
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
use crate::commands;
|
||||||
|
use crate::compositor::Compositor;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
|
||||||
|
use crate::job::{dispatch, dispatch_blocking};
|
||||||
|
use crate::keymap::MappableCommand;
|
||||||
|
use crate::ui::editor::InsertEvent;
|
||||||
|
use crate::ui::lsp::SignatureHelp;
|
||||||
|
use crate::ui::{self, CompletionItem, Popup};
|
||||||
|
|
||||||
|
use super::Handlers;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum TriggerKind {
|
||||||
|
Auto,
|
||||||
|
TriggerChar,
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Trigger {
|
||||||
|
pos: usize,
|
||||||
|
view: ViewId,
|
||||||
|
doc: DocumentId,
|
||||||
|
kind: TriggerKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct CompletionHandler {
|
||||||
|
/// currently active trigger which will cause a
|
||||||
|
/// completion request after the timeout
|
||||||
|
trigger: Option<Trigger>,
|
||||||
|
/// A handle for currently active completion request.
|
||||||
|
/// This can be used to determine whether the current
|
||||||
|
/// request is still active (and new triggers should be
|
||||||
|
/// ignored) and can also be used to abort the current
|
||||||
|
/// request (by dropping the handle)
|
||||||
|
request: Option<CancelTx>,
|
||||||
|
config: Arc<ArcSwap<Config>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionHandler {
|
||||||
|
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
request: None,
|
||||||
|
trigger: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl helix_event::AsyncHook for CompletionHandler {
|
||||||
|
type Event = CompletionEvent;
|
||||||
|
|
||||||
|
fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
event: Self::Event,
|
||||||
|
_old_timeout: Option<Instant>,
|
||||||
|
) -> Option<Instant> {
|
||||||
|
match event {
|
||||||
|
CompletionEvent::AutoTrigger {
|
||||||
|
cursor: trigger_pos,
|
||||||
|
doc,
|
||||||
|
view,
|
||||||
|
} => {
|
||||||
|
// techically it shouldn't be possible to switch views/documents in insert mode
|
||||||
|
// but people may create weird keymaps/use the mouse so lets be extra careful
|
||||||
|
if self
|
||||||
|
.trigger
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
|
||||||
|
{
|
||||||
|
self.trigger = Some(Trigger {
|
||||||
|
pos: trigger_pos,
|
||||||
|
view,
|
||||||
|
doc,
|
||||||
|
kind: TriggerKind::Auto,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CompletionEvent::TriggerChar { cursor, doc, view } => {
|
||||||
|
// immediately request completions and drop all auto completion requests
|
||||||
|
self.request = None;
|
||||||
|
self.trigger = Some(Trigger {
|
||||||
|
pos: cursor,
|
||||||
|
view,
|
||||||
|
doc,
|
||||||
|
kind: TriggerKind::TriggerChar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CompletionEvent::ManualTrigger { cursor, doc, view } => {
|
||||||
|
// immediately request completions and drop all auto completion requests
|
||||||
|
self.request = None;
|
||||||
|
self.trigger = Some(Trigger {
|
||||||
|
pos: cursor,
|
||||||
|
view,
|
||||||
|
doc,
|
||||||
|
kind: TriggerKind::Manual,
|
||||||
|
});
|
||||||
|
// stop debouncing immediately and request the completion
|
||||||
|
self.finish_debounce();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
CompletionEvent::Cancel => {
|
||||||
|
self.trigger = None;
|
||||||
|
self.request = None;
|
||||||
|
}
|
||||||
|
CompletionEvent::DeleteText { cursor } => {
|
||||||
|
// if we deleted the original trigger, abort the completion
|
||||||
|
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) {
|
||||||
|
self.trigger = None;
|
||||||
|
self.request = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.trigger.map(|trigger| {
|
||||||
|
// if the current request was closed forget about it
|
||||||
|
// otherwise immediately restart the completion request
|
||||||
|
let cancel = self.request.take().map_or(false, |req| !req.is_closed());
|
||||||
|
let timeout = if trigger.kind == TriggerKind::Auto && !cancel {
|
||||||
|
self.config.load().editor.completion_timeout
|
||||||
|
} else {
|
||||||
|
// we want almost instant completions for trigger chars
|
||||||
|
// and restarting completion requests. The small timeout here mainly
|
||||||
|
// serves to better handle cases where the completion handler
|
||||||
|
// may fall behind (so multiple events in the channel) and macros
|
||||||
|
Duration::from_millis(5)
|
||||||
|
};
|
||||||
|
Instant::now() + timeout
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_debounce(&mut self) {
|
||||||
|
let trigger = self.trigger.take().expect("debounce always has a trigger");
|
||||||
|
let (tx, rx) = cancelation();
|
||||||
|
self.request = Some(tx);
|
||||||
|
dispatch_blocking(move |editor, compositor| {
|
||||||
|
request_completion(trigger, rx, editor, compositor)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_completion(
|
||||||
|
mut trigger: Trigger,
|
||||||
|
cancel: CancelRx,
|
||||||
|
editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
if compositor
|
||||||
|
.find::<ui::EditorView>()
|
||||||
|
.unwrap()
|
||||||
|
.completion
|
||||||
|
.is_some()
|
||||||
|
|| editor.mode != Mode::Insert
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = doc.text();
|
||||||
|
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
|
||||||
|
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// this looks odd... Why are we not using the trigger position from
|
||||||
|
// the `trigger` here? Won't that mean that the trigger char doesn't get
|
||||||
|
// send to the LS if we type fast enougn? Yes that is true but it's
|
||||||
|
// not actually a problem. The LSP will resolve the completion to the identifier
|
||||||
|
// anyway (in fact sending the later position is necessary to get the right results
|
||||||
|
// from LSPs that provide incomplete completion list). We rely on trigger offset
|
||||||
|
// and primary cursor matching for multi-cursor completions so this is definitely
|
||||||
|
// necessary from our side too.
|
||||||
|
trigger.pos = cursor;
|
||||||
|
let trigger_text = text.slice(..cursor);
|
||||||
|
|
||||||
|
let mut seen_language_servers = HashSet::new();
|
||||||
|
let mut futures: FuturesUnordered<_> = doc
|
||||||
|
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||||
|
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||||
|
.map(|ls| {
|
||||||
|
let language_server_id = ls.id();
|
||||||
|
let offset_encoding = ls.offset_encoding();
|
||||||
|
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
|
||||||
|
let doc_id = doc.identifier();
|
||||||
|
let context = if trigger.kind == TriggerKind::Manual {
|
||||||
|
lsp::CompletionContext {
|
||||||
|
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
|
||||||
|
trigger_character: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let trigger_char =
|
||||||
|
ls.capabilities()
|
||||||
|
.completion_provider
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|provider| {
|
||||||
|
provider
|
||||||
|
.trigger_characters
|
||||||
|
.as_deref()?
|
||||||
|
.iter()
|
||||||
|
.find(|&trigger| trigger_text.ends_with(trigger))
|
||||||
|
});
|
||||||
|
lsp::CompletionContext {
|
||||||
|
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||||
|
trigger_character: trigger_char.cloned(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
|
||||||
|
async move {
|
||||||
|
let json = completion_response.await?;
|
||||||
|
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
|
||||||
|
let items = match response {
|
||||||
|
Some(lsp::CompletionResponse::Array(items)) => items,
|
||||||
|
// TODO: do something with is_incomplete
|
||||||
|
Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||||
|
is_incomplete: _is_incomplete,
|
||||||
|
items,
|
||||||
|
})) => items,
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| CompletionItem {
|
||||||
|
item,
|
||||||
|
language_server_id,
|
||||||
|
resolved: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
anyhow::Ok(items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let future = async move {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
while let Some(lsp_items) = futures.next().await {
|
||||||
|
match lsp_items {
|
||||||
|
Ok(mut lsp_items) => items.append(&mut lsp_items),
|
||||||
|
Err(err) => {
|
||||||
|
log::debug!("completion request failed: {err:?}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
items
|
||||||
|
};
|
||||||
|
|
||||||
|
let savepoint = doc.savepoint(view);
|
||||||
|
|
||||||
|
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||||
|
ui.last_insert.1.push(InsertEvent::RequestCompletion);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let items = cancelable_future(future, cancel).await.unwrap_or_default();
|
||||||
|
if items.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(move |editor, compositor| {
|
||||||
|
show_completion(editor, compositor, items, trigger, savepoint)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_completion(
|
||||||
|
editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
items: Vec<CompletionItem>,
|
||||||
|
trigger: Trigger,
|
||||||
|
savepoint: Arc<SavePoint>,
|
||||||
|
) {
|
||||||
|
let (view, doc) = current_ref!(editor);
|
||||||
|
// check if the completion request is stale.
|
||||||
|
//
|
||||||
|
// Completions are completed asynchronously and therefore the user could
|
||||||
|
//switch document/view or leave insert mode. In all of thoise cases the
|
||||||
|
// completion should be discarded
|
||||||
|
if editor.mode != Mode::Insert || view.id != trigger.view || doc.id() != trigger.doc {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = compositor.size();
|
||||||
|
let ui = compositor.find::<ui::EditorView>().unwrap();
|
||||||
|
if ui.completion.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
|
||||||
|
let signature_help_area = compositor
|
||||||
|
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
|
||||||
|
.map(|signature_help| signature_help.area(size, editor));
|
||||||
|
// Delete the signature help popup if they intersect.
|
||||||
|
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) {
|
||||||
|
compositor.remove(SignatureHelp::ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_auto_completion(
|
||||||
|
tx: &Sender<CompletionEvent>,
|
||||||
|
editor: &Editor,
|
||||||
|
trigger_char_only: bool,
|
||||||
|
) {
|
||||||
|
let config = editor.config.load();
|
||||||
|
if !config.auto_completion {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (view, doc): (&helix_view::View, &helix_view::Document) = current_ref!(editor);
|
||||||
|
let mut text = doc.text().slice(..);
|
||||||
|
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||||
|
text = doc.text().slice(..cursor);
|
||||||
|
|
||||||
|
let is_trigger_char = doc
|
||||||
|
.language_servers_with_feature(LanguageServerFeature::Completion)
|
||||||
|
.any(|ls| {
|
||||||
|
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
|
||||||
|
trigger_characters: Some(triggers),
|
||||||
|
..
|
||||||
|
}) if triggers.iter().any(|trigger| text.ends_with(trigger)))
|
||||||
|
});
|
||||||
|
if is_trigger_char {
|
||||||
|
send_blocking(
|
||||||
|
tx,
|
||||||
|
CompletionEvent::TriggerChar {
|
||||||
|
cursor,
|
||||||
|
doc: doc.id(),
|
||||||
|
view: view.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_auto_trigger = !trigger_char_only
|
||||||
|
&& doc
|
||||||
|
.text()
|
||||||
|
.chars_at(cursor)
|
||||||
|
.reversed()
|
||||||
|
.take(config.completion_trigger_len as usize)
|
||||||
|
.all(char_is_word);
|
||||||
|
|
||||||
|
if is_auto_trigger {
|
||||||
|
send_blocking(
|
||||||
|
tx,
|
||||||
|
CompletionEvent::AutoTrigger {
|
||||||
|
cursor,
|
||||||
|
doc: doc.id(),
|
||||||
|
view: view.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_completions(cx: &mut commands::Context, c: Option<char>) {
|
||||||
|
cx.callback.push(Box::new(move |compositor, cx| {
|
||||||
|
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||||
|
if let Some(completion) = &mut editor_view.completion {
|
||||||
|
completion.update_filter(c);
|
||||||
|
if completion.is_empty() {
|
||||||
|
editor_view.clear_completion(cx.editor);
|
||||||
|
// clearing completions might mean we want to immediately rerequest them (usually
|
||||||
|
// this occurs if typing a trigger char)
|
||||||
|
if c.is_some() {
|
||||||
|
trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_completions(cx: &mut commands::Context) {
|
||||||
|
cx.callback.push(Box::new(|compositor, cx| {
|
||||||
|
let editor_view = compositor.find::<ui::EditorView>().unwrap();
|
||||||
|
editor_view.clear_completion(cx.editor);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completion_post_command_hook(
|
||||||
|
tx: &Sender<CompletionEvent>,
|
||||||
|
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if cx.editor.mode == Mode::Insert {
|
||||||
|
if cx.editor.last_completion.is_some() {
|
||||||
|
match command {
|
||||||
|
MappableCommand::Static {
|
||||||
|
name: "delete_word_forward" | "delete_char_forward" | "completion",
|
||||||
|
..
|
||||||
|
} => (),
|
||||||
|
MappableCommand::Static {
|
||||||
|
name: "delete_char_backward",
|
||||||
|
..
|
||||||
|
} => update_completions(cx, None),
|
||||||
|
_ => clear_completions(cx),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let event = match command {
|
||||||
|
MappableCommand::Static {
|
||||||
|
name: "delete_char_backward" | "delete_word_forward" | "delete_char_forward",
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
let primary_cursor = doc
|
||||||
|
.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..));
|
||||||
|
CompletionEvent::DeleteText {
|
||||||
|
cursor: primary_cursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// hacks: some commands are handeled elsewhere and we don't want to
|
||||||
|
// cancel in that case
|
||||||
|
MappableCommand::Static {
|
||||||
|
name: "completion" | "insert_mode" | "append_mode",
|
||||||
|
..
|
||||||
|
} => return Ok(()),
|
||||||
|
_ => CompletionEvent::Cancel,
|
||||||
|
};
|
||||||
|
send_blocking(tx, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||||
|
let tx = handlers.completions.clone();
|
||||||
|
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
|
||||||
|
|
||||||
|
let tx = handlers.completions.clone();
|
||||||
|
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||||
|
if event.old_mode == Mode::Insert {
|
||||||
|
send_blocking(&tx, CompletionEvent::Cancel);
|
||||||
|
clear_completions(event.cx);
|
||||||
|
} else if event.new_mode == Mode::Insert {
|
||||||
|
trigger_auto_completion(&tx, event.cx.editor, false)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx = handlers.completions.clone();
|
||||||
|
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
|
||||||
|
if event.cx.editor.last_completion.is_some() {
|
||||||
|
update_completions(event.cx, Some(event.c))
|
||||||
|
} else {
|
||||||
|
trigger_auto_completion(&tx, event.cx.editor, false);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
335
helix-term/src/handlers/signature_help.rs
Normal file
335
helix-term/src/handlers/signature_help.rs
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use helix_core::syntax::LanguageServerFeature;
|
||||||
|
use helix_event::{
|
||||||
|
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||||
|
};
|
||||||
|
use helix_lsp::lsp;
|
||||||
|
use helix_stdx::rope::RopeSliceExt;
|
||||||
|
use helix_view::document::Mode;
|
||||||
|
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||||
|
use helix_view::handlers::lsp::{SignatureHelpEvent, SignatureHelpInvoked};
|
||||||
|
use helix_view::Editor;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
|
||||||
|
use crate::commands::Open;
|
||||||
|
use crate::compositor::Compositor;
|
||||||
|
use crate::events::{OnModeSwitch, PostInsertChar};
|
||||||
|
use crate::handlers::Handlers;
|
||||||
|
use crate::ui::lsp::SignatureHelp;
|
||||||
|
use crate::ui::Popup;
|
||||||
|
use crate::{job, ui};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum State {
|
||||||
|
Open,
|
||||||
|
Closed,
|
||||||
|
Pending { request: CancelTx },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// debounce timeout in ms, value taken from VSCode
|
||||||
|
/// TODO: make this configurable?
|
||||||
|
const TIMEOUT: u64 = 120;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct SignatureHelpHandler {
|
||||||
|
trigger: Option<SignatureHelpInvoked>,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignatureHelpHandler {
|
||||||
|
pub fn new() -> SignatureHelpHandler {
|
||||||
|
SignatureHelpHandler {
|
||||||
|
trigger: None,
|
||||||
|
state: State::Closed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl helix_event::AsyncHook for SignatureHelpHandler {
|
||||||
|
type Event = SignatureHelpEvent;
|
||||||
|
|
||||||
|
fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
event: Self::Event,
|
||||||
|
timeout: Option<tokio::time::Instant>,
|
||||||
|
) -> Option<Instant> {
|
||||||
|
match event {
|
||||||
|
SignatureHelpEvent::Invoked => {
|
||||||
|
self.trigger = Some(SignatureHelpInvoked::Manual);
|
||||||
|
self.state = State::Closed;
|
||||||
|
self.finish_debounce();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
SignatureHelpEvent::Trigger => {}
|
||||||
|
SignatureHelpEvent::ReTrigger => {
|
||||||
|
// don't retrigger if we aren't open/pending yet
|
||||||
|
if matches!(self.state, State::Closed) {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignatureHelpEvent::Cancel => {
|
||||||
|
self.state = State::Closed;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
SignatureHelpEvent::RequestComplete { open } => {
|
||||||
|
// don't cancel rerequest that was already triggered
|
||||||
|
if let State::Pending { request } = &self.state {
|
||||||
|
if !request.is_closed() {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state = if open { State::Open } else { State::Closed };
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.trigger.is_none() {
|
||||||
|
self.trigger = Some(SignatureHelpInvoked::Automatic)
|
||||||
|
}
|
||||||
|
Some(Instant::now() + Duration::from_millis(TIMEOUT))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_debounce(&mut self) {
|
||||||
|
let invocation = self.trigger.take().unwrap();
|
||||||
|
let (tx, rx) = cancelation();
|
||||||
|
self.state = State::Pending { request: tx };
|
||||||
|
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_signature_help(
|
||||||
|
editor: &mut Editor,
|
||||||
|
invoked: SignatureHelpInvoked,
|
||||||
|
cancel: CancelRx,
|
||||||
|
) {
|
||||||
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
|
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
|
||||||
|
let future = doc
|
||||||
|
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||||
|
.find_map(|language_server| {
|
||||||
|
let pos = doc.position(view.id, language_server.offset_encoding());
|
||||||
|
language_server.text_document_signature_help(doc.identifier(), pos, None)
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(future) = future else {
|
||||||
|
// Do not show the message if signature help was invoked
|
||||||
|
// automatically on backspace, trigger characters, etc.
|
||||||
|
if invoked == SignatureHelpInvoked::Manual {
|
||||||
|
editor
|
||||||
|
.set_error("No configured language server supports signature-help");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match cancelable_future(future, cancel).await {
|
||||||
|
Some(Ok(res)) => {
|
||||||
|
job::dispatch(move |editor, compositor| {
|
||||||
|
show_signature_help(editor, compositor, invoked, res)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Some(Err(err)) => log::error!("signature help request failed: {err}"),
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_signature_help(
|
||||||
|
editor: &mut Editor,
|
||||||
|
compositor: &mut Compositor,
|
||||||
|
invoked: SignatureHelpInvoked,
|
||||||
|
response: Option<lsp::SignatureHelp>,
|
||||||
|
) {
|
||||||
|
let config = &editor.config();
|
||||||
|
|
||||||
|
if !(config.lsp.auto_signature_help
|
||||||
|
|| SignatureHelp::visible_popup(compositor).is_some()
|
||||||
|
|| invoked == SignatureHelpInvoked::Manual)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
|
||||||
|
// it very probably means the server was a little slow to respond and the user has
|
||||||
|
// already moved on to something else, making a signature help popup will just be an
|
||||||
|
// annoyance, see https://github.com/helix-editor/helix/issues/3112
|
||||||
|
// For the most part this should not be needed as the request gets canceled automatically now
|
||||||
|
// but it's technically possible for the mode change to just preempt this callback so better safe than sorry
|
||||||
|
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
|
||||||
|
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,
|
||||||
|
_ => {
|
||||||
|
send_blocking(
|
||||||
|
&editor.handlers.signature_hints,
|
||||||
|
SignatureHelpEvent::RequestComplete { open: false },
|
||||||
|
);
|
||||||
|
compositor.remove(SignatureHelp::ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
send_blocking(
|
||||||
|
&editor.handlers.signature_hints,
|
||||||
|
SignatureHelpEvent::RequestComplete { open: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
let doc = doc!(editor);
|
||||||
|
let language = doc.language_name().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]) => {
|
||||||
|
// LS sends offsets based on utf-16 based string representation
|
||||||
|
// but highlighting in helix is done using byte offset.
|
||||||
|
use helix_core::str_utils::char_to_byte_idx;
|
||||||
|
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||||
|
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||||
|
Some((from, to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
contents.set_active_param_range(active_param_range());
|
||||||
|
|
||||||
|
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
||||||
|
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
||||||
|
.position(old_popup.and_then(|p| p.get_position()))
|
||||||
|
.position_bias(Open::Above)
|
||||||
|
.ignore_escape_key(true);
|
||||||
|
|
||||||
|
// Don't create a popup if it intersects the auto-complete menu.
|
||||||
|
let size = compositor.size();
|
||||||
|
if compositor
|
||||||
|
.find::<ui::EditorView>()
|
||||||
|
.unwrap()
|
||||||
|
.completion
|
||||||
|
.as_mut()
|
||||||
|
.map(|completion| completion.area(size, editor))
|
||||||
|
.filter(|area| area.intersects(popup.area(size, editor)))
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
compositor.replace_or_push(SignatureHelp::ID, popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature_help_post_insert_char_hook(
|
||||||
|
tx: &Sender<SignatureHelpEvent>,
|
||||||
|
PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if !cx.editor.config().lsp.auto_signature_help {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
|
||||||
|
let Some(language_server) = doc
|
||||||
|
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
|
||||||
|
.next()
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let capabilities = language_server.capabilities();
|
||||||
|
|
||||||
|
if let lsp::ServerCapabilities {
|
||||||
|
signature_help_provider:
|
||||||
|
Some(lsp::SignatureHelpOptions {
|
||||||
|
trigger_characters: Some(triggers),
|
||||||
|
// TODO: retrigger_characters
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
} = capabilities
|
||||||
|
{
|
||||||
|
let mut text = doc.text().slice(..);
|
||||||
|
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||||
|
text = text.slice(..cursor);
|
||||||
|
if triggers.iter().any(|trigger| text.ends_with(trigger)) {
|
||||||
|
send_blocking(tx, SignatureHelpEvent::Trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn register_hooks(handlers: &Handlers) {
|
||||||
|
let tx = handlers.signature_hints.clone();
|
||||||
|
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
||||||
|
match (event.old_mode, event.new_mode) {
|
||||||
|
(Mode::Insert, _) => {
|
||||||
|
send_blocking(&tx, SignatureHelpEvent::Cancel);
|
||||||
|
event.cx.callback.push(Box::new(|compositor, _| {
|
||||||
|
compositor.remove(SignatureHelp::ID);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
(_, Mode::Insert) => {
|
||||||
|
if event.cx.editor.config().lsp.auto_signature_help {
|
||||||
|
send_blocking(&tx, SignatureHelpEvent::Trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx = handlers.signature_hints.clone();
|
||||||
|
register_hook!(
|
||||||
|
move |event: &mut PostInsertChar<'_, '_>| signature_help_post_insert_char_hook(&tx, event)
|
||||||
|
);
|
||||||
|
|
||||||
|
let tx = handlers.signature_hints.clone();
|
||||||
|
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||||
|
if event.doc.config.load().lsp.auto_signature_help {
|
||||||
|
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx = handlers.signature_hints.clone();
|
||||||
|
register_hook!(move |event: &mut SelectionDidChange<'_>| {
|
||||||
|
if event.doc.config.load().lsp.auto_signature_help {
|
||||||
|
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
use crate::compositor::{Component, Context, Event, EventResult};
|
use crate::{
|
||||||
|
compositor::{Component, Context, Event, EventResult},
|
||||||
|
handlers::trigger_auto_completion,
|
||||||
|
};
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
document::SavePoint,
|
document::SavePoint,
|
||||||
editor::CompleteAction,
|
editor::CompleteAction,
|
||||||
graphics::Margin,
|
graphics::Margin,
|
||||||
|
handlers::lsp::SignatureHelpInvoked,
|
||||||
theme::{Modifier, Style},
|
theme::{Modifier, Style},
|
||||||
ViewId,
|
ViewId,
|
||||||
};
|
};
|
||||||
@ -10,7 +14,7 @@
|
|||||||
|
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use helix_core::{Change, Transaction};
|
use helix_core::{chars, Change, Transaction};
|
||||||
use helix_view::{graphics::Rect, Document, Editor};
|
use helix_view::{graphics::Rect, Document, Editor};
|
||||||
|
|
||||||
use crate::commands;
|
use crate::commands;
|
||||||
@ -95,10 +99,9 @@ pub struct CompletionItem {
|
|||||||
/// Wraps a Menu.
|
/// Wraps a Menu.
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
popup: Popup<Menu<CompletionItem>>,
|
popup: Popup<Menu<CompletionItem>>,
|
||||||
start_offset: usize,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
// TODO: maintain a completioncontext with trigger kind & trigger char
|
filter: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completion {
|
impl Completion {
|
||||||
@ -108,7 +111,6 @@ pub fn new(
|
|||||||
editor: &Editor,
|
editor: &Editor,
|
||||||
savepoint: Arc<SavePoint>,
|
savepoint: Arc<SavePoint>,
|
||||||
mut items: Vec<CompletionItem>,
|
mut items: Vec<CompletionItem>,
|
||||||
start_offset: usize,
|
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let preview_completion_insert = editor.config().preview_completion_insert;
|
let preview_completion_insert = editor.config().preview_completion_insert;
|
||||||
@ -246,7 +248,7 @@ macro_rules! language_server {
|
|||||||
// (also without sending the transaction to the LS) *before any further transaction is applied*.
|
// (also without sending the transaction to the LS) *before any further transaction is applied*.
|
||||||
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
|
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
|
||||||
// is applied to).
|
// is applied to).
|
||||||
if editor.last_completion.is_none() {
|
if matches!(editor.last_completion, Some(CompleteAction::Triggered)) {
|
||||||
editor.last_completion = Some(CompleteAction::Selected {
|
editor.last_completion = Some(CompleteAction::Selected {
|
||||||
savepoint: doc.savepoint(view),
|
savepoint: doc.savepoint(view),
|
||||||
})
|
})
|
||||||
@ -324,8 +326,18 @@ macro_rules! language_server {
|
|||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// we could have just inserted a trigger char (like a `crate::` completion for rust
|
||||||
|
// so we want to retrigger immediately when accepting a completion.
|
||||||
|
trigger_auto_completion(&editor.handlers.completions, editor, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
|
||||||
|
if event != PromptEvent::Update {
|
||||||
|
editor
|
||||||
|
.handlers
|
||||||
|
.trigger_signature_help(SignatureHelpInvoked::Automatic, editor);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let margin = if editor.menu_border() {
|
let margin = if editor.menu_border() {
|
||||||
@ -339,14 +351,30 @@ macro_rules! language_server {
|
|||||||
.ignore_escape_key(true)
|
.ignore_escape_key(true)
|
||||||
.margin(margin);
|
.margin(margin);
|
||||||
|
|
||||||
|
let (view, doc) = current_ref!(editor);
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||||
|
let offset = text
|
||||||
|
.chars_at(cursor)
|
||||||
|
.reversed()
|
||||||
|
.take_while(|ch| chars::char_is_word(*ch))
|
||||||
|
.count();
|
||||||
|
let start_offset = cursor.saturating_sub(offset);
|
||||||
|
|
||||||
|
let fragment = doc.text().slice(start_offset..cursor);
|
||||||
let mut completion = Self {
|
let mut completion = Self {
|
||||||
popup,
|
popup,
|
||||||
start_offset,
|
|
||||||
trigger_offset,
|
trigger_offset,
|
||||||
|
// TODO: expand nucleo api to allow moving straight to a Utf32String here
|
||||||
|
// and avoid allocation during matching
|
||||||
|
filter: String::from(fragment),
|
||||||
};
|
};
|
||||||
|
|
||||||
// need to recompute immediately in case start_offset != trigger_offset
|
// need to recompute immediately in case start_offset != trigger_offset
|
||||||
completion.recompute_filter(editor);
|
completion
|
||||||
|
.popup
|
||||||
|
.contents_mut()
|
||||||
|
.score(&completion.filter, false);
|
||||||
|
|
||||||
completion
|
completion
|
||||||
}
|
}
|
||||||
@ -366,39 +394,22 @@ fn resolve_completion_item(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recompute_filter(&mut self, editor: &Editor) {
|
/// Appends (`c: Some(c)`) or removes (`c: None`) a character to/from the filter
|
||||||
|
/// this should be called whenever the user types or deletes a character in insert mode.
|
||||||
|
pub fn update_filter(&mut self, c: Option<char>) {
|
||||||
// recompute menu based on matches
|
// recompute menu based on matches
|
||||||
let menu = self.popup.contents_mut();
|
let menu = self.popup.contents_mut();
|
||||||
let (view, doc) = current_ref!(editor);
|
match c {
|
||||||
|
Some(c) => self.filter.push(c),
|
||||||
// cx.hooks()
|
None => {
|
||||||
// cx.add_hook(enum type, ||)
|
self.filter.pop();
|
||||||
// cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
|
if self.filter.is_empty() {
|
||||||
// callback with editor & compositor
|
menu.clear();
|
||||||
//
|
return;
|
||||||
// trigger_hook sends event into channel, that's consumed in the global loop and
|
}
|
||||||
// triggers all registered callbacks
|
}
|
||||||
// TODO: hooks should get processed immediately so maybe do it after select!(), before
|
|
||||||
// looping?
|
|
||||||
|
|
||||||
let cursor = doc
|
|
||||||
.selection(view.id)
|
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..));
|
|
||||||
if self.trigger_offset <= cursor {
|
|
||||||
let fragment = doc.text().slice(self.start_offset..cursor);
|
|
||||||
let text = Cow::from(fragment);
|
|
||||||
// TODO: logic is same as ui/picker
|
|
||||||
menu.score(&text);
|
|
||||||
} else {
|
|
||||||
// we backspaced before the start offset, clear the menu
|
|
||||||
// this will cause the editor to remove the completion popup
|
|
||||||
menu.clear();
|
|
||||||
}
|
}
|
||||||
}
|
menu.score(&self.filter, c.is_some());
|
||||||
|
|
||||||
pub fn update(&mut self, cx: &mut commands::Context) {
|
|
||||||
self.recompute_filter(cx.editor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{self, OnKeyCallback},
|
commands::{self, OnKeyCallback},
|
||||||
compositor::{Component, Context, Event, EventResult},
|
compositor::{Component, Context, Event, EventResult},
|
||||||
job::{self, Callback},
|
|
||||||
events::{OnModeSwitch, PostCommand},
|
events::{OnModeSwitch, PostCommand},
|
||||||
key,
|
key,
|
||||||
keymap::{KeymapResult, Keymaps},
|
keymap::{KeymapResult, Keymaps},
|
||||||
@ -34,8 +33,8 @@
|
|||||||
|
|
||||||
use tui::{buffer::Buffer as Surface, text::Span};
|
use tui::{buffer::Buffer as Surface, text::Span};
|
||||||
|
|
||||||
|
use super::document::LineDecoration;
|
||||||
use super::{completion::CompletionItem, statusline};
|
use super::{completion::CompletionItem, statusline};
|
||||||
use super::{document::LineDecoration, lsp::SignatureHelp};
|
|
||||||
|
|
||||||
pub struct EditorView {
|
pub struct EditorView {
|
||||||
pub keymaps: Keymaps,
|
pub keymaps: Keymaps,
|
||||||
@ -837,11 +836,8 @@ fn handle_keymap_event(
|
|||||||
let mut execute_command = |command: &commands::MappableCommand| {
|
let mut execute_command = |command: &commands::MappableCommand| {
|
||||||
command.execute(cxt);
|
command.execute(cxt);
|
||||||
helix_event::dispatch(PostCommand { command, cx: cxt });
|
helix_event::dispatch(PostCommand { command, cx: cxt });
|
||||||
|
|
||||||
let current_mode = cxt.editor.mode();
|
let current_mode = cxt.editor.mode();
|
||||||
match (last_mode, current_mode) {
|
|
||||||
(Mode::Normal, Mode::Insert) => {
|
|
||||||
// HAXX: if we just entered insert mode from normal, clear key buf
|
|
||||||
// and record the command that got us into this mode.
|
|
||||||
if current_mode != last_mode {
|
if current_mode != last_mode {
|
||||||
helix_event::dispatch(OnModeSwitch {
|
helix_event::dispatch(OnModeSwitch {
|
||||||
old_mode: last_mode,
|
old_mode: last_mode,
|
||||||
@ -849,29 +845,16 @@ fn handle_keymap_event(
|
|||||||
cx: cxt,
|
cx: cxt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// HAXX: if we just entered insert mode from normal, clear key buf
|
||||||
|
// and record the command that got us into this mode.
|
||||||
|
if current_mode == Mode::Insert {
|
||||||
// how we entered insert mode is important, and we should track that so
|
// how we entered insert mode is important, and we should track that so
|
||||||
// we can repeat the side effect.
|
// we can repeat the side effect.
|
||||||
self.last_insert.0 = command.clone();
|
self.last_insert.0 = command.clone();
|
||||||
self.last_insert.1.clear();
|
self.last_insert.1.clear();
|
||||||
|
|
||||||
commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic);
|
|
||||||
}
|
}
|
||||||
(Mode::Insert, Mode::Normal) => {
|
|
||||||
// if exiting insert mode, remove completion
|
|
||||||
self.clear_completion(cxt.editor);
|
|
||||||
cxt.editor.completion_request_handle = None;
|
|
||||||
|
|
||||||
// TODO: Use an on_mode_change hook to remove signature help
|
|
||||||
cxt.jobs.callback(async {
|
|
||||||
let call: job::Callback =
|
|
||||||
Callback::EditorCompositor(Box::new(|_editor, compositor| {
|
|
||||||
compositor.remove(SignatureHelp::ID);
|
|
||||||
}));
|
|
||||||
Ok(call)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
last_mode = current_mode;
|
last_mode = current_mode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -999,12 +982,10 @@ pub fn set_completion(
|
|||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
savepoint: Arc<SavePoint>,
|
savepoint: Arc<SavePoint>,
|
||||||
items: Vec<CompletionItem>,
|
items: Vec<CompletionItem>,
|
||||||
start_offset: usize,
|
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
size: Rect,
|
size: Rect,
|
||||||
) -> Option<Rect> {
|
) -> Option<Rect> {
|
||||||
let mut completion =
|
let mut completion = Completion::new(editor, savepoint, items, trigger_offset);
|
||||||
Completion::new(editor, savepoint, items, start_offset, trigger_offset);
|
|
||||||
|
|
||||||
if completion.is_empty() {
|
if completion.is_empty() {
|
||||||
// skip if we got no completion results
|
// skip if we got no completion results
|
||||||
@ -1025,6 +1006,7 @@ pub fn clear_completion(&mut self, editor: &mut Editor) {
|
|||||||
self.completion = None;
|
self.completion = None;
|
||||||
if let Some(last_completion) = editor.last_completion.take() {
|
if let Some(last_completion) = editor.last_completion.take() {
|
||||||
match last_completion {
|
match last_completion {
|
||||||
|
CompleteAction::Triggered => (),
|
||||||
CompleteAction::Applied {
|
CompleteAction::Applied {
|
||||||
trigger_offset,
|
trigger_offset,
|
||||||
changes,
|
changes,
|
||||||
@ -1038,9 +1020,6 @@ pub fn clear_completion(&mut self, editor: &mut Editor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any savepoints
|
|
||||||
editor.clear_idle_timer(); // don't retrigger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
||||||
@ -1054,13 +1033,7 @@ pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
|
EventResult::Ignored(None)
|
||||||
return EventResult::Ignored(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::commands::insert::idle_completion(cx);
|
|
||||||
|
|
||||||
EventResult::Consumed(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1346,12 +1319,6 @@ fn handle_event(
|
|||||||
if callback.is_some() {
|
if callback.is_some() {
|
||||||
// assume close_fn
|
// assume close_fn
|
||||||
self.clear_completion(cx.editor);
|
self.clear_completion(cx.editor);
|
||||||
|
|
||||||
// In case the popup was deleted because of an intersection w/ the auto-complete menu.
|
|
||||||
commands::signature_help_impl(
|
|
||||||
&mut cx,
|
|
||||||
commands::SignatureHelpInvoked::Automatic,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1362,14 +1329,6 @@ fn handle_event(
|
|||||||
|
|
||||||
// record last_insert key
|
// record last_insert key
|
||||||
self.last_insert.1.push(InsertEvent::Key(key));
|
self.last_insert.1.push(InsertEvent::Key(key));
|
||||||
|
|
||||||
// lastly we recalculate completion
|
|
||||||
if let Some(completion) = &mut self.completion {
|
|
||||||
completion.update(&mut cx);
|
|
||||||
if completion.is_empty() {
|
|
||||||
self.clear_completion(cx.editor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mode => self.command_mode(mode, &mut cx, key),
|
mode => self.command_mode(mode, &mut cx, key),
|
||||||
|
@ -96,20 +96,34 @@ pub fn new(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn score(&mut self, pattern: &str) {
|
pub fn score(&mut self, pattern: &str, incremental: bool) {
|
||||||
// reuse the matches allocation
|
|
||||||
self.matches.clear();
|
|
||||||
let mut matcher = MATCHER.lock();
|
let mut matcher = MATCHER.lock();
|
||||||
matcher.config = Config::DEFAULT;
|
matcher.config = Config::DEFAULT;
|
||||||
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
|
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
|
if incremental {
|
||||||
let text = option.filter_text(&self.editor_data);
|
self.matches.retain_mut(|(index, score)| {
|
||||||
pattern
|
let option = &self.options[*index as usize];
|
||||||
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
|
let text = option.filter_text(&self.editor_data);
|
||||||
.map(|score| (i as u32, score as u32))
|
let new_score = pattern.score(Utf32Str::new(&text, &mut buf), &mut matcher);
|
||||||
});
|
match new_score {
|
||||||
self.matches.extend(matches);
|
Some(new_score) => {
|
||||||
|
*score = new_score as u32;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.matches.clear();
|
||||||
|
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
|
||||||
|
let text = option.filter_text(&self.editor_data);
|
||||||
|
pattern
|
||||||
|
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
|
||||||
|
.map(|score| (i as u32, score as u32))
|
||||||
|
});
|
||||||
|
self.matches.extend(matches);
|
||||||
|
}
|
||||||
self.matches
|
self.matches
|
||||||
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
|
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
|
||||||
|
|
||||||
|
@ -115,19 +115,6 @@ pub struct SavePoint {
|
|||||||
/// The view this savepoint is associated with
|
/// The view this savepoint is associated with
|
||||||
pub view: ViewId,
|
pub view: ViewId,
|
||||||
revert: Mutex<Transaction>,
|
revert: Mutex<Transaction>,
|
||||||
pub text: Rope,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SavePoint {
|
|
||||||
pub fn cursor(&self) -> usize {
|
|
||||||
// we always create transactions with selections
|
|
||||||
self.revert
|
|
||||||
.lock()
|
|
||||||
.selection()
|
|
||||||
.unwrap()
|
|
||||||
.primary()
|
|
||||||
.cursor(self.text.slice(..))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Document {
|
pub struct Document {
|
||||||
@ -1404,7 +1391,6 @@ pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> {
|
|||||||
let savepoint = Arc::new(SavePoint {
|
let savepoint = Arc::new(SavePoint {
|
||||||
view: view.id,
|
view: view.id,
|
||||||
revert: Mutex::new(revert),
|
revert: Mutex::new(revert),
|
||||||
text: self.text.clone(),
|
|
||||||
});
|
});
|
||||||
self.savepoints.push(Arc::downgrade(&savepoint));
|
self.savepoints.push(Arc::downgrade(&savepoint));
|
||||||
savepoint
|
savepoint
|
||||||
|
@ -31,10 +31,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{
|
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
|
||||||
oneshot,
|
|
||||||
},
|
|
||||||
time::{sleep, Duration, Instant, Sleep},
|
time::{sleep, Duration, Instant, Sleep},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,12 +241,19 @@ pub struct Config {
|
|||||||
/// Set a global text_width
|
/// Set a global text_width
|
||||||
pub text_width: usize,
|
pub text_width: usize,
|
||||||
/// Time in milliseconds since last keypress before idle timers trigger.
|
/// Time in milliseconds since last keypress before idle timers trigger.
|
||||||
/// Used for autocompletion, set to 0 for instant. Defaults to 250ms.
|
/// Used for various UI timeouts. Defaults to 250ms.
|
||||||
#[serde(
|
#[serde(
|
||||||
serialize_with = "serialize_duration_millis",
|
serialize_with = "serialize_duration_millis",
|
||||||
deserialize_with = "deserialize_duration_millis"
|
deserialize_with = "deserialize_duration_millis"
|
||||||
)]
|
)]
|
||||||
pub idle_timeout: Duration,
|
pub idle_timeout: Duration,
|
||||||
|
/// Time in milliseconds after typing a word character before auto completions
|
||||||
|
/// are shown, set to 5 for instant. Defaults to 250ms.
|
||||||
|
#[serde(
|
||||||
|
serialize_with = "serialize_duration_millis",
|
||||||
|
deserialize_with = "deserialize_duration_millis"
|
||||||
|
)]
|
||||||
|
pub completion_timeout: Duration,
|
||||||
/// Whether to insert the completion suggestion on hover. Defaults to true.
|
/// Whether to insert the completion suggestion on hover. Defaults to true.
|
||||||
pub preview_completion_insert: bool,
|
pub preview_completion_insert: bool,
|
||||||
pub completion_trigger_len: u8,
|
pub completion_trigger_len: u8,
|
||||||
@ -829,6 +833,7 @@ fn default() -> Self {
|
|||||||
auto_format: true,
|
auto_format: true,
|
||||||
auto_save: false,
|
auto_save: false,
|
||||||
idle_timeout: Duration::from_millis(250),
|
idle_timeout: Duration::from_millis(250),
|
||||||
|
completion_timeout: Duration::from_millis(250),
|
||||||
preview_completion_insert: true,
|
preview_completion_insert: true,
|
||||||
completion_trigger_len: 2,
|
completion_trigger_len: 2,
|
||||||
auto_info: true,
|
auto_info: true,
|
||||||
@ -953,14 +958,6 @@ pub struct Editor {
|
|||||||
/// avoid calculating the cursor position multiple
|
/// avoid calculating the cursor position multiple
|
||||||
/// times during rendering and should not be set by other functions.
|
/// times during rendering and should not be set by other functions.
|
||||||
pub cursor_cache: Cell<Option<Option<Position>>>,
|
pub cursor_cache: Cell<Option<Option<Position>>>,
|
||||||
/// When a new completion request is sent to the server old
|
|
||||||
/// unfinished request must be dropped. Each completion
|
|
||||||
/// request is associated with a channel that cancels
|
|
||||||
/// when the channel is dropped. That channel is stored
|
|
||||||
/// here. When a new completion request is sent this
|
|
||||||
/// field is set and any old requests are automatically
|
|
||||||
/// canceled as a result
|
|
||||||
pub completion_request_handle: Option<oneshot::Sender<()>>,
|
|
||||||
pub handlers: Handlers,
|
pub handlers: Handlers,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -989,13 +986,16 @@ enum ThemeAction {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum CompleteAction {
|
pub enum CompleteAction {
|
||||||
|
Triggered,
|
||||||
|
/// A savepoint of the currently selected completion. The savepoint
|
||||||
|
/// MUST be restored before sending any event to the LSP
|
||||||
|
Selected {
|
||||||
|
savepoint: Arc<SavePoint>,
|
||||||
|
},
|
||||||
Applied {
|
Applied {
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
changes: Vec<Change>,
|
changes: Vec<Change>,
|
||||||
},
|
},
|
||||||
/// A savepoint of the currently selected completion. The savepoint
|
|
||||||
/// MUST be restored before sending any event to the LSP
|
|
||||||
Selected { savepoint: Arc<SavePoint> },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
@ -1029,6 +1029,7 @@ pub fn new(
|
|||||||
theme_loader: Arc<theme::Loader>,
|
theme_loader: Arc<theme::Loader>,
|
||||||
syn_loader: Arc<syntax::Loader>,
|
syn_loader: Arc<syntax::Loader>,
|
||||||
config: Arc<dyn DynAccess<Config>>,
|
config: Arc<dyn DynAccess<Config>>,
|
||||||
|
handlers: Handlers,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
|
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
|
||||||
let conf = config.load();
|
let conf = config.load();
|
||||||
@ -1073,7 +1074,7 @@ pub fn new(
|
|||||||
config_events: unbounded_channel(),
|
config_events: unbounded_channel(),
|
||||||
needs_redraw: false,
|
needs_redraw: false,
|
||||||
cursor_cache: Cell::new(None),
|
cursor_cache: Cell::new(None),
|
||||||
completion_request_handle: None,
|
handlers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,41 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use helix_event::send_blocking;
|
use helix_event::send_blocking;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::handlers::lsp::SignatureHelpInvoked;
|
use crate::handlers::lsp::SignatureHelpInvoked;
|
||||||
use crate::Editor;
|
use crate::{DocumentId, Editor, ViewId};
|
||||||
|
|
||||||
pub mod dap;
|
pub mod dap;
|
||||||
pub mod lsp;
|
pub mod lsp;
|
||||||
|
|
||||||
pub struct Handlers {}
|
pub struct Handlers {
|
||||||
|
// only public because most of the actual implementation is in helix-term right now :/
|
||||||
|
pub completions: Sender<lsp::CompletionEvent>,
|
||||||
|
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handlers {
|
||||||
|
/// Manually trigger completion (c-x)
|
||||||
|
pub fn trigger_completions(&self, trigger_pos: usize, doc: DocumentId, view: ViewId) {
|
||||||
|
send_blocking(
|
||||||
|
&self.completions,
|
||||||
|
lsp::CompletionEvent::ManualTrigger {
|
||||||
|
cursor: trigger_pos,
|
||||||
|
doc,
|
||||||
|
view,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_signature_help(&self, invocation: SignatureHelpInvoked, editor: &Editor) {
|
||||||
|
let event = match invocation {
|
||||||
|
SignatureHelpInvoked::Automatic => {
|
||||||
|
if !editor.config().lsp.auto_signature_help {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lsp::SignatureHelpEvent::Trigger
|
||||||
|
}
|
||||||
|
SignatureHelpInvoked::Manual => lsp::SignatureHelpEvent::Invoked,
|
||||||
|
};
|
||||||
|
send_blocking(&self.signature_hints, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
use crate::{DocumentId, ViewId};
|
use crate::{DocumentId, ViewId};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct CompletionTrigger {
|
|
||||||
/// The char position of the primary cursor when the
|
|
||||||
/// completion was triggered
|
|
||||||
pub trigger_pos: usize,
|
|
||||||
pub doc: DocumentId,
|
|
||||||
pub view: ViewId,
|
|
||||||
/// Whether the cause of the trigger was an automatic completion (any word
|
|
||||||
/// char for words longer than minimum word length).
|
|
||||||
/// This is false for trigger chars send by the LS
|
|
||||||
pub auto: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CompletionEvent {
|
pub enum CompletionEvent {
|
||||||
/// Auto completion was triggered by typing a word char
|
/// Auto completion was triggered by typing a word char
|
||||||
/// or a completion trigger
|
AutoTrigger {
|
||||||
Trigger(CompletionTrigger),
|
cursor: usize,
|
||||||
|
doc: DocumentId,
|
||||||
|
view: ViewId,
|
||||||
|
},
|
||||||
|
/// Auto completion was triggered by typing a trigger char
|
||||||
|
/// specified by the LSP
|
||||||
|
TriggerChar {
|
||||||
|
cursor: usize,
|
||||||
|
doc: DocumentId,
|
||||||
|
view: ViewId,
|
||||||
|
},
|
||||||
/// A completion was manually requested (c-x)
|
/// A completion was manually requested (c-x)
|
||||||
Manual,
|
ManualTrigger {
|
||||||
|
cursor: usize,
|
||||||
|
doc: DocumentId,
|
||||||
|
view: ViewId,
|
||||||
|
},
|
||||||
/// Some text was deleted and the cursor is now at `pos`
|
/// Some text was deleted and the cursor is now at `pos`
|
||||||
DeleteText { pos: usize },
|
DeleteText { cursor: usize },
|
||||||
/// Invalidate the current auto completion trigger
|
/// Invalidate the current auto completion trigger
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user