From 6761d98eb19fc35aa02b562dd62c77ab1fe65c4a Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Fri, 10 Apr 2026 22:37:22 -0400 Subject: [PATCH 01/26] Move built-in themes to top level --- src/view/theme_loader.rs | 4 ++-- {src/themes => themes}/solarized_dark.tmTheme | 0 {src/themes => themes}/solarized_light.tmTheme | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename {src/themes => themes}/solarized_dark.tmTheme (100%) rename {src/themes => themes}/solarized_light.tmTheme (100%) diff --git a/src/view/theme_loader.rs b/src/view/theme_loader.rs index b82f6d42..0934bdef 100644 --- a/src/view/theme_loader.rs +++ b/src/view/theme_loader.rs @@ -57,11 +57,11 @@ impl ThemeLoader { fn load_defaults(&mut self) -> Result<()> { self.insert_theme( "solarized_dark", - Cursor::new(include_str!("../themes/solarized_dark.tmTheme")), + Cursor::new(include_str!("../../themes/solarized_dark.tmTheme")), )?; self.insert_theme( "solarized_light", - Cursor::new(include_str!("../themes/solarized_light.tmTheme")), + Cursor::new(include_str!("../../themes/solarized_light.tmTheme")), )?; Ok(()) diff --git a/src/themes/solarized_dark.tmTheme b/themes/solarized_dark.tmTheme similarity index 100% rename from src/themes/solarized_dark.tmTheme rename to themes/solarized_dark.tmTheme diff --git a/src/themes/solarized_light.tmTheme b/themes/solarized_light.tmTheme similarity index 100% rename from src/themes/solarized_light.tmTheme rename to themes/solarized_light.tmTheme From e5c8b4a42497cf52f2812fdce0d93cdc135f805f Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 11 Apr 2026 15:14:11 -0400 Subject: [PATCH 02/26] Bake and load built-in themes This includes theme test fixtures to decouple the test suite from user themes living in ~/.config. It also adds test coverage to ensure user themes are loaded. --- build.rs | 13 +++++++ src/view/mod.rs | 22 +++++++++++- src/view/theme_loader.rs | 35 ++++++++++++++----- .../user_themes/fixture_theme.tmTheme | 26 ++++++++++++++ 4 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/user_themes/fixture_theme.tmTheme diff --git a/build.rs b/build.rs index 33e8a4a8..afa9e39f 100644 --- a/build.rs +++ b/build.rs @@ -6,15 +6,19 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::result::Result; use syntect::dumps::dump_to_uncompressed_file; +use syntect::highlighting::ThemeSet; use syntect::parsing::SyntaxSet; const COMMAND_REGEX: &str = r"pub fn (.*)\(app: &mut Application\) -> Result"; const APP_SYNTAX_DIR: &str = "syntaxes"; const APP_SYNTAX_SOURCE: &str = "app_syntaxes.packdump"; +const APP_THEME_DIR: &str = "themes"; +const APP_THEME_SOURCE: &str = "app_themes.packdump"; fn main() { generate_commands(); bake_app_syntaxes(); + bake_app_themes(); set_build_revision(); } @@ -129,3 +133,12 @@ fn bake_app_syntaxes() { dump_to_uncompressed_file(&builder.build(), output_path) .expect("Failed to write bundled syntax dump"); } + +fn bake_app_themes() { + let out_dir = env::var("OUT_DIR").expect("The compiler did not provide $OUT_DIR"); + let output_path = PathBuf::from(out_dir).join(APP_THEME_SOURCE); + let theme_dir = Path::new(APP_THEME_DIR); + let theme_set = ThemeSet::load_from_folder(theme_dir).expect("Failed to load bundled themes"); + + dump_to_uncompressed_file(&theme_set, output_path).expect("Failed to write bundled theme dump"); +} diff --git a/src/view/mod.rs b/src/view/mod.rs index a2fed5d1..9afd8061 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -28,6 +28,7 @@ use std::cell::RefCell; use std::cmp; use std::collections::HashMap; use std::ops::Drop; +use std::path::PathBuf; use std::process::Command; use std::rc::Rc; use std::sync::mpsc::{self, Sender, SyncSender}; @@ -53,7 +54,7 @@ impl View { event_channel: Sender, ) -> Result { let terminal = build_terminal().context("Failed to initialize terminal")?; - let theme_path = preferences.borrow().theme_path()?; + let theme_path = user_theme_path(&preferences.borrow())?; let theme_set = ThemeLoader::new(theme_path).load()?; let (killswitch_tx, killswitch_rx) = mpsc::sync_channel(0); @@ -196,6 +197,16 @@ impl View { } } +#[cfg(not(test))] +fn user_theme_path(preferences: &Preferences) -> Result { + preferences.theme_path() +} + +#[cfg(test)] +fn user_theme_path(_: &Preferences) -> Result { + Ok(PathBuf::from("tests/fixtures/user_themes")) +} + impl Drop for View { fn drop(&mut self) { debug!("drop triggered; killing event listener"); @@ -223,6 +234,15 @@ mod tests { use std::sync::mpsc; use syntect::highlighting::{Highlighter, ThemeSet}; + #[test] + fn new_loads_fixture_user_themes_in_tests() { + let preferences = Rc::new(RefCell::new(Preferences::new(None))); + let (tx, _) = mpsc::channel(); + let view = View::new(preferences, tx).unwrap(); + + assert!(view.theme_set.themes.contains_key("fixture_theme")); + } + #[test] fn scroll_down_prevents_scrolling_completely_beyond_buffer() { let preferences = Rc::new(RefCell::new(Preferences::new(None))); diff --git a/src/view/theme_loader.rs b/src/view/theme_loader.rs index 0934bdef..5709e371 100644 --- a/src/view/theme_loader.rs +++ b/src/view/theme_loader.rs @@ -2,8 +2,9 @@ use crate::errors::*; use std::collections::BTreeMap; use std::ffi::OsStr; use std::fs::File; -use std::io::{BufReader, Cursor, Read, Seek}; +use std::io::{BufReader, Read, Seek}; use std::path::PathBuf; +use syntect::dumps::from_uncompressed_data; use syntect::highlighting::{Theme, ThemeSet}; pub struct ThemeLoader { @@ -55,14 +56,14 @@ impl ThemeLoader { } fn load_defaults(&mut self) -> Result<()> { - self.insert_theme( - "solarized_dark", - Cursor::new(include_str!("../../themes/solarized_dark.tmTheme")), - )?; - self.insert_theme( - "solarized_light", - Cursor::new(include_str!("../../themes/solarized_light.tmTheme")), - )?; + self.themes.extend( + from_uncompressed_data::(include_bytes!(concat!( + env!("OUT_DIR"), + "/app_themes.packdump" + ))) + .context("Couldn't load bundled themes")? + .themes, + ); Ok(()) } @@ -78,3 +79,19 @@ impl ThemeLoader { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::ThemeLoader; + use std::path::PathBuf; + + #[test] + fn load_includes_bundled_and_fixture_themes() { + let theme_set = ThemeLoader::new(PathBuf::from("tests/fixtures/user_themes")) + .load() + .unwrap(); + + assert!(theme_set.themes.contains_key("solarized_dark")); + assert!(theme_set.themes.contains_key("fixture_theme")); + } +} diff --git a/tests/fixtures/user_themes/fixture_theme.tmTheme b/tests/fixtures/user_themes/fixture_theme.tmTheme new file mode 100644 index 00000000..8a552de5 --- /dev/null +++ b/tests/fixtures/user_themes/fixture_theme.tmTheme @@ -0,0 +1,26 @@ + + + + + name + Fixture Theme + settings + + + settings + + background + #101820 + caret + #f2aa4c + foreground + #f8f4e3 + lineHighlight + #1d2731 + selection + #2f3e46 + + + + + From 8f13617093d6b09db146d5b9634863bc3a6cff8f Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 11 Apr 2026 21:58:59 -0400 Subject: [PATCH 03/26] Don't create syntaxes/themes directories --- src/models/application/preferences/mod.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/models/application/preferences/mod.rs b/src/models/application/preferences/mod.rs index 2cbdb408..b7b8f060 100644 --- a/src/models/application/preferences/mod.rs +++ b/src/models/application/preferences/mod.rs @@ -2,7 +2,7 @@ use crate::errors::*; use crate::input::KeyMap; use crate::models::application::modes::open; use crate::models::application::modes::SearchSelectConfig; -use app_dirs2::{app_dir, app_root, get_app_root, AppDataType, AppInfo}; +use app_dirs2::{app_root, get_app_root, AppDataType, AppInfo}; use bloodhound::ExclusionPattern; use scribe::Buffer; use std::fs::OpenOptions; @@ -98,8 +98,8 @@ impl Preferences { /// A path pointing to the user syntax definition directory. pub fn syntax_path() -> Result { - app_dir(AppDataType::UserConfig, &APP_INFO, SYNTAX_PATH) - .context("Couldn't create syntax directory or build a path to it.") + config_subdirectory(SYNTAX_PATH) + .context("Couldn't build a path to the user syntax directory.") } /// Returns the preference file loaded into a buffer for editing. @@ -154,8 +154,8 @@ impl Preferences { /// Returns the theme path, making sure the directory exists. pub fn theme_path(&self) -> Result { - app_dir(AppDataType::UserConfig, &APP_INFO, THEME_PATH) - .context("Couldn't create themes directory or build a path to it.") + config_subdirectory(THEME_PATH) + .context("Couldn't build a path to the user themes directory.") } /// Updates the in-memory theme value. @@ -439,6 +439,14 @@ fn load_document() -> Result> { Ok(parsed_data.into_iter().next()) } +fn config_subdirectory(path: &str) -> Result { + let mut directory = app_root(AppDataType::UserConfig, &APP_INFO) + .context("Couldn't create preferences directory or build a path to it.")?; + directory.push(path); + + Ok(directory) +} + fn load_default_document() -> Result { YamlLoader::load_from_str(include_str!("default.yml")) .context("Couldn't parse default config file")? From bd4f064912ab72bdf3d59ef3aa8860763a05a8cb Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 11 Apr 2026 22:33:22 -0400 Subject: [PATCH 04/26] Update syntax/theme loaders to handle missing config directories Extract syntax loading logic into a discrete type, since it's getting more complex and is harder to test when embedded in the application type. --- src/models/application/mod.rs | 16 ++----- src/models/application/syntax_loader.rs | 57 +++++++++++++++++++++++++ src/view/theme_loader.rs | 14 ++++++ 3 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 src/models/application/syntax_loader.rs diff --git a/src/models/application/mod.rs b/src/models/application/mod.rs index fc851f1c..4f8c4a12 100644 --- a/src/models/application/mod.rs +++ b/src/models/application/mod.rs @@ -2,6 +2,7 @@ mod clipboard; mod event; pub mod modes; mod preferences; +mod syntax_loader; // Published API pub use self::clipboard::ClipboardContent; @@ -11,6 +12,7 @@ pub use self::preferences::Preferences; use self::clipboard::Clipboard; use self::modes::*; +use self::syntax_loader::SyntaxLoader; use crate::commands; use crate::errors::*; use crate::presenters; @@ -26,7 +28,6 @@ use std::mem; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::mpsc::{self, Receiver, Sender}; -use syntect::dumps::from_uncompressed_data; use syntect::parsing::SyntaxSet; pub struct Application { @@ -471,18 +472,7 @@ fn create_workspace( } fn build_full_syntax_set() -> Result { - // Load syntect default + app-bundled syntax sets serialized in build.rs - let mut builder = from_uncompressed_data::(include_bytes!(concat!( - env!("OUT_DIR"), - "/app_syntaxes.packdump" - ))) - .context("Couldn't load bundled syntax definitions")? - .into_builder(); - - // Add user syntaxes to built-in set - builder.add_from_folder(user_syntax_path()?, true)?; - - Ok(builder.build()) + SyntaxLoader::new(user_syntax_path()?).load() } #[cfg(not(test))] diff --git a/src/models/application/syntax_loader.rs b/src/models/application/syntax_loader.rs new file mode 100644 index 00000000..737fa948 --- /dev/null +++ b/src/models/application/syntax_loader.rs @@ -0,0 +1,57 @@ +use crate::errors::*; +use std::path::PathBuf; +use syntect::dumps::from_uncompressed_data; +use syntect::parsing::SyntaxSet; + +pub struct SyntaxLoader { + path: PathBuf, +} + +impl SyntaxLoader { + pub fn new(path: PathBuf) -> SyntaxLoader { + SyntaxLoader { path } + } + + pub fn load(self) -> Result { + // Load syntect default + app-bundled syntax sets serialized in build.rs. + let mut builder = from_uncompressed_data::(include_bytes!(concat!( + env!("OUT_DIR"), + "/app_syntaxes.packdump" + ))) + .context("Couldn't load bundled syntax definitions")? + .into_builder(); + + // Merge user-defined syntaxes when a user syntax directory is present. + if self.path.is_dir() { + builder.add_from_folder(&self.path, true)?; + } + + Ok(builder.build()) + } +} + +#[cfg(test)] +mod tests { + use super::SyntaxLoader; + use std::path::PathBuf; + + #[test] + fn load_includes_bundled_and_fixture_syntaxes() { + let syntax_set = SyntaxLoader::new(PathBuf::from("tests/fixtures/user_syntaxes")) + .load() + .unwrap(); + + assert!(syntax_set.find_syntax_by_name("Rust").is_some()); + assert!(syntax_set.find_syntax_by_name("Amp").is_some()); + } + + #[test] + fn load_ignores_missing_user_syntax_directory() { + let missing_path = PathBuf::from("tests/fixtures/missing_syntaxes"); + assert!(!missing_path.exists()); + + let syntax_set = SyntaxLoader::new(missing_path).load().unwrap(); + + assert!(syntax_set.find_syntax_by_name("Rust").is_some()); + } +} diff --git a/src/view/theme_loader.rs b/src/view/theme_loader.rs index 5709e371..f0464096 100644 --- a/src/view/theme_loader.rs +++ b/src/view/theme_loader.rs @@ -31,6 +31,10 @@ impl ThemeLoader { } fn load_user(&mut self) -> Result<()> { + if !self.path.is_dir() { + return Ok(()); + } + let theme_dir_entries = self .path .read_dir() @@ -94,4 +98,14 @@ mod tests { assert!(theme_set.themes.contains_key("solarized_dark")); assert!(theme_set.themes.contains_key("fixture_theme")); } + + #[test] + fn load_ignores_missing_user_theme_directory() { + let missing_path = PathBuf::from("tests/fixtures/missing_themes"); + assert!(!missing_path.exists()); + + let theme_set = ThemeLoader::new(missing_path).load().unwrap(); + + assert!(theme_set.themes.contains_key("solarized_dark")); + } } From 537826bd30d1f89f579919bfbf9f37ca93f6e370 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 11 Apr 2026 23:33:10 -0400 Subject: [PATCH 05/26] Initial theme definition skill --- .agents/skills/theme-definition/SKILL.md | 92 ++++++++++++++++++ .../references/token-color-standard.md | 93 +++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 .agents/skills/theme-definition/SKILL.md create mode 100644 .agents/skills/theme-definition/references/token-color-standard.md diff --git a/.agents/skills/theme-definition/SKILL.md b/.agents/skills/theme-definition/SKILL.md new file mode 100644 index 00000000..4065546a --- /dev/null +++ b/.agents/skills/theme-definition/SKILL.md @@ -0,0 +1,92 @@ +--- +name: theme-definition +description: Create or update a TextMate `.tmTheme` file in the repository `themes/` directory when asked for a specific color theme such as Gruvbox, Nord, or a custom dark or light palette. +--- + +# Theme Definition + +Use this skill when asked to create or update a theme definition for a named color theme or palette. + +## Goal + +Create or update exactly one theme file in the repository top-level `themes/` directory: + +- `themes/.tmTheme` + +Examples: + +- `Gruvbox Dark` -> `themes/gruvbox_dark.tmTheme` +- `Nord Light` -> `themes/nord_light.tmTheme` +- `My Theme` -> `themes/my_theme.tmTheme` + +If a matching file already exists, update it instead of creating a duplicate. + +## Required Constraints + +- Only produce TextMate `.tmTheme` files. +- Keep the file as XML plist format. +- Do not write the theme anywhere except the top-level `themes/` directory unless the user explicitly asks for something else. +- Do not treat the bundled themes as the authoritative source for required settings or scope coverage. +- The theme must be parseable by `syntect::highlighting::ThemeSet::load_from_reader`. +- Use the semantic token-family standard in [references/token-color-standard.md](references/token-color-standard.md) whenever the user does not provide a custom scope map. + +Amp itself requires these top-level theme settings: + +- `foreground` +- `background` +- `lineHighlight` + +Amp does not require these top-level settings: + +- `caret` +- `selection` +- `invisibles` + +When the user does not provide a complete scope list, do not stop at a small "practical first pass". Instead, map the requested palette onto the canonical token families in the reference file so common code tokens do not collapse to the default foreground. + +## Workflow + +1. Identify the requested theme name and whether it is dark, light, or otherwise palette-driven. +2. Inspect nearby files in `themes/` for file naming and plist formatting conventions only. +3. Read [references/token-color-standard.md](references/token-color-standard.md) and choose colors for each required family. +4. Create or update a `.tmTheme` file with: + - a top-level `name` + - a base settings entry containing `foreground`, `background`, and `lineHighlight` + - scope-specific settings for the canonical token families in the reference, using the requested palette +5. Prefer broadly useful TextMate scopes and stable fallback tiers instead of copying an existing bundled theme's exact rule list. +6. Validate that the theme is parseable through Amp's existing theme-loading path before finishing. + +## Output Guidelines + +### For Generated Or Updated Themes + +- Use a filename stem that matches the theme key Amp will expose in theme selection. +- Keep the plist readable and conventional rather than minimizing or over-structuring it. +- If the requested palette is underspecified, make the smallest reasonable set of assumptions and state them briefly. +- Prefer semantically complete coverage over an artificially tiny rule set. +- Ensure the base settings are sufficient for Amp's UI color mapping: + - `foreground` is used for default text + - `background` is used for inverted background mappings + - `lineHighlight` is used for the focused or current-line background +- Use the default foreground as a last-resort fallback, not as the intended color for common code tokens. +- Keep at least two punctuation tiers when the palette allows it: + - structural punctuation can stay muted + - semantic operators should remain clearly visible +- Distinguish the following families whenever the palette allows it: + - comments + - strings + - numbers and constants + - keywords and storage + - functions and methods + - types and namespaces + - local variables + - parameters + - support or builtin symbols + - annotations or attributes +- Rust is a useful stress case for validation, but the output must remain cross-language and useful for markup and configuration formats too. + +### For Validation + +- Prefer the lightest command that proves the generated `.tmTheme` parses successfully in the repository context. +- If there is no dedicated theme test, still perform a parse-level smoke check before finishing. +- When updating a bundled example theme, prefer adding or running a test that confirms the theme loads and that key token families are represented by explicit scope rules. diff --git a/.agents/skills/theme-definition/references/token-color-standard.md b/.agents/skills/theme-definition/references/token-color-standard.md new file mode 100644 index 00000000..68a030d0 --- /dev/null +++ b/.agents/skills/theme-definition/references/token-color-standard.md @@ -0,0 +1,93 @@ +# Token Color Standard + +Use this reference when generating or updating a `.tmTheme` and the user has not provided a detailed scope map. + +## Goal + +Map a palette onto stable semantic token families so common syntax elements stay differentiated across languages. Rust is the stress case: if a verbose Rust file still looks mostly like plain foreground text, coverage is too weak. + +## Core Rules + +- Start with broad TextMate scope families, not language-specific one-offs. +- Use the base foreground only for truly unclassified text and low-signal fallback. +- Prefer distinct hues for major semantic families over minimalist sameness. +- Keep structural punctuation readable but quieter than semantic operators. +- Parameters must not share the plain-text color when local variables are also colored. +- Support or builtin scopes should usually differ from user-defined names. + +## Canonical Families + +Every generated theme should include explicit selectors for these families unless the palette is intentionally constrained. + +| Family | Purpose | Typical selectors | +| --- | --- | --- | +| Plain text | Unclassified fallback text | `variable`, `source`, `text` fallback only | +| Comments | Comments and docs | `comment`, `comment.block.documentation`, `punctuation.definition.comment` | +| Strings | String bodies | `string`, `string.quoted`, `string.unquoted` | +| String escapes | Escape sequences and regex escapes | `constant.character.escape`, `constant.other.escape`, `string.regexp` | +| Numbers | Numeric literals | `constant.numeric` | +| Constants | Language and user constants | `constant.language`, `constant.character`, `constant.other`, `support.constant` | +| Keywords | Control flow and declarations | `keyword`, `keyword.control`, `keyword.declaration`, `keyword.other` | +| Storage/modifiers | Types, modifiers, mutability, ownership-like markers | `storage`, `storage.type`, `storage.modifier` | +| Functions and methods | Declared and invoked callables | `entity.name.function`, `meta.function-call`, `support.function`, `variable.function` | +| Types | Structs, enums, classes, traits, primitive types | `entity.name.type`, `entity.name.class`, `entity.name.struct`, `entity.name.enum`, `entity.name.trait`, `support.type` | +| Namespaces/modules | Paths, modules, packages | `entity.name.namespace`, `entity.name.module`, `support.module` | +| Macros/preprocessor | Macro-like or generated forms | `entity.name.macro`, `support.macro`, `meta.preprocessor` | +| Local variables | Normal bindings and fields when exposed as variables | `variable.other`, `variable.object`, `variable.other.member` | +| Parameters | Function and closure parameters | `variable.parameter` | +| Language/self variables | `self`, `this`, shell vars, special bindings | `variable.language`, `support.variable` | +| Attributes/annotations | Rust attributes, decorators, annotation names | `entity.other.attribute-name`, `storage.annotation`, `punctuation.definition.annotation` | +| Semantic operators | Accessors, assignment, ranges, arrows, logical ops | `keyword.operator`, `keyword.operator.assignment`, `keyword.operator.accessor`, `keyword.operator.range` | +| Structural punctuation | Braces, commas, delimiters, separators | `punctuation.separator`, `punctuation.terminator`, `meta.brace`, `meta.delimiter`, `punctuation.section` | +| Markup/config extras | Tags, headings, emphasis, diffs | `entity.name.tag`, `markup.heading`, `markup.bold`, `markup.italic`, `markup.inserted`, `markup.deleted` | +| Invalid/deprecated | Errors and deprecated syntax | `invalid`, `invalid.deprecated` | + +## Fallback Order + +Use this fallback order when a palette is underspecified: + +1. Comments +2. Strings +3. Numbers/constants +4. Keywords/storage +5. Functions +6. Types/namespaces +7. Variables/parameters +8. Support or builtin symbols +9. Attributes/annotations +10. Operators +11. Structural punctuation +12. Plain text fallback + +Do not skip from a specialized family straight to plain text if a nearby family already expresses similar semantics. For example: + +- `variable.parameter` should fall back to the variable family, not the base foreground. +- `support.function` should fall back to function or support coloring, not plain text. +- `entity.name.namespace` should fall back to types or support, not plain text. + +## Palette Mapping Guidance + +- Comments: muted and lower-contrast than code. +- Strings: warm or otherwise clearly distinct from comments and keywords. +- Numbers/constants: usually share a family, but language constants may be slightly stronger. +- Keywords/storage: strong and consistent; these anchor the syntax. +- Functions: distinct from types and variables. +- Types/namespaces: related but not identical when the palette has enough room. +- Local variables: subtle but still distinguishable from plain text. +- Parameters: warmer or otherwise more specific than local variables. +- Support/builtins: separate from user-defined names when possible. +- Attributes/annotations: visible but not louder than keywords. +- Semantic operators: visible enough to show expression structure. +- Structural punctuation: muted but never invisible. + +## Validation Checklist + +Before finishing, confirm: + +- The theme parses through Syntect. +- The theme contains explicit selectors for parameters, support or builtins, annotations or attributes, operators, and structural punctuation. +- A Rust-heavy file such as `build.rs` would show visible differences between: + - function names and type names + - parameters and local variables + - namespace paths and plain text + - semantic operators and structural delimiters From 8b398066cf692ed28dc21da63264568ca65d14a8 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 11 Apr 2026 23:33:31 -0400 Subject: [PATCH 06/26] Initial monokai extended theme --- themes/monokai_extended.tmTheme | 263 ++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 themes/monokai_extended.tmTheme diff --git a/themes/monokai_extended.tmTheme b/themes/monokai_extended.tmTheme new file mode 100644 index 00000000..874320fb --- /dev/null +++ b/themes/monokai_extended.tmTheme @@ -0,0 +1,263 @@ + + + + + name + Monokai Extended + settings + + + settings + + background + #1F1F1C + caret + #F8F8F2 + foreground + #F8F8F2 + invisibles + #5B5E57 + lineHighlight + #34352F + selection + #49483E + + + + name + Comments + scope + comment, comment.block.documentation, punctuation.definition.comment + settings + + foreground + #75715E + fontStyle + italic + + + + name + Strings + scope + string, string.quoted, string.unquoted + settings + + foreground + #E6DB74 + + + + name + String Escapes and Regex + scope + constant.character.escape, constant.other.escape, string.regexp, punctuation.definition.string + settings + + foreground + #FD971F + + + + name + Numbers + scope + constant.numeric + settings + + foreground + #AE81FF + + + + name + Constants + scope + constant.language, constant.character, constant.other, support.constant + settings + + foreground + #AB9DF2 + + + + name + Keywords + scope + keyword, keyword.control, keyword.declaration, keyword.other, punctuation.definition.keyword + settings + + foreground + #F92672 + + + + name + Storage and Modifiers + scope + storage, storage.type, storage.modifier + settings + + foreground + #FF6188 + + + + name + Function Names + scope + entity.name.function, variable.function + settings + + foreground + #A6E22E + + + + name + Function Calls and Builtins + scope + meta.function-call, support.function, support.function.builtin, support.macro + settings + + foreground + #78DCE8 + + + + name + Types + scope + entity.name.type, entity.name.class, entity.name.struct, entity.name.enum, entity.name.trait, support.type, support.class + settings + + foreground + #66D9EF + + + + name + Namespaces and Modules + scope + entity.name.namespace, entity.name.module, support.module + settings + + foreground + #78DCE8 + + + + name + Macros and Preprocessor + scope + entity.name.macro, meta.preprocessor + settings + + foreground + #AB9DF2 + + + + name + Local Variables + scope + variable.other, variable.object, variable.other.member + settings + + foreground + #FFD866 + + + + name + Parameters + scope + variable.parameter + settings + + foreground + #FC9867 + + + + name + Language Variables + scope + variable.language, support.variable + settings + + foreground + #FF6188 + + + + name + Attributes and Annotations + scope + entity.other.attribute-name, storage.annotation, punctuation.definition.annotation + settings + + foreground + #FFD866 + + + + name + Semantic Operators + scope + keyword.operator, keyword.operator.assignment, keyword.operator.accessor, keyword.operator.range + settings + + foreground + #FF6188 + + + + name + Structural Punctuation + scope + punctuation.separator, punctuation.terminator, punctuation.section, meta.brace, meta.delimiter + settings + + foreground + #939293 + + + + name + Tags + scope + entity.name.tag, meta.tag, punctuation.definition.tag + settings + + foreground + #F92672 + + + + name + Markup and Diff + scope + markup.heading, markup.bold, markup.italic, markup.inserted, markup.deleted, markup.changed + settings + + foreground + #A6E22E + + + + name + Invalid + scope + invalid, invalid.deprecated + settings + + foreground + #F8F8F0 + background + #F92672 + + + + + From 9bd1850af6fec3b0a79fae2c29ddcb60db69f5b1 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 12 Apr 2026 13:31:39 -0400 Subject: [PATCH 07/26] Use yaml-based built-in theme format --- AGENTS.md | 2 +- Cargo.toml | 1 + build.rs | 10 +- docs/architecture.md | 2 +- themes/monokai_extended.tmTheme | 263 ---- themes/monokai_extended.yml | 86 ++ themes/solarized_dark.tmTheme | 2061 ------------------------------- themes/solarized_dark.yml | 109 ++ themes/solarized_light.tmTheme | 2026 ------------------------------ themes/solarized_light.yml | 106 ++ 10 files changed, 312 insertions(+), 4354 deletions(-) delete mode 100644 themes/monokai_extended.tmTheme create mode 100644 themes/monokai_extended.yml delete mode 100644 themes/solarized_dark.tmTheme create mode 100644 themes/solarized_dark.yml delete mode 100644 themes/solarized_light.tmTheme create mode 100644 themes/solarized_light.yml diff --git a/AGENTS.md b/AGENTS.md index c03bf7ff..e4d0fd75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ ## Project Structure & Module Organization - `src/` contains the Rust source: `main.rs` bootstraps the app, `lib.rs` exposes core logic. - Key modules live under `src/commands/`, `src/models/`, `src/view/`, `src/input/`, `src/presenters/`, and `src/util/`. -- Themes and defaults are stored under `src/themes/` and `src/models/application/preferences/`. +- Bundled theme sources and defaults are stored under `themes/` and `src/models/application/preferences/`. - Documentation sources live in `documentation/` (Zensical/MkDocs content). - Benchmarks live in `benches/`. - For a deeper walkthrough of module roles, the event loop, and command/keymap mechanics, see `docs/architecture.md`. diff --git a/Cargo.toml b/Cargo.toml index 788a200e..a7beb562 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ edition="2021" [build-dependencies] regex = "1.10" syntect = "5.1" +yaml-rust = "0.4" [dependencies] app_dirs2 = "2.5" diff --git a/build.rs b/build.rs index afa9e39f..4baabb86 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,6 @@ +#[path = "build/theme_compiler.rs"] +mod theme_compiler; + use regex::Regex; use std::env; use std::fs::{self, read_to_string, File}; @@ -137,8 +140,11 @@ fn bake_app_syntaxes() { fn bake_app_themes() { let out_dir = env::var("OUT_DIR").expect("The compiler did not provide $OUT_DIR"); let output_path = PathBuf::from(out_dir).join(APP_THEME_SOURCE); - let theme_dir = Path::new(APP_THEME_DIR); - let theme_set = ThemeSet::load_from_folder(theme_dir).expect("Failed to load bundled themes"); + let generated_theme_dir = PathBuf::from(env::var("OUT_DIR").unwrap()).join("generated_themes"); + theme_compiler::compile_themes(Path::new(APP_THEME_DIR), &generated_theme_dir) + .expect("Failed to compile bundled theme sources"); + let theme_set = ThemeSet::load_from_folder(&generated_theme_dir) + .expect("Failed to load generated bundled themes"); dump_to_uncompressed_file(&theme_set, output_path).expect("Failed to write bundled theme dump"); } diff --git a/docs/architecture.md b/docs/architecture.md index 3693ba65..959abe33 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,7 +16,7 @@ Amp is a terminal text editor with a clear separation between state, command log - `src/view/`: terminal rendering, scroll regions, render caches, theming, and event listening. - `src/presenters/`: mode-specific renderers that format workspace state into view components. - `src/util/`: shared helpers (tokens, lexing, selection helpers). -- `src/themes/`: bundled TextMate theme files. +- `themes/`: bundled YAML theme sources compiled into TextMate themes at build time. ## Modes and Presentation Modes are defined in `src/models/application/modes/` and collected into the `Mode` enum. Each mode has a corresponding presenter under `src/presenters/modes/` that renders the UI for that mode using a `Presenter` built from the `View`. diff --git a/themes/monokai_extended.tmTheme b/themes/monokai_extended.tmTheme deleted file mode 100644 index 874320fb..00000000 --- a/themes/monokai_extended.tmTheme +++ /dev/null @@ -1,263 +0,0 @@ - - - - - name - Monokai Extended - settings - - - settings - - background - #1F1F1C - caret - #F8F8F2 - foreground - #F8F8F2 - invisibles - #5B5E57 - lineHighlight - #34352F - selection - #49483E - - - - name - Comments - scope - comment, comment.block.documentation, punctuation.definition.comment - settings - - foreground - #75715E - fontStyle - italic - - - - name - Strings - scope - string, string.quoted, string.unquoted - settings - - foreground - #E6DB74 - - - - name - String Escapes and Regex - scope - constant.character.escape, constant.other.escape, string.regexp, punctuation.definition.string - settings - - foreground - #FD971F - - - - name - Numbers - scope - constant.numeric - settings - - foreground - #AE81FF - - - - name - Constants - scope - constant.language, constant.character, constant.other, support.constant - settings - - foreground - #AB9DF2 - - - - name - Keywords - scope - keyword, keyword.control, keyword.declaration, keyword.other, punctuation.definition.keyword - settings - - foreground - #F92672 - - - - name - Storage and Modifiers - scope - storage, storage.type, storage.modifier - settings - - foreground - #FF6188 - - - - name - Function Names - scope - entity.name.function, variable.function - settings - - foreground - #A6E22E - - - - name - Function Calls and Builtins - scope - meta.function-call, support.function, support.function.builtin, support.macro - settings - - foreground - #78DCE8 - - - - name - Types - scope - entity.name.type, entity.name.class, entity.name.struct, entity.name.enum, entity.name.trait, support.type, support.class - settings - - foreground - #66D9EF - - - - name - Namespaces and Modules - scope - entity.name.namespace, entity.name.module, support.module - settings - - foreground - #78DCE8 - - - - name - Macros and Preprocessor - scope - entity.name.macro, meta.preprocessor - settings - - foreground - #AB9DF2 - - - - name - Local Variables - scope - variable.other, variable.object, variable.other.member - settings - - foreground - #FFD866 - - - - name - Parameters - scope - variable.parameter - settings - - foreground - #FC9867 - - - - name - Language Variables - scope - variable.language, support.variable - settings - - foreground - #FF6188 - - - - name - Attributes and Annotations - scope - entity.other.attribute-name, storage.annotation, punctuation.definition.annotation - settings - - foreground - #FFD866 - - - - name - Semantic Operators - scope - keyword.operator, keyword.operator.assignment, keyword.operator.accessor, keyword.operator.range - settings - - foreground - #FF6188 - - - - name - Structural Punctuation - scope - punctuation.separator, punctuation.terminator, punctuation.section, meta.brace, meta.delimiter - settings - - foreground - #939293 - - - - name - Tags - scope - entity.name.tag, meta.tag, punctuation.definition.tag - settings - - foreground - #F92672 - - - - name - Markup and Diff - scope - markup.heading, markup.bold, markup.italic, markup.inserted, markup.deleted, markup.changed - settings - - foreground - #A6E22E - - - - name - Invalid - scope - invalid, invalid.deprecated - settings - - foreground - #F8F8F0 - background - #F92672 - - - - - diff --git a/themes/monokai_extended.yml b/themes/monokai_extended.yml new file mode 100644 index 00000000..b053afd4 --- /dev/null +++ b/themes/monokai_extended.yml @@ -0,0 +1,86 @@ +name: Monokai Extended +palette: + foreground: "#F8F8F2" + background: "#222222" + line: "#333333" + comment: "#75715E" + string: "#E6DB74" + escape: "#F6AA11" + number: "#BE84FF" + keyword: "#F92672" + function: "#A6E22E" + type: "#66D9EF" + variable: "#F4F1DE" + parameter: "#FD971F" + punctuation: "#908F88" + white: "#FFFFFF" +settings: + foreground: foreground + background: background + line_highlight: line +rules: + - name: Comments + scope: "comment, comment.block.documentation, punctuation.definition.comment" + foreground: comment + font_style: [italic] + - name: Strings + scope: "string, string.quoted, string.unquoted" + foreground: string + - name: String Escapes and Regex + scope: "constant.character.escape, constant.other.escape, string.regexp, punctuation.definition.string" + foreground: escape + - name: Numbers + scope: constant.numeric + foreground: number + - name: Constants + scope: "constant.language, constant.character, constant.other, support.constant" + foreground: number + - name: Keywords + scope: "keyword, keyword.control, keyword.declaration, keyword.other, punctuation.definition.keyword" + foreground: keyword + - name: Storage and Modifiers + scope: "storage, storage.type, storage.modifier" + foreground: keyword + - name: Function Names + scope: "entity.name.function, variable.function" + foreground: function + - name: Function Calls and Builtins + scope: "meta.function-call, support.function, support.function.builtin, support.macro" + foreground: type + - name: Types + scope: "entity.name.type, entity.name.class, entity.name.struct, entity.name.enum, entity.name.trait, support.type, support.class" + foreground: type + - name: Namespaces and Modules + scope: "entity.name.namespace, entity.name.module, support.module" + foreground: type + - name: Macros and Preprocessor + scope: "entity.name.macro, meta.preprocessor" + foreground: number + - name: Local Variables + scope: "variable.other, variable.object, variable.other.member" + foreground: variable + - name: Parameters + scope: variable.parameter + foreground: parameter + - name: Language Variables + scope: "variable.language, support.variable" + foreground: white + - name: Attributes and Annotations + scope: "entity.other.attribute-name, storage.annotation, punctuation.definition.annotation" + foreground: function + - name: Semantic Operators + scope: "keyword.operator, keyword.operator.assignment, keyword.operator.accessor, keyword.operator.range" + foreground: keyword + - name: Structural Punctuation + scope: "punctuation.separator, punctuation.terminator, punctuation.section, meta.brace, meta.delimiter" + foreground: punctuation + - name: Tags + scope: "entity.name.tag, meta.tag, punctuation.definition.tag" + foreground: keyword + - name: Markup and Diff + scope: "markup.heading, markup.bold, markup.italic, markup.inserted, markup.deleted, markup.changed" + foreground: function + - name: Invalid + scope: "invalid, invalid.deprecated" + foreground: foreground + background: keyword diff --git a/themes/solarized_dark.tmTheme b/themes/solarized_dark.tmTheme deleted file mode 100644 index 47bd6035..00000000 --- a/themes/solarized_dark.tmTheme +++ /dev/null @@ -1,2061 +0,0 @@ - - - - - name - Solarized Dark - settings - - - settings - - background - #002B36 - caret - #839496 - foreground - #b2b2b2 - invisibles - #073642 - lineHighlight - #303030 - selection - #EEE8D5 - - - - name - Comment - scope - comment - settings - - fontStyle - - foreground - #586E75 - - - - name - String - scope - string - settings - - foreground - #2AA198 - - - - name - StringNumber - scope - string - settings - - foreground - #586E75 - - - - name - Regexp - scope - string.regexp - settings - - foreground - #DC322F - - - - name - Number - scope - constant.numeric - settings - - foreground - #D33682 - - - - name - Variable - scope - variable.language, variable.other - settings - - foreground - #268BD2 - - - - name - Keyword - scope - keyword - settings - - foreground - #859900 - - - - name - Storage - scope - storage - settings - - fontStyle - - foreground - #859900 - - - - name - Class name - scope - entity.name.class, entity.name.type.class - settings - - foreground - #268BD2 - - - - name - Function name - scope - entity.name.function - settings - - foreground - #268BD2 - - - - name - Variable start - scope - punctuation.definition.variable - settings - - foreground - #859900 - - - - name - Embedded code markers - scope - punctuation.section.embedded.begin, punctuation.section.embedded.end - settings - - foreground - #DC322F - - - - name - Built-in constant - scope - constant.language, meta.preprocessor - settings - - foreground - #B58900 - - - - name - Support.construct - scope - support.function.construct, keyword.other.new - settings - - foreground - #DC322F - - - - name - User-defined constant - scope - constant.character, constant.other - settings - - foreground - #CB4B16 - - - - name - Inherited class - scope - entity.other.inherited-class - settings - - - - name - Function argument - scope - variable.parameter - settings - - - - name - Tag name - scope - entity.name.tag - settings - - foreground - #268BD2 - - - - name - Tag start/end - scope - punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end - settings - - foreground - #586E75 - - - - name - Tag attribute - scope - entity.other.attribute-name - settings - - foreground - #93A1A1 - - - - name - Library function - scope - support.function - settings - - foreground - #268BD2 - - - - name - Continuation - scope - punctuation.separator.continuation - settings - - foreground - #DC322F - - - - name - Library constant - scope - support.constant - settings - - - - name - Library class/type - scope - support.type, support.class - settings - - foreground - #859900 - - - - name - Library Exception - scope - support.type.exception - settings - - foreground - #CB4B16 - - - - name - Special - scope - keyword.other.special-method - settings - - foreground - #CB4B16 - - - - name - Library variable - scope - support.other.variable - settings - - - - name - Invalid - scope - invalid - settings - - - - name - Quoted String - scope - string.quoted.double, string.quoted.single - settings - - foreground - #2AA198 - - - - name - Quotes - scope - punctuation.definition.string.begin, punctuation.definition.string.end - settings - - foreground - #DC322F - - - - name - CSS: Property name (body) - scope - entity.name.tag.css, support.type.property-name.css, meta.property-name.css, support.type.property-name.scss - settings - - fontStyle - - foreground - #839496 - - - - name - CSS: @ rules (purple) - scope - punctuation.definition.keyword.scss, punctuation.definition.keyword.css, keyword.control.at-rule.charset.css, keyword.control.at-rule.charset.scss, keyword.control.each.css, keyword.control.each.scss, keyword.control.else.css, keyword.control.else.scss, keyword.control.at-rule.import.css, keyword.control.at-rule.import.scss, keyword.control.at-rule.fontface.css, keyword.control.at-rule.fontface.scss, keyword.control.for.css, keyword.control.for.scss, keyword.control.at-rule.function.css, keyword.control.at-rule.function.scss, keyword.control.if.css, keyword.control.if.scss, keyword.control.at-rule.include.scss, keyword.control.at-rule.media.css, keyword.control.at-rule.media.scss, keyword.control.at-rule.font-face.css, keyword.control.at-rule.font-face.scss, meta.at-rule.import.css, variable.other.less, variable.declaration.less, variable.interpolation.less, meta.at-rule.media.scss - settings - - foreground - #6C71c4 - - - - name - CSS: Numeric Value (blue) - scope - constant.numeric.css, keyword.other.unit.css, keyword.unit.css, constant.other.color.rgb-value.css, constant.numeric.scss, constant.other.color.rgb-value.scss, keyword.other.unit.scss, punctuation.definition.constant.scss, punctuation.definition.constant.css, constant.other.rgb-value.css - settings - - fontStyle - - foreground - #268BD2 - - - - name - CSS: String, value and constants (azure) - scope - variable.parameter.url.scss, meta.property-value.css, meta.property-value.scss, support.constant.property-value.scss, support.constant.font-name.scss, string.quoted.single.css, string.quoted.double.css, constant.character.escaped.css, string.quoted.variable.parameter.url, punctuation.definition.string.begin.scss, punctuation.definition.string.begin.css, punctuation.definition.string.end.scss, punctuation.definition.string.end.css, support.constant.property-value.css - settings - - fontStyle - - foreground - #2AA198 - - - - name - CSS: !Important (red) - scope - keyword.other.important.css, keyword.other.important.scss - settings - - foreground - #DC322F - - - - name - CSS: Standard color value (orange) - scope - support.constant.color, invalid.deprecated.color.w3c-non-standard-color-name.scss - settings - - foreground - #CB4b16 - - - - name - CSS: : , () (body) - scope - punctuation.terminator.rule.css, punctuation.section.function.css, punctuation.section.function.scss, punctuation.separator.key-value.csspunctuation.scss, punctuation.css, keyword.operator.less, entity.name.tag.wildcard.scss, entity.name.tag.wildcard.css, entity.name.tag.reference.scss - settings - - fontStyle - - foreground - #657B83 - - - - name - CSS: Selector > [] and non-spec tags (body) - scope - meta.selector.css - settings - - fontStyle - - foreground - #657B83 - - - - name - CSS: Tag (green) - scope - entity.name.tag.css, entity.name.tag.scss - settings - - fontStyle - - foreground - #859900 - - - - name - CSS .class (yellow) - scope - entity.other.attribute-name.class.css, entity.other.less.mixin - settings - - fontStyle - - foreground - #B58900 - - - - name - CSS: #id (yellow) - scope - source.css entity.other.attribute-name.id, source.scss entity.other.attribute-name.id - settings - - foreground - #B58900 - - - - name - CSS :pseudo (orange) - scope - entity.other.attribute-name.pseudo-element.css, entity.other.attribute-name.pseudo-class.css - settings - - fontStyle - - foreground - #CB4B16 - - - - name - SCSS: Variables (pink) - scope - variable, variable.scss - settings - - foreground - #D33682 - - - - name - JS: Function Name - scope - meta.function.js, entity.name.function.js, support.function.dom.js - settings - - foreground - #B58900 - - - - name - JS: Source - scope - text.html.basic source.js.embedded.html - settings - - fontStyle - - foreground - #B58900 - - - - name - JS: Function - scope - storage.type.function.js - settings - - foreground - #268BD2 - - - - name - JS: Numeric Constant - scope - constant.numeric.js - settings - - foreground - #2AA198 - - - - name - JS: [] - scope - meta.brace.square.js - settings - - foreground - #268BD2 - - - - name - JS: Storage Type - scope - storage.type.js - settings - - foreground - #268BD2 - - - - name - () - scope - meta.brace.round, punctuation.definition.parameters.begin.js, punctuation.definition.parameters.end.js - settings - - foreground - #93A1A1 - - - - name - {} - scope - meta.brace.curly.js - settings - - foreground - #839496 - - - - name - HTML: Doctype - scope - entity.name.tag.doctype.html, meta.tag.sgml.html, string.quoted.double.doctype.identifiers-and-DTDs.html - settings - - fontStyle - italic - foreground - #839496 - - - - name - HTML: Comment Block - scope - comment.block.html - settings - - fontStyle - italic - foreground - #839496 - - - - name - HTML: Script - scope - entity.name.tag.script.html - settings - - fontStyle - italic - - - - name - HTML: Style - scope - source.css.embedded.html string.quoted.double.html - settings - - fontStyle - - foreground - #2AA198 - - - - name - HTML: Text - scope - text.html.ruby - settings - - foreground - #839496 - - - - name - HTML: = - scope - text.html.basic meta.tag.other.html, text.html.basic meta.tag.any.html, text.html.basic meta.tag.block.any, text.html.basic meta.tag.inline.any, text.html.basic meta.tag.structure.any.html, text.html.basic source.js.embedded.html, punctuation.separator.key-value.html - settings - - fontStyle - - foreground - #657B83 - - - - name - HTML: something= - scope - text.html.basic entity.other.attribute-name.html - settings - - foreground - #657B83 - - - - name - HTML: " - scope - text.html.basic meta.tag.structure.any.html punctuation.definition.string.begin.html, punctuation.definition.string.begin.html, punctuation.definition.string.end.html - settings - - fontStyle - - foreground - #2AA198 - - - - name - HTML: <tag> - scope - entity.name.tag.block.any.html - settings - - foreground - #268BD2 - - - - name - HTML: style - scope - source.css.embedded.html entity.name.tag.style.html - settings - - fontStyle - italic - - - - name - HTML: <style> - scope - entity.name.tag.style.html - settings - - fontStyle - - - - - name - HTML: {} - scope - text.html.basic, punctuation.section.property-list.css - settings - - fontStyle - - - - - name - HTML: Embeddable - scope - source.css.embedded.html, comment.block.html - settings - - fontStyle - italic - foreground - #839496 - - - - name - Ruby: Variable definition - scope - punctuation.definition.variable.ruby - settings - - fontStyle - - foreground - #268BD2 - - - - name - Ruby: Function Name - scope - meta.function.method.with-arguments.ruby - settings - - foreground - #657B83 - - - - name - Ruby: Variable - scope - variable.language.ruby - settings - - foreground - #2AA198 - - - - name - Ruby: Function - scope - entity.name.function.ruby - settings - - foreground - #268BD2 - - - - name - Ruby: Keyword Control - scope - keyword.control.ruby, keyword.control.def.ruby - settings - - foreground - #859900 - - - - name - Ruby: Class - scope - keyword.control.class.ruby, meta.class.ruby - settings - - foreground - #859900 - - - - name - Ruby: Class Name - scope - entity.name.type.class.ruby - settings - - fontStyle - - foreground - #B58900 - - - - name - Ruby: Keyword - scope - keyword.control.ruby - settings - - fontStyle - - foreground - #859900 - - - - name - Ruby: Support Class - scope - support.class.ruby - settings - - fontStyle - - foreground - #B58900 - - - - name - Ruby: Special Method - scope - keyword.other.special-method.ruby - settings - - foreground - #859900 - - - - name - Ruby: Constant - scope - constant.language.ruby, constant.numeric.ruby - settings - - foreground - #2AA198 - - - - name - Ruby: Constant Other - scope - variable.other.constant.ruby - settings - - fontStyle - - foreground - #B58900 - - - - name - Ruby: :symbol - scope - constant.other.symbol.ruby - settings - - fontStyle - - foreground - #2AA198 - - - - name - Ruby: Punctuation Section '' - scope - punctuation.section.embedded.ruby, punctuation.definition.string.begin.ruby, punctuation.definition.string.end.ruby - settings - - foreground - #DC322F - - - - name - Ruby: Special Method - scope - keyword.other.special-method.ruby - settings - - foreground - #CB4B16 - - - - name - PHP: Include - scope - keyword.control.import.include.php - settings - - foreground - #CB4B16 - - - - name - Ruby: erb = - scope - text.html.ruby meta.tag.inline.any.html - settings - - fontStyle - - foreground - #839496 - - - - name - Ruby: erb "" - scope - text.html.ruby punctuation.definition.string.begin, text.html.ruby punctuation.definition.string.end - settings - - fontStyle - - foreground - #2AA198 - - - - name - PHP: Quoted Single - scope - punctuation.definition.string.begin, punctuation.definition.string.end - settings - - foreground - #839496 - - - - name - PHP: Class Names - scope - support.class.php - settings - - foreground - #93A1A1 - - - - name - PHP: [] - scope - keyword.operator.index-start.php, keyword.operator.index-end.php - settings - - foreground - #DC322F - - - - name - PHP: Array - scope - meta.array.php - settings - - foreground - #586E75 - - - - name - PHP: Array() - scope - meta.array.php support.function.construct.php, meta.array.empty.php support.function.construct.php - settings - - fontStyle - - foreground - #B58900 - - - - name - PHP: Array Construct - scope - support.function.construct.php - settings - - foreground - #B58900 - - - - name - PHP: Array Begin - scope - punctuation.definition.array.begin, punctuation.definition.array.end - settings - - foreground - #DC322F - - - - name - PHP: Numeric Constant - scope - constant.numeric.php - settings - - foreground - #2AA198 - - - - name - PHP: New - scope - keyword.other.new.php - settings - - foreground - #CB4B16 - - - - name - PHP: :: - scope - keyword.operator.class - settings - - fontStyle - - foreground - #93A1A1 - - - - name - PHP: Other Property - scope - variable.other.property.php - settings - - foreground - #839496 - - - - name - PHP: Class - scope - storage.modifier.extends.php, storage.type.class.php, keyword.operator.class.php - settings - - foreground - #B58900 - - - - name - PHP: Semicolon - scope - punctuation.terminator.expression.php - settings - - foreground - #839496 - - - - name - PHP: Inherited Class - scope - meta.other.inherited-class.php - settings - - fontStyle - - foreground - #586E75 - - - - name - PHP: Storage Type - scope - storage.type.php - settings - - foreground - #859900 - - - - name - PHP: Function - scope - entity.name.function.php - settings - - foreground - #839496 - - - - name - PHP: Function Construct - scope - support.function.construct.php - settings - - foreground - #859900 - - - - name - PHP: Function Call - scope - entity.name.type.class.php, meta.function-call.php, meta.function-call.static.php, meta.function-call.object.php - settings - - foreground - #839496 - - - - name - PHP: Comment - scope - keyword.other.phpdoc - settings - - fontStyle - - foreground - #839496 - - - - name - PHP: Source Emebedded - scope - source.php.embedded.block.html - settings - - foreground - #CB4B16 - - - - name - PHP: Storage Type Function - scope - storage.type.function.php - settings - - foreground - #CB4B16 - - - - name - C: constant - scope - constant.numeric.c - settings - - fontStyle - - foreground - #2AA198 - - - - name - C: Meta Preprocessor - scope - meta.preprocessor.c.include, meta.preprocessor.macro.c - settings - - fontStyle - - foreground - #CB4B16 - - - - name - C: Keyword - scope - keyword.control.import.define.c, keyword.control.import.include.c - settings - - fontStyle - - foreground - #CB4B16 - - - - name - C: Function Preprocessor - scope - entity.name.function.preprocessor.c - settings - - fontStyle - - foreground - #CB4B16 - - - - name - C: include <something.c> - scope - meta.preprocessor.c.include string.quoted.other.lt-gt.include.c, meta.preprocessor.c.include punctuation.definition.string.begin.c, meta.preprocessor.c.include punctuation.definition.string.end.c - settings - - fontStyle - - foreground - #2AA198 - - - - name - C: Function - scope - support.function.C99.c, support.function.any-method.c, entity.name.function.c - settings - - fontStyle - - foreground - #93A1A1 - - - - name - C: " - scope - punctuation.definition.string.begin.c, punctuation.definition.string.end.c - settings - - fontStyle - - foreground - #2AA198 - - - - name - C: Storage Type - scope - storage.type.c - settings - - fontStyle - - foreground - #B58900 - - - - name - diff: header - scope - meta.diff, meta.diff.header - settings - - background - #B58900 - fontStyle - italic - foreground - #EEE8D5 - - - - name - diff: deleted - scope - markup.deleted - settings - - background - #EEE8D5 - fontStyle - - foreground - #DC322F - - - - name - diff: changed - scope - markup.changed - settings - - background - #EEE8D5 - fontStyle - - foreground - #CB4B16 - - - - name - diff: inserted - scope - markup.inserted - settings - - background - #EEE8D5 - foreground - #2AA198 - - - - name - reST raw - scope - text.restructuredtext markup.raw - settings - - foreground - #2AA198 - - - - name - Other: Removal - scope - other.package.exclude, other.remove - settings - - fontStyle - - foreground - #DC322F - - - - name - Other: Add - scope - other.add - settings - - foreground - #2AA198 - - - - name - Tex: {} - scope - punctuation.section.group.tex , punctuation.definition.arguments.begin.latex, punctuation.definition.arguments.end.latex, punctuation.definition.arguments.latex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: {text} - scope - meta.group.braces.tex - settings - - fontStyle - - foreground - #B58900 - - - - name - Tex: Other Math - scope - string.other.math.tex - settings - - fontStyle - - foreground - #B58900 - - - - name - Tex: {var} - scope - variable.parameter.function.latex - settings - - fontStyle - - foreground - #CB4B16 - - - - name - Tex: Math \\ - scope - punctuation.definition.constant.math.tex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: Constant Math - scope - text.tex.latex constant.other.math.tex, constant.other.general.math.tex, constant.other.general.math.tex, constant.character.math.tex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Tex: Other Math String - scope - string.other.math.tex - settings - - fontStyle - - foreground - #B58900 - - - - name - Tex: $ - scope - punctuation.definition.string.begin.tex, punctuation.definition.string.end.tex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: \label - scope - keyword.control.label.latex, text.tex.latex constant.other.general.math.tex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Tex: \label { } - scope - variable.parameter.definition.label.latex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: Function - scope - support.function.be.latex - settings - - fontStyle - - foreground - #859900 - - - - name - Tex: Support Function Section - scope - support.function.section.latex - settings - - fontStyle - - foreground - #CB4B16 - - - - name - Tex: Support Function - scope - support.function.general.tex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Tex: Comment - scope - punctuation.definition.comment.tex, comment.line.percentage.tex - settings - - fontStyle - italic - - - - name - Tex: Reference Label - scope - keyword.control.ref.latex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Python: storage - scope - storage.type.class.python, storage.type.function.python, storage.modifier.global.python - settings - - fontStyle - - foreground - #859900 - - - - name - Python: import - scope - keyword.control.import.python, keyword.control.import.from.python - settings - - foreground - #CB4B16 - - - - name - Python: Support.exception - scope - support.type.exception.python - settings - - foreground - #B58900 - - - - name - Shell: builtin - scope - support.function.builtin.shell - settings - - foreground - #859900 - - - - name - Shell: variable - scope - variable.other.normal.shell - settings - - foreground - #CB4B16 - - - - name - Shell: DOT_FILES - scope - source.shell - settings - - fontStyle - - foreground - #268BD2 - - - - name - Shell: meta scope in loop - scope - meta.scope.for-in-loop.shell, variable.other.loop.shell - settings - - fontStyle - - foreground - #586E75 - - - - name - Shell: "" - scope - punctuation.definition.string.end.shell, punctuation.definition.string.begin.shell - settings - - fontStyle - - foreground - #859900 - - - - name - Shell: Meta Block - scope - meta.scope.case-block.shell, meta.scope.case-body.shell - settings - - fontStyle - - foreground - #586E75 - - - - name - Shell: [] - scope - punctuation.definition.logical-expression.shell - settings - - fontStyle - - foreground - #DC322F - - - - name - Shell: Comment - scope - comment.line.number-sign.shell - settings - - fontStyle - italic - - - - name - Java: import - scope - keyword.other.import.java - settings - - fontStyle - - foreground - #CB4B16 - - - - name - Java: meta-import - scope - storage.modifier.import.java - settings - - fontStyle - - foreground - #586E75 - - - - name - Java: Class - scope - meta.class.java storage.modifier.java - settings - - fontStyle - - foreground - #B58900 - - - - name - Java: /* comment */ - scope - source.java comment.block - settings - - fontStyle - - foreground - #586E75 - - - - name - Java: /* @param */ - scope - comment.block meta.documentation.tag.param.javadoc keyword.other.documentation.param.javadoc - settings - - fontStyle - - foreground - #586E75 - - - - name - Perl: variables - scope - punctuation.definition.variable.perl, variable.other.readwrite.global.perl, variable.other.predefined.perl, keyword.operator.comparison.perl - settings - - foreground - #B58900 - - - - name - Perl: functions - scope - support.function.perl - settings - - foreground - #859900 - - - - name - Perl: comments - scope - comment.line.number-sign.perl - settings - - fontStyle - italic - foreground - #586E75 - - - - name - Perl: quotes - scope - punctuation.definition.string.begin.perl, punctuation.definition.string.end.perl - settings - - foreground - #2AA198 - - - - name - Perl: \char - scope - constant.character.escape.perl - settings - - foreground - #DC322F - - - - Name - Markdown punctuation - scope - markup.list, text.html.markdown punctuation.definition, meta.separator.markdown - settings - - foreground - #CB4b16 - - - - Name - Markdown heading - scope - markup.heading - settings - - foreground - #268BD2 - - - - Name - Markdown text inside some block element - scope - markup.quote, meta.paragraph.list - settings - - foreground - #2AA198 - - - - Name - Markdown em - scope - markup.italic - settings - - fontStyle - italic - - - - Name - Markdown strong - scope - markup.bold - settings - - fontStyle - bold - - - - Name - Markdown reference - scope - markup.underline.link.markdown, meta.link.inline punctuation.definition.metadata, meta.link.reference.markdown punctuation.definition.constant, meta.link.reference.markdown constant.other.reference - settings - - foreground - #B58900 - - - - Name - Markdown linebreak - scope - meta.paragraph.markdown meta.dummy.line-break - settings - - background - #6C71c4 - - - - - name - GitGutter deleted - scope - markup.deleted.git_gutter - settings - - foreground - #F92672 - - - - name - GitGutter inserted - scope - markup.inserted.git_gutter - settings - - foreground - #A6E22E - - - - name - GitGutter changed - scope - markup.changed.git_gutter - settings - - foreground - #967EFB - - - - - name - SublimeLinter Annotations - scope - sublimelinter.notes - settings - - background - #eee8d5 - foreground - #eee8d5 - - - - name - SublimeLinter Error Outline - scope - sublimelinter.outline.illegal - settings - - background - #93a1a1 - foreground - #93a1a1 - - - - name - SublimeLinter Error Underline - scope - sublimelinter.underline.illegal - settings - - background - #dc322f - - - - name - SublimeLinter Warning Outline - scope - sublimelinter.outline.warning - settings - - background - #839496 - foreground - #839496 - - - - name - SublimeLinter Warning Underline - scope - sublimelinter.underline.warning - settings - - background - #b58900 - - - - name - SublimeLinter Violation Outline - scope - sublimelinter.outline.violation - settings - - background - #657b83 - foreground - #657b83 - - - - name - SublimeLinter Violation Underline - scope - sublimelinter.underline.violation - settings - - background - #cb4b16 - - - - name - SublimeBracketHighlighter - scope - brackethighlighter.all - settings - - background - #002b36 - foreground - #cb4b16 - - - - uuid - A4299D9B-1DE5-4BC4-87F6-A757E71B1597 - - diff --git a/themes/solarized_dark.yml b/themes/solarized_dark.yml new file mode 100644 index 00000000..2ffb3bfa --- /dev/null +++ b/themes/solarized_dark.yml @@ -0,0 +1,109 @@ +name: Solarized Dark +palette: + base3: "#002B36" + base2: "#073642" + base1: "#586E75" + base0: "#657B83" + base00: "#839496" + base01: "#93A1A1" + base02: "#b2b2b2" + base3_light: "#EEE8D5" + yellow: "#B58900" + orange: "#CB4B16" + red: "#DC322F" + magenta: "#D33682" + violet: "#6C71C4" + blue: "#268BD2" + cyan: "#2AA198" + green: "#859900" +settings: + foreground: base02 + background: base3 + line_highlight: "#303030" +rules: + - name: Comment + scope: "comment, comment.block.documentation, punctuation.definition.comment" + foreground: base1 + font_style: [] + - name: String + scope: "string, string.quoted.double, string.quoted.single" + foreground: cyan + - name: Regexp + scope: "string.regexp, constant.character.escape, punctuation.definition.string.begin, punctuation.definition.string.end" + foreground: red + - name: Number + scope: constant.numeric + foreground: magenta + - name: Variable + scope: "variable.language, variable.other" + foreground: blue + - name: Keyword + scope: "keyword, keyword.control" + foreground: green + - name: Storage + scope: "storage, storage.type.class.python, storage.type.function.python, storage.modifier.global.python" + foreground: green + font_style: [] + - name: Functions + scope: "entity.name.function, support.function, support.function.builtin.shell, meta.function.js, entity.name.function.js" + foreground: blue + - name: Support Constructs + scope: "support.function.construct, keyword.other.new, support.function.construct.php" + foreground: red + - name: Types + scope: "entity.name.class, entity.name.type.class, entity.name.type, support.type.exception, support.class" + foreground: blue + - name: Support Types + scope: "support.type, storage.type.c, storage.type.js, storage.type.php" + foreground: green + - name: Built-in Constant + scope: "constant.language, meta.preprocessor, keyword.control.import.define.c, keyword.control.import.include.c" + foreground: yellow + - name: User-defined Constant + scope: "constant.character, constant.other, keyword.other.special-method, keyword.control.import.include.php" + foreground: orange + - name: Parameters + scope: "variable.parameter, variable.parameter.function.latex, variable.parameter.definition.label.latex" + foreground: orange + - name: Operators + scope: "keyword.operator, punctuation.separator.continuation, punctuation.definition.array.begin, punctuation.definition.array.end" + foreground: red + - name: Structural Punctuation + scope: "punctuation.separator, punctuation.terminator, punctuation.section, meta.brace, meta.delimiter" + foreground: base0 + - name: Tags + scope: "entity.name.tag, punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end" + foreground: blue + - name: Attributes + scope: "entity.other.attribute-name, entity.other.attribute-name.class.css, entity.other.attribute-name.id, entity.other.attribute-name.pseudo-element.css, entity.other.attribute-name.pseudo-class.css" + foreground: base01 + - name: Markup + scope: "markup.heading, markup.quote, markup.list, meta.separator.markdown" + foreground: blue + - name: Diff Deleted + scope: "markup.deleted, markup.deleted.git_gutter" + foreground: red + background: base3_light + font_style: [] + - name: Diff Changed + scope: "markup.changed, markup.changed.git_gutter" + foreground: orange + background: base3_light + font_style: [] + - name: Diff Inserted + scope: "markup.inserted, markup.inserted.git_gutter, other.add" + foreground: cyan + background: base3_light + - name: Invalid + scope: "invalid, invalid.deprecated, sublimelinter.underline.illegal" + foreground: base3_light + background: red + - name: Underline Warning + scope: "sublimelinter.underline.warning, sublimelinter.underline.violation" + background: yellow + - name: Tex Functions + scope: "support.function.be.latex, support.function.section.latex, support.function.general.tex, keyword.control.ref.latex, keyword.control.label.latex" + foreground: cyan + - name: Markdown Styles + scope: "markup.italic, markup.bold, markup.underline.link.markdown, meta.link.reference.markdown constant.other.reference" + foreground: yellow diff --git a/themes/solarized_light.tmTheme b/themes/solarized_light.tmTheme deleted file mode 100644 index 0b4835d4..00000000 --- a/themes/solarized_light.tmTheme +++ /dev/null @@ -1,2026 +0,0 @@ - - - - - name - Solarized Light - settings - - - settings - - background - #FDF6E3 - caret - #657B83 - foreground - #657B83 - invisibles - #EEE8D5 - lineHighlight - #EEE8D5 - selection - #073642 - - - - name - Comment - scope - comment - settings - - fontStyle - - foreground - #93A1A1 - - - - name - String - scope - string - settings - - foreground - #2AA198 - - - - name - StringNumber - scope - string - settings - - foreground - #586E75 - - - - name - Regexp - scope - string.regexp - settings - - foreground - #DC322F - - - - name - Number - scope - constant.numeric - settings - - foreground - #D33682 - - - - name - Variable - scope - variable.language, variable.other - settings - - foreground - #268BD2 - - - - name - Keyword - scope - keyword - settings - - foreground - #859900 - - - - name - Storage - scope - storage - settings - - fontStyle - - foreground - #859900 - - - - name - Class name - scope - entity.name.class, entity.name.type.class - settings - - foreground - #268BD2 - - - - name - Function name - scope - entity.name.function - settings - - foreground - #268BD2 - - - - name - Variable start - scope - punctuation.definition.variable - settings - - foreground - #859900 - - - - name - Embedded code markers - scope - punctuation.section.embedded.begin, punctuation.section.embedded.end - settings - - foreground - #DC322F - - - - name - Built-in constant - scope - constant.language, meta.preprocessor - settings - - foreground - #B58900 - - - - name - Support.construct - scope - support.function.construct, keyword.other.new - settings - - foreground - #DC322F - - - - name - User-defined constant - scope - constant.character, constant.other - settings - - foreground - #CB4B16 - - - - name - Inherited class - scope - entity.other.inherited-class - settings - - - - name - Function argument - scope - variable.parameter - settings - - - - name - Tag name - scope - entity.name.tag - settings - - foreground - #268BD2 - - - - name - Tag start/end - scope - punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end - settings - - foreground - #93A1A1 - - - - name - Tag attribute - scope - entity.other.attribute-name - settings - - foreground - #93A1A1 - - - - name - Library function - scope - support.function - settings - - foreground - #268BD2 - - - - name - Continuation - scope - punctuation.separator.continuation - settings - - foreground - #DC322F - - - - name - Library constant - scope - support.constant - settings - - - - name - Library class/type - scope - support.type, support.class - settings - - foreground - #859900 - - - - name - Library Exception - scope - support.type.exception - settings - - foreground - #CB4B16 - - - - name - Special - scope - keyword.other.special-method - settings - - foreground - #CB4B16 - - - - name - Library variable - scope - support.other.variable - settings - - - - name - Invalid - scope - invalid - settings - - - - name - Quoted String - scope - string.quoted.double, string.quoted.single - settings - - foreground - #2AA198 - - - - name - Quotes - scope - punctuation.definition.string.begin, punctuation.definition.string.end - settings - - foreground - #DC322F - - - - name - CSS: Property name (body) - scope - entity.name.tag.css, support.type.property-name.css, meta.property-name.css, support.type.property-name.scss - settings - - fontStyle - - foreground - #657B83 - - - - name - CSS: @ rules (purple) - scope - punctuation.definition.keyword.scss, punctuation.definition.keyword.css, keyword.control.at-rule.charset.css, keyword.control.at-rule.charset.scss, keyword.control.each.css, keyword.control.each.scss, keyword.control.else.css, keyword.control.else.scss, keyword.control.at-rule.import.css, keyword.control.at-rule.import.scss, keyword.control.at-rule.fontface.css, keyword.control.at-rule.fontface.scss, keyword.control.for.css, keyword.control.for.scss, keyword.control.at-rule.function.css, keyword.control.at-rule.function.scss, keyword.control.if.css, keyword.control.if.scss, keyword.control.at-rule.include.scss, keyword.control.at-rule.media.css, keyword.control.at-rule.media.scss, keyword.control.at-rule.font-face.css, keyword.control.at-rule.font-face.scss, meta.at-rule.import.css, variable.other.less, variable.declaration.less, variable.interpolation.less, meta.at-rule.media.scss - settings - - foreground - #6C71c4 - - - - name - CSS: Numeric Value (blue) - scope - constant.numeric.css, keyword.other.unit.css, keyword.unit.css, constant.other.color.rgb-value.css, constant.numeric.scss, constant.other.color.rgb-value.scss, keyword.other.unit.scss, punctuation.definition.constant.scss, punctuation.definition.constant.css, constant.other.rgb-value.css - settings - - fontStyle - - foreground - #268BD2 - - - - name - CSS: String, value and constants (azure) - scope - variable.parameter.url.scss, meta.property-value.css, meta.property-value.scss, support.constant.property-value.scss, support.constant.font-name.scss, string.quoted.single.css, string.quoted.double.css, constant.character.escaped.css, string.quoted.variable.parameter.url, punctuation.definition.string.begin.scss, punctuation.definition.string.begin.css, punctuation.definition.string.end.scss, punctuation.definition.string.end.css, support.constant.property-value.css - settings - - fontStyle - - foreground - #2AA198 - - - - name - CSS: !Important (red) - scope - keyword.other.important.css - settings - - foreground - #DC322F - - - - name - CSS: Standard color value (orange) - scope - support.constant.color, invalid.deprecated.color.w3c-non-standard-color-name.scss - settings - - foreground - #CB4b16 - - - - name - CSS: : , () (body) - scope - punctuation.terminator.rule.css, punctuation.section.function.css, punctuation.section.function.scss, punctuation.separator.key-value.csspunctuation.scss, punctuation.css, keyword.operator.less, entity.name.tag.wildcard.scss, entity.name.tag.wildcard.css, entity.name.tag.reference.scss - settings - - fontStyle - - foreground - #657B83 - - - - name - CSS: Selector > [] and non-spec tags (body) - scope - meta.selector.css - settings - - fontStyle - - foreground - #657B83 - - - - name - CSS: Tag (green) - scope - entity.name.tag.css, entity.name.tag.scss - settings - - fontStyle - - foreground - #859900 - - - - name - CSS .class (yellow) - scope - entity.other.attribute-name.class.css, entity.other.less.mixin - settings - - fontStyle - - foreground - #B58900 - - - - name - CSS: #id (yellow) - scope - source.css entity.other.attribute-name.id - settings - - foreground - #B58900 - - - - name - CSS :pseudo (orange) - scope - entity.other.attribute-name.pseudo-element.css, entity.other.attribute-name.pseudo-class.css - settings - - fontStyle - - foreground - #CB4B16 - - - - name - SCSS: Variables (pink) - scope - variable, variable.scss - settings - - foreground - #D33682 - - - - name - JS: Function Name - scope - meta.function.js, entity.name.function.js, support.function.dom.js - settings - - foreground - #B58900 - - - - name - JS: Source - scope - text.html.basic source.js.embedded.html - settings - - fontStyle - - foreground - #B58900 - - - - name - JS: Function - scope - storage.type.function.js - settings - - foreground - #268BD2 - - - - name - JS: Numeric Constant - scope - constant.numeric.js - settings - - foreground - #2AA198 - - - - name - JS: [] - scope - meta.brace.square.js - settings - - foreground - #268BD2 - - - - name - JS: Storage Type - scope - storage.type.js - settings - - foreground - #268BD2 - - - - name - () - scope - meta.brace.round, punctuation.definition.parameters.begin.js, punctuation.definition.parameters.end.js - settings - - foreground - #93A1A1 - - - - name - {} - scope - meta.brace.curly.js - settings - - foreground - #657B83 - - - - name - HTML: Doctype - scope - entity.name.tag.doctype.html, meta.tag.sgml.html, string.quoted.double.doctype.identifiers-and-DTDs.html - settings - - fontStyle - italic - foreground - #839496 - - - - name - HTML: Comment Block - scope - comment.block.html - settings - - fontStyle - italic - foreground - #839496 - - - - name - HTML: Script - scope - entity.name.tag.script.html - settings - - fontStyle - italic - - - - name - HTML: Style - scope - source.css.embedded.html string.quoted.double.html - settings - - fontStyle - - foreground - #2AA198 - - - - name - HTML: Text - scope - text.html.ruby - settings - - foreground - #657b83 - - - - name - HTML: = - scope - text.html.basic meta.tag.other.html, text.html.basic meta.tag.any.html, text.html.basic meta.tag.block.any, text.html.basic meta.tag.inline.any, text.html.basic meta.tag.structure.any.html, text.html.basic source.js.embedded.html, punctuation.separator.key-value.html - settings - - fontStyle - - foreground - #657B83 - - - - name - HTML: something= - scope - text.html.basic entity.other.attribute-name.html - settings - - foreground - #657B83 - - - - name - HTML: " - scope - text.html.basic meta.tag.structure.any.html punctuation.definition.string.begin.html, punctuation.definition.string.begin.html, punctuation.definition.string.end.html - settings - - fontStyle - - foreground - #2AA198 - - - - name - HTML: <tag> - scope - entity.name.tag.block.any.html - settings - - foreground - #268BD2 - - - - name - HTML: style - scope - source.css.embedded.html entity.name.tag.style.html - settings - - fontStyle - italic - - - - name - HTML: <style> - scope - entity.name.tag.style.html - settings - - fontStyle - - - - - name - HTML: {} - scope - text.html.basic, punctuation.section.property-list.css - settings - - fontStyle - - - - - name - HTML: Embeddable - scope - source.css.embedded.html, comment.block.html - settings - - fontStyle - italic - foreground - #839496 - - - - name - Ruby: Variable definition - scope - punctuation.definition.variable.ruby - settings - - fontStyle - - foreground - #268BD2 - - - - name - Ruby: Function Name - scope - meta.function.method.with-arguments.ruby - settings - - foreground - #657B83 - - - - name - Ruby: Variable - scope - variable.language.ruby - settings - - foreground - #2AA198 - - - - name - Ruby: Function - scope - entity.name.function.ruby - settings - - foreground - #268BD2 - - - - name - Ruby: Keyword Control - scope - keyword.control.ruby, keyword.control.def.ruby - settings - - foreground - #859900 - - - - name - Ruby: Class - scope - keyword.control.class.ruby, meta.class.ruby - settings - - foreground - #859900 - - - - name - Ruby: Class Name - scope - entity.name.type.class.ruby - settings - - fontStyle - - foreground - #B58900 - - - - name - Ruby: Keyword - scope - keyword.control.ruby - settings - - fontStyle - - foreground - #859900 - - - - name - Ruby: Support Class - scope - support.class.ruby - settings - - fontStyle - - foreground - #B58900 - - - - name - Ruby: Special Method - scope - keyword.other.special-method.ruby - settings - - foreground - #859900 - - - - name - Ruby: Constant - scope - constant.language.ruby, constant.numeric.ruby - settings - - foreground - #2AA198 - - - - name - Ruby: Constant Other - scope - variable.other.constant.ruby - settings - - fontStyle - - foreground - #B58900 - - - - name - Ruby: :symbol - scope - constant.other.symbol.ruby - settings - - fontStyle - - foreground - #2AA198 - - - - name - Ruby: Punctuation Section '' - scope - punctuation.section.embedded.ruby, punctuation.definition.string.begin.ruby, punctuation.definition.string.end.ruby - settings - - foreground - #DC322F - - - - name - Ruby: Special Method - scope - keyword.other.special-method.ruby - settings - - foreground - #CB4B16 - - - - name - PHP: Include - scope - keyword.control.import.include.php - settings - - foreground - #CB4B16 - - - - name - Ruby: erb = - scope - text.html.ruby meta.tag.inline.any.html - settings - - fontStyle - - foreground - #839496 - - - - name - Ruby: erb "" - scope - text.html.ruby punctuation.definition.string.begin, text.html.ruby punctuation.definition.string.end - settings - - fontStyle - - foreground - #2AA198 - - - - name - PHP: Quoted Single - scope - punctuation.definition.string.begin, punctuation.definition.string.end - settings - - foreground - #839496 - - - - name - PHP: Class Names - scope - support.class.php - settings - - foreground - #586E75 - - - - name - PHP: [] - scope - keyword.operator.index-start.php, keyword.operator.index-end.php - settings - - foreground - #DC322F - - - - name - PHP: Array - scope - meta.array.php - settings - - foreground - #586E75 - - - - name - PHP: Array() - scope - meta.array.php support.function.construct.php, meta.array.empty.php support.function.construct.php - settings - - fontStyle - - foreground - #B58900 - - - - name - PHP: Array Construct - scope - support.function.construct.php - settings - - foreground - #B58900 - - - - name - PHP: Array Begin - scope - punctuation.definition.array.begin, punctuation.definition.array.end - settings - - foreground - #DC322F - - - - name - PHP: Numeric Constant - scope - constant.numeric.php - settings - - foreground - #2AA198 - - - - name - PHP: New - scope - keyword.other.new.php - settings - - foreground - #CB4B16 - - - - name - PHP: :: - scope - keyword.operator.class - settings - - fontStyle - - foreground - #586E75 - - - - name - PHP: Other Property - scope - variable.other.property.php - settings - - foreground - #839496 - - - - name - PHP: Class - scope - storage.modifier.extends.php, storage.type.class.php, keyword.operator.class.php - settings - - foreground - #B58900 - - - - name - PHP: Semicolon - scope - punctuation.terminator.expression.php - settings - - foreground - #657B83 - - - - name - PHP: Inherited Class - scope - meta.other.inherited-class.php - settings - - fontStyle - - foreground - #586E75 - - - - name - PHP: Storage Type - scope - storage.type.php - settings - - foreground - #859900 - - - - name - PHP: Function - scope - entity.name.function.php - settings - - foreground - #839496 - - - - name - PHP: Function Construct - scope - support.function.construct.php - settings - - foreground - #859900 - - - - name - PHP: Function Call - scope - entity.name.type.class.php, meta.function-call.php, meta.function-call.static.php, meta.function-call.object.php - settings - - foreground - #839496 - - - - name - PHP: Comment - scope - keyword.other.phpdoc - settings - - fontStyle - - foreground - #839496 - - - - name - PHP: Source Emebedded - scope - source.php.embedded.block.html - settings - - foreground - #CB4B16 - - - - name - PHP: Storage Type Function - scope - storage.type.function.php - settings - - foreground - #CB4B16 - - - - name - C: constant - scope - constant.numeric.c - settings - - fontStyle - - foreground - #2AA198 - - - - name - C: Meta Preprocessor - scope - meta.preprocessor.c.include, meta.preprocessor.macro.c - settings - - fontStyle - - foreground - #CB4B16 - - - - name - C: Keyword - scope - keyword.control.import.define.c, keyword.control.import.include.c - settings - - fontStyle - - foreground - #CB4B16 - - - - name - C: Function Preprocessor - scope - entity.name.function.preprocessor.c - settings - - fontStyle - - foreground - #CB4B16 - - - - name - C: include <something.c> - scope - meta.preprocessor.c.include string.quoted.other.lt-gt.include.c, meta.preprocessor.c.include punctuation.definition.string.begin.c, meta.preprocessor.c.include punctuation.definition.string.end.c - settings - - fontStyle - - foreground - #2AA198 - - - - name - C: Function - scope - support.function.C99.c, support.function.any-method.c, entity.name.function.c - settings - - fontStyle - - foreground - #586E75 - - - - name - C: " - scope - punctuation.definition.string.begin.c, punctuation.definition.string.end.c - settings - - fontStyle - - foreground - #2AA198 - - - - name - C: Storage Type - scope - storage.type.c - settings - - fontStyle - - foreground - #B58900 - - - - name - diff: header - scope - meta.diff, meta.diff.header - settings - - background - #B58900 - fontStyle - italic - foreground - #EEE8D5 - - - - name - diff: deleted - scope - markup.deleted - settings - - background - #EEE8D5 - fontStyle - - foreground - #DC322F - - - - name - diff: changed - scope - markup.changed - settings - - background - #EEE8D5 - fontStyle - - foreground - #CB4B16 - - - - name - diff: inserted - scope - markup.inserted - settings - - background - #EEE8D5 - foreground - #2AA198 - - - - name - reST raw - scope - text.restructuredtext markup.raw - settings - - foreground - #2AA198 - - - - name - Other: Removal - scope - other.package.exclude, other.remove - settings - - fontStyle - - foreground - #DC322F - - - - name - Other: Add - scope - other.add - settings - - foreground - #2AA198 - - - - name - Tex: {} - scope - punctuation.section.group.tex , punctuation.definition.arguments.begin.latex, punctuation.definition.arguments.end.latex, punctuation.definition.arguments.latex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: {text} - scope - meta.group.braces.tex - settings - - fontStyle - - foreground - #B58900 - - - - name - Tex: Other Math - scope - string.other.math.tex - settings - - fontStyle - - foreground - #B58900 - - - - name - Tex: {var} - scope - variable.parameter.function.latex - settings - - fontStyle - - foreground - #CB4B16 - - - - name - Tex: Math \\ - scope - punctuation.definition.constant.math.tex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: Constant Math - scope - text.tex.latex constant.other.math.tex, constant.other.general.math.tex, constant.other.general.math.tex, constant.character.math.tex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Tex: Other Math String - scope - string.other.math.tex - settings - - fontStyle - - foreground - #B58900 - - - - name - Tex: $ - scope - punctuation.definition.string.begin.tex, punctuation.definition.string.end.tex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: \label - scope - keyword.control.label.latex, text.tex.latex constant.other.general.math.tex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Tex: \label { } - scope - variable.parameter.definition.label.latex - settings - - fontStyle - - foreground - #DC322F - - - - name - Tex: Function - scope - support.function.be.latex - settings - - fontStyle - - foreground - #859900 - - - - name - Tex: Support Function Section - scope - support.function.section.latex - settings - - fontStyle - - foreground - #CB4B16 - - - - name - Tex: Support Function - scope - support.function.general.tex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Tex: Comment - scope - punctuation.definition.comment.tex, comment.line.percentage.tex - settings - - fontStyle - italic - - - - name - Tex: Reference Label - scope - keyword.control.ref.latex - settings - - fontStyle - - foreground - #2AA198 - - - - name - Python: storage - scope - storage.type.class.python, storage.type.function.python, storage.modifier.global.python - settings - - fontStyle - - foreground - #859900 - - - - name - Python: import - scope - keyword.control.import.python, keyword.control.import.from.python - settings - - foreground - #CB4B16 - - - - name - Python: Support.exception - scope - support.type.exception.python - settings - - foreground - #B58900 - - - - name - Shell: builtin - scope - support.function.builtin.shell - settings - - foreground - #859900 - - - - name - Shell: variable - scope - variable.other.normal.shell - settings - - foreground - #CB4B16 - - - - name - Shell: DOT_FILES - scope - source.shell - settings - - fontStyle - - foreground - #268BD2 - - - - name - Shell: meta scope in loop - scope - meta.scope.for-in-loop.shell, variable.other.loop.shell - settings - - fontStyle - - foreground - #586E75 - - - - name - Shell: "" - scope - punctuation.definition.string.end.shell, punctuation.definition.string.begin.shell - settings - - fontStyle - - foreground - #859900 - - - - name - Shell: Meta Block - scope - meta.scope.case-block.shell, meta.scope.case-body.shell - settings - - fontStyle - - foreground - #586E75 - - - - name - Shell: [] - scope - punctuation.definition.logical-expression.shell - settings - - fontStyle - - foreground - #DC322F - - - - name - Shell: Comment - scope - comment.line.number-sign.shell - settings - - fontStyle - italic - - - - name - Java: import - scope - keyword.other.import.java - settings - - fontStyle - - foreground - #CB4B16 - - - - name - Java: meta-import - scope - storage.modifier.import.java - settings - - fontStyle - - foreground - #586E75 - - - - name - Java: Class - scope - meta.class.java storage.modifier.java - settings - - fontStyle - - foreground - #B58900 - - - - name - Java: /* comment */ - scope - source.java comment.block - settings - - fontStyle - - foreground - #586E75 - - - - name - Java: /* @param */ - scope - comment.block meta.documentation.tag.param.javadoc keyword.other.documentation.param.javadoc - settings - - fontStyle - - foreground - #586E75 - - - - name - Perl: variables - scope - punctuation.definition.variable.perl, variable.other.readwrite.global.perl, variable.other.predefined.perl, keyword.operator.comparison.perl - settings - - foreground - #B58900 - - - - name - Perl: functions - scope - support.function.perl - settings - - foreground - #859900 - - - - name - Perl: comments - scope - comment.line.number-sign.perl - settings - - fontStyle - italic - foreground - #586E75 - - - - name - Perl: quotes - scope - punctuation.definition.string.begin.perl, punctuation.definition.string.end.perl - settings - - foreground - #2AA198 - - - - name - Perl: \char - scope - constant.character.escape.perl - settings - - foreground - #DC322F - - - - Name - Markdown punctuation - scope - markup.list, text.html.markdown punctuation.definition, meta.separator.markdown - settings - - foreground - #CB4b16 - - - - Name - Markdown heading - scope - markup.heading - settings - - foreground - #268BD2 - - - - Name - Markdown text inside some block element - scope - markup.quote, meta.paragraph.list - settings - - foreground - #2AA198 - - - - Name - Markdown em - scope - markup.italic - settings - - fontStyle - italic - - - - Name - Markdown strong - scope - markup.bold - settings - - fontStyle - bold - - - - Name - Markdown reference - scope - markup.underline.link.markdown, meta.link.inline punctuation.definition.metadata, meta.link.reference.markdown punctuation.definition.constant, meta.link.reference.markdown constant.other.reference - settings - - foreground - #B58900 - - - - Name - Markdown linebreak - scope - meta.paragraph.markdown meta.dummy.line-break - settings - - background - #6C71c4 - - - - name - SublimeLinter Annotations - scope - sublimelinter.notes - settings - - background - #eee8d5 - foreground - #eee8d5 - - - - name - SublimeLinter Error Outline - scope - sublimelinter.outline.illegal - settings - - background - #93a1a1 - foreground - #93a1a1 - - - - name - SublimeLinter Error Underline - scope - sublimelinter.underline.illegal - settings - - background - #dc322f - - - - name - SublimeLinter Warning Outline - scope - sublimelinter.outline.warning - settings - - background - #839496 - foreground - #839496 - - - - name - SublimeLinter Warning Underline - scope - sublimelinter.underline.warning - settings - - background - #b58900 - - - - name - SublimeLinter Violation Outline - scope - sublimelinter.outline.violation - settings - - background - #657b83 - foreground - #657b83 - - - - name - SublimeLinter Violation Underline - scope - sublimelinter.underline.violation - settings - - background - #cb4b16 - - - - name - SublimeBracketHighlighter - scope - brackethighlighter.all - settings - - background - #FDF6E3 - foreground - #cb4b16 - - - - uuid - 38E819D9-AE02-452F-9231-ECC3B204AFD7 - - diff --git a/themes/solarized_light.yml b/themes/solarized_light.yml new file mode 100644 index 00000000..0a238e19 --- /dev/null +++ b/themes/solarized_light.yml @@ -0,0 +1,106 @@ +name: Solarized Light +palette: + base3: "#FDF6E3" + base2: "#EEE8D5" + base1: "#93A1A1" + base0: "#839496" + body: "#657B83" + yellow: "#B58900" + orange: "#CB4B16" + red: "#DC322F" + magenta: "#D33682" + violet: "#6C71C4" + blue: "#268BD2" + cyan: "#2AA198" + green: "#859900" +settings: + foreground: body + background: base3 + line_highlight: base2 +rules: + - name: Comment + scope: "comment, comment.block.documentation, punctuation.definition.comment" + foreground: base1 + font_style: [] + - name: String + scope: "string, string.quoted.double, string.quoted.single" + foreground: cyan + - name: Regexp + scope: "string.regexp, constant.character.escape, punctuation.definition.string.begin, punctuation.definition.string.end" + foreground: red + - name: Number + scope: constant.numeric + foreground: magenta + - name: Variable + scope: "variable.language, variable.other" + foreground: blue + - name: Keyword + scope: "keyword, keyword.control" + foreground: green + - name: Storage + scope: "storage, storage.type.class.python, storage.type.function.python, storage.modifier.global.python" + foreground: green + font_style: [] + - name: Functions + scope: "entity.name.function, support.function, support.function.builtin.shell, meta.function.js, entity.name.function.js" + foreground: blue + - name: Support Constructs + scope: "support.function.construct, keyword.other.new, support.function.construct.php" + foreground: red + - name: Types + scope: "entity.name.class, entity.name.type.class, entity.name.type, support.type.exception, support.class" + foreground: blue + - name: Support Types + scope: "support.type, storage.type.c, storage.type.js, storage.type.php" + foreground: green + - name: Built-in Constant + scope: "constant.language, meta.preprocessor, keyword.control.import.define.c, keyword.control.import.include.c" + foreground: yellow + - name: User-defined Constant + scope: "constant.character, constant.other, keyword.other.special-method, keyword.control.import.include.php" + foreground: orange + - name: Parameters + scope: "variable.parameter, variable.parameter.function.latex, variable.parameter.definition.label.latex" + foreground: orange + - name: Operators + scope: "keyword.operator, punctuation.separator.continuation, punctuation.definition.array.begin, punctuation.definition.array.end" + foreground: red + - name: Structural Punctuation + scope: "punctuation.separator, punctuation.terminator, punctuation.section, meta.brace, meta.delimiter" + foreground: body + - name: Tags + scope: "entity.name.tag, punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end" + foreground: blue + - name: Attributes + scope: "entity.other.attribute-name, entity.other.attribute-name.class.css, entity.other.attribute-name.id, entity.other.attribute-name.pseudo-element.css, entity.other.attribute-name.pseudo-class.css" + foreground: base1 + - name: Markup + scope: "markup.heading, markup.quote, markup.list, meta.separator.markdown" + foreground: blue + - name: Diff Deleted + scope: "markup.deleted, markup.deleted.git_gutter" + foreground: red + background: base2 + font_style: [] + - name: Diff Changed + scope: "markup.changed, markup.changed.git_gutter" + foreground: orange + background: base2 + font_style: [] + - name: Diff Inserted + scope: "markup.inserted, markup.inserted.git_gutter, other.add" + foreground: cyan + background: base2 + - name: Invalid + scope: "invalid, invalid.deprecated, sublimelinter.underline.illegal" + foreground: base2 + background: red + - name: Underline Warning + scope: "sublimelinter.underline.warning, sublimelinter.underline.violation" + background: yellow + - name: Tex Functions + scope: "support.function.be.latex, support.function.section.latex, support.function.general.tex, keyword.control.ref.latex, keyword.control.label.latex" + foreground: cyan + - name: Markdown Styles + scope: "markup.italic, markup.bold, markup.underline.link.markdown, meta.link.reference.markdown constant.other.reference" + foreground: yellow From 83ff0510b2537359295f99190605b7b51c11f3da Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 12 Apr 2026 13:47:20 -0400 Subject: [PATCH 08/26] Initial theme compiler --- build/theme_compiler.rs | 419 ++++++++++++++++++++++++++++++++++++++++ tests/theme_compiler.rs | 164 ++++++++++++++++ 2 files changed, 583 insertions(+) create mode 100644 build/theme_compiler.rs create mode 100644 tests/theme_compiler.rs diff --git a/build/theme_compiler.rs b/build/theme_compiler.rs new file mode 100644 index 00000000..7a1b2675 --- /dev/null +++ b/build/theme_compiler.rs @@ -0,0 +1,419 @@ +use std::collections::{BTreeMap, HashSet}; +use std::fmt::Write as _; +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use syntect::highlighting::ScopeSelectors; +use yaml_rust::yaml::{Hash, Yaml}; +use yaml_rust::YamlLoader; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThemeSource { + pub key: String, + pub name: String, + pub palette: BTreeMap, + pub settings: ThemeSettingsSource, + pub rules: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThemeSettingsSource { + pub foreground: String, + pub background: String, + pub line_highlight: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThemeRuleSource { + pub name: Option, + pub scope: String, + pub foreground: Option, + pub background: Option, + pub font_style: Option>, +} + +pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result, String> { + if output_dir.exists() { + fs::remove_dir_all(output_dir) + .map_err(|error| format!("Failed to clear generated theme directory: {error}"))?; + } + + fs::create_dir_all(output_dir) + .map_err(|error| format!("Failed to create generated theme directory: {error}"))?; + + let mut sources = Vec::new(); + let mut seen_keys = HashSet::new(); + for entry in fs::read_dir(source_dir) + .map_err(|error| format!("Failed to read theme source directory: {error}"))? + { + let path = entry + .map_err(|error| format!("Failed to read theme source entry: {error}"))? + .path(); + if !path.is_file() || path.extension().and_then(|ext| ext.to_str()) != Some("yml") { + continue; + } + + let key = path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| format!("Invalid theme source filename: {}", path.display()))? + .to_string(); + + if !seen_keys.insert(key.clone()) { + return Err(format!("Duplicate theme key: {key}")); + } + + let content = fs::read_to_string(&path) + .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; + sources.push(parse_theme_source(&key, &content)?); + } + + sources.sort_by(|left, right| left.key.cmp(&right.key)); + + let mut outputs = Vec::new(); + for source in sources { + let output_path = output_dir.join(format!("{}.tmTheme", source.key)); + fs::write(&output_path, render_tmtheme(&source)) + .map_err(|error| format!("Failed to write {}: {error}", output_path.display()))?; + outputs.push(output_path); + } + + Ok(outputs) +} + +pub fn parse_theme_source(theme_key: &str, content: &str) -> Result { + let documents = YamlLoader::load_from_str(content) + .map_err(|error| format!("Failed to parse {theme_key}.yml: {error}"))?; + let document = documents + .into_iter() + .next() + .ok_or_else(|| format!("{theme_key}.yml is empty"))?; + let root = as_hash(&document, theme_key, "root")?; + + let allowed_root_keys = ["name", "palette", "settings", "rules"]; + ensure_only_keys(root, theme_key, "root", &allowed_root_keys)?; + + let name = required_string(root, theme_key, "root", "name")?; + let palette = parse_palette(root, theme_key)?; + let settings = parse_settings(root, theme_key, &palette)?; + let rules = parse_rules(root, theme_key, &palette)?; + + Ok(ThemeSource { + key: theme_key.to_string(), + name, + palette, + settings, + rules, + }) +} + +pub fn render_tmtheme(source: &ThemeSource) -> String { + let mut output = String::new(); + output.push_str("\n"); + output.push_str("\n"); + output.push_str("\n"); + output.push_str("\n"); + write_key_string(&mut output, 1, "name", &source.name); + output.push_str(" settings\n"); + output.push_str(" \n"); + output.push_str(" \n"); + output.push_str(" settings\n"); + output.push_str(" \n"); + write_key_string(&mut output, 4, "foreground", &source.settings.foreground); + write_key_string(&mut output, 4, "background", &source.settings.background); + write_key_string( + &mut output, + 4, + "lineHighlight", + &source.settings.line_highlight, + ); + output.push_str(" \n"); + output.push_str(" \n"); + + for rule in &source.rules { + output.push_str(" \n"); + if let Some(name) = &rule.name { + write_key_string(&mut output, 3, "name", name); + } + write_key_string(&mut output, 3, "scope", &rule.scope); + output.push_str(" settings\n"); + output.push_str(" \n"); + if let Some(foreground) = &rule.foreground { + write_key_string(&mut output, 4, "foreground", foreground); + } + if let Some(background) = &rule.background { + write_key_string(&mut output, 4, "background", background); + } + if let Some(font_style) = &rule.font_style { + write_key_string(&mut output, 4, "fontStyle", &font_style.join(" ")); + } + output.push_str(" \n"); + output.push_str(" \n"); + } + + output.push_str(" \n"); + output.push_str("\n"); + output.push_str("\n"); + output +} + +fn parse_palette(root: &Hash, theme_key: &str) -> Result, String> { + let Some(value) = root.get(&Yaml::String("palette".into())) else { + return Ok(BTreeMap::new()); + }; + + let palette_hash = as_hash(value, theme_key, "palette")?; + let mut palette = BTreeMap::new(); + for (key, value) in palette_hash { + let key = key + .as_str() + .ok_or_else(|| format!("{theme_key}.yml palette keys must be strings"))?; + let color = value + .as_str() + .ok_or_else(|| format!("{theme_key}.yml palette value for {key} must be a string"))?; + validate_literal_color(theme_key, &format!("palette.{key}"), color)?; + if palette.insert(key.to_string(), color.to_string()).is_some() { + return Err(format!( + "{theme_key}.yml has a duplicate palette key: {key}" + )); + } + } + + Ok(palette) +} + +fn parse_settings( + root: &Hash, + theme_key: &str, + palette: &BTreeMap, +) -> Result { + let settings = as_hash( + root.get(&Yaml::String("settings".into())) + .ok_or_else(|| format!("{theme_key}.yml is missing required key: settings"))?, + theme_key, + "settings", + )?; + + let allowed_keys = ["foreground", "background", "line_highlight"]; + ensure_only_keys(settings, theme_key, "settings", &allowed_keys)?; + + Ok(ThemeSettingsSource { + foreground: resolve_color( + required_string(settings, theme_key, "settings", "foreground")?.as_str(), + theme_key, + "settings.foreground", + palette, + )?, + background: resolve_color( + required_string(settings, theme_key, "settings", "background")?.as_str(), + theme_key, + "settings.background", + palette, + )?, + line_highlight: resolve_color( + required_string(settings, theme_key, "settings", "line_highlight")?.as_str(), + theme_key, + "settings.line_highlight", + palette, + )?, + }) +} + +fn parse_rules( + root: &Hash, + theme_key: &str, + palette: &BTreeMap, +) -> Result, String> { + let rules = root + .get(&Yaml::String("rules".into())) + .ok_or_else(|| format!("{theme_key}.yml is missing required key: rules"))?; + let rules = rules + .as_vec() + .ok_or_else(|| format!("{theme_key}.yml rules must be an array"))?; + + let mut parsed = Vec::new(); + for (index, rule) in rules.iter().enumerate() { + let path = format!("rules[{index}]"); + let rule = as_hash(rule, theme_key, &path)?; + let allowed_keys = ["name", "scope", "foreground", "background", "font_style"]; + ensure_only_keys(rule, theme_key, &path, &allowed_keys)?; + + let name = optional_string(rule, "name"); + let scope = required_string(rule, theme_key, &path, "scope")?; + ScopeSelectors::from_str(&scope).map_err(|error| { + format!("{theme_key}.yml {path}.scope is not a valid selector: {error}") + })?; + + let foreground = optional_resolved_color( + rule, + "foreground", + theme_key, + &format!("{path}.foreground"), + palette, + )?; + let background = optional_resolved_color( + rule, + "background", + theme_key, + &format!("{path}.background"), + palette, + )?; + let font_style = optional_font_style(rule, theme_key, &path)?; + + if foreground.is_none() && background.is_none() && font_style.is_none() { + return Err(format!( + "{theme_key}.yml {path} must define at least one of foreground, background, or font_style" + )); + } + + parsed.push(ThemeRuleSource { + name, + scope, + foreground, + background, + font_style, + }); + } + + if parsed.is_empty() { + return Err(format!("{theme_key}.yml rules must not be empty")); + } + + Ok(parsed) +} + +fn optional_font_style( + rule: &Hash, + theme_key: &str, + path: &str, +) -> Result>, String> { + let Some(value) = rule.get(&Yaml::String("font_style".into())) else { + return Ok(None); + }; + + let values = value + .as_vec() + .ok_or_else(|| format!("{theme_key}.yml {path}.font_style must be an array of strings"))?; + + let mut parsed = Vec::new(); + for item in values { + let style = item.as_str().ok_or_else(|| { + format!("{theme_key}.yml {path}.font_style must contain only strings") + })?; + match style { + "bold" | "italic" | "underline" => parsed.push(style.to_string()), + _ => { + return Err(format!( + "{theme_key}.yml {path}.font_style contains unsupported style: {style}" + )) + } + } + } + + Ok(Some(parsed)) +} + +fn optional_resolved_color( + rule: &Hash, + key: &str, + theme_key: &str, + path: &str, + palette: &BTreeMap, +) -> Result, String> { + let Some(value) = rule.get(&Yaml::String(key.into())) else { + return Ok(None); + }; + + let value = value + .as_str() + .ok_or_else(|| format!("{theme_key}.yml {path} must be a string"))?; + + Ok(Some(resolve_color(value, theme_key, path, palette)?)) +} + +fn resolve_color( + value: &str, + theme_key: &str, + path: &str, + palette: &BTreeMap, +) -> Result { + if value.starts_with('#') { + validate_literal_color(theme_key, path, value)?; + return Ok(value.to_string()); + } + + palette + .get(value) + .cloned() + .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {value}")) +} + +fn validate_literal_color(theme_key: &str, path: &str, color: &str) -> Result<(), String> { + let is_valid = matches!(color.len(), 4 | 7 | 9) + && color.starts_with('#') + && color.chars().skip(1).all(|char| char.is_ascii_hexdigit()); + + if is_valid { + Ok(()) + } else { + Err(format!( + "{theme_key}.yml {path} must be a hex color in #RGB, #RRGGBB, or #RRGGBBAA format" + )) + } +} + +fn ensure_only_keys( + hash: &Hash, + theme_key: &str, + path: &str, + allowed: &[&str], +) -> Result<(), String> { + for key in hash.keys() { + let key = key + .as_str() + .ok_or_else(|| format!("{theme_key}.yml {path} keys must be strings"))?; + if !allowed.contains(&key) { + return Err(format!( + "{theme_key}.yml {path} contains unsupported key: {key}" + )); + } + } + Ok(()) +} + +fn as_hash<'a>(value: &'a Yaml, theme_key: &str, path: &str) -> Result<&'a Hash, String> { + value + .as_hash() + .ok_or_else(|| format!("{theme_key}.yml {path} must be a mapping")) +} + +fn required_string(hash: &Hash, theme_key: &str, path: &str, key: &str) -> Result { + hash.get(&Yaml::String(key.into())) + .ok_or_else(|| format!("{theme_key}.yml {path} is missing required key: {key}"))? + .as_str() + .map(str::to_string) + .ok_or_else(|| format!("{theme_key}.yml {path}.{key} must be a string")) +} + +fn optional_string(hash: &Hash, key: &str) -> Option { + hash.get(&Yaml::String(key.into())) + .and_then(Yaml::as_str) + .map(str::to_string) +} + +fn write_key_string(output: &mut String, indent: usize, key: &str, value: &str) { + let padding = " ".repeat(indent); + let escaped = xml_escape(value); + let _ = writeln!(output, "{padding}{key}"); + let _ = writeln!(output, "{padding}{escaped}"); +} + +fn xml_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs new file mode 100644 index 00000000..b914b96f --- /dev/null +++ b/tests/theme_compiler.rs @@ -0,0 +1,164 @@ +#[path = "../build/theme_compiler.rs"] +mod theme_compiler; + +use std::fs; +use std::io::Cursor; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use syntect::highlighting::ThemeSet; + +#[test] +fn parse_theme_source_resolves_palette_references() { + let source = theme_compiler::parse_theme_source( + "test_theme", + r##" +name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" +settings: + foreground: fg + background: bg + line_highlight: line +rules: + - name: Comment + scope: comment + foreground: fg + font_style: [italic] +"##, + ) + .unwrap(); + + assert_eq!(source.settings.foreground, "#112233"); + assert_eq!(source.settings.background, "#445566"); + assert_eq!(source.settings.line_highlight, "#778899"); + assert_eq!(source.rules[0].foreground.as_deref(), Some("#112233")); +} + +#[test] +fn parse_theme_source_rejects_unknown_keys() { + let error = theme_compiler::parse_theme_source( + "bad_theme", + r##" +name: Bad Theme +settings: + foreground: "#112233" + background: "#445566" + line_highlight: "#778899" + selection: "#000000" +rules: + - scope: comment + foreground: "#112233" +"##, + ) + .unwrap_err(); + + assert!(error.contains("unsupported key: selection")); +} + +#[test] +fn parse_theme_source_rejects_invalid_rule_color_reference() { + let error = theme_compiler::parse_theme_source( + "bad_theme", + r##" +name: Bad Theme +settings: + foreground: "#112233" + background: "#445566" + line_highlight: "#778899" +rules: + - scope: comment + foreground: missing +"##, + ) + .unwrap_err(); + + assert!(error.contains("unknown palette key: missing")); +} + +#[test] +fn parse_theme_source_rejects_non_string_scope() { + let error = theme_compiler::parse_theme_source( + "bad_theme", + r##" +name: Bad Theme +settings: + foreground: "#112233" + background: "#445566" + line_highlight: "#778899" +rules: + - scope: [comment] + foreground: "#112233" +"##, + ) + .unwrap_err(); + + assert!(error.contains("scope must be a string")); +} + +#[test] +fn render_tmtheme_is_parseable_and_preserves_empty_font_style() { + let source = theme_compiler::parse_theme_source( + "test_theme", + r##" +name: Test Theme +settings: + foreground: "#112233" + background: "#445566" + line_highlight: "#778899" +rules: + - scope: comment + foreground: "#112233" + font_style: [] +"##, + ) + .unwrap(); + + let rendered = theme_compiler::render_tmtheme(&source); + assert!(rendered.contains("fontStyle")); + assert!(rendered.contains("")); + + let mut cursor = Cursor::new(rendered.into_bytes()); + ThemeSet::load_from_reader(&mut cursor).unwrap(); +} + +#[test] +fn compile_themes_writes_generated_tmtheme_files() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let base = std::env::temp_dir().join(format!("amp-theme-compiler-{unique}")); + let source_dir = base.join("source"); + let output_dir = base.join("output"); + fs::create_dir_all(&source_dir).unwrap(); + + fs::write( + source_dir.join("sample.yml"), + r##" +name: Sample Theme +settings: + foreground: "#112233" + background: "#445566" + line_highlight: "#778899" +rules: + - scope: comment + foreground: "#112233" +"##, + ) + .unwrap(); + + let outputs = theme_compiler::compile_themes(&source_dir, &output_dir).unwrap(); + assert_eq!( + outputs, + vec![PathBuf::from(output_dir.join("sample.tmTheme"))] + ); + + let file = fs::File::open(output_dir.join("sample.tmTheme")).unwrap(); + let mut reader = std::io::BufReader::new(file); + ThemeSet::load_from_reader(&mut reader).unwrap(); + + fs::remove_dir_all(base).unwrap(); +} From b48fcafd9e06b1488ef70542ce1d126988577bb4 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 12 Apr 2026 16:44:38 -0400 Subject: [PATCH 09/26] Use serde for theme compiler --- Cargo.lock | 21 +++ Cargo.toml | 5 +- build/theme_compiler.rs | 374 +++++++++++++++++----------------------- tests/theme_compiler.rs | 5 +- 4 files changed, 183 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82eb6837..173cecd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,8 @@ dependencies = [ "mio 1.0.3", "regex", "scribe", + "serde", + "serde_yaml", "serial_test", "signal-hook 0.1.17", "signal-hook-mio", @@ -1611,6 +1613,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -1881,6 +1896,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.5.7" diff --git a/Cargo.toml b/Cargo.toml index a7beb562..98aac868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,9 @@ edition="2021" [build-dependencies] regex = "1.10" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" syntect = "5.1" -yaml-rust = "0.4" [dependencies] app_dirs2 = "2.5" @@ -51,6 +52,8 @@ default-features = false # removes unused openssl dependency [dev-dependencies] criterion = "0.5" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" [[bench]] name = "draw_buffer" diff --git a/build/theme_compiler.rs b/build/theme_compiler.rs index 7a1b2675..54c34d39 100644 --- a/build/theme_compiler.rs +++ b/build/theme_compiler.rs @@ -4,9 +4,9 @@ use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; +use serde::de::{self, Deserializer}; +use serde::Deserialize; use syntect::highlighting::ScopeSelectors; -use yaml_rust::yaml::{Hash, Yaml}; -use yaml_rust::YamlLoader; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ThemeSource { @@ -33,6 +33,54 @@ pub struct ThemeRuleSource { pub font_style: Option>, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawThemeSource { + name: String, + #[serde(default)] + palette: BTreeMap, + settings: RawThemeSettingsSource, + rules: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawThemeSettingsSource { + foreground: ColorRef, + background: ColorRef, + line_highlight: ColorRef, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawThemeRuleSource { + name: Option, + scope: ScopeSelectorString, + foreground: Option, + background: Option, + font_style: Option>, +} + +#[derive(Debug, Clone)] +struct HexColor(String); + +#[derive(Debug, Clone)] +enum ColorRef { + Literal(HexColor), + Palette(String), +} + +#[derive(Debug, Clone)] +struct ScopeSelectorString(String); + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] +enum FontStyle { + Bold, + Italic, + Underline, +} + pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result, String> { if output_dir.exists() { fs::remove_dir_all(output_dir) @@ -83,29 +131,9 @@ pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result Result { - let documents = YamlLoader::load_from_str(content) + let raw: RawThemeSource = serde_yaml::from_str(content) .map_err(|error| format!("Failed to parse {theme_key}.yml: {error}"))?; - let document = documents - .into_iter() - .next() - .ok_or_else(|| format!("{theme_key}.yml is empty"))?; - let root = as_hash(&document, theme_key, "root")?; - - let allowed_root_keys = ["name", "palette", "settings", "rules"]; - ensure_only_keys(root, theme_key, "root", &allowed_root_keys)?; - - let name = required_string(root, theme_key, "root", "name")?; - let palette = parse_palette(root, theme_key)?; - let settings = parse_settings(root, theme_key, &palette)?; - let rules = parse_rules(root, theme_key, &palette)?; - - Ok(ThemeSource { - key: theme_key.to_string(), - name, - palette, - settings, - rules, - }) + normalize_theme_source(theme_key, raw) } pub fn render_tmtheme(source: &ThemeSource) -> String { @@ -158,108 +186,59 @@ pub fn render_tmtheme(source: &ThemeSource) -> String { output } -fn parse_palette(root: &Hash, theme_key: &str) -> Result, String> { - let Some(value) = root.get(&Yaml::String("palette".into())) else { - return Ok(BTreeMap::new()); - }; - - let palette_hash = as_hash(value, theme_key, "palette")?; - let mut palette = BTreeMap::new(); - for (key, value) in palette_hash { - let key = key - .as_str() - .ok_or_else(|| format!("{theme_key}.yml palette keys must be strings"))?; - let color = value - .as_str() - .ok_or_else(|| format!("{theme_key}.yml palette value for {key} must be a string"))?; - validate_literal_color(theme_key, &format!("palette.{key}"), color)?; - if palette.insert(key.to_string(), color.to_string()).is_some() { - return Err(format!( - "{theme_key}.yml has a duplicate palette key: {key}" - )); - } - } - - Ok(palette) -} +fn normalize_theme_source(theme_key: &str, raw: RawThemeSource) -> Result { + let palette = raw + .palette + .into_iter() + .map(|(key, value)| (key, value.0)) + .collect::>(); -fn parse_settings( - root: &Hash, - theme_key: &str, - palette: &BTreeMap, -) -> Result { - let settings = as_hash( - root.get(&Yaml::String("settings".into())) - .ok_or_else(|| format!("{theme_key}.yml is missing required key: settings"))?, - theme_key, - "settings", - )?; - - let allowed_keys = ["foreground", "background", "line_highlight"]; - ensure_only_keys(settings, theme_key, "settings", &allowed_keys)?; - - Ok(ThemeSettingsSource { - foreground: resolve_color( - required_string(settings, theme_key, "settings", "foreground")?.as_str(), + let settings = ThemeSettingsSource { + foreground: resolve_color_ref( theme_key, "settings.foreground", - palette, + raw.settings.foreground, + &palette, )?, - background: resolve_color( - required_string(settings, theme_key, "settings", "background")?.as_str(), + background: resolve_color_ref( theme_key, "settings.background", - palette, + raw.settings.background, + &palette, )?, - line_highlight: resolve_color( - required_string(settings, theme_key, "settings", "line_highlight")?.as_str(), + line_highlight: resolve_color_ref( theme_key, "settings.line_highlight", - palette, + raw.settings.line_highlight, + &palette, )?, - }) -} + }; -fn parse_rules( - root: &Hash, - theme_key: &str, - palette: &BTreeMap, -) -> Result, String> { - let rules = root - .get(&Yaml::String("rules".into())) - .ok_or_else(|| format!("{theme_key}.yml is missing required key: rules"))?; - let rules = rules - .as_vec() - .ok_or_else(|| format!("{theme_key}.yml rules must be an array"))?; - - let mut parsed = Vec::new(); - for (index, rule) in rules.iter().enumerate() { + if raw.rules.is_empty() { + return Err(format!("{theme_key}.yml rules must not be empty")); + } + + let mut rules = Vec::with_capacity(raw.rules.len()); + for (index, raw_rule) in raw.rules.into_iter().enumerate() { let path = format!("rules[{index}]"); - let rule = as_hash(rule, theme_key, &path)?; - let allowed_keys = ["name", "scope", "foreground", "background", "font_style"]; - ensure_only_keys(rule, theme_key, &path, &allowed_keys)?; - - let name = optional_string(rule, "name"); - let scope = required_string(rule, theme_key, &path, "scope")?; - ScopeSelectors::from_str(&scope).map_err(|error| { - format!("{theme_key}.yml {path}.scope is not a valid selector: {error}") - })?; - - let foreground = optional_resolved_color( - rule, - "foreground", - theme_key, - &format!("{path}.foreground"), - palette, - )?; - let background = optional_resolved_color( - rule, - "background", - theme_key, - &format!("{path}.background"), - palette, - )?; - let font_style = optional_font_style(rule, theme_key, &path)?; + let foreground = raw_rule + .foreground + .map(|value| { + resolve_color_ref(theme_key, &format!("{path}.foreground"), value, &palette) + }) + .transpose()?; + let background = raw_rule + .background + .map(|value| { + resolve_color_ref(theme_key, &format!("{path}.background"), value, &palette) + }) + .transpose()?; + let font_style = raw_rule.font_style.map(|styles| { + styles + .into_iter() + .map(|style| style.as_str().to_string()) + .collect::>() + }); if foreground.is_none() && background.is_none() && font_style.is_none() { return Err(format!( @@ -267,89 +246,87 @@ fn parse_rules( )); } - parsed.push(ThemeRuleSource { - name, - scope, + rules.push(ThemeRuleSource { + name: raw_rule.name, + scope: raw_rule.scope.0, foreground, background, font_style, }); } - if parsed.is_empty() { - return Err(format!("{theme_key}.yml rules must not be empty")); - } - - Ok(parsed) + Ok(ThemeSource { + key: theme_key.to_string(), + name: raw.name, + palette, + settings, + rules, + }) } -fn optional_font_style( - rule: &Hash, +fn resolve_color_ref( theme_key: &str, path: &str, -) -> Result>, String> { - let Some(value) = rule.get(&Yaml::String("font_style".into())) else { - return Ok(None); - }; - - let values = value - .as_vec() - .ok_or_else(|| format!("{theme_key}.yml {path}.font_style must be an array of strings"))?; - - let mut parsed = Vec::new(); - for item in values { - let style = item.as_str().ok_or_else(|| { - format!("{theme_key}.yml {path}.font_style must contain only strings") - })?; - match style { - "bold" | "italic" | "underline" => parsed.push(style.to_string()), - _ => { - return Err(format!( - "{theme_key}.yml {path}.font_style contains unsupported style: {style}" - )) - } - } + color_ref: ColorRef, + palette: &BTreeMap, +) -> Result { + match color_ref { + ColorRef::Literal(color) => Ok(color.0), + ColorRef::Palette(key) => palette + .get(&key) + .cloned() + .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {key}")), } - - Ok(Some(parsed)) } -fn optional_resolved_color( - rule: &Hash, - key: &str, - theme_key: &str, - path: &str, - palette: &BTreeMap, -) -> Result, String> { - let Some(value) = rule.get(&Yaml::String(key.into())) else { - return Ok(None); - }; - - let value = value - .as_str() - .ok_or_else(|| format!("{theme_key}.yml {path} must be a string"))?; +impl<'de> Deserialize<'de> for HexColor { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + validate_literal_color(&value).map_err(de::Error::custom)?; + Ok(HexColor(value)) + } +} - Ok(Some(resolve_color(value, theme_key, path, palette)?)) +impl<'de> Deserialize<'de> for ColorRef { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value.starts_with('#') { + validate_literal_color(&value).map_err(de::Error::custom)?; + Ok(ColorRef::Literal(HexColor(value))) + } else { + Ok(ColorRef::Palette(value)) + } + } } -fn resolve_color( - value: &str, - theme_key: &str, - path: &str, - palette: &BTreeMap, -) -> Result { - if value.starts_with('#') { - validate_literal_color(theme_key, path, value)?; - return Ok(value.to_string()); +impl<'de> Deserialize<'de> for ScopeSelectorString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + ScopeSelectors::from_str(&value).map_err(de::Error::custom)?; + Ok(ScopeSelectorString(value)) } +} - palette - .get(value) - .cloned() - .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {value}")) +impl FontStyle { + fn as_str(&self) -> &'static str { + match self { + FontStyle::Bold => "bold", + FontStyle::Italic => "italic", + FontStyle::Underline => "underline", + } + } } -fn validate_literal_color(theme_key: &str, path: &str, color: &str) -> Result<(), String> { +fn validate_literal_color(color: &str) -> Result<(), String> { let is_valid = matches!(color.len(), 4 | 7 | 9) && color.starts_with('#') && color.chars().skip(1).all(|char| char.is_ascii_hexdigit()); @@ -357,51 +334,10 @@ fn validate_literal_color(theme_key: &str, path: &str, color: &str) -> Result<() if is_valid { Ok(()) } else { - Err(format!( - "{theme_key}.yml {path} must be a hex color in #RGB, #RRGGBB, or #RRGGBBAA format" - )) + Err("must be a hex color in #RGB, #RRGGBB, or #RRGGBBAA format".to_string()) } } -fn ensure_only_keys( - hash: &Hash, - theme_key: &str, - path: &str, - allowed: &[&str], -) -> Result<(), String> { - for key in hash.keys() { - let key = key - .as_str() - .ok_or_else(|| format!("{theme_key}.yml {path} keys must be strings"))?; - if !allowed.contains(&key) { - return Err(format!( - "{theme_key}.yml {path} contains unsupported key: {key}" - )); - } - } - Ok(()) -} - -fn as_hash<'a>(value: &'a Yaml, theme_key: &str, path: &str) -> Result<&'a Hash, String> { - value - .as_hash() - .ok_or_else(|| format!("{theme_key}.yml {path} must be a mapping")) -} - -fn required_string(hash: &Hash, theme_key: &str, path: &str, key: &str) -> Result { - hash.get(&Yaml::String(key.into())) - .ok_or_else(|| format!("{theme_key}.yml {path} is missing required key: {key}"))? - .as_str() - .map(str::to_string) - .ok_or_else(|| format!("{theme_key}.yml {path}.{key} must be a string")) -} - -fn optional_string(hash: &Hash, key: &str) -> Option { - hash.get(&Yaml::String(key.into())) - .and_then(Yaml::as_str) - .map(str::to_string) -} - fn write_key_string(output: &mut String, indent: usize, key: &str, value: &str) { let padding = " ".repeat(indent); let escaped = xml_escape(value); diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index b914b96f..ce8743f3 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -55,7 +55,7 @@ rules: ) .unwrap_err(); - assert!(error.contains("unsupported key: selection")); + assert!(error.contains("selection")); } #[test] @@ -95,7 +95,8 @@ rules: ) .unwrap_err(); - assert!(error.contains("scope must be a string")); + assert!(error.contains("invalid type")); + assert!(error.contains("sequence")); } #[test] From 49f900c0d0922f561aa441f4c409421bad9e2dcd Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 12 Apr 2026 17:31:28 -0400 Subject: [PATCH 10/26] Split theme compiler into phase-specific submodules --- build.rs | 2 +- build/theme_compiler.rs | 355 ------------------------------ build/theme_compiler/mod.rs | 77 +++++++ build/theme_compiler/parsed.rs | 118 ++++++++++ build/theme_compiler/textmate.rs | 69 ++++++ build/theme_compiler/validated.rs | 121 ++++++++++ tests/theme_compiler.rs | 50 ++++- 7 files changed, 425 insertions(+), 367 deletions(-) delete mode 100644 build/theme_compiler.rs create mode 100644 build/theme_compiler/mod.rs create mode 100644 build/theme_compiler/parsed.rs create mode 100644 build/theme_compiler/textmate.rs create mode 100644 build/theme_compiler/validated.rs diff --git a/build.rs b/build.rs index 4baabb86..fefe4aa5 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -#[path = "build/theme_compiler.rs"] +#[path = "build/theme_compiler/mod.rs"] mod theme_compiler; use regex::Regex; diff --git a/build/theme_compiler.rs b/build/theme_compiler.rs deleted file mode 100644 index 54c34d39..00000000 --- a/build/theme_compiler.rs +++ /dev/null @@ -1,355 +0,0 @@ -use std::collections::{BTreeMap, HashSet}; -use std::fmt::Write as _; -use std::fs; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use serde::de::{self, Deserializer}; -use serde::Deserialize; -use syntect::highlighting::ScopeSelectors; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ThemeSource { - pub key: String, - pub name: String, - pub palette: BTreeMap, - pub settings: ThemeSettingsSource, - pub rules: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ThemeSettingsSource { - pub foreground: String, - pub background: String, - pub line_highlight: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ThemeRuleSource { - pub name: Option, - pub scope: String, - pub foreground: Option, - pub background: Option, - pub font_style: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct RawThemeSource { - name: String, - #[serde(default)] - palette: BTreeMap, - settings: RawThemeSettingsSource, - rules: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct RawThemeSettingsSource { - foreground: ColorRef, - background: ColorRef, - line_highlight: ColorRef, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct RawThemeRuleSource { - name: Option, - scope: ScopeSelectorString, - foreground: Option, - background: Option, - font_style: Option>, -} - -#[derive(Debug, Clone)] -struct HexColor(String); - -#[derive(Debug, Clone)] -enum ColorRef { - Literal(HexColor), - Palette(String), -} - -#[derive(Debug, Clone)] -struct ScopeSelectorString(String); - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "snake_case")] -enum FontStyle { - Bold, - Italic, - Underline, -} - -pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result, String> { - if output_dir.exists() { - fs::remove_dir_all(output_dir) - .map_err(|error| format!("Failed to clear generated theme directory: {error}"))?; - } - - fs::create_dir_all(output_dir) - .map_err(|error| format!("Failed to create generated theme directory: {error}"))?; - - let mut sources = Vec::new(); - let mut seen_keys = HashSet::new(); - for entry in fs::read_dir(source_dir) - .map_err(|error| format!("Failed to read theme source directory: {error}"))? - { - let path = entry - .map_err(|error| format!("Failed to read theme source entry: {error}"))? - .path(); - if !path.is_file() || path.extension().and_then(|ext| ext.to_str()) != Some("yml") { - continue; - } - - let key = path - .file_stem() - .and_then(|stem| stem.to_str()) - .ok_or_else(|| format!("Invalid theme source filename: {}", path.display()))? - .to_string(); - - if !seen_keys.insert(key.clone()) { - return Err(format!("Duplicate theme key: {key}")); - } - - let content = fs::read_to_string(&path) - .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; - sources.push(parse_theme_source(&key, &content)?); - } - - sources.sort_by(|left, right| left.key.cmp(&right.key)); - - let mut outputs = Vec::new(); - for source in sources { - let output_path = output_dir.join(format!("{}.tmTheme", source.key)); - fs::write(&output_path, render_tmtheme(&source)) - .map_err(|error| format!("Failed to write {}: {error}", output_path.display()))?; - outputs.push(output_path); - } - - Ok(outputs) -} - -pub fn parse_theme_source(theme_key: &str, content: &str) -> Result { - let raw: RawThemeSource = serde_yaml::from_str(content) - .map_err(|error| format!("Failed to parse {theme_key}.yml: {error}"))?; - normalize_theme_source(theme_key, raw) -} - -pub fn render_tmtheme(source: &ThemeSource) -> String { - let mut output = String::new(); - output.push_str("\n"); - output.push_str("\n"); - output.push_str("\n"); - output.push_str("\n"); - write_key_string(&mut output, 1, "name", &source.name); - output.push_str(" settings\n"); - output.push_str(" \n"); - output.push_str(" \n"); - output.push_str(" settings\n"); - output.push_str(" \n"); - write_key_string(&mut output, 4, "foreground", &source.settings.foreground); - write_key_string(&mut output, 4, "background", &source.settings.background); - write_key_string( - &mut output, - 4, - "lineHighlight", - &source.settings.line_highlight, - ); - output.push_str(" \n"); - output.push_str(" \n"); - - for rule in &source.rules { - output.push_str(" \n"); - if let Some(name) = &rule.name { - write_key_string(&mut output, 3, "name", name); - } - write_key_string(&mut output, 3, "scope", &rule.scope); - output.push_str(" settings\n"); - output.push_str(" \n"); - if let Some(foreground) = &rule.foreground { - write_key_string(&mut output, 4, "foreground", foreground); - } - if let Some(background) = &rule.background { - write_key_string(&mut output, 4, "background", background); - } - if let Some(font_style) = &rule.font_style { - write_key_string(&mut output, 4, "fontStyle", &font_style.join(" ")); - } - output.push_str(" \n"); - output.push_str(" \n"); - } - - output.push_str(" \n"); - output.push_str("\n"); - output.push_str("\n"); - output -} - -fn normalize_theme_source(theme_key: &str, raw: RawThemeSource) -> Result { - let palette = raw - .palette - .into_iter() - .map(|(key, value)| (key, value.0)) - .collect::>(); - - let settings = ThemeSettingsSource { - foreground: resolve_color_ref( - theme_key, - "settings.foreground", - raw.settings.foreground, - &palette, - )?, - background: resolve_color_ref( - theme_key, - "settings.background", - raw.settings.background, - &palette, - )?, - line_highlight: resolve_color_ref( - theme_key, - "settings.line_highlight", - raw.settings.line_highlight, - &palette, - )?, - }; - - if raw.rules.is_empty() { - return Err(format!("{theme_key}.yml rules must not be empty")); - } - - let mut rules = Vec::with_capacity(raw.rules.len()); - for (index, raw_rule) in raw.rules.into_iter().enumerate() { - let path = format!("rules[{index}]"); - let foreground = raw_rule - .foreground - .map(|value| { - resolve_color_ref(theme_key, &format!("{path}.foreground"), value, &palette) - }) - .transpose()?; - let background = raw_rule - .background - .map(|value| { - resolve_color_ref(theme_key, &format!("{path}.background"), value, &palette) - }) - .transpose()?; - let font_style = raw_rule.font_style.map(|styles| { - styles - .into_iter() - .map(|style| style.as_str().to_string()) - .collect::>() - }); - - if foreground.is_none() && background.is_none() && font_style.is_none() { - return Err(format!( - "{theme_key}.yml {path} must define at least one of foreground, background, or font_style" - )); - } - - rules.push(ThemeRuleSource { - name: raw_rule.name, - scope: raw_rule.scope.0, - foreground, - background, - font_style, - }); - } - - Ok(ThemeSource { - key: theme_key.to_string(), - name: raw.name, - palette, - settings, - rules, - }) -} - -fn resolve_color_ref( - theme_key: &str, - path: &str, - color_ref: ColorRef, - palette: &BTreeMap, -) -> Result { - match color_ref { - ColorRef::Literal(color) => Ok(color.0), - ColorRef::Palette(key) => palette - .get(&key) - .cloned() - .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {key}")), - } -} - -impl<'de> Deserialize<'de> for HexColor { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - validate_literal_color(&value).map_err(de::Error::custom)?; - Ok(HexColor(value)) - } -} - -impl<'de> Deserialize<'de> for ColorRef { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value.starts_with('#') { - validate_literal_color(&value).map_err(de::Error::custom)?; - Ok(ColorRef::Literal(HexColor(value))) - } else { - Ok(ColorRef::Palette(value)) - } - } -} - -impl<'de> Deserialize<'de> for ScopeSelectorString { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - ScopeSelectors::from_str(&value).map_err(de::Error::custom)?; - Ok(ScopeSelectorString(value)) - } -} - -impl FontStyle { - fn as_str(&self) -> &'static str { - match self { - FontStyle::Bold => "bold", - FontStyle::Italic => "italic", - FontStyle::Underline => "underline", - } - } -} - -fn validate_literal_color(color: &str) -> Result<(), String> { - let is_valid = matches!(color.len(), 4 | 7 | 9) - && color.starts_with('#') - && color.chars().skip(1).all(|char| char.is_ascii_hexdigit()); - - if is_valid { - Ok(()) - } else { - Err("must be a hex color in #RGB, #RRGGBB, or #RRGGBBAA format".to_string()) - } -} - -fn write_key_string(output: &mut String, indent: usize, key: &str, value: &str) { - let padding = " ".repeat(indent); - let escaped = xml_escape(value); - let _ = writeln!(output, "{padding}{key}"); - let _ = writeln!(output, "{padding}{escaped}"); -} - -fn xml_escape(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} diff --git a/build/theme_compiler/mod.rs b/build/theme_compiler/mod.rs new file mode 100644 index 00000000..82a3df01 --- /dev/null +++ b/build/theme_compiler/mod.rs @@ -0,0 +1,77 @@ +mod parsed; +mod textmate; +mod validated; + +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; + +pub use validated::Theme; + +pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result, String> { + if output_dir.exists() { + fs::remove_dir_all(output_dir) + .map_err(|error| format!("Failed to clear generated theme directory: {error}"))?; + } + + fs::create_dir_all(output_dir) + .map_err(|error| format!("Failed to create generated theme directory: {error}"))?; + + let mut themes = Vec::new(); + let mut seen_keys = HashSet::new(); + for entry in fs::read_dir(source_dir) + .map_err(|error| format!("Failed to read theme source directory: {error}"))? + { + let path = entry + .map_err(|error| format!("Failed to read theme source entry: {error}"))? + .path(); + if !path.is_file() || path.extension().and_then(|ext| ext.to_str()) != Some("yml") { + continue; + } + + let key = path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| format!("Invalid theme source filename: {}", path.display()))? + .to_string(); + + if !seen_keys.insert(key.clone()) { + return Err(format!("Duplicate theme key: {key}")); + } + + let content = fs::read_to_string(&path) + .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; + themes.push(parse_theme(&key, &content)?); + } + + themes.sort_by(|left, right| left.key.cmp(&right.key)); + + let mut outputs = Vec::new(); + for theme in themes { + let output_path = output_dir.join(format!("{}.tmTheme", theme.key)); + fs::write(&output_path, render_tmtheme(&theme)) + .map_err(|error| format!("Failed to write {}: {error}", output_path.display()))?; + outputs.push(output_path); + } + + Ok(outputs) +} + +pub fn parse_theme(theme_key: &str, content: &str) -> Result { + let parsed_theme = parsed::parse(theme_key, content)?; + validated::Theme::try_from_parsed(theme_key, parsed_theme) +} + +pub fn render_tmtheme(theme: &Theme) -> String { + textmate::render(theme) +} + +#[cfg(test)] +pub fn parse_parsed_theme(theme_key: &str, content: &str) -> Result { + parsed::parse(theme_key, content) +} + +#[cfg(test)] +pub fn validate_theme(theme_key: &str, parsed_theme: parsed::Theme) -> Result { + validated::Theme::try_from_parsed(theme_key, parsed_theme) +} diff --git a/build/theme_compiler/parsed.rs b/build/theme_compiler/parsed.rs new file mode 100644 index 00000000..75908880 --- /dev/null +++ b/build/theme_compiler/parsed.rs @@ -0,0 +1,118 @@ +use std::collections::BTreeMap; +use std::str::FromStr; + +use serde::de::{self, Deserializer}; +use serde::Deserialize; +use syntect::highlighting::ScopeSelectors; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Theme { + pub name: String, + #[serde(default)] + pub palette: BTreeMap, + pub settings: Settings, + pub rules: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Settings { + pub foreground: ColorRef, + pub background: ColorRef, + pub line_highlight: ColorRef, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Rule { + pub name: Option, + pub scope: ScopeSelector, + pub foreground: Option, + pub background: Option, + pub font_style: Option>, +} + +#[derive(Debug, Clone)] +pub struct HexColor(pub String); + +#[derive(Debug, Clone)] +pub enum ColorRef { + Literal(HexColor), + Palette(String), +} + +#[derive(Debug, Clone)] +pub struct ScopeSelector(pub String); + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FontStyle { + Bold, + Italic, + Underline, +} + +pub fn parse(theme_key: &str, content: &str) -> Result { + serde_yaml::from_str(content) + .map_err(|error| format!("Failed to parse {theme_key}.yml: {error}")) +} + +impl<'de> Deserialize<'de> for HexColor { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + validate_literal_color(&value).map_err(de::Error::custom)?; + Ok(HexColor(value)) + } +} + +impl<'de> Deserialize<'de> for ColorRef { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value.starts_with('#') { + validate_literal_color(&value).map_err(de::Error::custom)?; + Ok(ColorRef::Literal(HexColor(value))) + } else { + Ok(ColorRef::Palette(value)) + } + } +} + +impl<'de> Deserialize<'de> for ScopeSelector { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + ScopeSelectors::from_str(&value).map_err(de::Error::custom)?; + Ok(ScopeSelector(value)) + } +} + +impl FontStyle { + pub fn as_str(&self) -> &'static str { + match self { + FontStyle::Bold => "bold", + FontStyle::Italic => "italic", + FontStyle::Underline => "underline", + } + } +} + +fn validate_literal_color(color: &str) -> Result<(), String> { + let is_valid = matches!(color.len(), 4 | 7 | 9) + && color.starts_with('#') + && color.chars().skip(1).all(|char| char.is_ascii_hexdigit()); + + if is_valid { + Ok(()) + } else { + Err("must be a hex color in #RGB, #RRGGBB, or #RRGGBBAA format".to_string()) + } +} diff --git a/build/theme_compiler/textmate.rs b/build/theme_compiler/textmate.rs new file mode 100644 index 00000000..b4df9a69 --- /dev/null +++ b/build/theme_compiler/textmate.rs @@ -0,0 +1,69 @@ +use std::fmt::Write as _; + +use super::validated; + +pub fn render(theme: &validated::Theme) -> String { + let mut output = String::new(); + output.push_str("\n"); + output.push_str("\n"); + output.push_str("\n"); + output.push_str("\n"); + write_key_string(&mut output, 1, "name", &theme.name); + output.push_str(" settings\n"); + output.push_str(" \n"); + output.push_str(" \n"); + output.push_str(" settings\n"); + output.push_str(" \n"); + write_key_string(&mut output, 4, "foreground", &theme.settings.foreground); + write_key_string(&mut output, 4, "background", &theme.settings.background); + write_key_string( + &mut output, + 4, + "lineHighlight", + &theme.settings.line_highlight, + ); + output.push_str(" \n"); + output.push_str(" \n"); + + for rule in &theme.rules { + output.push_str(" \n"); + if let Some(name) = &rule.name { + write_key_string(&mut output, 3, "name", name); + } + write_key_string(&mut output, 3, "scope", &rule.scope); + output.push_str(" settings\n"); + output.push_str(" \n"); + if let Some(foreground) = &rule.foreground { + write_key_string(&mut output, 4, "foreground", foreground); + } + if let Some(background) = &rule.background { + write_key_string(&mut output, 4, "background", background); + } + if let Some(font_style) = &rule.font_style { + write_key_string(&mut output, 4, "fontStyle", &font_style.join(" ")); + } + output.push_str(" \n"); + output.push_str(" \n"); + } + + output.push_str(" \n"); + output.push_str("\n"); + output.push_str("\n"); + output +} + +fn write_key_string(output: &mut String, indent: usize, key: &str, value: &str) { + let padding = " ".repeat(indent); + let escaped = xml_escape(value); + let _ = writeln!(output, "{padding}{key}"); + let _ = writeln!(output, "{padding}{escaped}"); +} + +fn xml_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/build/theme_compiler/validated.rs b/build/theme_compiler/validated.rs new file mode 100644 index 00000000..07031c9e --- /dev/null +++ b/build/theme_compiler/validated.rs @@ -0,0 +1,121 @@ +use std::collections::BTreeMap; + +use super::parsed; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Theme { + pub key: String, + pub name: String, + pub settings: Settings, + pub rules: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Settings { + pub foreground: String, + pub background: String, + pub line_highlight: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rule { + pub name: Option, + pub scope: String, + pub foreground: Option, + pub background: Option, + pub font_style: Option>, +} + +impl Theme { + pub fn try_from_parsed(theme_key: &str, parsed_theme: parsed::Theme) -> Result { + let palette = parsed_theme + .palette + .into_iter() + .map(|(key, value)| (key, value.0)) + .collect::>(); + + let settings = Settings { + foreground: resolve_color_ref( + theme_key, + "settings.foreground", + parsed_theme.settings.foreground, + &palette, + )?, + background: resolve_color_ref( + theme_key, + "settings.background", + parsed_theme.settings.background, + &palette, + )?, + line_highlight: resolve_color_ref( + theme_key, + "settings.line_highlight", + parsed_theme.settings.line_highlight, + &palette, + )?, + }; + + if parsed_theme.rules.is_empty() { + return Err(format!("{theme_key}.yml rules must not be empty")); + } + + let mut rules = Vec::with_capacity(parsed_theme.rules.len()); + for (index, parsed_rule) in parsed_theme.rules.into_iter().enumerate() { + let path = format!("rules[{index}]"); + let foreground = parsed_rule + .foreground + .map(|value| { + resolve_color_ref(theme_key, &format!("{path}.foreground"), value, &palette) + }) + .transpose()?; + let background = parsed_rule + .background + .map(|value| { + resolve_color_ref(theme_key, &format!("{path}.background"), value, &palette) + }) + .transpose()?; + let font_style = parsed_rule.font_style.map(|styles| { + styles + .into_iter() + .map(|style| style.as_str().to_string()) + .collect::>() + }); + + if foreground.is_none() && background.is_none() && font_style.is_none() { + return Err(format!( + "{theme_key}.yml {path} must define at least one of foreground, background, or font_style" + )); + } + + rules.push(Rule { + name: parsed_rule.name, + scope: parsed_rule.scope.0, + foreground, + background, + font_style, + }); + } + + Ok(Self { + key: theme_key.to_string(), + name: parsed_theme.name, + settings, + rules, + }) + } +} + +fn resolve_color_ref( + theme_key: &str, + path: &str, + color_ref: parsed::ColorRef, + palette: &BTreeMap, +) -> Result { + match color_ref { + parsed::ColorRef::Literal(color) => Ok(color.0), + parsed::ColorRef::Palette(key) => palette + .get(&key) + .cloned() + .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {key}")), + } +} diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index ce8743f3..66507cbe 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -1,4 +1,4 @@ -#[path = "../build/theme_compiler.rs"] +#[path = "../build/theme_compiler/mod.rs"] mod theme_compiler; use std::fs; @@ -10,7 +10,7 @@ use syntect::highlighting::ThemeSet; #[test] fn parse_theme_source_resolves_palette_references() { - let source = theme_compiler::parse_theme_source( + let theme = theme_compiler::parse_theme( "test_theme", r##" name: Test Theme @@ -31,15 +31,15 @@ rules: ) .unwrap(); - assert_eq!(source.settings.foreground, "#112233"); - assert_eq!(source.settings.background, "#445566"); - assert_eq!(source.settings.line_highlight, "#778899"); - assert_eq!(source.rules[0].foreground.as_deref(), Some("#112233")); + assert_eq!(theme.settings.foreground, "#112233"); + assert_eq!(theme.settings.background, "#445566"); + assert_eq!(theme.settings.line_highlight, "#778899"); + assert_eq!(theme.rules[0].foreground.as_deref(), Some("#112233")); } #[test] fn parse_theme_source_rejects_unknown_keys() { - let error = theme_compiler::parse_theme_source( + let error = theme_compiler::parse_theme( "bad_theme", r##" name: Bad Theme @@ -60,7 +60,7 @@ rules: #[test] fn parse_theme_source_rejects_invalid_rule_color_reference() { - let error = theme_compiler::parse_theme_source( + let error = theme_compiler::parse_theme( "bad_theme", r##" name: Bad Theme @@ -80,7 +80,7 @@ rules: #[test] fn parse_theme_source_rejects_non_string_scope() { - let error = theme_compiler::parse_theme_source( + let error = theme_compiler::parse_theme( "bad_theme", r##" name: Bad Theme @@ -101,7 +101,7 @@ rules: #[test] fn render_tmtheme_is_parseable_and_preserves_empty_font_style() { - let source = theme_compiler::parse_theme_source( + let theme = theme_compiler::parse_theme( "test_theme", r##" name: Test Theme @@ -117,7 +117,7 @@ rules: ) .unwrap(); - let rendered = theme_compiler::render_tmtheme(&source); + let rendered = theme_compiler::render_tmtheme(&theme); assert!(rendered.contains("fontStyle")); assert!(rendered.contains("")); @@ -163,3 +163,31 @@ rules: fs::remove_dir_all(base).unwrap(); } + +#[test] +fn stage_pipeline_parses_validates_and_renders() { + let parsed = theme_compiler::parse_parsed_theme( + "test_theme", + r##" +name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" +settings: + foreground: fg + background: bg + line_highlight: line +rules: + - scope: comment + foreground: fg +"##, + ) + .unwrap(); + + let theme = theme_compiler::validate_theme("test_theme", parsed).unwrap(); + let rendered = theme_compiler::render_tmtheme(&theme); + let mut cursor = Cursor::new(rendered.into_bytes()); + + ThemeSet::load_from_reader(&mut cursor).unwrap(); +} From b5a02c4f05a1c3bd46b8e764e1ecafa534defe38 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 11:24:06 -0400 Subject: [PATCH 11/26] Rename build-time theme directory variable --- build.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.rs b/build.rs index fefe4aa5..1495ec46 100644 --- a/build.rs +++ b/build.rs @@ -140,11 +140,11 @@ fn bake_app_syntaxes() { fn bake_app_themes() { let out_dir = env::var("OUT_DIR").expect("The compiler did not provide $OUT_DIR"); let output_path = PathBuf::from(out_dir).join(APP_THEME_SOURCE); - let generated_theme_dir = PathBuf::from(env::var("OUT_DIR").unwrap()).join("generated_themes"); - theme_compiler::compile_themes(Path::new(APP_THEME_DIR), &generated_theme_dir) + let compiled_theme_dir = PathBuf::from(env::var("OUT_DIR").unwrap()).join("compiled_themes"); + theme_compiler::compile_themes(Path::new(APP_THEME_DIR), &compiled_theme_dir) .expect("Failed to compile bundled theme sources"); - let theme_set = ThemeSet::load_from_folder(&generated_theme_dir) - .expect("Failed to load generated bundled themes"); + let theme_set = ThemeSet::load_from_folder(&compiled_theme_dir) + .expect("Failed to load compiled bundled themes"); dump_to_uncompressed_file(&theme_set, output_path).expect("Failed to write bundled theme dump"); } From 61ee871f7dff34e9a98bc4a36e6d191a51156dbd Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 11:25:24 -0400 Subject: [PATCH 12/26] Make user_theme_path consistent with user_syntax_path --- src/models/application/preferences/mod.rs | 12 ++++++------ src/view/mod.rs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/models/application/preferences/mod.rs b/src/models/application/preferences/mod.rs index b7b8f060..7c5ee1d4 100644 --- a/src/models/application/preferences/mod.rs +++ b/src/models/application/preferences/mod.rs @@ -102,6 +102,12 @@ impl Preferences { .context("Couldn't build a path to the user syntax directory.") } + /// Returns the theme path, making sure the directory exists. + pub fn theme_path() -> Result { + config_subdirectory(THEME_PATH) + .context("Couldn't build a path to the user themes directory.") + } + /// Returns the preference file loaded into a buffer for editing. /// If the file doesn't already exist, it will return a new in-memory buffer /// with a pre-populated path, creating the parent config directories @@ -152,12 +158,6 @@ impl Preferences { .expect("Couldn't find default theme name!") } - /// Returns the theme path, making sure the directory exists. - pub fn theme_path(&self) -> Result { - config_subdirectory(THEME_PATH) - .context("Couldn't build a path to the user themes directory.") - } - /// Updates the in-memory theme value. pub fn set_theme>(&mut self, theme: T) { self.theme = Some(theme.into()); diff --git a/src/view/mod.rs b/src/view/mod.rs index 9afd8061..489eb242 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -54,7 +54,7 @@ impl View { event_channel: Sender, ) -> Result { let terminal = build_terminal().context("Failed to initialize terminal")?; - let theme_path = user_theme_path(&preferences.borrow())?; + let theme_path = user_theme_path()?; let theme_set = ThemeLoader::new(theme_path).load()?; let (killswitch_tx, killswitch_rx) = mpsc::sync_channel(0); @@ -198,12 +198,12 @@ impl View { } #[cfg(not(test))] -fn user_theme_path(preferences: &Preferences) -> Result { - preferences.theme_path() +fn user_theme_path() -> Result { + Preferences::theme_path() } #[cfg(test)] -fn user_theme_path(_: &Preferences) -> Result { +fn user_theme_path() -> Result { Ok(PathBuf::from("tests/fixtures/user_themes")) } From b5026211a4edba4f40b9d172cee582964a328daf Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 11:35:42 -0400 Subject: [PATCH 13/26] Rename view test to reflect integration focus/intent --- src/view/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/mod.rs b/src/view/mod.rs index 489eb242..705a0192 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -235,7 +235,7 @@ mod tests { use syntect::highlighting::{Highlighter, ThemeSet}; #[test] - fn new_loads_fixture_user_themes_in_tests() { + fn new_populates_theme_set_via_theme_loader() { let preferences = Rc::new(RefCell::new(Preferences::new(None))); let (tx, _) = mpsc::channel(); let view = View::new(preferences, tx).unwrap(); From c4676ef1e43e937aa4efad8829ea1209944e1b70 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 12:07:59 -0400 Subject: [PATCH 14/26] Update theme definition skill to use YAML format --- .agents/skills/theme-definition/SKILL.md | 90 +++++++++++-------- .../references/token-color-standard.md | 4 +- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/.agents/skills/theme-definition/SKILL.md b/.agents/skills/theme-definition/SKILL.md index 4065546a..fc3945c1 100644 --- a/.agents/skills/theme-definition/SKILL.md +++ b/.agents/skills/theme-definition/SKILL.md @@ -1,73 +1,84 @@ --- name: theme-definition -description: Create or update a TextMate `.tmTheme` file in the repository `themes/` directory when asked for a specific color theme such as Gruvbox, Nord, or a custom dark or light palette. +description: Create or update a bundled YAML theme source in the repository `themes/` directory when asked for a specific color theme such as Gruvbox, Nord, or a custom dark or light palette. These YAML files are compiled into TextMate `.tmTheme` files during the build. --- # Theme Definition -Use this skill when asked to create or update a theme definition for a named color theme or palette. +Use this skill when asked to create or update a bundled theme definition for a named color theme or palette. ## Goal -Create or update exactly one theme file in the repository top-level `themes/` directory: +Create or update exactly one bundled theme source file in the repository top-level `themes/` directory: -- `themes/.tmTheme` +- `themes/.yml` Examples: -- `Gruvbox Dark` -> `themes/gruvbox_dark.tmTheme` -- `Nord Light` -> `themes/nord_light.tmTheme` -- `My Theme` -> `themes/my_theme.tmTheme` +- `Gruvbox Dark` -> `themes/gruvbox_dark.yml` +- `Nord Light` -> `themes/nord_light.yml` +- `My Theme` -> `themes/my_theme.yml` If a matching file already exists, update it instead of creating a duplicate. +The YAML source is the authored artifact. Amp's build compiles it into a generated `.tmTheme` file with the same key during `build.rs`. + ## Required Constraints -- Only produce TextMate `.tmTheme` files. -- Keep the file as XML plist format. +- Only author bundled YAML theme sources in `themes/`. +- Do not manually create or edit generated `.tmTheme` files for bundled themes. - Do not write the theme anywhere except the top-level `themes/` directory unless the user explicitly asks for something else. -- Do not treat the bundled themes as the authoritative source for required settings or scope coverage. -- The theme must be parseable by `syntect::highlighting::ThemeSet::load_from_reader`. - Use the semantic token-family standard in [references/token-color-standard.md](references/token-color-standard.md) whenever the user does not provide a custom scope map. - -Amp itself requires these top-level theme settings: - -- `foreground` -- `background` -- `lineHighlight` - -Amp does not require these top-level settings: - -- `caret` -- `selection` -- `invisibles` - -When the user does not provide a complete scope list, do not stop at a small "practical first pass". Instead, map the requested palette onto the canonical token families in the reference file so common code tokens do not collapse to the default foreground. +- Treat `palette` as the single source of truth for authored colors: + - define hex literals in `palette` + - reference palette keys from `settings` and `rules` + - do not place inline hex literals directly in `settings` or `rules` unless the user explicitly asks for an exception +- Match the actual compiler schema in `build/theme_compiler/`: + - required top-level keys: `name`, `settings`, `rules` + - optional top-level key: `palette` + - required `settings` keys: `foreground`, `background`, `line_highlight` + - `rules` must not be empty + - each rule may contain `name`, `scope`, `foreground`, `background`, `font_style` + - each rule must define at least one of `foreground`, `background`, or `font_style` + - `scope` must be a single valid TextMate selector string + - the compiler accepts hex literals or references to keys defined in `palette`, but this skill should prefer palette references everywhere outside `palette` + - `font_style` values are `bold`, `italic`, and `underline` +- Treat unknown YAML keys as invalid rather than inventing extra structure. + +Amp requires these base theme settings: + +- `settings.foreground` +- `settings.background` +- `settings.line_highlight` + +When the user does not provide a complete scope list, do not stop at a small "practical first pass". Map the requested palette onto the canonical token families in the reference file so common code tokens do not collapse to the default foreground. ## Workflow 1. Identify the requested theme name and whether it is dark, light, or otherwise palette-driven. -2. Inspect nearby files in `themes/` for file naming and plist formatting conventions only. +2. Inspect nearby files in `themes/` for filename and YAML rule-shape conventions. 3. Read [references/token-color-standard.md](references/token-color-standard.md) and choose colors for each required family. -4. Create or update a `.tmTheme` file with: - - a top-level `name` - - a base settings entry containing `foreground`, `background`, and `lineHighlight` - - scope-specific settings for the canonical token families in the reference, using the requested palette +4. Create or update a YAML theme source with: + - top-level `name` + - a `palette` containing the authored hex colors + - `settings.foreground`, `settings.background`, and `settings.line_highlight` as palette-key references + - `rules` covering the canonical token families in the reference, using the requested palette 5. Prefer broadly useful TextMate scopes and stable fallback tiers instead of copying an existing bundled theme's exact rule list. -6. Validate that the theme is parseable through Amp's existing theme-loading path before finishing. +6. Validate through the repository build path before finishing. ## Output Guidelines ### For Generated Or Updated Themes - Use a filename stem that matches the theme key Amp will expose in theme selection. -- Keep the plist readable and conventional rather than minimizing or over-structuring it. +- Keep the YAML concise and readable rather than encoding generated `.tmTheme` structure by hand. +- Put literal hex values in `palette`, not directly in `settings` or per-rule color fields. - If the requested palette is underspecified, make the smallest reasonable set of assumptions and state them briefly. - Prefer semantically complete coverage over an artificially tiny rule set. - Ensure the base settings are sufficient for Amp's UI color mapping: - - `foreground` is used for default text - - `background` is used for inverted background mappings - - `lineHighlight` is used for the focused or current-line background + - `settings.foreground` is used for default text + - `settings.background` is used for inverted background mappings + - `settings.line_highlight` is used for the focused or current-line background - Use the default foreground as a last-resort fallback, not as the intended color for common code tokens. - Keep at least two punctuation tiers when the palette allows it: - structural punctuation can stay muted @@ -83,10 +94,13 @@ When the user does not provide a complete scope list, do not stop at a small "pr - parameters - support or builtin symbols - annotations or attributes + - semantic operators + - structural punctuation - Rust is a useful stress case for validation, but the output must remain cross-language and useful for markup and configuration formats too. ### For Validation -- Prefer the lightest command that proves the generated `.tmTheme` parses successfully in the repository context. -- If there is no dedicated theme test, still perform a parse-level smoke check before finishing. -- When updating a bundled example theme, prefer adding or running a test that confirms the theme loads and that key token families are represented by explicit scope rules. +- Prefer `cargo check` as the main smoke test. It runs `build.rs`, compiles `themes/*.yml`, and loads the generated bundled themes. +- Use `cargo test --test theme_compiler` when validating compiler-schema behavior or debugging theme-source failures. +- If there is no dedicated theme test for a specific change, still perform at least a build-path smoke check before finishing. +- When updating a bundled example theme, prefer adding or running a test that confirms the compiled theme loads and that key token families are represented by explicit scope rules. diff --git a/.agents/skills/theme-definition/references/token-color-standard.md b/.agents/skills/theme-definition/references/token-color-standard.md index 68a030d0..bd7a44c1 100644 --- a/.agents/skills/theme-definition/references/token-color-standard.md +++ b/.agents/skills/theme-definition/references/token-color-standard.md @@ -1,6 +1,8 @@ # Token Color Standard -Use this reference when generating or updating a `.tmTheme` and the user has not provided a detailed scope map. +Use this reference when generating or updating a bundled YAML theme source in `themes/` and the user has not provided a detailed scope map. The authored file is YAML, but the rule scopes should still target stable TextMate token families because Amp compiles the source into a `.tmTheme` during the build. + +For authored theme sources in this repo, treat `palette` as the only place for literal hex colors. `settings` and `rules` should reference palette keys so color intent stays named and reviewable. ## Goal From 9d53af6adccb5360d5c98bec60494b82478b49fd Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 12:15:39 -0400 Subject: [PATCH 15/26] Enforce theme palette references at the compiler level --- build/theme_compiler/validated.rs | 27 +++++++--- tests/theme_compiler.rs | 86 ++++++++++++++++++++++++------- themes/solarized_dark.yml | 3 +- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/build/theme_compiler/validated.rs b/build/theme_compiler/validated.rs index 07031c9e..174221bd 100644 --- a/build/theme_compiler/validated.rs +++ b/build/theme_compiler/validated.rs @@ -35,19 +35,19 @@ impl Theme { .collect::>(); let settings = Settings { - foreground: resolve_color_ref( + foreground: resolve_palette_color_ref( theme_key, "settings.foreground", parsed_theme.settings.foreground, &palette, )?, - background: resolve_color_ref( + background: resolve_palette_color_ref( theme_key, "settings.background", parsed_theme.settings.background, &palette, )?, - line_highlight: resolve_color_ref( + line_highlight: resolve_palette_color_ref( theme_key, "settings.line_highlight", parsed_theme.settings.line_highlight, @@ -65,13 +65,23 @@ impl Theme { let foreground = parsed_rule .foreground .map(|value| { - resolve_color_ref(theme_key, &format!("{path}.foreground"), value, &palette) + resolve_palette_color_ref( + theme_key, + &format!("{path}.foreground"), + value, + &palette, + ) }) .transpose()?; let background = parsed_rule .background .map(|value| { - resolve_color_ref(theme_key, &format!("{path}.background"), value, &palette) + resolve_palette_color_ref( + theme_key, + &format!("{path}.background"), + value, + &palette, + ) }) .transpose()?; let font_style = parsed_rule.font_style.map(|styles| { @@ -105,14 +115,17 @@ impl Theme { } } -fn resolve_color_ref( +fn resolve_palette_color_ref( theme_key: &str, path: &str, color_ref: parsed::ColorRef, palette: &BTreeMap, ) -> Result { match color_ref { - parsed::ColorRef::Literal(color) => Ok(color.0), + parsed::ColorRef::Literal(color) => Err(format!( + "{theme_key}.yml {path} must reference a palette key, found literal color: {}", + color.0 + )), parsed::ColorRef::Palette(key) => palette .get(&key) .cloned() diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index 66507cbe..fa010d0a 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -43,14 +43,19 @@ fn parse_theme_source_rejects_unknown_keys() { "bad_theme", r##" name: Bad Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" + selection_color: "#000000" settings: - foreground: "#112233" - background: "#445566" - line_highlight: "#778899" - selection: "#000000" + foreground: fg + background: bg + line_highlight: line + selection: selection_color rules: - scope: comment - foreground: "#112233" + foreground: fg "##, ) .unwrap_err(); @@ -64,10 +69,14 @@ fn parse_theme_source_rejects_invalid_rule_color_reference() { "bad_theme", r##" name: Bad Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" settings: - foreground: "#112233" - background: "#445566" - line_highlight: "#778899" + foreground: fg + background: bg + line_highlight: line rules: - scope: comment foreground: missing @@ -84,13 +93,17 @@ fn parse_theme_source_rejects_non_string_scope() { "bad_theme", r##" name: Bad Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" settings: - foreground: "#112233" - background: "#445566" - line_highlight: "#778899" + foreground: fg + background: bg + line_highlight: line rules: - scope: [comment] - foreground: "#112233" + foreground: fg "##, ) .unwrap_err(); @@ -105,13 +118,17 @@ fn render_tmtheme_is_parseable_and_preserves_empty_font_style() { "test_theme", r##" name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" settings: - foreground: "#112233" - background: "#445566" - line_highlight: "#778899" + foreground: fg + background: bg + line_highlight: line rules: - scope: comment - foreground: "#112233" + foreground: fg font_style: [] "##, ) @@ -140,13 +157,17 @@ fn compile_themes_writes_generated_tmtheme_files() { source_dir.join("sample.yml"), r##" name: Sample Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" settings: - foreground: "#112233" - background: "#445566" - line_highlight: "#778899" + foreground: fg + background: bg + line_highlight: line rules: - scope: comment - foreground: "#112233" + foreground: fg "##, ) .unwrap(); @@ -191,3 +212,28 @@ rules: ThemeSet::load_from_reader(&mut cursor).unwrap(); } + +#[test] +fn validate_theme_rejects_literal_colors_outside_palette() { + let parsed = theme_compiler::parse_parsed_theme( + "bad_theme", + r##" +name: Bad Theme +palette: + bg: "#445566" + line: "#778899" +settings: + foreground: "#112233" + background: bg + line_highlight: line +rules: + - scope: comment + foreground: bg +"##, + ) + .unwrap(); + + let error = theme_compiler::validate_theme("bad_theme", parsed).unwrap_err(); + assert!(error.contains("settings.foreground must reference a palette key")); + assert!(error.contains("#112233")); +} diff --git a/themes/solarized_dark.yml b/themes/solarized_dark.yml index 2ffb3bfa..abf336e7 100644 --- a/themes/solarized_dark.yml +++ b/themes/solarized_dark.yml @@ -8,6 +8,7 @@ palette: base01: "#93A1A1" base02: "#b2b2b2" base3_light: "#EEE8D5" + line: "#303030" yellow: "#B58900" orange: "#CB4B16" red: "#DC322F" @@ -19,7 +20,7 @@ palette: settings: foreground: base02 background: base3 - line_highlight: "#303030" + line_highlight: line rules: - name: Comment scope: "comment, comment.block.documentation, punctuation.definition.comment" From a760ea59849c800995fad0d5f2bad0d94a8c6a8d Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 12:25:39 -0400 Subject: [PATCH 16/26] Update compiler to outright refuse non-palette colors in scope mappings --- build/theme_compiler/parsed.rs | 23 +++++++++++------------ build/theme_compiler/validated.rs | 29 ++++++++++++----------------- tests/theme_compiler.rs | 11 +++++------ 3 files changed, 28 insertions(+), 35 deletions(-) diff --git a/build/theme_compiler/parsed.rs b/build/theme_compiler/parsed.rs index 75908880..aa97329e 100644 --- a/build/theme_compiler/parsed.rs +++ b/build/theme_compiler/parsed.rs @@ -18,9 +18,9 @@ pub struct Theme { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Settings { - pub foreground: ColorRef, - pub background: ColorRef, - pub line_highlight: ColorRef, + pub foreground: PaletteKey, + pub background: PaletteKey, + pub line_highlight: PaletteKey, } #[derive(Debug, Deserialize)] @@ -28,8 +28,8 @@ pub struct Settings { pub struct Rule { pub name: Option, pub scope: ScopeSelector, - pub foreground: Option, - pub background: Option, + pub foreground: Option, + pub background: Option, pub font_style: Option>, } @@ -37,10 +37,7 @@ pub struct Rule { pub struct HexColor(pub String); #[derive(Debug, Clone)] -pub enum ColorRef { - Literal(HexColor), - Palette(String), -} +pub struct PaletteKey(pub String); #[derive(Debug, Clone)] pub struct ScopeSelector(pub String); @@ -69,7 +66,7 @@ impl<'de> Deserialize<'de> for HexColor { } } -impl<'de> Deserialize<'de> for ColorRef { +impl<'de> Deserialize<'de> for PaletteKey { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -77,9 +74,11 @@ impl<'de> Deserialize<'de> for ColorRef { let value = String::deserialize(deserializer)?; if value.starts_with('#') { validate_literal_color(&value).map_err(de::Error::custom)?; - Ok(ColorRef::Literal(HexColor(value))) + Err(de::Error::custom( + "must reference a palette key; literal colors belong in palette", + )) } else { - Ok(ColorRef::Palette(value)) + Ok(PaletteKey(value)) } } } diff --git a/build/theme_compiler/validated.rs b/build/theme_compiler/validated.rs index 174221bd..ce931f9d 100644 --- a/build/theme_compiler/validated.rs +++ b/build/theme_compiler/validated.rs @@ -35,19 +35,19 @@ impl Theme { .collect::>(); let settings = Settings { - foreground: resolve_palette_color_ref( + foreground: resolve_palette_key( theme_key, "settings.foreground", parsed_theme.settings.foreground, &palette, )?, - background: resolve_palette_color_ref( + background: resolve_palette_key( theme_key, "settings.background", parsed_theme.settings.background, &palette, )?, - line_highlight: resolve_palette_color_ref( + line_highlight: resolve_palette_key( theme_key, "settings.line_highlight", parsed_theme.settings.line_highlight, @@ -65,7 +65,7 @@ impl Theme { let foreground = parsed_rule .foreground .map(|value| { - resolve_palette_color_ref( + resolve_palette_key( theme_key, &format!("{path}.foreground"), value, @@ -76,7 +76,7 @@ impl Theme { let background = parsed_rule .background .map(|value| { - resolve_palette_color_ref( + resolve_palette_key( theme_key, &format!("{path}.background"), value, @@ -115,20 +115,15 @@ impl Theme { } } -fn resolve_palette_color_ref( +fn resolve_palette_key( theme_key: &str, path: &str, - color_ref: parsed::ColorRef, + color_ref: parsed::PaletteKey, palette: &BTreeMap, ) -> Result { - match color_ref { - parsed::ColorRef::Literal(color) => Err(format!( - "{theme_key}.yml {path} must reference a palette key, found literal color: {}", - color.0 - )), - parsed::ColorRef::Palette(key) => palette - .get(&key) - .cloned() - .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {key}")), - } + let key = color_ref.0; + palette + .get(&key) + .cloned() + .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {key}")) } diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index fa010d0a..9d7ac704 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -214,8 +214,8 @@ rules: } #[test] -fn validate_theme_rejects_literal_colors_outside_palette() { - let parsed = theme_compiler::parse_parsed_theme( +fn parse_theme_source_rejects_literal_colors_outside_palette() { + let error = theme_compiler::parse_theme( "bad_theme", r##" name: Bad Theme @@ -231,9 +231,8 @@ rules: foreground: bg "##, ) - .unwrap(); + .unwrap_err(); - let error = theme_compiler::validate_theme("bad_theme", parsed).unwrap_err(); - assert!(error.contains("settings.foreground must reference a palette key")); - assert!(error.contains("#112233")); + assert!(error.contains("must reference a palette key")); + assert!(error.contains("literal colors belong in palette")); } From 40e143017322cf080806815aba4e7e683974a344 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 12:42:06 -0400 Subject: [PATCH 17/26] Update skill to avoid color literals outside of palette --- .agents/skills/theme-definition/SKILL.md | 7 +++++-- .../theme-definition/references/token-color-standard.md | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.agents/skills/theme-definition/SKILL.md b/.agents/skills/theme-definition/SKILL.md index fc3945c1..dd4a478f 100644 --- a/.agents/skills/theme-definition/SKILL.md +++ b/.agents/skills/theme-definition/SKILL.md @@ -32,16 +32,18 @@ The YAML source is the authored artifact. Amp's build compiles it into a generat - Treat `palette` as the single source of truth for authored colors: - define hex literals in `palette` - reference palette keys from `settings` and `rules` - - do not place inline hex literals directly in `settings` or `rules` unless the user explicitly asks for an exception + - do not place inline hex literals directly in `settings` or `rules` - Match the actual compiler schema in `build/theme_compiler/`: - required top-level keys: `name`, `settings`, `rules` - optional top-level key: `palette` + - `palette` values must be hex colors - required `settings` keys: `foreground`, `background`, `line_highlight` + - `settings.foreground`, `settings.background`, and `settings.line_highlight` must reference palette keys - `rules` must not be empty - each rule may contain `name`, `scope`, `foreground`, `background`, `font_style` + - `rules[*].foreground` and `rules[*].background`, when present, must reference palette keys - each rule must define at least one of `foreground`, `background`, or `font_style` - `scope` must be a single valid TextMate selector string - - the compiler accepts hex literals or references to keys defined in `palette`, but this skill should prefer palette references everywhere outside `palette` - `font_style` values are `bold`, `italic`, and `underline` - Treat unknown YAML keys as invalid rather than inventing extra structure. @@ -102,5 +104,6 @@ When the user does not provide a complete scope list, do not stop at a small "pr - Prefer `cargo check` as the main smoke test. It runs `build.rs`, compiles `themes/*.yml`, and loads the generated bundled themes. - Use `cargo test --test theme_compiler` when validating compiler-schema behavior or debugging theme-source failures. +- Treat inline color literals outside `palette` as source-format errors. - If there is no dedicated theme test for a specific change, still perform at least a build-path smoke check before finishing. - When updating a bundled example theme, prefer adding or running a test that confirms the compiled theme loads and that key token families are represented by explicit scope rules. diff --git a/.agents/skills/theme-definition/references/token-color-standard.md b/.agents/skills/theme-definition/references/token-color-standard.md index bd7a44c1..14e7031d 100644 --- a/.agents/skills/theme-definition/references/token-color-standard.md +++ b/.agents/skills/theme-definition/references/token-color-standard.md @@ -2,7 +2,7 @@ Use this reference when generating or updating a bundled YAML theme source in `themes/` and the user has not provided a detailed scope map. The authored file is YAML, but the rule scopes should still target stable TextMate token families because Amp compiles the source into a `.tmTheme` during the build. -For authored theme sources in this repo, treat `palette` as the only place for literal hex colors. `settings` and `rules` should reference palette keys so color intent stays named and reviewable. +For authored theme sources in this repo, `palette` is the only place for literal hex colors. `settings` and `rules` must reference palette keys so color intent stays named and reviewable. ## Goal From e48d56915cbe56c0971d7e8936e4b41be5887b91 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 22:27:24 -0400 Subject: [PATCH 18/26] Move build entrypoint into dedicated directory --- Cargo.toml | 1 + build.rs => build/main.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) rename build.rs => build/main.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index 98aac868..c5fbf6d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ readme = "README.md" license-file = "LICENSE" keywords = ["text", "editor", "terminal", "modal"] edition="2021" +build = "build/main.rs" [build-dependencies] regex = "1.10" diff --git a/build.rs b/build/main.rs similarity index 99% rename from build.rs rename to build/main.rs index 1495ec46..0c7424f9 100644 --- a/build.rs +++ b/build/main.rs @@ -1,4 +1,3 @@ -#[path = "build/theme_compiler/mod.rs"] mod theme_compiler; use regex::Regex; From ad06ec36872a721b120df7c1e781473aa7445b21 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 22:41:36 -0400 Subject: [PATCH 19/26] Limit fixture theme setting keys to used values --- tests/fixtures/user_themes/fixture_theme.tmTheme | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/fixtures/user_themes/fixture_theme.tmTheme b/tests/fixtures/user_themes/fixture_theme.tmTheme index 8a552de5..52b63b38 100644 --- a/tests/fixtures/user_themes/fixture_theme.tmTheme +++ b/tests/fixtures/user_themes/fixture_theme.tmTheme @@ -9,15 +9,11 @@ settings - background - #101820 - caret - #f2aa4c foreground #f8f4e3 - lineHighlight - #1d2731 - selection + background + #101820 + line_highlight #2f3e46 From 5fb87bb106b15902e536f5a26faf7aed64be085e Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sat, 2 May 2026 23:01:29 -0400 Subject: [PATCH 20/26] Fix preferences theme_path comment --- src/models/application/preferences/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/application/preferences/mod.rs b/src/models/application/preferences/mod.rs index 7c5ee1d4..432e4a99 100644 --- a/src/models/application/preferences/mod.rs +++ b/src/models/application/preferences/mod.rs @@ -102,7 +102,7 @@ impl Preferences { .context("Couldn't build a path to the user syntax directory.") } - /// Returns the theme path, making sure the directory exists. + /// A path pointing to the user theme directory. pub fn theme_path() -> Result { config_subdirectory(THEME_PATH) .context("Couldn't build a path to the user themes directory.") From 6975e4685bb5514ed6beb1fb17d875b9b4f46bb3 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 3 May 2026 11:40:42 -0400 Subject: [PATCH 21/26] Reduce proliferation of theme key across parsing/validation --- build/theme_compiler/mod.rs | 24 +++++++++-------- build/theme_compiler/parsed.rs | 5 ++-- build/theme_compiler/validated.rs | 44 ++++++------------------------- tests/theme_compiler.rs | 19 +++++-------- 4 files changed, 29 insertions(+), 63 deletions(-) diff --git a/build/theme_compiler/mod.rs b/build/theme_compiler/mod.rs index 82a3df01..8559fe50 100644 --- a/build/theme_compiler/mod.rs +++ b/build/theme_compiler/mod.rs @@ -41,14 +41,16 @@ pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result Result Result { - let parsed_theme = parsed::parse(theme_key, content)?; - validated::Theme::try_from_parsed(theme_key, parsed_theme) +pub fn parse_theme(content: &str) -> Result { + let parsed_theme = parsed::parse(content)?; + validated::Theme::try_from_parsed(parsed_theme) } pub fn render_tmtheme(theme: &Theme) -> String { @@ -67,11 +69,11 @@ pub fn render_tmtheme(theme: &Theme) -> String { } #[cfg(test)] -pub fn parse_parsed_theme(theme_key: &str, content: &str) -> Result { - parsed::parse(theme_key, content) +pub fn parse_parsed_theme(content: &str) -> Result { + parsed::parse(content) } #[cfg(test)] -pub fn validate_theme(theme_key: &str, parsed_theme: parsed::Theme) -> Result { - validated::Theme::try_from_parsed(theme_key, parsed_theme) +pub fn validate_theme(parsed_theme: parsed::Theme) -> Result { + validated::Theme::try_from_parsed(parsed_theme) } diff --git a/build/theme_compiler/parsed.rs b/build/theme_compiler/parsed.rs index aa97329e..56d50ab2 100644 --- a/build/theme_compiler/parsed.rs +++ b/build/theme_compiler/parsed.rs @@ -50,9 +50,8 @@ pub enum FontStyle { Underline, } -pub fn parse(theme_key: &str, content: &str) -> Result { - serde_yaml::from_str(content) - .map_err(|error| format!("Failed to parse {theme_key}.yml: {error}")) +pub fn parse(content: &str) -> Result { + serde_yaml::from_str(content).map_err(|error| error.to_string()) } impl<'de> Deserialize<'de> for HexColor { diff --git a/build/theme_compiler/validated.rs b/build/theme_compiler/validated.rs index ce931f9d..e8bd48fa 100644 --- a/build/theme_compiler/validated.rs +++ b/build/theme_compiler/validated.rs @@ -4,7 +4,6 @@ use super::parsed; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Theme { - pub key: String, pub name: String, pub settings: Settings, pub rules: Vec, @@ -27,7 +26,7 @@ pub struct Rule { } impl Theme { - pub fn try_from_parsed(theme_key: &str, parsed_theme: parsed::Theme) -> Result { + pub fn try_from_parsed(parsed_theme: parsed::Theme) -> Result { let palette = parsed_theme .palette .into_iter() @@ -35,20 +34,9 @@ impl Theme { .collect::>(); let settings = Settings { - foreground: resolve_palette_key( - theme_key, - "settings.foreground", - parsed_theme.settings.foreground, - &palette, - )?, - background: resolve_palette_key( - theme_key, - "settings.background", - parsed_theme.settings.background, - &palette, - )?, + foreground: resolve_palette_key("settings.foreground", parsed_theme.settings.foreground, &palette)?, + background: resolve_palette_key("settings.background", parsed_theme.settings.background, &palette)?, line_highlight: resolve_palette_key( - theme_key, "settings.line_highlight", parsed_theme.settings.line_highlight, &palette, @@ -56,7 +44,7 @@ impl Theme { }; if parsed_theme.rules.is_empty() { - return Err(format!("{theme_key}.yml rules must not be empty")); + return Err("rules must not be empty".to_string()); } let mut rules = Vec::with_capacity(parsed_theme.rules.len()); @@ -64,25 +52,11 @@ impl Theme { let path = format!("rules[{index}]"); let foreground = parsed_rule .foreground - .map(|value| { - resolve_palette_key( - theme_key, - &format!("{path}.foreground"), - value, - &palette, - ) - }) + .map(|value| resolve_palette_key(&format!("{path}.foreground"), value, &palette)) .transpose()?; let background = parsed_rule .background - .map(|value| { - resolve_palette_key( - theme_key, - &format!("{path}.background"), - value, - &palette, - ) - }) + .map(|value| resolve_palette_key(&format!("{path}.background"), value, &palette)) .transpose()?; let font_style = parsed_rule.font_style.map(|styles| { styles @@ -93,7 +67,7 @@ impl Theme { if foreground.is_none() && background.is_none() && font_style.is_none() { return Err(format!( - "{theme_key}.yml {path} must define at least one of foreground, background, or font_style" + "{path} must define at least one of foreground, background, or font_style" )); } @@ -107,7 +81,6 @@ impl Theme { } Ok(Self { - key: theme_key.to_string(), name: parsed_theme.name, settings, rules, @@ -116,7 +89,6 @@ impl Theme { } fn resolve_palette_key( - theme_key: &str, path: &str, color_ref: parsed::PaletteKey, palette: &BTreeMap, @@ -125,5 +97,5 @@ fn resolve_palette_key( palette .get(&key) .cloned() - .ok_or_else(|| format!("{theme_key}.yml {path} references unknown palette key: {key}")) + .ok_or_else(|| format!("{path} references unknown palette key: {key}")) } diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index 9d7ac704..5967c7b1 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -9,9 +9,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; use syntect::highlighting::ThemeSet; #[test] -fn parse_theme_source_resolves_palette_references() { +fn parse_theme_resolves_palette_references() { let theme = theme_compiler::parse_theme( - "test_theme", r##" name: Test Theme palette: @@ -38,9 +37,8 @@ rules: } #[test] -fn parse_theme_source_rejects_unknown_keys() { +fn parse_theme_rejects_unknown_keys() { let error = theme_compiler::parse_theme( - "bad_theme", r##" name: Bad Theme palette: @@ -64,9 +62,8 @@ rules: } #[test] -fn parse_theme_source_rejects_invalid_rule_color_reference() { +fn parse_theme_rejects_invalid_rule_color_reference() { let error = theme_compiler::parse_theme( - "bad_theme", r##" name: Bad Theme palette: @@ -88,9 +85,8 @@ rules: } #[test] -fn parse_theme_source_rejects_non_string_scope() { +fn parse_theme_rejects_non_string_scope() { let error = theme_compiler::parse_theme( - "bad_theme", r##" name: Bad Theme palette: @@ -115,7 +111,6 @@ rules: #[test] fn render_tmtheme_is_parseable_and_preserves_empty_font_style() { let theme = theme_compiler::parse_theme( - "test_theme", r##" name: Test Theme palette: @@ -188,7 +183,6 @@ rules: #[test] fn stage_pipeline_parses_validates_and_renders() { let parsed = theme_compiler::parse_parsed_theme( - "test_theme", r##" name: Test Theme palette: @@ -206,7 +200,7 @@ rules: ) .unwrap(); - let theme = theme_compiler::validate_theme("test_theme", parsed).unwrap(); + let theme = theme_compiler::validate_theme(parsed).unwrap(); let rendered = theme_compiler::render_tmtheme(&theme); let mut cursor = Cursor::new(rendered.into_bytes()); @@ -214,9 +208,8 @@ rules: } #[test] -fn parse_theme_source_rejects_literal_colors_outside_palette() { +fn parse_theme_rejects_literal_colors_outside_palette() { let error = theme_compiler::parse_theme( - "bad_theme", r##" name: Bad Theme palette: From 505aacce90aa5300f6d59218ee93e45513356b69 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 3 May 2026 11:54:05 -0400 Subject: [PATCH 22/26] Remove pipeline tests --- build/theme_compiler/mod.rs | 10 ---------- tests/theme_compiler.rs | 27 --------------------------- 2 files changed, 37 deletions(-) diff --git a/build/theme_compiler/mod.rs b/build/theme_compiler/mod.rs index 8559fe50..24e120dd 100644 --- a/build/theme_compiler/mod.rs +++ b/build/theme_compiler/mod.rs @@ -67,13 +67,3 @@ pub fn parse_theme(content: &str) -> Result { pub fn render_tmtheme(theme: &Theme) -> String { textmate::render(theme) } - -#[cfg(test)] -pub fn parse_parsed_theme(content: &str) -> Result { - parsed::parse(content) -} - -#[cfg(test)] -pub fn validate_theme(parsed_theme: parsed::Theme) -> Result { - validated::Theme::try_from_parsed(parsed_theme) -} diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index 5967c7b1..c7f3716f 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -180,33 +180,6 @@ rules: fs::remove_dir_all(base).unwrap(); } -#[test] -fn stage_pipeline_parses_validates_and_renders() { - let parsed = theme_compiler::parse_parsed_theme( - r##" -name: Test Theme -palette: - fg: "#112233" - bg: "#445566" - line: "#778899" -settings: - foreground: fg - background: bg - line_highlight: line -rules: - - scope: comment - foreground: fg -"##, - ) - .unwrap(); - - let theme = theme_compiler::validate_theme(parsed).unwrap(); - let rendered = theme_compiler::render_tmtheme(&theme); - let mut cursor = Cursor::new(rendered.into_bytes()); - - ThemeSet::load_from_reader(&mut cursor).unwrap(); -} - #[test] fn parse_theme_rejects_literal_colors_outside_palette() { let error = theme_compiler::parse_theme( From 529bb5f0817284719a49cf23a5588bc1321c75d3 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 3 May 2026 12:11:31 -0400 Subject: [PATCH 23/26] Update symbol jump test to open itself rather than the (now-missing) build file --- src/models/application/modes/symbol_jump.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/models/application/modes/symbol_jump.rs b/src/models/application/modes/symbol_jump.rs index a2c2605d..a1b2680a 100644 --- a/src/models/application/modes/symbol_jump.rs +++ b/src/models/application/modes/symbol_jump.rs @@ -214,12 +214,15 @@ mod tests { let config = SearchSelectConfig::default(); let mut mode = SymbolJumpMode::new(config.clone()).unwrap(); let mut app = Application::new(&[]).unwrap(); - app.workspace.open_buffer(&Path::new("build.rs")).unwrap(); + + // Open this file so the test can search for its own function symbol. + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(file!()); + app.workspace.open_buffer(&path).unwrap(); let token_set = app.workspace.current_buffer_tokens().unwrap(); // Do an initial reset to get the results populated mode.reset(&token_set, config.clone()).unwrap(); - mode.query().push_str("main"); + mode.query().push_str("reset_clears_query_mode_and_results"); mode.set_insert_mode(false); mode.search(); From 1823192c04d7802d6bd9e6bb444a5df302892e41 Mon Sep 17 00:00:00 2001 From: Jordan MacDonald Date: Sun, 3 May 2026 12:20:24 -0400 Subject: [PATCH 24/26] Add comment explaining skipping non-yaml files --- build/theme_compiler/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/theme_compiler/mod.rs b/build/theme_compiler/mod.rs index 24e120dd..5fd5658f 100644 --- a/build/theme_compiler/mod.rs +++ b/build/theme_compiler/mod.rs @@ -26,7 +26,7 @@ pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result Date: Sun, 3 May 2026 12:20:33 -0400 Subject: [PATCH 25/26] Remove theme sorting in compiler --- build/theme_compiler/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/theme_compiler/mod.rs b/build/theme_compiler/mod.rs index 5fd5658f..8be62436 100644 --- a/build/theme_compiler/mod.rs +++ b/build/theme_compiler/mod.rs @@ -46,8 +46,6 @@ pub fn compile_themes(source_dir: &Path, output_dir: &Path) -> Result Date: Sun, 3 May 2026 15:11:41 -0400 Subject: [PATCH 26/26] Improve theme compiler tests --- tests/theme_compiler.rs | 170 ++++++++++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 31 deletions(-) diff --git a/tests/theme_compiler.rs b/tests/theme_compiler.rs index c7f3716f..c9d54ba5 100644 --- a/tests/theme_compiler.rs +++ b/tests/theme_compiler.rs @@ -6,7 +6,13 @@ use std::io::Cursor; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; -use syntect::highlighting::ThemeSet; +use syntect::highlighting::{FontStyle, ThemeSet}; + +fn load_rendered_theme(theme: &theme_compiler::Theme) -> syntect::highlighting::Theme { + let rendered = theme_compiler::render_tmtheme(theme); + let mut cursor = Cursor::new(rendered.into_bytes()); + ThemeSet::load_from_reader(&mut cursor).unwrap() +} #[test] fn parse_theme_resolves_palette_references() { @@ -25,7 +31,6 @@ rules: - name: Comment scope: comment foreground: fg - font_style: [italic] "##, ) .unwrap(); @@ -84,6 +89,29 @@ rules: assert!(error.contains("unknown palette key: missing")); } +#[test] +fn parse_theme_rejects_literal_colors_outside_palette() { + let error = theme_compiler::parse_theme( + r##" +name: Bad Theme +palette: + bg: "#445566" + line: "#778899" +settings: + foreground: "#112233" + background: bg + line_highlight: line +rules: + - scope: comment + foreground: bg +"##, + ) + .unwrap_err(); + + assert!(error.contains("must reference a palette key")); + assert!(error.contains("literal colors belong in palette")); +} + #[test] fn parse_theme_rejects_non_string_scope() { let error = theme_compiler::parse_theme( @@ -109,7 +137,32 @@ rules: } #[test] -fn render_tmtheme_is_parseable_and_preserves_empty_font_style() { +fn parse_theme_preserves_font_style() { + let theme = theme_compiler::parse_theme( + r##" +name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" +settings: + foreground: fg + background: bg + line_highlight: line +rules: + - name: Comment + scope: comment + foreground: fg + font_style: [italic] +"##, + ) + .unwrap(); + + assert_eq!(theme.rules[0].font_style, Some(vec!["italic".to_string()])); +} + +#[test] +fn parse_theme_preserves_empty_font_style() { let theme = theme_compiler::parse_theme( r##" name: Test Theme @@ -129,12 +182,90 @@ rules: ) .unwrap(); - let rendered = theme_compiler::render_tmtheme(&theme); - assert!(rendered.contains("fontStyle")); - assert!(rendered.contains("")); + assert_eq!(theme.rules[0].font_style, Some(Vec::new())); +} - let mut cursor = Cursor::new(rendered.into_bytes()); - ThemeSet::load_from_reader(&mut cursor).unwrap(); +#[test] +fn render_tmtheme_is_parseable() { + let theme = theme_compiler::parse_theme( + r##" +name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" +settings: + foreground: fg + background: bg + line_highlight: line +rules: + - scope: comment + foreground: fg +"##, + ) + .unwrap(); + + load_rendered_theme(&theme); +} + +#[test] +fn render_tmtheme_preserves_empty_font_style() { + let theme = theme_compiler::parse_theme( + r##" +name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" +settings: + foreground: fg + background: bg + line_highlight: line +rules: + - scope: comment + foreground: fg + font_style: [] +"##, + ) + .unwrap(); + + let rendered_theme = load_rendered_theme(&theme); + + assert_eq!(rendered_theme.scopes.len(), 1); + assert_eq!( + rendered_theme.scopes[0].style.font_style, + Some(FontStyle::empty()) + ); +} + +#[test] +fn render_tmtheme_preserves_font_style() { + let theme = theme_compiler::parse_theme( + r##" +name: Test Theme +palette: + fg: "#112233" + bg: "#445566" + line: "#778899" +settings: + foreground: fg + background: bg + line_highlight: line +rules: + - scope: comment + foreground: fg + font_style: [italic, underline] +"##, + ) + .unwrap(); + + let rendered_theme = load_rendered_theme(&theme); + + assert_eq!(rendered_theme.scopes.len(), 1); + assert_eq!( + rendered_theme.scopes[0].style.font_style, + Some(FontStyle::ITALIC | FontStyle::UNDERLINE) + ); } #[test] @@ -179,26 +310,3 @@ rules: fs::remove_dir_all(base).unwrap(); } - -#[test] -fn parse_theme_rejects_literal_colors_outside_palette() { - let error = theme_compiler::parse_theme( - r##" -name: Bad Theme -palette: - bg: "#445566" - line: "#778899" -settings: - foreground: "#112233" - background: bg - line_highlight: line -rules: - - scope: comment - foreground: bg -"##, - ) - .unwrap_err(); - - assert!(error.contains("must reference a palette key")); - assert!(error.contains("literal colors belong in palette")); -}