mirror of
https://github.com/helix-editor/helix.git
synced 2025-03-31 09:27:45 +03:00
Color swatches ( 🟩 green 🟥 #ffaaaa ) (#12308)
This commit is contained in:
parent
8ff544757f
commit
0ee5850016
11 changed files with 363 additions and 4 deletions
|
@ -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` |
|
||||
|
|
|
@ -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}",)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
204
helix-term/src/handlers/document_colors.rs
Normal file
204
helix-term/src/handlers/document_colors.rs
Normal 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(())
|
||||
});
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue