Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/handlers/document_links.rs')
| -rw-r--r-- | helix-term/src/handlers/document_links.rs | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/helix-term/src/handlers/document_links.rs b/helix-term/src/handlers/document_links.rs new file mode 100644 index 00000000..5be6176d --- /dev/null +++ b/helix-term/src/handlers/document_links.rs @@ -0,0 +1,173 @@ +use std::{collections::HashSet, time::Duration}; + +use futures_util::{stream::FuturesOrdered, StreamExt}; +use helix_core::{syntax::config::LanguageServerFeature, Assoc}; +use helix_event::{cancelable_future, register_hook}; +use helix_view::{ + document::DocumentLink, + events::{DocumentDidChange, DocumentDidOpen, LanguageServerExited, LanguageServerInitialized}, + handlers::{lsp::DocumentLinksEvent, Handlers}, + DocumentId, Editor, +}; +use tokio::time::Instant; + +use crate::job; + +#[derive(Default)] +pub(super) struct DocumentLinksHandler { + docs: HashSet<DocumentId>, +} + +const DOCUMENT_CHANGE_DEBOUNCE: Duration = Duration::from_millis(250); + +impl helix_event::AsyncHook for DocumentLinksHandler { + type Event = DocumentLinksEvent; + + fn handle_event(&mut self, event: Self::Event, _timeout: Option<Instant>) -> Option<Instant> { + let DocumentLinksEvent(doc_id) = event; + self.docs.insert(doc_id); + Some(Instant::now() + DOCUMENT_CHANGE_DEBOUNCE) + } + + fn finish_debounce(&mut self) { + let docs = std::mem::take(&mut self.docs); + + job::dispatch_blocking(move |editor, _compositor| { + for doc in docs { + request_document_links(editor, doc); + } + }); + } +} + +/// Request document links for a specific document and cache them for navigation. +fn request_document_links(editor: &mut Editor, doc_id: DocumentId) { + let Some(doc) = editor.document_mut(doc_id) else { + return; + }; + + let cancel = doc.document_link_controller.restart(); + + let mut seen_language_servers = HashSet::new(); + let mut futures: FuturesOrdered<_> = doc + .language_servers_with_feature(LanguageServerFeature::DocumentLinks) + .filter(|ls| seen_language_servers.insert(ls.id())) + .filter_map(|language_server| { + let text = doc.text().clone(); + let offset_encoding = language_server.offset_encoding(); + let language_server_id = language_server.id(); + let future = language_server.text_document_document_link(doc.identifier(), None)?; + + Some(async move { + let links = future.await?.unwrap_or_default(); + let links: Vec<_> = links + .into_iter() + .filter_map(|link| { + let start = helix_lsp::util::lsp_pos_to_pos( + &text, + link.range.start, + offset_encoding, + )?; + let end = helix_lsp::util::lsp_pos_to_pos( + &text, + link.range.end, + offset_encoding, + )?; + if start > end { + return None; + } + Some(DocumentLink { + start, + end, + link, + language_server_id, + }) + }) + .collect(); + anyhow::Ok(links) + }) + }) + .collect(); + + if futures.is_empty() { + return; + } + + tokio::spawn(async move { + let mut all_links = Vec::new(); + loop { + match cancelable_future(futures.next(), &cancel).await { + Some(Some(Ok(items))) => all_links.extend(items), + Some(Some(Err(err))) => log::error!("document link request failed: {err}"), + Some(None) => break, + None => return, + } + } + + job::dispatch(move |editor, _| attach_document_links(editor, doc_id, all_links)).await; + }); +} + +fn attach_document_links(editor: &mut Editor, doc_id: DocumentId, mut links: Vec<DocumentLink>) { + let Some(doc) = editor.documents.get_mut(&doc_id) else { + return; + }; + + if links.is_empty() { + doc.document_links.clear(); + return; + } + + links.sort_by_key(|link| (link.start, link.end)); + doc.document_links = links; +} + +pub(super) fn register_hooks(handlers: &Handlers) { + register_hook!(move |event: &mut DocumentDidOpen<'_>| { + request_document_links(event.editor, event.doc); + Ok(()) + }); + + let tx = handlers.document_links.clone(); + register_hook!(move |event: &mut DocumentDidChange<'_>| { + event + .changes + .update_positions(event.doc.document_links.iter_mut().flat_map(|link| { + std::iter::once((&mut link.start, Assoc::After)) + .chain(std::iter::once((&mut link.end, Assoc::After))) + })); + + if !event.ghost_transaction { + event.doc.document_link_controller.cancel(); + helix_event::send_blocking(&tx, DocumentLinksEvent(event.doc.id())); + } + + Ok(()) + }); + + register_hook!(move |event: &mut LanguageServerInitialized<'_>| { + let doc_ids: Vec<_> = event.editor.documents().map(|doc| doc.id()).collect(); + + for doc_id in doc_ids { + request_document_links(event.editor, doc_id); + } + + Ok(()) + }); + + register_hook!(move |event: &mut LanguageServerExited<'_>| { + for doc in event.editor.documents_mut() { + if doc.supports_language_server(event.server_id) { + doc.document_links.clear(); + } + } + + let doc_ids: Vec<_> = event.editor.documents().map(|doc| doc.id()).collect(); + + for doc_id in doc_ids { + request_document_links(event.editor, doc_id); + } + + Ok(()) + }); +} |