Color swatches ( 🟩 green 🟥 #ffaaaa ) (#12308)

This commit is contained in:
Nik Revenco 2025-03-23 21:07:02 +00:00 committed by GitHub
parent 8ff544757f
commit 0ee5850016
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 363 additions and 4 deletions

View file

@ -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` |

View file

@ -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}",)
}

View file

@ -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,

View file

@ -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
}

View file

@ -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(())
});
}

View file

@ -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(),
}
}

View file

@ -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,
}
}
}

View file

@ -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 {

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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