1use std::collections::HashMap;
22use std::sync::{Arc, RwLock};
23
24use arc_swap::ArcSwap;
25
26use crate::element::{ColorRef, ElementId, ElementName, ElementOwner, StyleSpec, ThemeElement};
27use crate::palette::{Palette, default_palette};
28use crate::themes::{NamedTheme, builtin_themes};
29use crate::{Color, FontScale, Style, Weight};
30
31const MAX_INHERIT_DEPTH: u8 = 16;
35
36#[derive(Debug, Clone, PartialEq)]
41pub struct ResolvedTheme {
42 styles: Box<[Style]>,
43 version: u64,
44}
45
46impl ResolvedTheme {
47 pub fn get(&self, id: ElementId) -> Style {
51 self.styles
52 .get(id.0 as usize)
53 .copied()
54 .unwrap_or_else(Style::empty)
55 }
56
57 pub fn version(&self) -> u64 {
61 self.version
62 }
63
64 pub fn len(&self) -> usize {
65 self.styles.len()
66 }
67
68 pub fn is_empty(&self) -> bool {
69 self.styles.is_empty()
70 }
71}
72
73impl Default for ResolvedTheme {
74 fn default() -> Self {
79 ResolvedTheme {
80 styles: Box::new([]),
81 version: 0,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq)]
93pub struct ElementInfo {
94 pub name: ElementName,
95 pub owner: ElementOwner,
96 pub default: StyleSpec,
101 pub doc: &'static str,
102 pub resolved: Style,
105}
106
107pub trait ThemeRegistry: Send + Sync {
111 fn register(
115 &self,
116 name: ElementName,
117 owner: ElementOwner,
118 default: StyleSpec,
119 doc: &'static str,
120 ) -> ElementId;
121
122 fn id(&self, name: &ElementName) -> Option<ElementId>;
124
125 fn resolved(&self) -> Arc<ResolvedTheme>;
128
129 fn set_override(&self, name: ElementName, spec: StyleSpec);
134
135 fn set_theme(&self, palette: Palette, overrides: Vec<(ElementName, StyleSpec)>);
139
140 fn describe(&self, name: &ElementName) -> Option<ElementInfo>;
148
149 fn element_names(&self) -> Vec<String>;
154
155 fn unregister_element(&self, name: &ElementName) -> bool;
173
174 fn register_theme(&self, theme: NamedTheme);
181
182 fn theme_names(&self) -> Vec<String>;
186
187 fn apply_theme(&self, name: &str) -> bool;
193
194 fn active_theme(&self) -> (Palette, Vec<(ElementName, StyleSpec)>);
201}
202
203pub type ThemeRegistryHandle = Arc<dyn ThemeRegistry>;
206
207struct RegistryInner {
208 elements: Vec<ThemeElement>,
210 by_name: HashMap<ElementName, ElementId>,
211 palette: Palette,
212 overrides: HashMap<ElementName, StyleSpec>,
218 themes: Vec<NamedTheme>,
224 version: u64,
225 dirty: bool,
226}
227
228pub struct InMemoryThemeRegistry {
232 inner: RwLock<RegistryInner>,
233 resolved: ArcSwap<ResolvedTheme>,
234}
235
236impl InMemoryThemeRegistry {
237 pub fn new(palette: Palette) -> Self {
240 InMemoryThemeRegistry {
241 inner: RwLock::new(RegistryInner {
242 elements: Vec::new(),
243 by_name: HashMap::new(),
244 palette,
245 overrides: HashMap::new(),
246 themes: Vec::new(),
247 version: 0,
248 dirty: true,
249 }),
250 resolved: ArcSwap::from_pointee(ResolvedTheme {
251 styles: Box::new([]),
252 version: 0,
253 }),
254 }
255 }
256
257 pub fn with_defaults() -> Self {
261 let reg = Self::new(default_palette());
262 register_builtins(®);
263 for theme in builtin_themes() {
267 reg.register_theme(theme);
268 }
269 reg.rebuild();
270 reg
271 }
272
273 pub fn set_palette(&self, palette: Palette) {
276 let mut inner = self.inner.write().expect("theme registry lock poisoned");
277 inner.palette = palette;
278 inner.dirty = true;
279 }
280
281 pub fn rebuild(&self) {
285 let mut guard = self.inner.write().expect("theme registry lock poisoned");
286 let n = guard.elements.len();
287 let mut styles = Vec::with_capacity(n);
288 {
289 let inner: &RegistryInner = &guard;
290 for i in 0..n {
291 styles.push(resolve_element(inner, ElementId(i as u32), 0));
292 }
293 }
294 guard.version = guard.version.wrapping_add(1);
295 let version = guard.version;
296 guard.dirty = false;
297 drop(guard);
298 self.resolved.store(Arc::new(ResolvedTheme {
299 styles: styles.into_boxed_slice(),
300 version,
301 }));
302 }
303}
304
305impl ThemeRegistry for InMemoryThemeRegistry {
306 fn register(
307 &self,
308 name: ElementName,
309 owner: ElementOwner,
310 default: StyleSpec,
311 doc: &'static str,
312 ) -> ElementId {
313 let mut inner = self.inner.write().expect("theme registry lock poisoned");
314 if let Some(id) = inner.by_name.get(&name) {
315 return *id;
316 }
317 let id = ElementId(inner.elements.len() as u32);
318 inner.by_name.insert(name.clone(), id);
319 inner.elements.push(ThemeElement {
320 id,
321 name,
322 owner,
323 default,
324 doc,
325 });
326 inner.dirty = true;
327 id
328 }
329
330 fn id(&self, name: &ElementName) -> Option<ElementId> {
331 self.inner
332 .read()
333 .expect("theme registry lock poisoned")
334 .by_name
335 .get(name)
336 .copied()
337 }
338
339 fn unregister_element(&self, name: &ElementName) -> bool {
340 let mut inner = self.inner.write().expect("theme registry lock poisoned");
341 let removed = inner.by_name.remove(name).is_some();
342 if removed {
343 inner.dirty = true;
347 }
348 removed
349 }
350
351 fn resolved(&self) -> Arc<ResolvedTheme> {
352 let dirty = self
353 .inner
354 .read()
355 .expect("theme registry lock poisoned")
356 .dirty;
357 if dirty {
358 self.rebuild();
359 }
360 self.resolved.load_full()
361 }
362
363 fn set_override(&self, name: ElementName, spec: StyleSpec) {
364 let mut inner = self.inner.write().expect("theme registry lock poisoned");
365 inner.overrides.insert(name, spec);
366 inner.dirty = true;
367 }
368
369 fn set_theme(&self, palette: Palette, overrides: Vec<(ElementName, StyleSpec)>) {
370 let mut inner = self.inner.write().expect("theme registry lock poisoned");
371 inner.palette = palette;
372 inner.overrides = overrides.into_iter().collect();
373 inner.dirty = true;
374 }
375
376 fn register_theme(&self, theme: NamedTheme) {
377 let mut inner = self.inner.write().expect("theme registry lock poisoned");
378 if let Some(slot) = inner.themes.iter_mut().find(|t| t.name == theme.name) {
381 *slot = theme;
382 } else {
383 inner.themes.push(theme);
384 }
385 }
386
387 fn theme_names(&self) -> Vec<String> {
388 let inner = self.inner.read().expect("theme registry lock poisoned");
389 inner.themes.iter().map(|t| t.name.to_string()).collect()
390 }
391
392 fn element_names(&self) -> Vec<String> {
393 let inner = self.inner.read().expect("theme registry lock poisoned");
394 let mut names: Vec<String> = inner
395 .by_name
396 .keys()
397 .map(|n| n.as_str().to_string())
398 .collect();
399 names.sort();
400 names
401 }
402
403 fn apply_theme(&self, name: &str) -> bool {
404 let found = {
408 let inner = self.inner.read().expect("theme registry lock poisoned");
409 inner
410 .themes
411 .iter()
412 .find(|t| t.name == name)
413 .map(|t| (t.palette.clone(), t.overrides.clone()))
414 };
415 match found {
416 Some((palette, overrides)) => {
417 self.set_theme(palette, overrides);
418 true
419 }
420 None => false,
421 }
422 }
423
424 fn active_theme(&self) -> (Palette, Vec<(ElementName, StyleSpec)>) {
425 let inner = self.inner.read().expect("theme registry lock poisoned");
426 let palette = inner.palette.clone();
427 let overrides = inner
428 .overrides
429 .iter()
430 .map(|(k, v)| (k.clone(), v.clone()))
431 .collect();
432 (palette, overrides)
433 }
434
435 fn describe(&self, name: &ElementName) -> Option<ElementInfo> {
436 let resolved = self.resolved();
442 let inner = self.inner.read().expect("theme registry lock poisoned");
443 let id = *inner.by_name.get(name)?;
444 let elem = inner.elements.get(id.0 as usize)?;
445 Some(ElementInfo {
446 name: elem.name.clone(),
447 owner: elem.owner.clone(),
448 default: elem.default.clone(),
449 doc: elem.doc,
450 resolved: resolved.get(id),
451 })
452 }
453}
454
455fn resolve_element(inner: &RegistryInner, id: ElementId, depth: u8) -> Style {
458 match inner.elements.get(id.0 as usize) {
459 Some(elem) => {
460 let mut style = resolve_spec(inner, &elem.default, depth);
461 if let Some(ovr) = inner.overrides.get(&elem.name) {
465 apply_overlay(inner, &mut style, ovr);
466 }
467 style
468 }
469 None => Style::empty(),
470 }
471}
472
473fn resolve_named(inner: &RegistryInner, name: &ElementName, depth: u8) -> Style {
474 if let Some(id) = inner.by_name.get(name) {
477 return resolve_element(inner, *id, depth);
478 }
479 match name.parent() {
480 Some(parent) => resolve_named(inner, &parent, depth),
481 None => Style::empty(),
482 }
483}
484
485fn resolve_spec(inner: &RegistryInner, spec: &StyleSpec, depth: u8) -> Style {
486 let mut style = match &spec.inherit {
487 Some(parent) if depth < MAX_INHERIT_DEPTH => resolve_named(inner, parent, depth + 1),
488 Some(parent) => {
489 tracing::warn!(
490 "theme: inherit chain too deep at `{}` — breaking cycle",
491 parent
492 );
493 Style::empty()
494 }
495 None => Style::empty(),
496 };
497 apply_overlay(inner, &mut style, spec);
498 style
499}
500
501fn apply_overlay(inner: &RegistryInner, style: &mut Style, spec: &StyleSpec) {
507 if let Some(cref) = &spec.fg
508 && let Some(c) = resolve_color(inner, cref)
509 {
510 style.fg = Some(c);
511 }
512 if let Some(cref) = &spec.bg
513 && let Some(c) = resolve_color(inner, cref)
514 {
515 style.bg = Some(c);
516 }
517
518 let m = &spec.modifiers;
519 if let Some(b) = m.bold {
520 style.modifiers.bold = b;
521 }
522 if let Some(b) = m.italic {
523 style.modifiers.italic = b;
524 }
525 if let Some(b) = m.underline {
526 style.modifiers.underline = b;
527 }
528 if let Some(b) = m.dim {
529 style.modifiers.dim = b;
530 }
531 if let Some(b) = m.reverse {
532 style.modifiers.reverse = b;
533 }
534
535 if let Some(ratio) = spec.scale {
536 style.scale = Some(FontScale::from_ratio(ratio));
537 }
538 if let Some(family) = spec.family {
539 style.family = Some(family);
540 }
541 if let Some(weight) = spec.weight {
542 style.weight = Some(weight);
543 }
544}
545
546fn resolve_color(inner: &RegistryInner, cref: &ColorRef) -> Option<Color> {
547 match cref {
548 ColorRef::Palette(key) => match inner.palette.get(key) {
549 Some(c) => Some(c),
550 None => {
551 tracing::warn!("theme: unknown palette key `{}`", key.as_str());
552 None
553 }
554 },
555 ColorRef::Literal(c) => Some(*c),
556 ColorRef::Default => Some(Color::Default),
557 }
558}
559
560pub fn register_builtins(reg: &dyn ThemeRegistry) {
568 let core = ElementOwner::Core;
569 let reg_one = |name: &'static str, spec: StyleSpec, doc: &'static str| {
570 reg.register(ElementName::from_static(name), core.clone(), spec, doc);
571 };
572 let spec = StyleSpec::new;
573
574 reg_one(
576 "pane.status.active",
577 spec().reverse().bold(),
578 "Active pane's status line.",
579 );
580 reg_one(
581 "pane.status.inactive",
582 spec().fg("overlay").dim(),
583 "Inactive pane's status line.",
584 );
585 reg_one(
586 "pane.inactive_overlay",
587 spec().dim(),
588 "Dim overlay on inactive panes' content.",
589 );
590 reg_one(
591 "pane.separator",
592 spec().fg("overlay"),
593 "Split separator between panes.",
594 );
595
596 reg_one(
603 "modeline.active",
604 spec().bg("surface1"),
605 "Active pane's modeline bar (base background; per-role fg overlays it).",
606 );
607 reg_one(
608 "modeline.inactive",
609 spec().bg("surface0").fg("overlay"),
610 "Inactive pane's modeline bar (uniform muted; no per-role colour).",
611 );
612 reg_one(
613 "modeline.mode",
614 spec().fg("blue").bold(),
615 "Lean modal-state tag (`NOR`/`INS`/…) in the active modeline.",
616 );
617 reg_one(
618 "modeline.path",
619 spec().fg("text"),
620 "Buffer path segment in the active modeline.",
621 );
622 reg_one(
623 "modeline.position",
624 spec().fg("subtext"),
625 "Cursor line:column segment in the active modeline.",
626 );
627 reg_one(
628 "modeline.lang",
629 spec().fg("teal"),
630 "Language label segment in the active modeline.",
631 );
632 reg_one(
633 "modeline.mode_item",
634 spec().fg("subtext"),
635 "Mode-contributed items (LSP / diff) in the active modeline.",
636 );
637
638 reg_one(
641 "editor.background",
642 spec().bg("base"),
643 "Editor canvas background.",
644 );
645 reg_one(
646 "editor.foreground",
647 spec().fg("text"),
648 "Editor default foreground text.",
649 );
650 reg_one(
651 "editor.cursor",
652 spec().bg("text").fg("base"),
653 "Block-cursor cell (inverted: bg=text fg=base).",
654 );
655 reg_one(
656 "ui.popup.background",
657 spec().bg("mantle"),
658 "Popup / overlay surface background.",
659 );
660 reg_one(
661 "ui.popup.title",
662 spec().fg("blue").bold(),
663 "Popup header title (bold accent — describe/help/hover popups).",
664 );
665 reg_one(
666 "ui.popup.hint",
667 spec().fg("overlay"),
668 "Popup header hint (dim — e.g. 'Esc to dismiss').",
669 );
670
671 reg_one(
673 "file_tree.dir",
674 spec().fg("blue").bold(),
675 "Directory entries in the file tree.",
676 );
677 reg_one(
678 "file_tree.hidden",
679 spec().fg("overlay").dim(),
680 "Hidden entries in the file tree.",
681 );
682 reg_one("file_tree.file", spec(), "Regular file entries.");
683
684 reg_one("terminal.ansi.0", spec().fg("surface1"), "ANSI 0 — black.");
691 reg_one("terminal.ansi.1", spec().fg("red"), "ANSI 1 — red.");
692 reg_one("terminal.ansi.2", spec().fg("green"), "ANSI 2 — green.");
693 reg_one("terminal.ansi.3", spec().fg("yellow"), "ANSI 3 — yellow.");
694 reg_one("terminal.ansi.4", spec().fg("blue"), "ANSI 4 — blue.");
695 reg_one("terminal.ansi.5", spec().fg("pink"), "ANSI 5 — magenta.");
696 reg_one("terminal.ansi.6", spec().fg("teal"), "ANSI 6 — cyan.");
697 reg_one("terminal.ansi.7", spec().fg("subtext"), "ANSI 7 — white.");
698 reg_one(
699 "terminal.ansi.8",
700 spec().fg("surface2"),
701 "ANSI 8 — bright black.",
702 );
703 reg_one("terminal.ansi.9", spec().fg("red"), "ANSI 9 — bright red.");
704 reg_one(
705 "terminal.ansi.10",
706 spec().fg("green"),
707 "ANSI 10 — bright green.",
708 );
709 reg_one(
710 "terminal.ansi.11",
711 spec().fg("yellow"),
712 "ANSI 11 — bright yellow.",
713 );
714 reg_one(
715 "terminal.ansi.12",
716 spec().fg("blue"),
717 "ANSI 12 — bright blue.",
718 );
719 reg_one(
720 "terminal.ansi.13",
721 spec().fg("pink"),
722 "ANSI 13 — bright magenta.",
723 );
724 reg_one(
725 "terminal.ansi.14",
726 spec().fg("teal"),
727 "ANSI 14 — bright cyan.",
728 );
729 reg_one(
730 "terminal.ansi.15",
731 spec().fg("text"),
732 "ANSI 15 — bright white.",
733 );
734
735 reg_one(
737 "diagnostic.error",
738 spec().fg("red").bold(),
739 "Error-severity diagnostic sign + text.",
740 );
741 reg_one(
742 "diagnostic.warning",
743 spec().fg("yellow").bold(),
744 "Warning-severity diagnostic.",
745 );
746 reg_one(
747 "diagnostic.info",
748 spec().fg("blue"),
749 "Info-severity diagnostic.",
750 );
751 reg_one(
752 "diagnostic.hint",
753 spec().fg("overlay").dim(),
754 "Hint-severity diagnostic.",
755 );
756
757 reg_one(
759 "whitespace",
760 spec().fg("overlay").dim(),
761 "Rendered whitespace markers.",
762 );
763 reg_one(
764 "whitespace.trailing",
765 spec().fg("red"),
766 "Trailing whitespace markers.",
767 );
768 reg_one(
769 "editor.cursor_line",
770 spec().bg("cursor_line.bg"),
771 "Current-line background tint.",
772 );
773
774 reg_one(
781 "indent.guide",
782 spec().fg("overlay").dim(),
783 "Indentation guide rule.",
784 );
785 reg_one(
786 "indent.guide.active",
787 spec().fg("overlay"),
788 "Indentation guide for the block enclosing the cursor.",
789 );
790
791 reg_one(
793 "messages.timestamp",
794 spec().fg("overlay").dim(),
795 "Timestamp column in *messages*.",
796 );
797 reg_one("messages.trace", spec().dim(), "TRACE-level message.");
798 reg_one("messages.debug", spec().fg("cyan"), "DEBUG-level message.");
799 reg_one("messages.info", spec().fg("green"), "INFO-level message.");
800 reg_one(
801 "messages.warn",
802 spec().fg("yellow").bold(),
803 "WARN-level message.",
804 );
805 reg_one(
806 "messages.error",
807 spec().fg("red").bold(),
808 "ERROR-level message.",
809 );
810
811 reg_one(
813 "diff.add.sign",
814 spec().fg("green").bold(),
815 "`+` gutter sign (added line).",
816 );
817 reg_one(
818 "diff.change.sign",
819 spec().fg("yellow").bold(),
820 "`~` gutter sign (changed line).",
821 );
822 reg_one(
823 "diff.remove.sign",
824 spec().fg("red").bold(),
825 "`-` gutter sign (removed line).",
826 );
827 reg_one(
828 "diff.conflict.sign",
829 spec().fg("purple").bold(),
830 "`?` gutter sign (three-way conflict).",
831 );
832 reg_one(
833 "diff.add.line",
834 spec().bg("diff.add.bg"),
835 "Added-line background tint.",
836 );
837 reg_one(
838 "diff.change.line",
839 spec().bg("diff.change.bg"),
840 "Changed-line background tint.",
841 );
842 reg_one(
843 "diff.remove.line",
844 spec().bg("diff.deletion.bg"),
845 "Removed-line background tint (baseline/left pane of a side-by-side diff). \
846 Reuses the deletion-block palette role for a consistent red.",
847 );
848 reg_one(
855 "diff.add.refine.bg",
856 spec().bg("diff.add.refine.bg"),
857 "Background for the added bytes within a refined diff line (intra-line refinement).",
858 );
859 reg_one(
860 "diff.remove.refine.bg",
861 spec().bg("diff.remove.refine.bg"),
862 "Background for the removed bytes within a refined diff line (intra-line refinement).",
863 );
864 reg_one(
865 "sticky.context.background",
866 spec().bg("surface0"),
867 "Sticky context strip: the backdrop behind the pinned scope headers. \
868 Host chrome — the strip's rows carry the source lines' own syntax \
869 colour, so this styles only what sits behind them.",
870 );
871 reg_one(
872 "sticky.context.line_number",
873 spec().fg("overlay"),
874 "Sticky context strip: the source line numbers in its gutter. Dimmer \
875 than the document's by default — the strip is orientation, not a \
876 place the cursor can be.",
877 );
878 reg_one(
879 "sticky.context.separator",
880 spec().fg("overlay"),
881 "Sticky context strip: the rule beneath it, when `context.separator` \
882 is set. Off by default — a separator is a preference, not an \
883 affordance.",
884 );
885 reg_one(
886 "sticky.context.active",
887 spec().bg("surface1"),
888 "Sticky context strip: the innermost row — the scope the cursor is \
889 actually in. Distinguished from its ancestors because that is the \
890 one line the reader is looking for.",
891 );
892 reg_one(
893 "diff.deletion_block",
894 spec().bg("diff.deletion.bg"),
895 "Deletion-block virtual-row background tint.",
896 );
897 reg_one(
898 "diff.conflict.line",
899 spec().bg("diff.conflict.bg"),
900 "Conflict-region background tint.",
901 );
902 reg_one(
904 "diff.add.text",
905 spec().fg("green"),
906 "Added-line text colour in a unified diff.",
907 );
908 reg_one(
909 "diff.remove.text",
910 spec().fg("red"),
911 "Removed-line text colour in a unified diff.",
912 );
913
914 reg_one(
921 "magit.sha",
922 spec().fg("blue"),
923 "Commit SHA colour (magit-log, magit-blame, magit-rebase's todo).",
924 );
925 reg_one(
926 "magit.branch.current",
927 spec().fg("green").bold(),
928 "The checked-out branch's `* ` marker + name in magit-branch's list.",
929 );
930 reg_one(
931 "magit.ref.decoration",
932 spec().fg("pink"),
933 "Ref-decoration list after a log SHA (`(HEAD -> main, ...)`).",
934 );
935 reg_one(
936 "magit.rebase.verb",
937 spec().fg("purple").bold(),
938 "A rebase-todo verb (pick/reword/edit/squash/fixup/drop).",
939 );
940 reg_one(
941 "magit.author",
942 spec().fg("overlay"),
943 "The author column in magit-blame output.",
944 );
945
946 reg_one(
962 "help.key",
963 spec().fg("yellow").bold(),
964 "A key or chord you press, in a help page (`gr`, `<C-c>g`).",
965 );
966 reg_one(
967 "help.command",
968 spec().fg("blue"),
969 "An ex-command you type, in a help page (`:magit-status`).",
970 );
971 reg_one(
972 "help.action",
973 spec().fg("purple"),
974 "An action id, in a help page (`action:magit-refresh`).",
975 );
976 reg_one(
977 "help.literal",
978 spec().fg("subtext"),
979 "Any other inline literal in a help page — a path, a flag, a filename.",
980 );
981
982 reg_one(
993 "transient.title",
994 spec().fg("text").bold(),
995 "The transient menu's title.",
996 );
997 reg_one(
998 "transient.group",
999 spec().fg("text").bold(),
1000 "A group heading inside a transient menu (`▸ Arguments`).",
1001 );
1002 reg_one(
1003 "transient.key",
1004 spec().fg("yellow"),
1005 "The key that fires a transient row — the most-scanned column.",
1006 );
1007 reg_one(
1008 "transient.key.inactive",
1009 spec().fg("overlay"),
1010 "A transient row ruled out by the multi-key prefix typed so far.",
1011 );
1012 reg_one(
1013 "transient.description",
1014 spec().fg("subtext"),
1015 "A transient row's description column.",
1016 );
1017 reg_one(
1018 "transient.value",
1019 spec().fg("green"),
1020 "A transient flag's `[x]` state or a variable's current value.",
1021 );
1022 reg_one(
1023 "transient.border",
1024 spec().fg("blue"),
1025 "The transient menu's border.",
1026 );
1027
1028 reg_one(
1038 "gutter.fold.open",
1039 spec().fg("overlay"),
1040 "`▾` fold marker on an open (expanded) foldable head row.",
1041 );
1042 reg_one(
1054 "gutter.sign",
1055 spec().fg("text"),
1056 "Fallback colour for a gutter sign whose own theme element is unregistered.",
1057 );
1058 reg_one(
1059 "gutter.fold.closed",
1060 spec().fg("subtext"),
1061 "`▸` fold marker on a closed (collapsed) fold head row.",
1062 );
1063 reg_one(
1070 "gutter.fold.summary",
1071 spec().fg("overlay"),
1072 "The ` ⋯ N lines` summary trailing a closed fold's head row.",
1073 );
1074
1075 reg_one(
1082 "search.match",
1083 spec().bg("overlay"),
1084 "All hlsearch matches (bg tint).",
1085 );
1086 reg_one(
1087 "search.current",
1088 spec().bg(Color::Rgb(0x6c, 0x5a, 0x1e)),
1089 "Current search match (warm bg tint).",
1090 );
1091 reg_one(
1092 "selection",
1093 spec().bg(Color::Rgb(0x45, 0x47, 0x5a)),
1094 "Visual-mode selection bg.",
1095 );
1096 reg_one(
1097 "doc_highlight.read",
1098 spec().bg(Color::Rgb(20, 50, 25)),
1099 "LSP document-highlight: read occurrence.",
1100 );
1101 reg_one(
1102 "doc_highlight.write",
1103 spec().bg(Color::Rgb(60, 20, 20)),
1104 "LSP document-highlight: write/mutation.",
1105 );
1106 reg_one(
1107 "doc_highlight.text",
1108 spec().bg(Color::Rgb(20, 30, 60)),
1109 "LSP document-highlight: text occurrence.",
1110 );
1111 reg_one(
1112 "substitute.preview",
1113 spec().bg(Color::Rgb(0xf3, 0x8b, 0xa8)),
1114 "`:s///` live-preview match (bg; TUI adds strikethrough).",
1115 );
1116 reg_one(
1117 "inlay.hint",
1118 spec().fg(Color::Rgb(0x7f, 0x84, 0x9c)),
1119 "Inlay hint virtual text (overlay1).",
1120 );
1121
1122 reg_one(
1126 "completion.annotation.kind",
1127 spec().fg("subtext"),
1128 "Completion annotation: kind.",
1129 );
1130 reg_one(
1131 "completion.annotation.doc",
1132 spec().fg(Color::Rgb(0x89, 0xdc, 0xeb)),
1133 "Completion annotation: doc snippet.",
1134 );
1135 reg_one(
1136 "completion.annotation.keybinding",
1137 spec().fg("yellow"),
1138 "Completion annotation: keybinding.",
1139 );
1140 reg_one(
1141 "completion.annotation.source",
1142 spec().fg("purple"),
1143 "Completion annotation: source.",
1144 );
1145 reg_one(
1146 "completion.annotation.custom",
1147 spec().fg("blue"),
1148 "Completion annotation: custom/plugin.",
1149 );
1150 reg_one(
1155 "completion.annotation.perm.type",
1156 spec().fg("blue"),
1157 "Marginalia: permission type char (d/l/b/c/p/s/-).",
1158 );
1159 reg_one(
1160 "completion.annotation.perm.read",
1161 spec().fg("yellow"),
1162 "Marginalia: permission read bit (r).",
1163 );
1164 reg_one(
1165 "completion.annotation.perm.write",
1166 spec().fg("red"),
1167 "Marginalia: permission write bit (w).",
1168 );
1169 reg_one(
1170 "completion.annotation.perm.exec",
1171 spec().fg("green"),
1172 "Marginalia: permission execute bit (x).",
1173 );
1174 reg_one(
1175 "completion.annotation.perm.special",
1176 spec().fg("pink"),
1177 "Marginalia: permission special bit (setuid/setgid/sticky).",
1178 );
1179 reg_one(
1180 "completion.annotation.perm.none",
1181 spec().fg("overlay"),
1182 "Marginalia: permission absent bit (-).",
1183 );
1184 reg_one(
1185 "completion.annotation.size",
1186 spec().fg("orange"),
1187 "Marginalia: file size column.",
1188 );
1189 reg_one(
1190 "completion.annotation.mtime",
1191 spec().fg("green"),
1192 "Marginalia: file modified-time column.",
1193 );
1194 reg_one(
1203 "picker.title",
1204 spec().fg("teal"),
1205 "Picker prompt: the source name (`buffers`, `project-files`).",
1206 );
1207 reg_one(
1208 "picker.prompt",
1209 spec().fg("teal"),
1210 "Picker prompt: the `>` marker where typing begins.",
1211 );
1212 reg_one(
1213 "picker.count",
1214 spec().fg("overlay"),
1215 "Picker prompt: the `(3/40)` match count and `searching…` status.",
1216 );
1217 reg_one(
1225 "picker.root",
1226 spec().fg("blue"),
1227 "Picker prompt: the root a rooted picker is scoped to.",
1228 );
1229 reg_one(
1233 "completion.annotation.location.path",
1234 spec().fg("overlay"),
1235 "Marginalia: location path head (grep/jumps/marks).",
1236 );
1237 reg_one(
1238 "completion.annotation.location.line",
1239 spec().fg("yellow"),
1240 "Marginalia: location line number.",
1241 );
1242 reg_one(
1243 "completion.annotation.location.col",
1244 spec().fg("overlay"),
1245 "Marginalia: location column number.",
1246 );
1247 reg_one(
1248 "completion.annotation.status.dirty",
1249 spec().fg("red"),
1250 "Marginalia: dirty-buffer marker.",
1251 );
1252 reg_one(
1253 "completion.annotation.status.active",
1254 spec().fg("green"),
1255 "Marginalia: current-buffer marker.",
1256 );
1257 reg_one(
1258 "completion.annotation.latency.reflex",
1259 spec().fg("green"),
1260 "Marginalia: reflex-latency command class.",
1261 );
1262 reg_one(
1263 "completion.annotation.latency.display",
1264 spec().fg("blue"),
1265 "Marginalia: display-latency command class.",
1266 );
1267 reg_one(
1268 "completion.annotation.latency.background",
1269 spec().fg("orange"),
1270 "Marginalia: background-latency command class.",
1271 );
1272 reg_one(
1273 "completion.annotation.args",
1274 spec().fg("subtext"),
1275 "Marginalia: command argument hint.",
1276 );
1277 reg_one(
1278 "completion.annotation.buffer-id",
1279 spec().fg("overlay"),
1280 "Marginalia: buffer id (#N).",
1281 );
1282 reg_one(
1283 "completion.annotation.register",
1284 spec().fg("purple"),
1285 "Marginalia: register / mark name.",
1286 );
1287
1288 reg_one(
1290 "syntax.default",
1291 spec().fg("text"),
1292 "Default foreground text.",
1293 );
1294 reg_one(
1295 "syntax.comment",
1296 spec().fg("overlay").italic(),
1297 "Block / doc comments.",
1298 );
1299 reg_one(
1301 "syntax.line_comment",
1302 spec().inherit("syntax.comment"),
1303 "Line comments (inherits syntax.comment).",
1304 );
1305 reg_one("syntax.string", spec().fg("green"), "String literals.");
1306 reg_one(
1307 "syntax.keyword",
1308 spec().fg("purple").bold(),
1309 "Language keywords.",
1310 );
1311 reg_one("syntax.type", spec().fg("yellow"), "Type names.");
1312 reg_one("syntax.number", spec().fg("orange"), "Numeric literals.");
1313 reg_one("syntax.function", spec().fg("blue"), "Function names.");
1314 reg_one("syntax.constant", spec().fg("orange"), "Constants.");
1315 reg_one("syntax.variable", spec().fg("text"), "Variables.");
1316 reg_one("syntax.operator", spec().fg("teal"), "Operators.");
1317 reg_one(
1318 "syntax.punctuation",
1319 spec().fg("subtext"),
1320 "Punctuation / delimiters.",
1321 );
1322 reg_one(
1323 "syntax.attribute",
1324 spec().fg("red"),
1325 "Attributes / annotations.",
1326 );
1327 reg_one(
1358 "syntax.heading.1",
1359 spec().fg("red").bold().weight(Weight::ExtraBold).scale(1.6),
1360 "Markup heading level 1.",
1361 );
1362 reg_one(
1363 "syntax.heading.2",
1364 spec().fg("orange").bold().weight(Weight::Bold).scale(1.4),
1365 "Markup heading level 2.",
1366 );
1367 reg_one(
1368 "syntax.heading.3",
1369 spec().fg("yellow").bold().weight(Weight::Bold).scale(1.25),
1370 "Markup heading level 3.",
1371 );
1372 reg_one(
1373 "syntax.heading.4",
1374 spec().fg("green").bold().scale(1.15),
1375 "Markup heading level 4.",
1376 );
1377 reg_one(
1378 "syntax.heading.5",
1379 spec().fg("blue").bold().scale(1.1),
1380 "Markup heading level 5.",
1381 );
1382 reg_one(
1383 "syntax.heading.6",
1384 spec().fg("purple").bold().scale(1.05),
1385 "Markup heading level 6.",
1386 );
1387 reg_one(
1388 "syntax.bold",
1389 spec().fg("maroon").bold(),
1390 "Strong / bold markup.",
1391 );
1392 reg_one(
1393 "syntax.italic",
1394 spec().fg("pink").italic(),
1395 "Emphasis / italic markup.",
1396 );
1397 reg_one(
1398 "syntax.link",
1399 spec().fg("blue").underline(),
1400 "Markup links.",
1401 );
1402 reg_one("syntax.url", spec().fg("cyan").underline(), "Bare URLs.");
1403 reg_one(
1404 "syntax.markup_raw",
1405 spec().fg("green"),
1412 "Inline code / raw markup.",
1413 );
1414 reg_one(
1415 "syntax.code_block",
1416 spec().bg("surface0"),
1417 "Full-width background tint for a fenced/indented code block.",
1418 );
1419 reg_one(
1420 "syntax.markup",
1421 spec().fg("subtext").bold(),
1422 "Generic markup punctuation.",
1423 );
1424}
1425
1426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1438pub struct BuiltinElementIds {
1439 pub diagnostic_error: ElementId,
1441 pub diagnostic_warning: ElementId,
1442 pub diagnostic_info: ElementId,
1443 pub diagnostic_hint: ElementId,
1444 pub diff_add_sign: ElementId,
1446 pub diff_change_sign: ElementId,
1447 pub diff_remove_sign: ElementId,
1448 pub diff_conflict_sign: ElementId,
1449 pub diff_add_line: ElementId,
1450 pub diff_add_refine_bg: ElementId,
1453 pub diff_remove_refine_bg: ElementId,
1454 pub diff_change_line: ElementId,
1455 pub diff_remove_line: ElementId,
1457 pub diff_deletion_block: ElementId,
1458 pub sticky_context_background: ElementId,
1460 pub sticky_context_line_number: ElementId,
1462 pub sticky_context_active: ElementId,
1464 pub sticky_context_separator: ElementId,
1466 pub diff_conflict_line: ElementId,
1467 pub diff_add_text: ElementId,
1469 pub diff_remove_text: ElementId,
1470 pub magit_sha: ElementId,
1474 pub magit_branch_current: ElementId,
1475 pub magit_ref_decoration: ElementId,
1476 pub magit_rebase_verb: ElementId,
1477 pub magit_author: ElementId,
1478 pub help_key: ElementId,
1485 pub help_command: ElementId,
1486 pub help_action: ElementId,
1487 pub help_literal: ElementId,
1488 pub transient_title: ElementId,
1492 pub transient_group: ElementId,
1493 pub transient_key: ElementId,
1494 pub transient_key_inactive: ElementId,
1495 pub transient_description: ElementId,
1496 pub transient_value: ElementId,
1497 pub transient_border: ElementId,
1498 pub gutter_sign: ElementId,
1507 pub gutter_fold_open: ElementId,
1508 pub gutter_fold_closed: ElementId,
1509 pub gutter_fold_summary: ElementId,
1512 pub pane_status_active: ElementId,
1516 pub pane_status_inactive: ElementId,
1517 pub pane_separator: ElementId,
1518 pub pane_inactive_overlay: ElementId,
1519 pub modeline_active: ElementId,
1521 pub modeline_inactive: ElementId,
1522 pub modeline_mode: ElementId,
1523 pub modeline_path: ElementId,
1524 pub modeline_position: ElementId,
1525 pub modeline_lang: ElementId,
1526 pub modeline_mode_item: ElementId,
1527 pub editor_background: ElementId,
1531 pub editor_foreground: ElementId,
1532 pub editor_cursor: ElementId,
1533 pub ui_popup_background: ElementId,
1534 pub ui_popup_title: ElementId,
1535 pub ui_popup_hint: ElementId,
1536 pub file_tree_dir: ElementId,
1537 pub file_tree_hidden: ElementId,
1538 pub file_tree_file: ElementId,
1539 pub editor_cursor_line: ElementId,
1543 pub messages_timestamp: ElementId,
1544 pub messages_trace: ElementId,
1545 pub messages_debug: ElementId,
1546 pub messages_info: ElementId,
1547 pub messages_warn: ElementId,
1548 pub messages_error: ElementId,
1549 pub syntax_default: ElementId,
1552 pub syntax_comment: ElementId,
1553 pub syntax_line_comment: ElementId,
1554 pub syntax_string: ElementId,
1555 pub syntax_keyword: ElementId,
1556 pub syntax_type: ElementId,
1557 pub syntax_number: ElementId,
1558 pub syntax_function: ElementId,
1559 pub syntax_constant: ElementId,
1560 pub syntax_variable: ElementId,
1561 pub syntax_operator: ElementId,
1562 pub syntax_punctuation: ElementId,
1563 pub syntax_attribute: ElementId,
1564 pub syntax_heading_1: ElementId,
1565 pub syntax_heading_2: ElementId,
1566 pub syntax_heading_3: ElementId,
1567 pub syntax_heading_4: ElementId,
1568 pub syntax_heading_5: ElementId,
1569 pub syntax_heading_6: ElementId,
1570 pub syntax_bold: ElementId,
1571 pub syntax_italic: ElementId,
1572 pub syntax_link: ElementId,
1573 pub syntax_url: ElementId,
1574 pub syntax_markup_raw: ElementId,
1575 pub syntax_code_block: ElementId,
1577 pub syntax_markup: ElementId,
1578 pub whitespace: ElementId,
1579 pub whitespace_trailing: ElementId,
1580 pub indent_guide: ElementId,
1583 pub indent_guide_active: ElementId,
1584 pub search_match: ElementId,
1588 pub search_current: ElementId,
1589 pub selection: ElementId,
1590 pub doc_highlight_read: ElementId,
1591 pub doc_highlight_write: ElementId,
1592 pub doc_highlight_text: ElementId,
1593 pub substitute_preview: ElementId,
1594 pub inlay_hint: ElementId,
1595 pub completion_annotation_kind: ElementId,
1598 pub completion_annotation_doc: ElementId,
1599 pub completion_annotation_keybinding: ElementId,
1600 pub completion_annotation_source: ElementId,
1601 pub completion_annotation_custom: ElementId,
1602 pub completion_annotation_perm_type: ElementId,
1604 pub completion_annotation_perm_read: ElementId,
1605 pub completion_annotation_perm_write: ElementId,
1606 pub completion_annotation_perm_exec: ElementId,
1607 pub completion_annotation_perm_special: ElementId,
1608 pub completion_annotation_perm_none: ElementId,
1609 pub completion_annotation_size: ElementId,
1610 pub completion_annotation_mtime: ElementId,
1611 pub picker_root: ElementId,
1613 pub picker_title: ElementId,
1614 pub picker_prompt: ElementId,
1615 pub picker_count: ElementId,
1616 pub completion_annotation_location_path: ElementId,
1618 pub completion_annotation_location_line: ElementId,
1619 pub completion_annotation_location_col: ElementId,
1620 pub completion_annotation_status_dirty: ElementId,
1621 pub completion_annotation_status_active: ElementId,
1622 pub completion_annotation_latency_reflex: ElementId,
1623 pub completion_annotation_latency_display: ElementId,
1624 pub completion_annotation_latency_background: ElementId,
1625 pub completion_annotation_args: ElementId,
1626 pub completion_annotation_buffer_id: ElementId,
1627 pub completion_annotation_register: ElementId,
1628 pub terminal_ansi: [ElementId; 16],
1633}
1634
1635impl Default for BuiltinElementIds {
1636 fn default() -> Self {
1640 BuiltinElementIds {
1641 diagnostic_error: ElementId::INVALID,
1642 diagnostic_warning: ElementId::INVALID,
1643 diagnostic_info: ElementId::INVALID,
1644 diagnostic_hint: ElementId::INVALID,
1645 diff_add_sign: ElementId::INVALID,
1646 diff_change_sign: ElementId::INVALID,
1647 diff_remove_sign: ElementId::INVALID,
1648 diff_conflict_sign: ElementId::INVALID,
1649 diff_add_line: ElementId::INVALID,
1650 diff_add_refine_bg: ElementId::INVALID,
1651 diff_remove_refine_bg: ElementId::INVALID,
1652 diff_change_line: ElementId::INVALID,
1653 diff_remove_line: ElementId::INVALID,
1654 diff_deletion_block: ElementId::INVALID,
1655 sticky_context_background: ElementId::INVALID,
1656 sticky_context_line_number: ElementId::INVALID,
1657 sticky_context_active: ElementId::INVALID,
1658 sticky_context_separator: ElementId::INVALID,
1659 diff_conflict_line: ElementId::INVALID,
1660 diff_add_text: ElementId::INVALID,
1661 diff_remove_text: ElementId::INVALID,
1662 magit_sha: ElementId::INVALID,
1663 magit_branch_current: ElementId::INVALID,
1664 magit_ref_decoration: ElementId::INVALID,
1665 magit_rebase_verb: ElementId::INVALID,
1666 magit_author: ElementId::INVALID,
1667 help_key: ElementId::INVALID,
1668 help_command: ElementId::INVALID,
1669 help_action: ElementId::INVALID,
1670 help_literal: ElementId::INVALID,
1671 transient_title: ElementId::INVALID,
1672 transient_group: ElementId::INVALID,
1673 transient_key: ElementId::INVALID,
1674 transient_key_inactive: ElementId::INVALID,
1675 transient_description: ElementId::INVALID,
1676 transient_value: ElementId::INVALID,
1677 transient_border: ElementId::INVALID,
1678 gutter_sign: ElementId::INVALID,
1679 gutter_fold_open: ElementId::INVALID,
1680 gutter_fold_summary: ElementId::INVALID,
1681 gutter_fold_closed: ElementId::INVALID,
1682 pane_status_active: ElementId::INVALID,
1683 pane_status_inactive: ElementId::INVALID,
1684 pane_separator: ElementId::INVALID,
1685 pane_inactive_overlay: ElementId::INVALID,
1686 terminal_ansi: [ElementId::INVALID; 16],
1687 modeline_active: ElementId::INVALID,
1688 modeline_inactive: ElementId::INVALID,
1689 modeline_mode: ElementId::INVALID,
1690 modeline_path: ElementId::INVALID,
1691 modeline_position: ElementId::INVALID,
1692 modeline_lang: ElementId::INVALID,
1693 modeline_mode_item: ElementId::INVALID,
1694 editor_background: ElementId::INVALID,
1695 editor_foreground: ElementId::INVALID,
1696 editor_cursor: ElementId::INVALID,
1697 ui_popup_background: ElementId::INVALID,
1698 ui_popup_title: ElementId::INVALID,
1699 ui_popup_hint: ElementId::INVALID,
1700 file_tree_dir: ElementId::INVALID,
1701 file_tree_hidden: ElementId::INVALID,
1702 file_tree_file: ElementId::INVALID,
1703 editor_cursor_line: ElementId::INVALID,
1704 messages_timestamp: ElementId::INVALID,
1705 messages_trace: ElementId::INVALID,
1706 messages_debug: ElementId::INVALID,
1707 messages_info: ElementId::INVALID,
1708 messages_warn: ElementId::INVALID,
1709 messages_error: ElementId::INVALID,
1710 syntax_default: ElementId::INVALID,
1711 syntax_comment: ElementId::INVALID,
1712 syntax_line_comment: ElementId::INVALID,
1713 syntax_string: ElementId::INVALID,
1714 syntax_keyword: ElementId::INVALID,
1715 syntax_type: ElementId::INVALID,
1716 syntax_number: ElementId::INVALID,
1717 syntax_function: ElementId::INVALID,
1718 syntax_constant: ElementId::INVALID,
1719 syntax_variable: ElementId::INVALID,
1720 syntax_operator: ElementId::INVALID,
1721 syntax_punctuation: ElementId::INVALID,
1722 syntax_attribute: ElementId::INVALID,
1723 syntax_heading_1: ElementId::INVALID,
1724 syntax_heading_2: ElementId::INVALID,
1725 syntax_heading_3: ElementId::INVALID,
1726 syntax_heading_4: ElementId::INVALID,
1727 syntax_heading_5: ElementId::INVALID,
1728 syntax_heading_6: ElementId::INVALID,
1729 syntax_bold: ElementId::INVALID,
1730 syntax_italic: ElementId::INVALID,
1731 syntax_link: ElementId::INVALID,
1732 syntax_url: ElementId::INVALID,
1733 syntax_markup_raw: ElementId::INVALID,
1734 syntax_code_block: ElementId::INVALID,
1735 syntax_markup: ElementId::INVALID,
1736 whitespace: ElementId::INVALID,
1737 whitespace_trailing: ElementId::INVALID,
1738 indent_guide: ElementId::INVALID,
1739 indent_guide_active: ElementId::INVALID,
1740 search_match: ElementId::INVALID,
1741 search_current: ElementId::INVALID,
1742 selection: ElementId::INVALID,
1743 doc_highlight_read: ElementId::INVALID,
1744 doc_highlight_write: ElementId::INVALID,
1745 doc_highlight_text: ElementId::INVALID,
1746 substitute_preview: ElementId::INVALID,
1747 inlay_hint: ElementId::INVALID,
1748 picker_root: ElementId::INVALID,
1749 picker_title: ElementId::INVALID,
1750 picker_prompt: ElementId::INVALID,
1751 picker_count: ElementId::INVALID,
1752 completion_annotation_kind: ElementId::INVALID,
1753 completion_annotation_doc: ElementId::INVALID,
1754 completion_annotation_keybinding: ElementId::INVALID,
1755 completion_annotation_source: ElementId::INVALID,
1756 completion_annotation_custom: ElementId::INVALID,
1757 completion_annotation_perm_type: ElementId::INVALID,
1758 completion_annotation_perm_read: ElementId::INVALID,
1759 completion_annotation_perm_write: ElementId::INVALID,
1760 completion_annotation_perm_exec: ElementId::INVALID,
1761 completion_annotation_perm_special: ElementId::INVALID,
1762 completion_annotation_perm_none: ElementId::INVALID,
1763 completion_annotation_size: ElementId::INVALID,
1764 completion_annotation_mtime: ElementId::INVALID,
1765 completion_annotation_location_path: ElementId::INVALID,
1766 completion_annotation_location_line: ElementId::INVALID,
1767 completion_annotation_location_col: ElementId::INVALID,
1768 completion_annotation_status_dirty: ElementId::INVALID,
1769 completion_annotation_status_active: ElementId::INVALID,
1770 completion_annotation_latency_reflex: ElementId::INVALID,
1771 completion_annotation_latency_display: ElementId::INVALID,
1772 completion_annotation_latency_background: ElementId::INVALID,
1773 completion_annotation_args: ElementId::INVALID,
1774 completion_annotation_buffer_id: ElementId::INVALID,
1775 completion_annotation_register: ElementId::INVALID,
1776 }
1777 }
1778}
1779
1780impl BuiltinElementIds {
1781 pub fn annotation_slot(&self, slot: &str) -> ElementId {
1791 match slot {
1792 "completion.annotation.kind" => self.completion_annotation_kind,
1793 "completion.annotation.doc" => self.completion_annotation_doc,
1794 "completion.annotation.keybinding" => self.completion_annotation_keybinding,
1795 "completion.annotation.source" => self.completion_annotation_source,
1796 "completion.annotation.perm.type" => self.completion_annotation_perm_type,
1797 "completion.annotation.perm.read" => self.completion_annotation_perm_read,
1798 "completion.annotation.perm.write" => self.completion_annotation_perm_write,
1799 "completion.annotation.perm.exec" => self.completion_annotation_perm_exec,
1800 "completion.annotation.perm.special" => self.completion_annotation_perm_special,
1801 "completion.annotation.perm.none" => self.completion_annotation_perm_none,
1802 "completion.annotation.size" => self.completion_annotation_size,
1803 "completion.annotation.mtime" => self.completion_annotation_mtime,
1804 "completion.annotation.location.path" => self.completion_annotation_location_path,
1805 "completion.annotation.location.line" => self.completion_annotation_location_line,
1806 "completion.annotation.location.col" => self.completion_annotation_location_col,
1807 "completion.annotation.status.dirty" => self.completion_annotation_status_dirty,
1808 "completion.annotation.status.active" => self.completion_annotation_status_active,
1809 "completion.annotation.latency.reflex" => self.completion_annotation_latency_reflex,
1810 "completion.annotation.latency.display" => self.completion_annotation_latency_display,
1811 "completion.annotation.latency.background" => {
1812 self.completion_annotation_latency_background
1813 }
1814 "completion.annotation.args" => self.completion_annotation_args,
1815 "completion.annotation.buffer-id" => self.completion_annotation_buffer_id,
1816 "completion.annotation.register" => self.completion_annotation_register,
1817 _ => self.completion_annotation_custom,
1818 }
1819 }
1820
1821 pub fn capture(reg: &dyn ThemeRegistry) -> Self {
1826 let id = |name: &'static str| match reg.id(&ElementName::from_static(name)) {
1827 Some(id) => id,
1828 None => {
1829 tracing::warn!("theme: builtin element `{name}` not registered at id-capture");
1830 ElementId::INVALID
1831 }
1832 };
1833 BuiltinElementIds {
1834 terminal_ansi: [
1835 id("terminal.ansi.0"),
1836 id("terminal.ansi.1"),
1837 id("terminal.ansi.2"),
1838 id("terminal.ansi.3"),
1839 id("terminal.ansi.4"),
1840 id("terminal.ansi.5"),
1841 id("terminal.ansi.6"),
1842 id("terminal.ansi.7"),
1843 id("terminal.ansi.8"),
1844 id("terminal.ansi.9"),
1845 id("terminal.ansi.10"),
1846 id("terminal.ansi.11"),
1847 id("terminal.ansi.12"),
1848 id("terminal.ansi.13"),
1849 id("terminal.ansi.14"),
1850 id("terminal.ansi.15"),
1851 ],
1852 diagnostic_error: id("diagnostic.error"),
1853 diagnostic_warning: id("diagnostic.warning"),
1854 diagnostic_info: id("diagnostic.info"),
1855 diagnostic_hint: id("diagnostic.hint"),
1856 diff_add_sign: id("diff.add.sign"),
1857 diff_change_sign: id("diff.change.sign"),
1858 diff_remove_sign: id("diff.remove.sign"),
1859 diff_conflict_sign: id("diff.conflict.sign"),
1860 diff_add_line: id("diff.add.line"),
1861 diff_add_refine_bg: id("diff.add.refine.bg"),
1862 diff_remove_refine_bg: id("diff.remove.refine.bg"),
1863 diff_change_line: id("diff.change.line"),
1864 diff_remove_line: id("diff.remove.line"),
1865 diff_deletion_block: id("diff.deletion_block"),
1866 sticky_context_background: id("sticky.context.background"),
1867 sticky_context_line_number: id("sticky.context.line_number"),
1868 sticky_context_active: id("sticky.context.active"),
1869 sticky_context_separator: id("sticky.context.separator"),
1870 diff_conflict_line: id("diff.conflict.line"),
1871 diff_add_text: id("diff.add.text"),
1872 diff_remove_text: id("diff.remove.text"),
1873 magit_sha: id("magit.sha"),
1874 magit_branch_current: id("magit.branch.current"),
1875 magit_ref_decoration: id("magit.ref.decoration"),
1876 magit_rebase_verb: id("magit.rebase.verb"),
1877 magit_author: id("magit.author"),
1878 help_key: id("help.key"),
1879 help_command: id("help.command"),
1880 help_action: id("help.action"),
1881 help_literal: id("help.literal"),
1882 transient_title: id("transient.title"),
1883 transient_group: id("transient.group"),
1884 transient_key: id("transient.key"),
1885 transient_key_inactive: id("transient.key.inactive"),
1886 transient_description: id("transient.description"),
1887 transient_value: id("transient.value"),
1888 transient_border: id("transient.border"),
1889 gutter_sign: id("gutter.sign"),
1890 gutter_fold_open: id("gutter.fold.open"),
1891 gutter_fold_closed: id("gutter.fold.closed"),
1892 gutter_fold_summary: id("gutter.fold.summary"),
1893 pane_status_active: id("pane.status.active"),
1894 pane_status_inactive: id("pane.status.inactive"),
1895 pane_separator: id("pane.separator"),
1896 pane_inactive_overlay: id("pane.inactive_overlay"),
1897 modeline_active: id("modeline.active"),
1898 modeline_inactive: id("modeline.inactive"),
1899 modeline_mode: id("modeline.mode"),
1900 modeline_path: id("modeline.path"),
1901 modeline_position: id("modeline.position"),
1902 modeline_lang: id("modeline.lang"),
1903 modeline_mode_item: id("modeline.mode_item"),
1904 editor_background: id("editor.background"),
1905 editor_foreground: id("editor.foreground"),
1906 editor_cursor: id("editor.cursor"),
1907 ui_popup_background: id("ui.popup.background"),
1908 ui_popup_title: id("ui.popup.title"),
1909 ui_popup_hint: id("ui.popup.hint"),
1910 file_tree_dir: id("file_tree.dir"),
1911 file_tree_hidden: id("file_tree.hidden"),
1912 file_tree_file: id("file_tree.file"),
1913 editor_cursor_line: id("editor.cursor_line"),
1914 messages_timestamp: id("messages.timestamp"),
1915 messages_trace: id("messages.trace"),
1916 messages_debug: id("messages.debug"),
1917 messages_info: id("messages.info"),
1918 messages_warn: id("messages.warn"),
1919 messages_error: id("messages.error"),
1920 syntax_default: id("syntax.default"),
1921 syntax_comment: id("syntax.comment"),
1922 syntax_line_comment: id("syntax.line_comment"),
1923 syntax_string: id("syntax.string"),
1924 syntax_keyword: id("syntax.keyword"),
1925 syntax_type: id("syntax.type"),
1926 syntax_number: id("syntax.number"),
1927 syntax_function: id("syntax.function"),
1928 syntax_constant: id("syntax.constant"),
1929 syntax_variable: id("syntax.variable"),
1930 syntax_operator: id("syntax.operator"),
1931 syntax_punctuation: id("syntax.punctuation"),
1932 syntax_attribute: id("syntax.attribute"),
1933 syntax_heading_1: id("syntax.heading.1"),
1934 syntax_heading_2: id("syntax.heading.2"),
1935 syntax_heading_3: id("syntax.heading.3"),
1936 syntax_heading_4: id("syntax.heading.4"),
1937 syntax_heading_5: id("syntax.heading.5"),
1938 syntax_heading_6: id("syntax.heading.6"),
1939 syntax_bold: id("syntax.bold"),
1940 syntax_italic: id("syntax.italic"),
1941 syntax_link: id("syntax.link"),
1942 syntax_url: id("syntax.url"),
1943 syntax_markup_raw: id("syntax.markup_raw"),
1944 syntax_code_block: id("syntax.code_block"),
1945 syntax_markup: id("syntax.markup"),
1946 whitespace: id("whitespace"),
1947 whitespace_trailing: id("whitespace.trailing"),
1948 indent_guide: id("indent.guide"),
1949 indent_guide_active: id("indent.guide.active"),
1950 search_match: id("search.match"),
1951 search_current: id("search.current"),
1952 selection: id("selection"),
1953 doc_highlight_read: id("doc_highlight.read"),
1954 doc_highlight_write: id("doc_highlight.write"),
1955 doc_highlight_text: id("doc_highlight.text"),
1956 substitute_preview: id("substitute.preview"),
1957 inlay_hint: id("inlay.hint"),
1958 picker_root: id("picker.root"),
1959 picker_title: id("picker.title"),
1960 picker_prompt: id("picker.prompt"),
1961 picker_count: id("picker.count"),
1962 completion_annotation_kind: id("completion.annotation.kind"),
1963 completion_annotation_doc: id("completion.annotation.doc"),
1964 completion_annotation_keybinding: id("completion.annotation.keybinding"),
1965 completion_annotation_source: id("completion.annotation.source"),
1966 completion_annotation_custom: id("completion.annotation.custom"),
1967 completion_annotation_perm_type: id("completion.annotation.perm.type"),
1968 completion_annotation_perm_read: id("completion.annotation.perm.read"),
1969 completion_annotation_perm_write: id("completion.annotation.perm.write"),
1970 completion_annotation_perm_exec: id("completion.annotation.perm.exec"),
1971 completion_annotation_perm_special: id("completion.annotation.perm.special"),
1972 completion_annotation_perm_none: id("completion.annotation.perm.none"),
1973 completion_annotation_size: id("completion.annotation.size"),
1974 completion_annotation_mtime: id("completion.annotation.mtime"),
1975 completion_annotation_location_path: id("completion.annotation.location.path"),
1976 completion_annotation_location_line: id("completion.annotation.location.line"),
1977 completion_annotation_location_col: id("completion.annotation.location.col"),
1978 completion_annotation_status_dirty: id("completion.annotation.status.dirty"),
1979 completion_annotation_status_active: id("completion.annotation.status.active"),
1980 completion_annotation_latency_reflex: id("completion.annotation.latency.reflex"),
1981 completion_annotation_latency_display: id("completion.annotation.latency.display"),
1982 completion_annotation_latency_background: id(
1983 "completion.annotation.latency.background",
1984 ),
1985 completion_annotation_args: id("completion.annotation.args"),
1986 completion_annotation_buffer_id: id("completion.annotation.buffer-id"),
1987 completion_annotation_register: id("completion.annotation.register"),
1988 }
1989 }
1990}
1991
1992#[cfg(test)]
1993mod tests {
1994 use super::*;
1995 use crate::Color;
1996
1997 fn reg() -> InMemoryThemeRegistry {
1998 InMemoryThemeRegistry::with_defaults()
1999 }
2000
2001 fn resolved_of(reg: &InMemoryThemeRegistry, name: &'static str) -> Style {
2002 let id = reg
2003 .id(&ElementName::from_static(name))
2004 .unwrap_or_else(|| panic!("element `{name}` not registered"));
2005 reg.resolved().get(id)
2006 }
2007
2008 #[test]
2009 fn resolved_builtins_match_legacy_literals() {
2010 let reg = reg();
2011 let rgb = Color::Rgb;
2016 assert_eq!(
2017 resolved_of(®, "pane.status.active"),
2018 Style::empty().reverse().bold()
2019 );
2020 assert_eq!(
2021 resolved_of(®, "pane.status.inactive"),
2022 Style::empty().fg(rgb(0x6c, 0x70, 0x86)).dim() );
2024 assert_eq!(
2025 resolved_of(®, "diagnostic.error"),
2026 Style::empty().fg(rgb(0xf3, 0x8b, 0xa8)).bold() );
2028 assert_eq!(
2029 resolved_of(®, "diagnostic.info"),
2030 Style::empty().fg(rgb(0x89, 0xb4, 0xfa)) );
2032 assert_eq!(
2033 resolved_of(®, "file_tree.dir"),
2034 Style::empty().fg(rgb(0x89, 0xb4, 0xfa)).bold() );
2036 assert_eq!(resolved_of(®, "file_tree.file"), Style::empty());
2037 assert_eq!(
2038 resolved_of(®, "diff.add.sign"),
2039 Style::empty().fg(rgb(0xa6, 0xe3, 0xa1)).bold() );
2041 assert_eq!(
2042 resolved_of(®, "diff.conflict.sign"),
2043 Style::empty().fg(rgb(0xcb, 0xa6, 0xf7)).bold() );
2045 assert_eq!(
2047 resolved_of(®, "editor.cursor_line"),
2048 Style::empty().bg(Color::Indexed(236))
2049 );
2050 assert_eq!(
2051 resolved_of(®, "diff.add.line"),
2052 Style::empty().bg(Color::Rgb(0, 50, 0))
2053 );
2054 assert_eq!(
2055 resolved_of(®, "diff.deletion_block"),
2056 Style::empty().bg(Color::Rgb(60, 0, 0))
2057 );
2058 assert_eq!(
2060 resolved_of(®, "syntax.keyword"),
2061 Style::empty().fg(Color::Rgb(0xcb, 0xa6, 0xf7)).bold()
2062 );
2063 assert_eq!(
2064 resolved_of(®, "syntax.comment"),
2065 Style::empty().fg(Color::Rgb(0x6c, 0x70, 0x86)).italic()
2066 );
2067 assert_eq!(
2073 resolved_of(®, "syntax.heading.1"),
2074 Style::empty()
2075 .fg(Color::Rgb(0xf3, 0x8b, 0xa8))
2076 .bold()
2077 .weight(Weight::ExtraBold)
2078 .scale(FontScale::from_ratio(1.6))
2079 );
2080 assert_eq!(
2081 resolved_of(®, "syntax.url"),
2082 Style::empty().fg(Color::Rgb(0x74, 0xc7, 0xec)).underline()
2083 );
2084 }
2085
2086 #[test]
2090 fn resolved_modeline_elements_are_palette_driven() {
2091 let reg = reg();
2092 assert_eq!(
2093 resolved_of(®, "modeline.active"),
2094 Style::empty().bg(Color::Rgb(0x45, 0x47, 0x5a)) );
2096 assert_eq!(
2097 resolved_of(®, "modeline.inactive"),
2098 Style::empty()
2099 .bg(Color::Rgb(0x31, 0x32, 0x44)) .fg(Color::Rgb(0x6c, 0x70, 0x86)) );
2102 assert_eq!(
2103 resolved_of(®, "modeline.mode"),
2104 Style::empty().fg(Color::Rgb(0x89, 0xb4, 0xfa)).bold() );
2106 assert_eq!(
2107 resolved_of(®, "modeline.path"),
2108 Style::empty().fg(Color::Rgb(0xcd, 0xd6, 0xf4)) );
2110 assert_eq!(
2111 resolved_of(®, "modeline.lang"),
2112 Style::empty().fg(Color::Rgb(0x94, 0xe2, 0xd5)) );
2114 }
2115
2116 #[test]
2121 fn every_builtin_theme_themes_the_modeline() {
2122 for theme in crate::themes::builtin_themes() {
2123 let reg = reg();
2124 assert!(
2125 reg.apply_theme(theme.name),
2126 "theme `{}` should be registered",
2127 theme.name
2128 );
2129 let resolved = reg.resolved();
2130 let style = |name: &'static str| {
2131 let id = reg
2132 .id(&ElementName::from_static(name))
2133 .expect("modeline element registered");
2134 resolved.get(id)
2135 };
2136 assert!(
2138 style("modeline.active").bg.is_some(),
2139 "theme `{}`: modeline.active needs a bar bg",
2140 theme.name
2141 );
2142 assert!(
2143 style("modeline.inactive").bg.is_some() && style("modeline.inactive").fg.is_some(),
2144 "theme `{}`: modeline.inactive needs a muted bar",
2145 theme.name
2146 );
2147 for role in [
2148 "modeline.mode",
2149 "modeline.path",
2150 "modeline.position",
2151 "modeline.lang",
2152 "modeline.mode_item",
2153 ] {
2154 assert!(
2155 style(role).fg.is_some(),
2156 "theme `{}`: {role} needs a themed fg",
2157 theme.name
2158 );
2159 }
2160 }
2161 }
2162
2163 #[test]
2164 fn builtin_ids_capture_resolves_diagnostics_to_legacy() {
2165 let reg = reg();
2170 let ids = BuiltinElementIds::capture(®);
2171 let resolved = reg.resolved();
2172 assert_ne!(ids.completion_annotation_perm_type, ElementId::INVALID);
2175 assert_eq!(
2176 resolved.get(ids.completion_annotation_perm_write).fg,
2177 Some(Color::Rgb(0xf3, 0x8b, 0xa8)) );
2179 assert_eq!(
2180 resolved.get(ids.completion_annotation_perm_exec).fg,
2181 Some(Color::Rgb(0xa6, 0xe3, 0xa1)) );
2183 assert_eq!(
2184 ids.annotation_slot("completion.annotation.perm.exec"),
2185 ids.completion_annotation_perm_exec,
2186 "annotation_slot maps the perm.exec key"
2187 );
2188 assert_eq!(
2189 ids.annotation_slot("completion.annotation.size"),
2190 ids.completion_annotation_size,
2191 );
2192 assert_eq!(
2193 ids.annotation_slot("totally.unknown.slot"),
2194 ids.completion_annotation_custom,
2195 "unknown slots fall back to the custom annotation id"
2196 );
2197 assert_ne!(ids.diagnostic_error, ElementId::INVALID);
2198 assert_ne!(ids.diagnostic_warning, ElementId::INVALID);
2199 assert_ne!(ids.diagnostic_info, ElementId::INVALID);
2200 assert_ne!(ids.diagnostic_hint, ElementId::INVALID);
2201 assert_eq!(
2202 resolved.get(ids.diagnostic_error),
2203 Style::empty().fg(Color::Rgb(0xf3, 0x8b, 0xa8)).bold()
2204 );
2205 assert_eq!(
2206 resolved.get(ids.diagnostic_warning),
2207 Style::empty().fg(Color::Rgb(0xf9, 0xe2, 0xaf)).bold()
2208 );
2209 assert_eq!(
2210 resolved.get(ids.diagnostic_info),
2211 Style::empty().fg(Color::Rgb(0x89, 0xb4, 0xfa))
2212 );
2213 assert_eq!(
2214 resolved.get(ids.diagnostic_hint),
2215 Style::empty().fg(Color::Rgb(0x6c, 0x70, 0x86)).dim()
2216 );
2217 }
2218
2219 #[test]
2220 fn builtin_ids_capture_resolves_picker_rollout_slots() {
2221 let reg = reg();
2227 let ids = BuiltinElementIds::capture(®);
2228 let resolved = reg.resolved();
2229
2230 for (key, id) in [
2231 (
2232 "completion.annotation.location.path",
2233 ids.completion_annotation_location_path,
2234 ),
2235 (
2236 "completion.annotation.location.line",
2237 ids.completion_annotation_location_line,
2238 ),
2239 (
2240 "completion.annotation.location.col",
2241 ids.completion_annotation_location_col,
2242 ),
2243 (
2244 "completion.annotation.status.dirty",
2245 ids.completion_annotation_status_dirty,
2246 ),
2247 (
2248 "completion.annotation.status.active",
2249 ids.completion_annotation_status_active,
2250 ),
2251 (
2252 "completion.annotation.latency.reflex",
2253 ids.completion_annotation_latency_reflex,
2254 ),
2255 (
2256 "completion.annotation.latency.display",
2257 ids.completion_annotation_latency_display,
2258 ),
2259 (
2260 "completion.annotation.latency.background",
2261 ids.completion_annotation_latency_background,
2262 ),
2263 ("completion.annotation.args", ids.completion_annotation_args),
2264 (
2265 "completion.annotation.buffer-id",
2266 ids.completion_annotation_buffer_id,
2267 ),
2268 (
2269 "completion.annotation.register",
2270 ids.completion_annotation_register,
2271 ),
2272 ] {
2273 assert_ne!(id, ElementId::INVALID, "`{key}` should intern");
2274 assert_eq!(ids.annotation_slot(key), id, "annotation_slot maps `{key}`");
2275 }
2276
2277 let fg = |id| resolved.get(id).fg;
2279 assert_eq!(
2280 fg(ids.completion_annotation_location_line),
2281 fg(ids.completion_annotation_keybinding), );
2283 assert_eq!(
2284 fg(ids.completion_annotation_status_dirty),
2285 fg(ids.completion_annotation_perm_write), );
2287 assert_eq!(
2288 fg(ids.completion_annotation_latency_reflex),
2289 fg(ids.completion_annotation_perm_exec), );
2291 assert_eq!(
2292 fg(ids.completion_annotation_latency_display),
2293 fg(ids.completion_annotation_perm_type), );
2295 assert_eq!(
2296 fg(ids.completion_annotation_register),
2297 fg(ids.completion_annotation_source), );
2299 assert_ne!(
2301 fg(ids.completion_annotation_location_line),
2302 fg(ids.completion_annotation_location_path),
2303 );
2304 }
2305
2306 #[test]
2307 fn builtin_ids_capture_resolves_diff_to_legacy() {
2308 let reg = reg();
2311 let ids = BuiltinElementIds::capture(®);
2312 let resolved = reg.resolved();
2313 assert_eq!(
2314 resolved.get(ids.diff_add_sign),
2315 Style::empty().fg(Color::Rgb(0xa6, 0xe3, 0xa1)).bold()
2316 );
2317 assert_eq!(
2318 resolved.get(ids.diff_conflict_sign),
2319 Style::empty().fg(Color::Rgb(0xcb, 0xa6, 0xf7)).bold()
2320 );
2321 assert_eq!(
2323 resolved.get(ids.diff_add_line).bg,
2324 Some(Color::Rgb(0, 50, 0))
2325 );
2326 assert_eq!(
2327 resolved.get(ids.diff_change_line).bg,
2328 Some(Color::Rgb(50, 50, 0))
2329 );
2330 assert_eq!(
2331 resolved.get(ids.diff_deletion_block).bg,
2332 Some(Color::Rgb(60, 0, 0))
2333 );
2334 assert_eq!(
2335 resolved.get(ids.diff_conflict_line).bg,
2336 Some(Color::Rgb(60, 0, 60))
2337 );
2338 }
2339
2340 #[test]
2341 fn terminal_ansi_palette_resolves_to_themed_accents() {
2342 let reg = reg();
2348 let ids = BuiltinElementIds::capture(®);
2349 let resolved = reg.resolved();
2350 let fg = |i: usize| {
2351 resolved
2352 .get(ids.terminal_ansi[i])
2353 .fg
2354 .map(|c| c.to_rgb_u32(0))
2355 };
2356 assert_eq!(fg(1), Some(0x00f3_8ba8), "ANSI red = mocha red");
2359 assert_eq!(fg(2), Some(0x00a6_e3a1), "ANSI green = mocha green");
2360 assert_eq!(
2361 fg(4),
2362 Some(0x0089_b4fa),
2363 "ANSI blue = mocha blue (not 0x0000ee)"
2364 );
2365 assert_eq!(fg(5), Some(0x00f5_c2e7), "ANSI magenta = pink");
2366 assert_eq!(fg(6), Some(0x0094_e2d5), "ANSI cyan = teal");
2367 assert_eq!(fg(9), fg(1), "bright red reuses red");
2369 assert_ne!(
2370 fg(0),
2371 fg(8),
2372 "black (surface1) and bright black (surface2) differ"
2373 );
2374 }
2375
2376 #[test]
2377 fn terminal_ansi_palette_follows_colorscheme_swap() {
2378 let mocha = InMemoryThemeRegistry::with_defaults();
2382 let ids = BuiltinElementIds::capture(&mocha);
2383 let mocha_blue = mocha
2384 .resolved()
2385 .get(ids.terminal_ansi[4])
2386 .fg
2387 .unwrap()
2388 .to_rgb_u32(0);
2389
2390 let gruv = InMemoryThemeRegistry::new(crate::palette::gruvbox_dark_palette());
2391 register_builtins(&gruv);
2392 let gids = BuiltinElementIds::capture(&gruv);
2393 let gruv_blue = gruv
2394 .resolved()
2395 .get(gids.terminal_ansi[4])
2396 .fg
2397 .unwrap()
2398 .to_rgb_u32(0);
2399
2400 assert_ne!(
2401 mocha_blue, gruv_blue,
2402 "terminal blue tracks the colorscheme"
2403 );
2404 }
2405
2406 #[test]
2407 fn styled_marginalia_slots_follow_colorscheme_swap() {
2408 let mocha = InMemoryThemeRegistry::with_defaults();
2414 let mids = BuiltinElementIds::capture(&mocha);
2415 let m_write = mocha
2416 .resolved()
2417 .get(mids.annotation_slot("completion.annotation.perm.write"))
2418 .fg
2419 .unwrap()
2420 .to_rgb_u32(0);
2421
2422 let gruv = InMemoryThemeRegistry::new(crate::palette::gruvbox_dark_palette());
2423 register_builtins(&gruv);
2424 let gids = BuiltinElementIds::capture(&gruv);
2425 let g_write = gruv
2426 .resolved()
2427 .get(gids.annotation_slot("completion.annotation.perm.write"))
2428 .fg
2429 .unwrap()
2430 .to_rgb_u32(0);
2431
2432 assert_ne!(m_write, g_write, "perm.write tracks the colorscheme");
2433 }
2434
2435 #[test]
2436 fn picker_rollout_slots_follow_colorscheme_swap() {
2437 let swap = |slot: &str| {
2442 let mocha = InMemoryThemeRegistry::with_defaults();
2443 let mids = BuiltinElementIds::capture(&mocha);
2444 let m = mocha
2445 .resolved()
2446 .get(mids.annotation_slot(slot))
2447 .fg
2448 .unwrap()
2449 .to_rgb_u32(0);
2450 let gruv = InMemoryThemeRegistry::new(crate::palette::gruvbox_dark_palette());
2451 register_builtins(&gruv);
2452 let gids = BuiltinElementIds::capture(&gruv);
2453 let g = gruv
2454 .resolved()
2455 .get(gids.annotation_slot(slot))
2456 .fg
2457 .unwrap()
2458 .to_rgb_u32(0);
2459 (m, g)
2460 };
2461 for slot in [
2462 "completion.annotation.location.line",
2463 "completion.annotation.status.dirty",
2464 "completion.annotation.latency.display",
2465 "completion.annotation.register",
2466 ] {
2467 let (m, g) = swap(slot);
2468 assert_ne!(m, g, "`{slot}` should track the colorscheme");
2469 }
2470 }
2471
2472 #[test]
2473 fn builtin_ids_capture_resolves_chrome_to_legacy() {
2474 let reg = reg();
2479 let ids = BuiltinElementIds::capture(®);
2480 let resolved = reg.resolved();
2481 assert_eq!(
2482 resolved.get(ids.pane_inactive_overlay),
2483 Style::empty().dim()
2484 );
2485 assert_eq!(
2486 resolved.get(ids.file_tree_dir),
2487 Style::empty().fg(Color::Rgb(0x89, 0xb4, 0xfa)).bold()
2488 );
2489 assert_eq!(
2490 resolved.get(ids.file_tree_hidden),
2491 Style::empty().fg(Color::Rgb(0x6c, 0x70, 0x86)).dim()
2492 );
2493 assert_eq!(resolved.get(ids.file_tree_file), Style::empty());
2494 }
2495
2496 #[test]
2497 fn editor_canvas_resolves_to_catppuccin_mocha() {
2498 let reg = reg();
2504 assert_eq!(
2505 resolved_of(®, "editor.background").bg,
2506 Some(Color::Rgb(0x1e, 0x1e, 0x2e))
2507 );
2508 assert_eq!(
2509 resolved_of(®, "editor.foreground").fg,
2510 Some(Color::Rgb(0xcd, 0xd6, 0xf4))
2511 );
2512 let cursor = resolved_of(®, "editor.cursor");
2514 assert_eq!(cursor.bg, Some(Color::Rgb(0xcd, 0xd6, 0xf4)));
2515 assert_eq!(cursor.fg, Some(Color::Rgb(0x1e, 0x1e, 0x2e)));
2516 assert_eq!(
2517 resolved_of(®, "ui.popup.background").bg,
2518 Some(Color::Rgb(0x18, 0x18, 0x25))
2519 );
2520 let title = resolved_of(®, "ui.popup.title");
2523 assert_eq!(title.fg, Some(Color::Rgb(0x89, 0xb4, 0xfa)));
2524 assert!(title.modifiers.bold);
2525 assert_eq!(
2526 resolved_of(®, "ui.popup.hint").fg,
2527 Some(Color::Rgb(0x6c, 0x70, 0x86))
2528 );
2529 }
2530
2531 #[test]
2532 fn builtin_ids_capture_resolves_cursorline_and_messages_to_legacy() {
2533 let reg = reg();
2536 let ids = BuiltinElementIds::capture(®);
2537 let resolved = reg.resolved();
2538 assert_eq!(
2539 resolved.get(ids.editor_cursor_line).bg,
2540 Some(Color::Indexed(236))
2541 );
2542 assert_eq!(resolved.get(ids.messages_trace), Style::empty().dim());
2543 assert_eq!(
2544 resolved.get(ids.messages_debug),
2545 Style::empty().fg(Color::Rgb(0x74, 0xc7, 0xec))
2546 );
2547 assert_eq!(
2552 resolved.get(ids.messages_info),
2553 Style::empty().fg(Color::Rgb(0xa6, 0xe3, 0xa1))
2554 );
2555 assert_eq!(
2556 resolved.get(ids.messages_warn),
2557 Style::empty().fg(Color::Rgb(0xf9, 0xe2, 0xaf)).bold()
2558 );
2559 assert_eq!(
2560 resolved.get(ids.messages_error),
2561 Style::empty().fg(Color::Rgb(0xf3, 0x8b, 0xa8)).bold()
2562 );
2563 assert_eq!(
2564 resolved.get(ids.messages_timestamp),
2565 Style::empty().fg(Color::Rgb(0x6c, 0x70, 0x86)).dim()
2566 );
2567 }
2568
2569 #[test]
2570 fn builtin_ids_capture_resolves_syntax_and_whitespace_to_legacy() {
2571 let reg = reg();
2575 let ids = BuiltinElementIds::capture(®);
2576 let resolved = reg.resolved();
2577 assert_eq!(
2578 resolved.get(ids.syntax_keyword),
2579 Style::empty().fg(Color::Rgb(0xcb, 0xa6, 0xf7)).bold()
2580 );
2581 assert_eq!(
2582 resolved.get(ids.syntax_string),
2583 Style::empty().fg(Color::Rgb(0xa6, 0xe3, 0xa1))
2584 );
2585 assert_eq!(
2587 resolved.get(ids.syntax_line_comment),
2588 resolved.get(ids.syntax_comment)
2589 );
2590 assert_eq!(
2593 resolved.get(ids.syntax_heading_1),
2594 Style::empty()
2595 .fg(Color::Rgb(0xf3, 0x8b, 0xa8))
2596 .bold()
2597 .weight(Weight::ExtraBold)
2598 .scale(FontScale::from_ratio(1.6))
2599 );
2600 assert_eq!(
2601 resolved.get(ids.whitespace_trailing),
2602 Style::empty().fg(Color::Rgb(0xf3, 0x8b, 0xa8))
2603 );
2604 assert_eq!(
2605 resolved.get(ids.whitespace),
2606 Style::empty().fg(Color::Rgb(0x6c, 0x70, 0x86)).dim()
2607 );
2608 }
2609
2610 #[test]
2611 fn inline_code_is_legible_on_the_cursorline() {
2612 let reg = reg();
2621 let ids = BuiltinElementIds::capture(®);
2622 let resolved = reg.resolved();
2623
2624 let raw = resolved.get(ids.syntax_markup_raw);
2625 assert!(!raw.modifiers.dim, "inline code must not be dimmed");
2627 assert_eq!(
2630 raw.fg,
2631 resolved.get(ids.syntax_string).fg,
2632 "inline code takes the string/literal accent"
2633 );
2634 assert_ne!(
2637 raw.fg,
2638 resolved.get(ids.editor_cursor_line).bg,
2639 "inline code foreground must differ from the cursorline background"
2640 );
2641 }
2642
2643 #[test]
2644 fn heading_builtins_carry_rich_weight() {
2645 let reg = reg();
2649 let ids = BuiltinElementIds::capture(®);
2650 let resolved = reg.resolved();
2651 assert_eq!(
2652 resolved.get(ids.syntax_heading_1).weight,
2653 Some(Weight::ExtraBold)
2654 );
2655 assert_eq!(
2656 resolved.get(ids.syntax_heading_2).weight,
2657 Some(Weight::Bold)
2658 );
2659 assert_eq!(
2660 resolved.get(ids.syntax_heading_3).weight,
2661 Some(Weight::Bold)
2662 );
2663 assert_eq!(resolved.get(ids.syntax_heading_4).weight, None);
2665 assert_eq!(resolved.get(ids.syntax_heading_5).weight, None);
2666 assert_eq!(resolved.get(ids.syntax_heading_6).weight, None);
2667 assert!(resolved.get(ids.syntax_heading_1).modifiers.bold);
2669 assert!(!resolved.get(ids.syntax_heading_1).modifiers.underline);
2674 }
2675
2676 #[test]
2677 fn heading_builtins_carry_descending_scale() {
2678 let reg = reg();
2684 let ids = BuiltinElementIds::capture(®);
2685 let resolved = reg.resolved();
2686 let scales = [
2687 resolved.get(ids.syntax_heading_1).scale,
2688 resolved.get(ids.syntax_heading_2).scale,
2689 resolved.get(ids.syntax_heading_3).scale,
2690 resolved.get(ids.syntax_heading_4).scale,
2691 resolved.get(ids.syntax_heading_5).scale,
2692 resolved.get(ids.syntax_heading_6).scale,
2693 ];
2694 assert_eq!(scales[0], Some(FontScale::from_ratio(1.6)));
2695 assert_eq!(scales[1], Some(FontScale::from_ratio(1.4)));
2696 assert_eq!(scales[2], Some(FontScale::from_ratio(1.25)));
2697 assert_eq!(scales[3], Some(FontScale::from_ratio(1.15)));
2698 assert_eq!(scales[4], Some(FontScale::from_ratio(1.1)));
2699 assert_eq!(scales[5], Some(FontScale::from_ratio(1.05)));
2700 for w in scales.windows(2) {
2702 assert!(w[0].unwrap().0 > w[1].unwrap().0);
2703 }
2704 assert!(scales[5].unwrap().0 > FontScale::ONE.0);
2705 }
2706
2707 #[test]
2708 fn builtin_ids_capture_resolves_overlays_to_registered() {
2709 let reg = reg();
2714 let ids = BuiltinElementIds::capture(®);
2715 let resolved = reg.resolved();
2716 assert_ne!(ids.selection, ElementId::INVALID);
2717 assert_eq!(
2719 resolved.get(ids.search_match).bg,
2720 Some(Color::Rgb(0x6c, 0x70, 0x86))
2721 );
2722 assert_eq!(
2723 resolved.get(ids.search_current).bg,
2724 Some(Color::Rgb(0x6c, 0x5a, 0x1e))
2725 );
2726 assert_eq!(
2727 resolved.get(ids.selection).bg,
2728 Some(Color::Rgb(0x45, 0x47, 0x5a))
2729 );
2730 assert_eq!(
2732 resolved.get(ids.doc_highlight_read).bg,
2733 Some(Color::Rgb(20, 50, 25))
2734 );
2735 assert_eq!(
2736 resolved.get(ids.doc_highlight_write).bg,
2737 Some(Color::Rgb(60, 20, 20))
2738 );
2739 assert_eq!(
2740 resolved.get(ids.doc_highlight_text).bg,
2741 Some(Color::Rgb(20, 30, 60))
2742 );
2743 assert_eq!(
2744 resolved.get(ids.substitute_preview).bg,
2745 Some(Color::Rgb(0xf3, 0x8b, 0xa8))
2746 );
2747 assert_eq!(
2748 resolved.get(ids.inlay_hint).fg,
2749 Some(Color::Rgb(0x7f, 0x84, 0x9c))
2750 );
2751 assert_eq!(
2753 resolved.get(ids.completion_annotation_keybinding).fg,
2754 Some(Color::Rgb(0xf9, 0xe2, 0xaf)) );
2756 assert_eq!(
2757 resolved.get(ids.completion_annotation_source).fg,
2758 Some(Color::Rgb(0xcb, 0xa6, 0xf7)) );
2760 assert_eq!(
2761 resolved.get(ids.completion_annotation_doc).fg,
2762 Some(Color::Rgb(0x89, 0xdc, 0xeb))
2763 );
2764 }
2765
2766 #[test]
2767 fn builtin_ids_default_is_invalid_and_styleless() {
2768 let ids = BuiltinElementIds::default();
2771 assert_eq!(ids.diagnostic_error, ElementId::INVALID);
2772 let resolved = ResolvedTheme::default();
2773 assert_eq!(resolved.get(ids.diagnostic_error), Style::empty());
2774 }
2775
2776 #[test]
2777 fn inherit_reproduces_parent_style() {
2778 let reg = reg();
2779 assert_eq!(
2781 resolved_of(®, "syntax.line_comment"),
2782 resolved_of(®, "syntax.comment")
2783 );
2784 }
2785
2786 #[test]
2787 fn register_is_idempotent_by_name() {
2788 let reg = InMemoryThemeRegistry::new(default_palette());
2789 let a = reg.register(
2790 ElementName::from_static("x.y"),
2791 ElementOwner::Core,
2792 StyleSpec::new().fg("purple"),
2793 "",
2794 );
2795 let b = reg.register(
2796 ElementName::from_static("x.y"),
2797 ElementOwner::Core,
2798 StyleSpec::new().fg("red"),
2799 "",
2800 );
2801 assert_eq!(a, b);
2802 }
2803
2804 #[test]
2805 fn dotted_fallback_resolves_through_parent() {
2806 let reg = InMemoryThemeRegistry::new(default_palette());
2807 reg.register(
2808 ElementName::from_static("markdown.heading"),
2809 ElementOwner::Core,
2810 StyleSpec::new().fg("purple").bold(),
2811 "",
2812 );
2813 let id = reg.register(
2816 ElementName::from_static("uses_fallback"),
2817 ElementOwner::Core,
2818 StyleSpec::new().inherit("markdown.heading.1"),
2819 "",
2820 );
2821 let s = reg.resolved().get(id);
2822 assert_eq!(s.fg, Some(Color::Rgb(0xcb, 0xa6, 0xf7)));
2823 assert!(s.modifiers.bold);
2824 }
2825
2826 #[test]
2827 fn unknown_palette_key_leaves_channel_unset() {
2828 let reg = InMemoryThemeRegistry::new(Palette::new()); let id = reg.register(
2830 ElementName::from_static("orphan"),
2831 ElementOwner::Core,
2832 StyleSpec::new().fg("no.such.key"),
2833 "",
2834 );
2835 assert_eq!(reg.resolved().get(id).fg, None);
2837 }
2838
2839 #[test]
2840 fn set_override_overlays_on_resolved_default() {
2841 let reg = reg();
2844 let base = resolved_of(®, "syntax.keyword"); assert_eq!(base.fg, Some(Color::Rgb(0xcb, 0xa6, 0xf7)));
2846 assert!(base.modifiers.bold);
2847 reg.set_override(
2849 ElementName::from_static("syntax.keyword"),
2850 StyleSpec::new().fg(Color::Rgb(1, 2, 3)),
2851 );
2852 let after = resolved_of(®, "syntax.keyword");
2853 assert_eq!(after.fg, Some(Color::Rgb(1, 2, 3)));
2854 assert!(
2855 after.modifiers.bold,
2856 "default bold survives a fg-only override"
2857 );
2858 }
2859
2860 #[test]
2861 fn set_theme_swaps_palette_and_overrides() {
2862 let reg = reg();
2865 reg.set_override(
2866 ElementName::from_static("syntax.string"),
2867 StyleSpec::new().fg(Color::Rgb(9, 9, 9)),
2868 );
2869 assert_eq!(
2870 resolved_of(®, "syntax.string").fg,
2871 Some(Color::Rgb(9, 9, 9))
2872 );
2873 let palette = default_palette().with("green", Color::Rgb(4, 5, 6));
2875 reg.set_theme(palette, Vec::new());
2876 assert_eq!(
2878 resolved_of(®, "syntax.string").fg,
2879 Some(Color::Rgb(4, 5, 6))
2880 );
2881 }
2882
2883 #[test]
2884 fn set_theme_to_macchiato_recolors_keyword_to_macchiato_mauve() {
2885 let reg = reg();
2889 let ids = BuiltinElementIds::capture(®);
2890 assert_eq!(
2892 reg.resolved().get(ids.syntax_keyword).fg,
2893 Some(Color::Rgb(0xcb, 0xa6, 0xf7))
2894 );
2895 reg.set_theme(crate::macchiato_palette(), Vec::new());
2896 assert_eq!(
2897 reg.resolved().get(ids.syntax_keyword).fg,
2898 Some(Color::Rgb(0xc6, 0xa0, 0xf6)),
2899 "keyword resolves to Macchiato mauve after the swap"
2900 );
2901 }
2902
2903 #[test]
2904 fn builtin_themes_lookup_by_name_returns_macchiato_palette() {
2905 let theme = crate::builtin_themes()
2908 .into_iter()
2909 .find(|t| t.name == "catppuccin-macchiato")
2910 .expect("macchiato registered");
2911 assert_eq!(
2912 theme.palette.get(&crate::PaletteKey::from_static("purple")),
2913 Some(Color::Rgb(0xc6, 0xa0, 0xf6))
2914 );
2915 assert!(theme.overrides.is_empty());
2916 }
2917
2918 #[test]
2919 fn theme_catalog_register_enumerate_and_apply() {
2920 let reg = reg();
2925 let names = reg.theme_names();
2926 assert!(names.iter().any(|n| n == "catppuccin-mocha"));
2927 assert!(names.iter().any(|n| n == "catppuccin-latte"));
2928
2929 assert!(reg.apply_theme("catppuccin-latte"));
2931 assert_eq!(
2932 resolved_of(®, "editor.background").bg,
2933 Some(Color::Rgb(0xef, 0xf1, 0xf5))
2934 );
2935 assert!(!reg.apply_theme("no-such-theme"));
2937 assert_eq!(
2938 resolved_of(®, "editor.background").bg,
2939 Some(Color::Rgb(0xef, 0xf1, 0xf5))
2940 );
2941
2942 reg.register_theme(NamedTheme {
2944 name: "test-custom",
2945 palette: crate::palette::default_palette(),
2946 overrides: Vec::new(),
2947 });
2948 assert!(reg.theme_names().iter().any(|n| n == "test-custom"));
2949 assert!(reg.apply_theme("test-custom"));
2950 assert_eq!(
2951 resolved_of(®, "editor.background").bg,
2952 Some(Color::Rgb(0x1e, 0x1e, 0x2e))
2953 );
2954 }
2955
2956 #[test]
2957 fn describe_returns_metadata_and_resolved_style() {
2958 let reg = reg();
2961 let info = reg
2962 .describe(&ElementName::from_static("syntax.keyword"))
2963 .expect("syntax.keyword registered");
2964 assert_eq!(info.name.as_str(), "syntax.keyword");
2965 assert_eq!(info.owner, ElementOwner::Core);
2966 assert_eq!(info.doc, "Language keywords.");
2967 assert_eq!(
2969 info.default.fg,
2970 Some(ColorRef::Palette(crate::PaletteKey::from_static("purple")))
2971 );
2972 assert_eq!(info.default.modifiers.bold, Some(true));
2973 assert_eq!(info.resolved.fg, Some(Color::Rgb(0xcb, 0xa6, 0xf7)));
2975 assert!(info.resolved.modifiers.bold);
2976 }
2977
2978 #[test]
2979 fn element_names_are_sorted_and_include_builtins() {
2980 let reg = reg();
2984 let names = reg.element_names();
2985 assert!(
2986 names.contains(&"syntax.keyword".to_string()),
2987 "builtin element must be enumerable for completion"
2988 );
2989 assert!(names.contains(&"syntax.comment".to_string()));
2990 let mut sorted = names.clone();
2991 sorted.sort();
2992 assert_eq!(names, sorted, "element_names must be sorted");
2993 for n in &names {
2995 assert!(
2996 reg.describe(&ElementName::from(n.clone())).is_some(),
2997 "enumerated element `{n}` must be describable"
2998 );
2999 }
3000 }
3001
3002 #[test]
3003 fn describe_unknown_element_is_none() {
3004 let reg = reg();
3005 assert!(
3006 reg.describe(&ElementName::from_static("no.such.element"))
3007 .is_none()
3008 );
3009 }
3010
3011 #[test]
3012 fn describe_reflects_active_override() {
3013 let reg = reg();
3018 reg.set_override(
3019 ElementName::from_static("syntax.keyword"),
3020 StyleSpec::new().fg(Color::Rgb(1, 2, 3)),
3021 );
3022 let info = reg
3023 .describe(&ElementName::from_static("syntax.keyword"))
3024 .expect("registered");
3025 assert_eq!(info.resolved.fg, Some(Color::Rgb(1, 2, 3)));
3026 assert!(info.resolved.modifiers.bold);
3028 assert_eq!(
3030 info.default.fg,
3031 Some(ColorRef::Palette(crate::PaletteKey::from_static("purple")))
3032 );
3033 }
3034
3035 #[test]
3036 fn describe_reports_inherit_parent() {
3037 let reg = reg();
3040 let info = reg
3041 .describe(&ElementName::from_static("syntax.line_comment"))
3042 .expect("registered");
3043 assert_eq!(
3044 info.default
3045 .inherit
3046 .as_ref()
3047 .map(|n| n.as_str().to_string()),
3048 Some("syntax.comment".to_string())
3049 );
3050 }
3051
3052 #[test]
3053 fn palette_swap_rebuilds_resolved_table() {
3054 let reg = reg();
3055 let before = resolved_of(®, "syntax.keyword");
3056 assert_eq!(before.fg, Some(Color::Rgb(0xcb, 0xa6, 0xf7)));
3057 let new_palette = default_palette().with("purple", Color::Rgb(1, 2, 3));
3059 reg.set_palette(new_palette);
3060 let after = resolved_of(®, "syntax.keyword");
3061 assert_eq!(after.fg, Some(Color::Rgb(1, 2, 3)));
3062 assert!(reg.resolved().version() > 0);
3063 }
3064}