Skip to content

Commit 60d22bc

Browse files
committed
refactor completion and signature help using hooks
1 parent 9b0fdc7 commit 60d22bc

File tree

12 files changed

+984
-531
lines changed

12 files changed

+984
-531
lines changed

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

@@ -2474,7 +2469,6 @@ fn delete_by_selection_insert_mode(
24742469
);
24752470
}
24762471
doc.apply(&transaction, view.id);
2477-
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
24782472
}
24792473

24802474
fn delete_selection(cx: &mut Context) {
@@ -2548,10 +2542,6 @@ fn insert_mode(cx: &mut Context) {
25482542
.transform(|range| Range::new(range.to(), range.from()));
25492543

25502544
doc.set_selection(view.id, selection);
2551-
2552-
// [TODO] temporary workaround until we're not using the idle timer to
2553-
// trigger auto completions any more
2554-
cx.editor.clear_idle_timer();
25552545
}
25562546

25572547
// inserts at the end of each selection
@@ -3374,9 +3364,9 @@ fn hunk_range(hunk: Hunk, text: RopeSlice) -> Range {
33743364

33753365
pub mod insert {
33763366
use crate::events::PostInsertChar;
3367+
33773368
use super::*;
33783369
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
3379-
pub type PostHook = fn(&mut Context, char);
33803370

33813371
/// Exclude the cursor in range.
33823372
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
@@ -3390,88 +3380,6 @@ pub mod insert {
33903380
}
33913381
}
33923382

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

3504-
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
3505-
// this could also generically look at Transaction, but it's a bit annoying to look at
3506-
// Operation instead of Change.
3507-
for hook in &[language_server_completion, signature_help] {
3508-
hook(cx, c);
3509-
}
35103412
helix_event::dispatch(PostInsertChar { c, cx });
35113413
}
35123414

@@ -3731,8 +3633,6 @@ pub mod insert {
37313633
});
37323634
let (view, doc) = current!(cx.editor);
37333635
doc.apply(&transaction, view.id);
3734-
3735-
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
37363636
}
37373637

37383638
pub fn delete_char_forward(cx: &mut Context) {
@@ -4369,151 +4269,7 @@ fn remove_primary_selection(cx: &mut Context) {
43694269
}
43704270

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

45194275
// comments
@@ -4692,10 +4448,6 @@ fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
46924448
);
46934449

46944450
doc.set_selection(view.id, selection);
4695-
4696-
// [TODO] temporary workaround until we're not using the idle timer to
4697-
// trigger auto completions any more
4698-
editor.clear_idle_timer();
46994451
}
47004452
};
47014453

0 commit comments

Comments
 (0)