Unnamed repository; edit this file 'description' to name the repository.
Color swatches ( 🟩 green 🟥 #ffaaaa ) (#12308)
Nik Revenco 11 months ago
parent 8ff5447 · commit 0ee5850
-rw-r--r--book/src/editor.md1
-rw-r--r--helix-core/src/syntax.rs2
-rw-r--r--helix-lsp/src/client.rs20
-rw-r--r--helix-term/src/handlers.rs6
-rw-r--r--helix-term/src/handlers/document_colors.rs204
-rw-r--r--helix-view/src/document.rs16
-rw-r--r--helix-view/src/editor.rs3
-rw-r--r--helix-view/src/handlers.rs1
-rw-r--r--helix-view/src/handlers/lsp.rs4
-rw-r--r--helix-view/src/theme.rs91
-rw-r--r--helix-view/src/view.rs19
11 files changed, 363 insertions, 4 deletions
diff --git a/book/src/editor.md b/book/src/editor.md
index 3fe650e0..1e5c2a50 100644
--- a/book/src/editor.md
+++ b/book/src/editor.md
@@ -152,6 +152,7 @@ The following statusline elements can be configured:
| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
+| `display-color-swatches` | Show color swatches next to colors | `true` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 7c50a579..677cdfa0 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -334,6 +334,7 @@ pub enum LanguageServerFeature {
Diagnostics,
RenameSymbol,
InlayHints,
+ DocumentColors,
}
impl Display for LanguageServerFeature {
@@ -357,6 +358,7 @@ impl Display for LanguageServerFeature {
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
+ DocumentColors => "document-colors",
};
write!(f, "{feature}",)
}
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index e5a116d7..f2b78a11 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -356,6 +356,7 @@ impl Client {
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
+ LanguageServerFeature::DocumentColors => capabilities.color_provider.is_some(),
}
}
@@ -1095,6 +1096,25 @@ impl Client {
Some(self.call::<lsp::request::InlayHintRequest>(params))
}
+ pub fn text_document_document_color(
+ &self,
+ text_document: lsp::TextDocumentIdentifier,
+ work_done_token: Option<lsp::ProgressToken>,
+ ) -> Option<impl Future<Output = Result<Vec<lsp::ColorInformation>>>> {
+ self.capabilities.get().unwrap().color_provider.as_ref()?;
+ let params = lsp::DocumentColorParams {
+ text_document,
+ work_done_progress_params: lsp::WorkDoneProgressParams {
+ work_done_token: work_done_token.clone(),
+ },
+ partial_result_params: helix_lsp_types::PartialResultParams {
+ partial_result_token: work_done_token,
+ },
+ };
+
+ Some(self.call::<lsp::request::DocumentColor>(params))
+ }
+
pub fn text_document_hover(
&self,
text_document: lsp::TextDocumentIdentifier,
diff --git a/helix-term/src/handlers.rs b/helix-term/src/handlers.rs
index 24d8491b..c7d71526 100644
--- a/helix-term/src/handlers.rs
+++ b/helix-term/src/handlers.rs
@@ -10,9 +10,12 @@ use crate::handlers::signature_help::SignatureHelpHandler;
pub use helix_view::handlers::Handlers;
+use self::document_colors::DocumentColorsHandler;
+
mod auto_save;
pub mod completion;
mod diagnostics;
+mod document_colors;
mod signature_help;
mod snippet;
@@ -22,11 +25,13 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let event_tx = completion::CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn();
+ let document_colors = DocumentColorsHandler::default().spawn();
let handlers = Handlers {
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
signature_hints,
auto_save,
+ document_colors,
};
helix_view::handlers::register_hooks(&handlers);
@@ -35,5 +40,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
auto_save::register_hooks(&handlers);
diagnostics::register_hooks(&handlers);
snippet::register_hooks(&handlers);
+ document_colors::register_hooks(&handlers);
handlers
}
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(())
+ });
+}
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index b75aebe7..41c9ee1e 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -11,6 +11,7 @@ use helix_core::encoding::Encoding;
use helix_core::snippets::{ActiveSnippet, SnippetRenderCtx};
use helix_core::syntax::{Highlight, LanguageServerFeature};
use helix_core::text_annotations::{InlineAnnotation, Overlay};
+use helix_event::TaskController;
use helix_lsp::util::lsp_pos_to_pos;
use helix_stdx::faccess::{copy_metadata, readonly};
use helix_vcs::{DiffHandle, DiffProviderRegistry};
@@ -200,6 +201,19 @@ pub struct Document {
pub focused_at: std::time::Instant,
pub readonly: bool,
+
+ /// Annotations for LSP document color swatches
+ pub color_swatches: Option<DocumentColorSwatches>,
+ // NOTE: ideally this would live on the handler for color swatches. This is blocked on a
+ // large refactor that would make `&mut Editor` available on the `DocumentDidChange` event.
+ pub color_swatch_controller: TaskController,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct DocumentColorSwatches {
+ pub color_swatches: Vec<InlineAnnotation>,
+ pub colors: Vec<Highlight>,
+ pub color_swatches_padding: Vec<InlineAnnotation>,
}
/// Inlay hints for a single `(Document, View)` combo.
@@ -703,6 +717,8 @@ impl Document {
focused_at: std::time::Instant::now(),
readonly: false,
jump_labels: HashMap::new(),
+ color_swatches: None,
+ color_swatch_controller: TaskController::new(),
}
}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 65976f2c..27a985ac 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -456,6 +456,8 @@ pub struct LspConfig {
pub display_signature_help_docs: bool,
/// Display inlay hints
pub display_inlay_hints: bool,
+ /// Display document color swatches
+ pub display_color_swatches: bool,
/// Whether to enable snippet support
pub snippets: bool,
/// Whether to include declaration in the goto reference query
@@ -473,6 +475,7 @@ impl Default for LspConfig {
display_inlay_hints: false,
snippets: true,
goto_reference_include_declaration: true,
+ display_color_swatches: true,
}
}
}
diff --git a/helix-view/src/handlers.rs b/helix-view/src/handlers.rs
index 86217c23..258ed89e 100644
--- a/helix-view/src/handlers.rs
+++ b/helix-view/src/handlers.rs
@@ -21,6 +21,7 @@ pub struct Handlers {
pub completions: CompletionHandler,
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
pub auto_save: Sender<AutoSaveEvent>,
+ pub document_colors: Sender<lsp::DocumentColorsEvent>,
}
impl Handlers {
diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs
index 81388540..c1041b2a 100644
--- a/helix-view/src/handlers/lsp.rs
+++ b/helix-view/src/handlers/lsp.rs
@@ -5,7 +5,7 @@ use crate::editor::Action;
use crate::events::{
DiagnosticsDidChange, DocumentDidChange, DocumentDidClose, LanguageServerInitialized,
};
-use crate::Editor;
+use crate::{DocumentId, Editor};
use helix_core::diagnostic::DiagnosticProvider;
use helix_core::Uri;
use helix_event::register_hook;
@@ -14,6 +14,8 @@ use helix_lsp::{lsp, LanguageServerId, OffsetEncoding};
use super::Handlers;
+pub struct DocumentColorsEvent(pub DocumentId);
+
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Automatic,
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index c3f6af88..af8f03bc 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -5,7 +5,7 @@ use std::{
};
use anyhow::{anyhow, Result};
-use helix_core::hashmap;
+use helix_core::{hashmap, syntax::Highlight};
use helix_loader::merge_toml_values;
use log::warn;
use once_cell::sync::Lazy;
@@ -293,9 +293,39 @@ fn build_theme_values(
}
impl Theme {
+ /// To allow `Highlight` to represent arbitrary RGB colors without turning it into an enum,
+ /// we interpret the last 3 bytes of a `Highlight` as RGB colors.
+ const RGB_START: usize = (usize::MAX << (8 + 8 + 8)) - 1;
+
+ /// Interpret a Highlight with the RGB foreground
+ fn decode_rgb_highlight(rgb: usize) -> Option<(u8, u8, u8)> {
+ (rgb > Self::RGB_START).then(|| {
+ let [b, g, r, ..] = rgb.to_ne_bytes();
+ (r, g, b)
+ })
+ }
+
+ /// Create a Highlight that represents an RGB color
+ pub fn rgb_highlight(r: u8, g: u8, b: u8) -> Highlight {
+ Highlight(usize::from_ne_bytes([
+ b,
+ g,
+ r,
+ u8::MAX,
+ u8::MAX,
+ u8::MAX,
+ u8::MAX,
+ u8::MAX,
+ ]))
+ }
+
#[inline]
pub fn highlight(&self, index: usize) -> Style {
- self.highlights[index]
+ if let Some((red, green, blue)) = Self::decode_rgb_highlight(index) {
+ Style::new().fg(Color::Rgb(red, green, blue))
+ } else {
+ self.highlights[index]
+ }
}
#[inline]
@@ -589,4 +619,61 @@ mod tests {
.add_modifier(Modifier::BOLD)
);
}
+
+ // tests for parsing an RGB `Highlight`
+
+ #[test]
+ fn convert_to_and_from() {
+ let (r, g, b) = (0xFF, 0xFE, 0xFA);
+ let highlight = Theme::rgb_highlight(r, g, b);
+ assert_eq!(Theme::decode_rgb_highlight(highlight.0), Some((r, g, b)));
+ }
+
+ /// make sure we can store all the colors at the end
+ /// ```
+ /// FF FF FF FF FF FF FF FF
+ /// xor
+ /// FF FF FF FF FF 00 00 00
+ /// =
+ /// 00 00 00 00 00 FF FF FF
+ /// ```
+ ///
+ /// where the ending `(FF, FF, FF)` represents `(r, g, b)`
+ #[test]
+ fn full_numeric_range() {
+ assert_eq!(usize::MAX ^ Theme::RGB_START, 256_usize.pow(3));
+ assert_eq!(Theme::RGB_START + 256_usize.pow(3), usize::MAX);
+ }
+
+ #[test]
+ fn retrieve_color() {
+ // color in the middle
+ let (r, g, b) = (0x14, 0xAA, 0xF7);
+ assert_eq!(
+ Theme::default().highlight(Theme::rgb_highlight(r, g, b).0),
+ Style::new().fg(Color::Rgb(r, g, b))
+ );
+ // pure black
+ let (r, g, b) = (0x00, 0x00, 0x00);
+ assert_eq!(
+ Theme::default().highlight(Theme::rgb_highlight(r, g, b).0),
+ Style::new().fg(Color::Rgb(r, g, b))
+ );
+ // pure white
+ let (r, g, b) = (0xff, 0xff, 0xff);
+ assert_eq!(
+ Theme::default().highlight(Theme::rgb_highlight(r, g, b).0),
+ Style::new().fg(Color::Rgb(r, g, b))
+ );
+ }
+
+ #[test]
+ #[should_panic(
+ expected = "index out of bounds: the len is 0 but the index is 18446744073692774399"
+ )]
+ fn out_of_bounds() {
+ let (r, g, b) = (0x00, 0x00, 0x00);
+
+ Theme::default().highlight(Theme::rgb_highlight(r, g, b).0 - 1);
+ }
}
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index a229f01e..d6f10753 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,7 +1,7 @@
use crate::{
align_view,
annotations::diagnostics::InlineDiagnostics,
- document::DocumentInlayHints,
+ document::{DocumentColorSwatches, DocumentInlayHints},
editor::{GutterConfig, GutterType},
graphics::Rect,
handlers::diagnostics::DiagnosticsHandler,
@@ -482,6 +482,23 @@ impl View {
.add_inline_annotations(padding_after_inlay_hints, None);
};
let config = doc.config.load();
+
+ if config.lsp.display_color_swatches {
+ if let Some(DocumentColorSwatches {
+ color_swatches,
+ colors,
+ color_swatches_padding,
+ }) = &doc.color_swatches
+ {
+ for (color_swatch, color) in color_swatches.iter().zip(colors) {
+ text_annotations
+ .add_inline_annotations(std::slice::from_ref(color_swatch), Some(*color));
+ }
+
+ text_annotations.add_inline_annotations(color_swatches_padding, None);
+ }
+ }
+
let width = self.inner_width(doc);
let enable_cursor_line = self
.diagnostics_handler