diff --git a/Cargo.lock b/Cargo.lock index 09cd4a2f2..1b2406830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.1.31" @@ -318,6 +327,21 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2570,6 +2594,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" version = "1.0.18" @@ -2793,7 +2823,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "steel-core" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#cf7a3df2c1cf0b0e1df53e127512f9fbde48476a" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" dependencies = [ "abi_stable", "anyhow", @@ -2802,6 +2832,7 @@ dependencies = [ "bincode", "chrono", "codespan-reporting", + "compact_str", "crossbeam", "dirs", "futures-executor", @@ -2842,7 +2873,7 @@ dependencies = [ [[package]] name = "steel-derive" version = "0.5.0" -source = "git+https://github.com/mattwparas/steel.git#cf7a3df2c1cf0b0e1df53e127512f9fbde48476a" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" dependencies = [ "proc-macro2", "quote", @@ -2852,7 +2883,7 @@ dependencies = [ [[package]] name = "steel-doc" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#cf7a3df2c1cf0b0e1df53e127512f9fbde48476a" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" dependencies = [ "steel-core", ] @@ -2860,7 +2891,7 @@ dependencies = [ [[package]] name = "steel-gen" version = "0.2.0" -source = "git+https://github.com/mattwparas/steel.git#cf7a3df2c1cf0b0e1df53e127512f9fbde48476a" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" dependencies = [ "codegen", "serde", @@ -2870,8 +2901,9 @@ dependencies = [ [[package]] name = "steel-parser" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#cf7a3df2c1cf0b0e1df53e127512f9fbde48476a" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" dependencies = [ + "compact_str", "fxhash", "lasso", "num", diff --git a/helix-term/src/commands/engine/components.rs b/helix-term/src/commands/engine/components.rs index 0b7e642aa..fb11f16a9 100644 --- a/helix-term/src/commands/engine/components.rs +++ b/helix-term/src/commands/engine/components.rs @@ -5,7 +5,7 @@ use helix_view::{ graphics::{Color, CursorKind, Rect, UnderlineStyle}, input::{Event, KeyEvent, MouseButton, MouseEvent}, keyboard::{KeyCode, KeyModifiers}, - theme::Style, + theme::{Modifier, Style}, Editor, }; use steel::{ @@ -258,12 +258,47 @@ pub fn helix_component_module() -> BuiltInModule { .register_value("Color/LightCyan", Color::LightCyan.into_steelval().unwrap()) .register_value("Color/LightGray", Color::LightGray.into_steelval().unwrap()) .register_fn("Color/rgb", Color::Rgb) + .register_fn("Color-red", Color::red) + .register_fn("Color-green", Color::green) + .register_fn("Color-blue", Color::blue) .register_fn("Color/Indexed", Color::Indexed) .register_fn("set-style-fg!", |style: &mut Style, color: Color| { style.fg = Some(color); }) .register_fn("style-fg", Style::fg) .register_fn("style-bg", Style::bg) + .register_fn("style-with-italics", |style: &Style| { + let patch = Style::default().add_modifier(Modifier::ITALIC); + style.patch(patch) + }) + .register_fn("style-with-bold", |style: Style| { + let patch = Style::default().add_modifier(Modifier::BOLD); + style.patch(patch) + }) + .register_fn("style-with-dim", |style: &Style| { + let patch = Style::default().add_modifier(Modifier::DIM); + style.patch(patch) + }) + .register_fn("style-with-slow-blink", |style: Style| { + let patch = Style::default().add_modifier(Modifier::SLOW_BLINK); + style.patch(patch) + }) + .register_fn("style-with-rapid-blink", |style: Style| { + let patch = Style::default().add_modifier(Modifier::RAPID_BLINK); + style.patch(patch) + }) + .register_fn("style-with-reversed", |style: Style| { + let patch = Style::default().add_modifier(Modifier::REVERSED); + style.patch(patch) + }) + .register_fn("style-with-hidden", |style: Style| { + let patch = Style::default().add_modifier(Modifier::HIDDEN); + style.patch(patch) + }) + .register_fn("style-with-crossed-out", |style: Style| { + let patch = Style::default().add_modifier(Modifier::CROSSED_OUT); + style.patch(patch) + }) .register_fn("style->fg", |style: &Style| style.fg) .register_fn("style->bg", |style: &Style| style.bg) .register_fn("set-style-bg!", |style: &mut Style, color: Color| { diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs index ae0300b86..ec13f1b97 100644 --- a/helix-term/src/commands/engine/steel.rs +++ b/helix-term/src/commands/engine/steel.rs @@ -19,7 +19,8 @@ use helix_view::{ }, extension::document_id_to_usize, input::KeyEvent, - DocumentId, Editor, ViewId, + theme::Color, + DocumentId, Editor, Theme, ViewId, }; use once_cell::sync::{Lazy, OnceCell}; use steel::{ @@ -31,6 +32,7 @@ use steel::{ steelerr, SteelErr, SteelVal, }; +use std::sync::Arc; use std::{ borrow::Cow, collections::HashMap, @@ -39,7 +41,6 @@ use std::{ sync::{atomic::AtomicBool, Mutex, MutexGuard}, time::Duration, }; -use std::{io::BufWriter, sync::Arc}; use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule}; @@ -850,6 +851,67 @@ fn load_configuration_api(engine: &mut Engine, generate_sources: bool) { engine.register_module(module); } +fn languages_api(engine: &mut Engine, generate_sources: bool) { + // TODO: Just look at the `cx.editor.syn_loader` for how to + // manipulate the languages bindings + todo!() +} + +// TODO: +// This isn't the best API since it pretty much requires deserializing +// the whole theme model each time. While its not _horrible_, it is +// certainly not as efficient as it could be. If we could just edit +// the loaded theme in memory already, then it would be a bit nicer. +fn load_theme_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/themes"); + module + .register_fn("hashmap->theme", theme_from_json_string) + .register_fn("add-theme!", add_theme) + .register_fn("theme-style", get_style) + .register_fn("theme-set-style!", set_style) + .register_fn("string->color", string_to_color); + + if generate_sources { + configure_lsp_builtins("themes", &module); + } + + engine.register_module(module); +} + +#[derive(Clone)] +struct SteelTheme(Theme); +impl Custom for SteelTheme {} + +fn theme_from_json_string(name: String, value: SteelVal) -> Result { + // TODO: Really don't love this at all. The deserialization should be a bit more elegant + let json_value = serde_json::Value::try_from(value)?; + let value: toml::Value = serde_json::from_str(&serde_json::to_string(&json_value)?)?; + + let (mut theme, _) = Theme::from_toml(value); + theme.set_name(name); + Ok(SteelTheme(theme)) +} + +// Mutate the theme? +fn add_theme(cx: &mut Context, theme: SteelTheme) { + cx.editor + .user_defined_themes + .insert(theme.0.name().to_owned(), theme.0); +} + +fn get_style(theme: &SteelTheme, name: SteelString) -> helix_view::theme::Style { + theme.0.get(name.as_str()).clone() +} + +fn set_style(theme: &mut SteelTheme, name: String, style: helix_view::theme::Style) { + theme.0.set(name, style) +} + +fn string_to_color(string: SteelString) -> Result { + // TODO: Don't expose this directly + helix_view::theme::ThemePalette::string_to_rgb(string.as_str()).map_err(anyhow::Error::msg) +} + fn load_editor_api(engine: &mut Engine, generate_sources: bool) { let mut module = BuiltInModule::new("helix/core/editor"); @@ -2192,6 +2254,7 @@ pub fn helix_runtime_search_path() -> PathBuf { pub fn configure_builtin_sources(engine: &mut Engine, generate_sources: bool) { load_editor_api(engine, generate_sources); + load_theme_api(engine, generate_sources); load_configuration_api(engine, generate_sources); load_typed_commands(engine, generate_sources); load_static_commands(engine, generate_sources); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f60029b3f..f96b9e8c5 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -888,21 +888,42 @@ fn theme( // Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty. cx.editor.unset_theme_preview(); } else if let Some(theme_name) = args.first() { - if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + // if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + // if !(true_color || theme.is_16_color()) { + // bail!("Unsupported theme: theme requires true color support"); + // } + // cx.editor.set_theme_preview(theme); + // }; + + if let Ok(theme) = cx.editor.theme_loader.load(theme_name).or_else(|_| { + cx.editor + .user_defined_themes + .get(theme_name.as_ref()) + .ok_or_else(|| anyhow::anyhow!("Could not load theme")) + .cloned() + }) { if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } cx.editor.set_theme_preview(theme); - }; + } }; } PromptEvent::Validate => { if let Some(theme_name) = args.first() { - let theme = cx - .editor - .theme_loader - .load(theme_name) - .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; + let theme = cx.editor.theme_loader.load(theme_name).or_else(|_| { + cx.editor + .user_defined_themes + .get(theme_name.as_ref()) + .ok_or_else(|| anyhow::anyhow!("Could not load theme")) + .cloned() + })?; + + // let theme = cx + // .editor + // .theme_loader + // .load(theme_name) + // .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index dd5ba67e2..a49c03751 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -295,7 +295,7 @@ pub mod completers { .collect() } - pub fn theme(_editor: &Editor, input: &str) -> Vec { + pub fn theme(editor: &Editor, input: &str) -> Vec { let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); for rt_dir in helix_loader::runtime_dirs() { names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); @@ -303,6 +303,10 @@ pub mod completers { names.push("default".into()); names.push("base16_default".into()); + + // Include any user defined themes as well + names.extend(editor.user_defined_themes.keys().map(|x| x.into())); + names.sort(); names.dedup(); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 0191a0f97..b63c1589a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1083,6 +1083,7 @@ pub struct Editor { pub cursor_cache: CursorCache, pub editor_clipping: ClippingConfiguration, + pub user_defined_themes: HashMap, } #[derive(Default)] @@ -1210,6 +1211,7 @@ impl Editor { mouse_down_range: None, cursor_cache: CursorCache::default(), editor_clipping: ClippingConfiguration::default(), + user_defined_themes: Default::default(), } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index a26823b97..1c47a9a25 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -292,6 +292,32 @@ impl From for crossterm::style::Color { } } +impl Color { + pub fn red(&self) -> Option { + if let Self::Rgb(r, _, _) = self { + Some(*r) + } else { + None + } + } + + pub fn green(&self) -> Option { + if let Self::Rgb(_, g, _) = self { + Some(*g) + } else { + None + } + } + + pub fn blue(&self) -> Option { + if let Self::Rgb(_, _, b) = self { + Some(*b) + } else { + None + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnderlineStyle { Reset, diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 9dc326444..e25e5cfc8 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -9,6 +9,7 @@ use helix_core::hashmap; use helix_loader::merge_toml_values; use log::warn; use once_cell::sync::Lazy; +use rustix::path::Arg; use serde::{Deserialize, Deserializer}; use toml::{map::Map, Value}; @@ -307,10 +308,24 @@ impl Theme { &self.name } + pub fn set_name(&mut self, name: String) { + self.name = name; + } + pub fn get(&self, scope: &str) -> Style { self.try_get(scope).unwrap_or_default() } + pub fn set(&mut self, scope: String, style: Style) { + self.styles.insert(scope.to_string(), style); + + for (name, highlights) in self.scopes.iter().zip(self.highlights.iter_mut()) { + if *name == scope { + *highlights = style; + } + } + } + /// Get the style of a scope, falling back to dot separated broader /// scopes. For example if `ui.text.focus` is not defined in the theme, /// `ui.text` is tried and then `ui` is tried. @@ -356,7 +371,7 @@ impl Theme { }) } - fn from_toml(value: Value) -> (Self, Vec) { + pub fn from_toml(value: Value) -> (Self, Vec) { if let Value::Table(table) = value { Theme::from_keys(table) } else { @@ -378,7 +393,7 @@ impl Theme { } } -struct ThemePalette { +pub struct ThemePalette { palette: HashMap, }