Skip to content

Commit 15fbfa9

Browse files
committed
refactor completion and signature help using hooks
1 parent 0ff13c9 commit 15fbfa9

File tree

13 files changed

+935
-537
lines changed

13 files changed

+935
-537
lines changed

helix-event/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
1818
//! Async hooks are based around a channels that receive events specific to
1919
//! that `AsyncHook` (usually an enum). These events can be send by synchronous
20-
//! [`Hook`]s. Due to some limtations around tokio channels the [`send_blocking`]
20+
//! hooks. Due to some limtations around tokio channels the [`send_blocking`]
2121
//! function exported in this crate should be used instead of the builtin
2222
//! `blocking_send`.
2323
//!

helix-lsp/src/client.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet,
88
use helix_loader::{self, VERSION_AND_GIT_HASH};
99
use lsp::{
1010
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
11-
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
11+
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, WorkspaceFolder,
1212
WorkspaceFoldersChangeEvent,
1313
};
1414
use lsp_types as lsp;
@@ -924,6 +924,7 @@ impl Client {
924924
text_document: lsp::TextDocumentIdentifier,
925925
position: lsp::Position,
926926
work_done_token: Option<lsp::ProgressToken>,
927+
context: lsp::CompletionContext,
927928
) -> Option<impl Future<Output = Result<Value>>> {
928929
let capabilities = self.capabilities.get().unwrap();
929930

@@ -935,13 +936,12 @@ impl Client {
935936
text_document,
936937
position,
937938
},
939+
context: Some(context),
938940
// TODO: support these tokens by async receiving and updating the choice list
939941
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
940942
partial_result_params: lsp::PartialResultParams {
941943
partial_result_token: None,
942944
},
943-
context: None,
944-
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
945945
};
946946

947947
Some(self.call::<lsp::request::Completion>(params))
@@ -988,7 +988,7 @@ impl Client {
988988
text_document: lsp::TextDocumentIdentifier,
989989
position: lsp::Position,
990990
work_done_token: Option<lsp::ProgressToken>,
991-
) -> Option<impl Future<Output = Result<Value>>> {
991+
) -> Option<impl Future<Output = Result<Option<SignatureHelp>>>> {
992992
let capabilities = self.capabilities.get().unwrap();
993993

994994
// Return early if the server does not support signature help.
@@ -1004,7 +1004,8 @@ impl Client {
10041004
// lsp::SignatureHelpContext
10051005
};
10061006

1007-
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
1007+
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
1008+
Some(async move { Ok(serde_json::from_value(res.await?)?) })
10081009
}
10091010

10101011
pub fn text_document_range_inlay_hints(

helix-term/src/commands.rs

+4-252
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ pub(crate) mod typed;
55
pub use dap::*;
66
use helix_vcs::Hunk;
77
pub use lsp::*;
8-
use tokio::sync::oneshot;
98
use tui::widgets::Row;
109
pub use typed::*;
1110

@@ -33,7 +32,7 @@ use helix_core::{
3332
};
3433
use helix_view::{
3534
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
36-
editor::{Action, CompleteAction},
35+
editor::Action,
3736
info::Info,
3837
input::KeyEvent,
3938
keyboard::KeyCode,
@@ -52,14 +51,10 @@ use crate::{
5251
filter_picker_entry,
5352
job::Callback,
5453
keymap::ReverseKeymap,
55-
ui::{
56-
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
57-
Popup, Prompt, PromptEvent,
58-
},
54+
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
5955
};
6056

6157
use crate::job::{self, Jobs};
62-
use futures_util::{stream::FuturesUnordered, TryStreamExt};
6358
use std::{collections::HashMap, fmt, future::Future};
6459
use std::{collections::HashSet, num::NonZeroUsize};
6560

@@ -2478,7 +2473,6 @@ fn delete_by_selection_insert_mode(
24782473
);
24792474
}
24802475
doc.apply(&transaction, view.id);
2481-
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
24822476
}
24832477

24842478
fn delete_selection(cx: &mut Context) {
@@ -2552,10 +2546,6 @@ fn insert_mode(cx: &mut Context) {
25522546
.transform(|range| Range::new(range.to(), range.from()));
25532547

25542548
doc.set_selection(view.id, selection);
2555-
2556-
// [TODO] temporary workaround until we're not using the idle timer to
2557-
// trigger auto completions any more
2558-
cx.editor.clear_idle_timer();
25592549
}
25602550

25612551
// inserts at the end of each selection
@@ -3378,9 +3368,9 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
33783368

33793369
pub mod insert {
33803370
use crate::events::PostInsertChar;
3371+
33813372
use super::*;
33823373
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3383-
pub type PostHook = fn(&mut Context, char);
33843374

33853375
/// Exclude the cursor in range.
33863376
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
@@ -3394,88 +3384,6 @@ pub mod insert {
33943384
}
33953385
}
33963386

3397-
// It trigger completion when idle timer reaches deadline
3398-
// Only trigger completion if the word under cursor is longer than n characters
3399-
pub fn idle_completion(cx: &mut Context) {
3400-
let config = cx.editor.config();
3401-
let (view, doc) = current!(cx.editor);
3402-
let text = doc.text().slice(..);
3403-
let cursor = doc.selection(view.id).primary().cursor(text);
3404-
3405-
use helix_core::chars::char_is_word;
3406-
let mut iter = text.chars_at(cursor);
3407-
iter.reverse();
3408-
for _ in 0..config.completion_trigger_len {
3409-
match iter.next() {
3410-
Some(c) if char_is_word(c) => {}
3411-
_ => return,
3412-
}
3413-
}
3414-
super::completion(cx);
3415-
}
3416-
3417-
fn language_server_completion(cx: &mut Context, ch: char) {
3418-
let config = cx.editor.config();
3419-
if !config.auto_completion {
3420-
return;
3421-
}
3422-
3423-
use helix_lsp::lsp;
3424-
// if ch matches completion char, trigger completion
3425-
let doc = doc_mut!(cx.editor);
3426-
let trigger_completion = doc
3427-
.language_servers_with_feature(LanguageServerFeature::Completion)
3428-
.any(|ls| {
3429-
// TODO: what if trigger is multiple chars long
3430-
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
3431-
trigger_characters: Some(triggers),
3432-
..
3433-
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
3434-
});
3435-
3436-
if trigger_completion {
3437-
cx.editor.clear_idle_timer();
3438-
super::completion(cx);
3439-
}
3440-
}
3441-
3442-
fn signature_help(cx: &mut Context, ch: char) {
3443-
use helix_lsp::lsp;
3444-
// if ch matches signature_help char, trigger
3445-
let doc = doc_mut!(cx.editor);
3446-
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
3447-
let Some(language_server) = doc
3448-
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
3449-
.next()
3450-
else {
3451-
return;
3452-
};
3453-
3454-
let capabilities = language_server.capabilities();
3455-
3456-
if let lsp::ServerCapabilities {
3457-
signature_help_provider:
3458-
Some(lsp::SignatureHelpOptions {
3459-
trigger_characters: Some(triggers),
3460-
// TODO: retrigger_characters
3461-
..
3462-
}),
3463-
..
3464-
} = capabilities
3465-
{
3466-
// TODO: what if trigger is multiple chars long
3467-
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3468-
// lsp doesn't tell us when to close the signature help, so we request
3469-
// the help information again after common close triggers which should
3470-
// return None, which in turn closes the popup.
3471-
let close_triggers = &[')', ';', '.'];
3472-
3473-
if is_trigger || close_triggers.contains(&ch) {
3474-
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
3475-
}
3476-
}
3477-
}
3478-
34793387
// The default insert hook: simply insert the character
34803388
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
34813389
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
@@ -3505,12 +3413,6 @@ pub mod insert {
35053413
doc.apply(&t, view.id);
35063414
}
35073415

3508-
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3509-
// this could also generically look at Transaction, but it's a bit annoying to look at
3510-
// Operation instead of Change.
3511-
for hook in &[language_server_completion, signature_help] {
3512-
hook(cx, c);
3513-
}
35143416
helix_event::dispatch(PostInsertChar { c, cx });
35153417
}
35163418

@@ -3735,8 +3637,6 @@ pub mod insert {
37353637
});
37363638
let (view, doc) = current!(cx.editor);
37373639
doc.apply(&transaction, view.id);
3738-
3739-
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
37403640
}
37413641

37423642
pub fn delete_char_forward(cx: &mut Context) {
@@ -4373,151 +4273,7 @@ fn remove_primary_selection(cx: &mut Context) {
43734273
}
43744274

43754275
pub fn completion(cx: &mut Context) {
4376-
use helix_lsp::{lsp, util::pos_to_lsp_pos};
4377-
4378-
let (view, doc) = current!(cx.editor);
4379-
4380-
let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion
4381-
{
4382-
savepoint.clone()
4383-
} else {
4384-
doc.savepoint(view)
4385-
};
4386-
4387-
let text = savepoint.text.clone();
4388-
let cursor = savepoint.cursor();
4389-
4390-
let mut seen_language_servers = HashSet::new();
4391-
4392-
let mut futures: FuturesUnordered<_> = doc
4393-
.language_servers_with_feature(LanguageServerFeature::Completion)
4394-
.filter(|ls| seen_language_servers.insert(ls.id()))
4395-
.map(|language_server| {
4396-
let language_server_id = language_server.id();
4397-
let offset_encoding = language_server.offset_encoding();
4398-
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
4399-
let doc_id = doc.identifier();
4400-
let completion_request = language_server.completion(doc_id, pos, None).unwrap();
4401-
4402-
async move {
4403-
let json = completion_request.await?;
4404-
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
4405-
4406-
let items = match response {
4407-
Some(lsp::CompletionResponse::Array(items)) => items,
4408-
// TODO: do something with is_incomplete
4409-
Some(lsp::CompletionResponse::List(lsp::CompletionList {
4410-
is_incomplete: _is_incomplete,
4411-
items,
4412-
})) => items,
4413-
None => Vec::new(),
4414-
}
4415-
.into_iter()
4416-
.map(|item| CompletionItem {
4417-
item,
4418-
language_server_id,
4419-
resolved: false,
4420-
})
4421-
.collect();
4422-
4423-
anyhow::Ok(items)
4424-
}
4425-
})
4426-
.collect();
4427-
4428-
// setup a channel that allows the request to be canceled
4429-
let (tx, rx) = oneshot::channel();
4430-
// set completion_request so that this request can be canceled
4431-
// by setting completion_request, the old channel stored there is dropped
4432-
// and the associated request is automatically dropped
4433-
cx.editor.completion_request_handle = Some(tx);
4434-
let future = async move {
4435-
let items_future = async move {
4436-
let mut items = Vec::new();
4437-
// TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
4438-
while let Some(mut lsp_items) = futures.try_next().await? {
4439-
items.append(&mut lsp_items);
4440-
}
4441-
anyhow::Ok(items)
4442-
};
4443-
tokio::select! {
4444-
biased;
4445-
_ = rx => {
4446-
Ok(Vec::new())
4447-
}
4448-
res = items_future => {
4449-
res
4450-
}
4451-
}
4452-
};
4453-
4454-
let trigger_offset = cursor;
4455-
4456-
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
4457-
// completion filtering. For example logger.te| should filter the initial suggestion list with "te".
4458-
4459-
use helix_core::chars;
4460-
let mut iter = text.chars_at(cursor);
4461-
iter.reverse();
4462-
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4463-
let start_offset = cursor.saturating_sub(offset);
4464-
4465-
let trigger_doc = doc.id();
4466-
let trigger_view = view.id;
4467-
4468-
// FIXME: The commands Context can only have a single callback
4469-
// which means it gets overwritten when executing keybindings
4470-
// with multiple commands or macros. This would mean that completion
4471-
// might be incorrectly applied when repeating the insertmode action
4472-
//
4473-
// TODO: to solve this either make cx.callback a Vec of callbacks or
4474-
// alternatively move `last_insert` to `helix_view::Editor`
4475-
cx.callback = Some(Box::new(
4476-
move |compositor: &mut Compositor, _cx: &mut compositor::Context| {
4477-
let ui = compositor.find::<ui::EditorView>().unwrap();
4478-
ui.last_insert.1.push(InsertEvent::RequestCompletion);
4479-
},
4480-
));
4481-
4482-
cx.jobs.callback(async move {
4483-
let items = future.await?;
4484-
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
4485-
let (view, doc) = current_ref!(editor);
4486-
// check if the completion request is stale.
4487-
//
4488-
// Completions are completed asynchronously and therefore the user could
4489-
//switch document/view or leave insert mode. In all of thoise cases the
4490-
// completion should be discarded
4491-
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {
4492-
return;
4493-
}
4494-
4495-
if items.is_empty() {
4496-
// editor.set_error("No completion available");
4497-
return;
4498-
}
4499-
let size = compositor.size();
4500-
let ui = compositor.find::<ui::EditorView>().unwrap();
4501-
let completion_area = ui.set_completion(
4502-
editor,
4503-
savepoint,
4504-
items,
4505-
start_offset,
4506-
trigger_offset,
4507-
size,
4508-
);
4509-
let size = compositor.size();
4510-
let signature_help_area = compositor
4511-
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
4512-
.map(|signature_help| signature_help.area(size, editor));
4513-
// Delete the signature help popup if they intersect.
4514-
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b))
4515-
{
4516-
compositor.remove(SignatureHelp::ID);
4517-
}
4518-
};
4519-
Ok(Callback::EditorCompositor(Box::new(call)))
4520-
});
4276+
cx.editor.handlers.trigger_completions();
45214277
}
45224278

45234279
// comments
@@ -4696,10 +4452,6 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
46964452
);
46974453

46984454
doc.set_selection(view.id, selection);
4699-
4700-
// [TODO] temporary workaround until we're not using the idle timer to
4701-
// trigger auto completions any more
4702-
editor.clear_idle_timer();
47034455
}
47044456
};
47054457

0 commit comments

Comments
 (0)