use std::{collections::HashSet, time::Duration};
use futures_util::{stream::FuturesOrdered, StreamExt};
use helix_core::{syntax::config::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();
if futures.is_empty() {
return;
}
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);
}
// Avoid re-requesting document colors if the change is a ghost transaction (completion)
// because the language server will not know about the updates to the document and will
// give out-of-date locations.
if !event.ghost_transaction {
// 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(())
});
}