use std::{collections::HashSet, time::Duration};
use futures_util::stream::FuturesUnordered;
use helix_event::{cancelable_future, register_hook, send_blocking, AsyncHook};
use helix_lsp::lsp::{CodeAction, CodeActionOrCommand, CodeActionTriggerKind};
use helix_view::{
events::{
ConfigDidChange, DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen,
LanguageServerExited, LanguageServerInitialized, SelectionDidChange,
},
handlers::{lsp::CodeActionHintEvent, Handlers},
DocumentId, Editor, ViewId,
};
use tokio::time::Instant;
use tokio_stream::StreamExt;
use crate::{commands::code_actions_for_range, job};
#[derive(Debug, Default)]
pub(super) struct Handler {
doc_ids: HashSet<(DocumentId, ViewId)>,
}
impl AsyncHook for Handler {
type Event = CodeActionHintEvent;
fn handle_event(
&mut self,
event: Self::Event,
_timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
self.doc_ids.insert((event.document_id, event.view_id));
Some(Instant::now() + Duration::from_millis(200))
}
fn finish_debounce(&mut self) {
let ids = std::mem::take(&mut self.doc_ids);
job::dispatch_blocking(move |editor, _| {
for (doc_id, view_id) in ids {
request_code_action_hint(editor, doc_id, view_id);
}
})
}
}
fn request_code_action_hint(editor: &mut Editor, doc_id: DocumentId, view_id: ViewId) {
if !editor.config().code_action_hint() {
return;
}
let Some(doc) = editor.document_mut(doc_id) else {
return;
};
doc.ensure_view_init(view_id);
let selection_range = doc.selection(view_id).primary();
let mut futures: FuturesUnordered<_> =
code_actions_for_range(doc, selection_range, None, CodeActionTriggerKind::AUTOMATIC)
.into_iter()
.map(|(request, _)| async move {
let Some(mut actions) = request.await? else {
return anyhow::Ok(Vec::new());
};
// remove disabled code actions
actions.retain(|action| {
matches!(
action,
CodeActionOrCommand::Command(_)
| CodeActionOrCommand::CodeAction(CodeAction { disabled: None, .. })
)
});
Ok(actions)
})
.collect();
if futures.is_empty() {
doc.clear_code_action_hints(view_id);
return;
};
let cancel = doc.code_action_controller(view_id).restart();
tokio::spawn(async move {
let mut actions = Vec::new();
loop {
match cancelable_future(futures.next(), &cancel).await {
Some(output) => match output {
Some(Ok(mut lsp_items)) => actions.append(&mut lsp_items),
Some(Err(err)) => log::error!("while gathering code actions: {err}"),
None => break,
},
// The request was cancelled.
None => return,
}
}
job::dispatch(move |editor, _| {
apply_code_action_hint(editor, doc_id, view_id, actions);
})
.await;
});
}
fn apply_code_action_hint(
editor: &mut Editor,
doc_id: DocumentId,
view_id: ViewId,
code_actions: Vec<CodeActionOrCommand>,
) {
let Some(doc) = editor.document_mut(doc_id) else {
return;
};
if code_actions.is_empty() {
doc.clear_code_action_hints(view_id);
return;
}
doc.set_code_action_hints(view_id);
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.code_action_hint.clone();
register_hook!(move |event: &mut SelectionDidChange<'_>| {
if event.doc.config.load().code_action_hint() {
let doc_id = event.doc.id();
let view_id = event.view;
// clear stale hints
event.doc.clear_code_action_hints(view_id);
send_blocking(
&tx,
CodeActionHintEvent {
document_id: doc_id,
view_id,
},
);
}
Ok(())
});
let tx = handlers.code_action_hint.clone();
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
if !event.editor.config().code_action_hint() {
return Ok(());
}
let view_id = event.editor.tree.focus;
if event.editor.tree.try_get(view_id).is_none() {
return Ok(());
}
send_blocking(
&tx,
CodeActionHintEvent {
document_id: event.doc,
view_id,
},
);
Ok(())
});
let tx = handlers.code_action_hint.clone();
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
if event.editor.config().code_action_hint() {
let doc_id = event.doc;
let views: Vec<_> = event
.editor
.tree
.views()
.map(|(view, _)| (view.id, view.doc))
.collect();
for (view_id, view_doc) in views {
if view_doc == doc_id {
send_blocking(
&tx,
CodeActionHintEvent {
document_id: doc_id,
view_id,
},
);
}
}
}
Ok(())
});
let tx = handlers.code_action_hint.clone();
register_hook!(move |event: &mut DocumentDidChange<'_>| {
if event.doc.config.load().code_action_hint() && !event.ghost_transaction {
let doc_id = event.doc.id();
let view_id = event.view;
// clear stale hints
event.doc.clear_code_action_hints(view_id);
send_blocking(
&tx,
CodeActionHintEvent {
document_id: doc_id,
view_id,
},
);
}
Ok(())
});
let tx = handlers.code_action_hint.clone();
register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
if !event.editor.config().code_action_hint() {
return Ok(());
}
let view_id = event.editor.tree.focus;
let Some(view) = event.editor.tree.try_get(view_id) else {
return Ok(());
};
let doc_id = view.doc;
send_blocking(
&tx,
CodeActionHintEvent {
document_id: doc_id,
view_id,
},
);
Ok(())
});
register_hook!(move |event: &mut LanguageServerExited<'_>| {
for doc in event.editor.documents_mut() {
if doc.supports_language_server(event.server_id) {
doc.clear_all_code_action_hints();
}
}
Ok(())
});
let tx = handlers.code_action_hint.clone();
register_hook!(move |event: &mut ConfigDidChange<'_>| {
// When code action hints are turned on, request them immediately
// for the focused view instead of waiting for the next selection change.
if !event.old.code_action_hint() && event.new.code_action_hint() {
let view_id = event.editor.tree.focus;
let Some(view) = event.editor.tree.try_get(view_id) else {
return Ok(());
};
send_blocking(
&tx,
CodeActionHintEvent {
document_id: view.doc,
view_id,
},
);
return Ok(());
}
// When code action hints are turned off, clear any that were
// previously rendered across open documents.
if event.old.code_action_hint() && !event.new.code_action_hint() {
for doc in event.editor.documents_mut() {
doc.clear_all_code_action_hints();
}
}
Ok(())
});
}