Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/handlers/document_colors.rs')
-rw-r--r--helix-term/src/handlers/document_colors.rs204
1 files changed, 204 insertions, 0 deletions
diff --git a/helix-term/src/handlers/document_colors.rs b/helix-term/src/handlers/document_colors.rs
new file mode 100644
index 00000000..cffe5688
--- /dev/null
+++ b/helix-term/src/handlers/document_colors.rs
@@ -0,0 +1,204 @@
+use std::{collections::HashSet, time::Duration};
+
+use futures_util::{stream::FuturesOrdered, StreamExt};
+use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation};
+use helix_event::{cancelable_future, register_hook};
+use helix_lsp::lsp;
+use helix_view::{
+ document::DocumentColorSwatches,
+ events::{DocumentDidChange, DocumentDidOpen, LanguageServerExited, LanguageServerInitialized},
+ handlers::{lsp::DocumentColorsEvent, Handlers},
+ DocumentId, Editor, Theme,
+};
+use tokio::time::Instant;
+
+use crate::job;
+
+#[derive(Default)]
+pub(super) struct DocumentColorsHandler {
+ docs: HashSet<DocumentId>,
+}
+
+const DOCUMENT_CHANGE_DEBOUNCE: Duration = Duration::from_millis(250);
+
+impl helix_event::AsyncHook for DocumentColorsHandler {
+ type Event = DocumentColorsEvent;
+
+ fn handle_event(&mut self, event: Self::Event, _timeout: Option<Instant>) -> Option<Instant> {
+ let DocumentColorsEvent(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_colors(editor, doc);
+ }
+ });
+ }
+}
+
+fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
+ if !editor.config().lsp.display_color_swatches {
+ return;
+ }
+
+ let Some(doc) = editor.document_mut(doc_id) else {
+ return;
+ };
+
+ let cancel = doc.color_swatch_controller.restart();
+
+ let mut seen_language_servers = HashSet::new();
+ let mut futures: FuturesOrdered<_> = doc
+ .language_servers_with_feature(LanguageServerFeature::DocumentColors)
+ .filter(|ls| seen_language_servers.insert(ls.id()))
+ .map(|language_server| {
+ let text = doc.text().clone();
+ let offset_encoding = language_server.offset_encoding();
+ let future = language_server
+ .text_document_document_color(doc.identifier(), None)
+ .unwrap();
+
+ async move {
+ let colors: Vec<_> = future
+ .await?
+ .into_iter()
+ .filter_map(|color_info| {
+ let pos = helix_lsp::util::lsp_pos_to_pos(
+ &text,
+ color_info.range.start,
+ offset_encoding,
+ )?;
+ Some((pos, color_info.color))
+ })
+ .collect();
+ anyhow::Ok(colors)
+ }
+ })
+ .collect();
+
+ tokio::spawn(async move {
+ let mut all_colors = Vec::new();
+ loop {
+ match cancelable_future(futures.next(), &cancel).await {
+ Some(Some(Ok(items))) => all_colors.extend(items),
+ Some(Some(Err(err))) => log::error!("document color request failed: {err}"),
+ Some(None) => break,
+ // The request was cancelled.
+ None => return,
+ }
+ }
+ job::dispatch(move |editor, _| attach_document_colors(editor, doc_id, all_colors)).await;
+ });
+}
+
+fn attach_document_colors(
+ editor: &mut Editor,
+ doc_id: DocumentId,
+ mut doc_colors: Vec<(usize, lsp::Color)>,
+) {
+ if !editor.config().lsp.display_color_swatches {
+ return;
+ }
+
+ let Some(doc) = editor.documents.get_mut(&doc_id) else {
+ return;
+ };
+
+ if doc_colors.is_empty() {
+ doc.color_swatches.take();
+ return;
+ }
+
+ doc_colors.sort_by_key(|(pos, _)| *pos);
+
+ let mut color_swatches = Vec::with_capacity(doc_colors.len());
+ let mut color_swatches_padding = Vec::with_capacity(doc_colors.len());
+ let mut colors = Vec::with_capacity(doc_colors.len());
+
+ for (pos, color) in doc_colors {
+ color_swatches_padding.push(InlineAnnotation::new(pos, " "));
+ color_swatches.push(InlineAnnotation::new(pos, "■"));
+ colors.push(Theme::rgb_highlight(
+ (color.red * 255.) as u8,
+ (color.green * 255.) as u8,
+ (color.blue * 255.) as u8,
+ ));
+ }
+
+ doc.color_swatches = Some(DocumentColorSwatches {
+ color_swatches,
+ colors,
+ color_swatches_padding,
+ });
+}
+
+pub(super) fn register_hooks(handlers: &Handlers) {
+ register_hook!(move |event: &mut DocumentDidOpen<'_>| {
+ // when a document is initially opened, request colors for it
+ request_document_colors(event.editor, event.doc);
+
+ Ok(())
+ });
+
+ let tx = handlers.document_colors.clone();
+ register_hook!(move |event: &mut DocumentDidChange<'_>| {
+ // Update the color swatch' positions, helping ensure they are displayed in the
+ // proper place.
+ let apply_color_swatch_changes = |annotations: &mut Vec<InlineAnnotation>| {
+ event.changes.update_positions(
+ annotations
+ .iter_mut()
+ .map(|annotation| (&mut annotation.char_idx, helix_core::Assoc::After)),
+ );
+ };
+
+ if let Some(DocumentColorSwatches {
+ color_swatches,
+ colors: _colors,
+ color_swatches_padding,
+ }) = &mut event.doc.color_swatches
+ {
+ apply_color_swatch_changes(color_swatches);
+ apply_color_swatch_changes(color_swatches_padding);
+ }
+
+ // Cancel the ongoing request, if present.
+ event.doc.color_swatch_controller.cancel();
+
+ helix_event::send_blocking(&tx, DocumentColorsEvent(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_colors(event.editor, doc_id);
+ }
+
+ Ok(())
+ });
+
+ register_hook!(move |event: &mut LanguageServerExited<'_>| {
+ // Clear and re-request all color swatches when a server exits.
+ for doc in event.editor.documents_mut() {
+ if doc.supports_language_server(event.server_id) {
+ doc.color_swatches.take();
+ }
+ }
+
+ let doc_ids: Vec<_> = event.editor.documents().map(|doc| doc.id()).collect();
+
+ for doc_id in doc_ids {
+ request_document_colors(event.editor, doc_id);
+ }
+
+ Ok(())
+ });
+}