diff options
Diffstat (limited to 'helix-view/src')
-rw-r--r-- | helix-view/src/clipboard.rs | 193 | ||||
-rw-r--r-- | helix-view/src/document.rs | 106 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 78 | ||||
-rw-r--r-- | helix-view/src/input.rs | 226 | ||||
-rw-r--r-- | helix-view/src/lib.rs | 9 | ||||
-rw-r--r-- | helix-view/src/theme.rs | 86 | ||||
-rw-r--r-- | helix-view/src/tree.rs | 4 |
7 files changed, 388 insertions, 314 deletions
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs new file mode 100644 index 00000000..dcc44340 --- /dev/null +++ b/helix-view/src/clipboard.rs @@ -0,0 +1,193 @@ +// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152 + +use anyhow::Result; +use std::borrow::Cow; + +pub trait ClipboardProvider: std::fmt::Debug { + fn name(&self) -> Cow<str>; + fn get_contents(&self) -> Result<String>; + fn set_contents(&self, contents: String) -> Result<()>; +} + +macro_rules! command_provider { + (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{ + Box::new(provider::CommandProvider { + get_cmd: provider::CommandConfig { + prg: $get_prg, + args: &[ $( $get_arg ),* ], + }, + set_cmd: provider::CommandConfig { + prg: $set_prg, + args: &[ $( $set_arg ),* ], + }, + }) + }}; +} + +pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { + // TODO: support for user-defined provider, probably when we have plugin support by setting a + // variable? + + if exists("pbcopy") && exists("pbpaste") { + command_provider! { + paste => "pbpaste"; + copy => "pbcopy"; + } + } else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") { + command_provider! { + paste => "wl-paste", "--no-newline"; + copy => "wl-copy", "--foreground", "--type", "text/plain"; + } + } else if env_var_is_set("DISPLAY") && exists("xclip") { + command_provider! { + paste => "xclip", "-o", "-selection", "clipboard"; + copy => "xclip", "-i", "-selection", "clipboard"; + } + } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"]) + { + // FIXME: check performance of is_exit_success + command_provider! { + paste => "xsel", "-o", "-b"; + copy => "xsel", "--nodetach", "-i", "-b"; + } + } else if exists("lemonade") { + command_provider! { + paste => "lemonade", "paste"; + copy => "lemonade", "copy"; + } + } else if exists("doitclient") { + command_provider! { + paste => "doitclient", "wclip", "-r"; + copy => "doitclient", "wclip"; + } + } else if exists("win32yank.exe") { + // FIXME: does it work within WSL? + command_provider! { + paste => "win32yank.exe", "-o", "--lf"; + copy => "win32yank.exe", "-i", "--crlf"; + } + } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") { + command_provider! { + paste => "termux-clipboard-get"; + copy => "termux-clipboard-set"; + } + } else if env_var_is_set("TMUX") && exists("tmux") { + command_provider! { + paste => "tmux", "save-buffer", "-"; + copy => "tmux", "load-buffer", "-"; + } + } else { + Box::new(provider::NopProvider) + } +} + +fn exists(executable_name: &str) -> bool { + which::which(executable_name).is_ok() +} + +fn env_var_is_set(env_var_name: &str) -> bool { + std::env::var_os(env_var_name).is_some() +} + +fn is_exit_success(program: &str, args: &[&str]) -> bool { + std::process::Command::new(program) + .args(args) + .output() + .ok() + .and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized + .is_some() +} + +mod provider { + use super::ClipboardProvider; + use anyhow::{bail, Context as _, Result}; + use std::borrow::Cow; + + #[derive(Debug)] + pub struct NopProvider; + + impl ClipboardProvider for NopProvider { + fn name(&self) -> Cow<str> { + Cow::Borrowed("none") + } + + fn get_contents(&self) -> Result<String> { + Ok(String::new()) + } + + fn set_contents(&self, _: String) -> Result<()> { + Ok(()) + } + } + + #[derive(Debug)] + pub struct CommandConfig { + pub prg: &'static str, + pub args: &'static [&'static str], + } + + impl CommandConfig { + fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> { + use std::io::Write; + use std::process::{Command, Stdio}; + + let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null); + let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null); + + let mut child = Command::new(self.prg) + .args(self.args) + .stdin(stdin) + .stdout(stdout) + .stderr(Stdio::null()) + .spawn()?; + + if let Some(input) = input { + let mut stdin = child.stdin.take().context("stdin is missing")?; + stdin + .write_all(input.as_bytes()) + .context("couldn't write in stdin")?; + } + + // TODO: add timer? + let output = child.wait_with_output()?; + + if !output.status.success() { + bail!("clipboard provider {} failed", self.prg); + } + + if pipe_output { + Ok(Some(String::from_utf8(output.stdout)?)) + } else { + Ok(None) + } + } + } + + #[derive(Debug)] + pub struct CommandProvider { + pub get_cmd: CommandConfig, + pub set_cmd: CommandConfig, + } + + impl ClipboardProvider for CommandProvider { + fn name(&self) -> Cow<str> { + if self.get_cmd.prg != self.set_cmd.prg { + Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg)) + } else { + Cow::Borrowed(self.get_cmd.prg) + } + } + + fn get_contents(&self) -> Result<String> { + let output = self + .get_cmd + .execute(None, true)? + .context("output is missing")?; + Ok(output) + } + + fn set_contents(&self, value: String) -> Result<()> { + self.set_cmd.execute(Some(&value), false).map(|_| ()) + } + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3e38c24d..9326fb79 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,7 +1,5 @@ use anyhow::{anyhow, Context, Error}; -use serde::de::{self, Deserialize, Deserializer}; use std::cell::Cell; -use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::path::{Component, Path, PathBuf}; @@ -12,12 +10,14 @@ use helix_core::{ auto_detect_line_ending, chars::{char_is_line_ending, char_is_whitespace}, history::History, - syntax::{LanguageConfiguration, LOADER}, + syntax::{self, LanguageConfiguration}, ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction, DEFAULT_LINE_ENDING, }; -use crate::{DocumentId, ViewId}; +use crate::{DocumentId, Theme, ViewId}; + +use std::collections::HashMap; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { @@ -26,40 +26,6 @@ pub enum Mode { Insert, } -impl Display for Mode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Mode::Normal => f.write_str("normal"), - Mode::Select => f.write_str("select"), - Mode::Insert => f.write_str("insert"), - } - } -} - -impl FromStr for Mode { - type Err = Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "normal" => Ok(Mode::Normal), - "select" => Ok(Mode::Select), - "insert" => Ok(Mode::Insert), - _ => Err(anyhow!("Invalid mode '{}'", s)), - } - } -} - -// toml deserializer doesn't seem to recognize string as enum -impl<'de> Deserialize<'de> for Mode { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - s.parse().map_err(de::Error::custom) - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum IndentStyle { Tabs, @@ -127,6 +93,29 @@ impl fmt::Debug for Document { } } +impl Display for Mode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Mode::Normal => f.write_str("normal"), + Mode::Select => f.write_str("select"), + Mode::Insert => f.write_str("insert"), + } + } +} + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "normal" => Ok(Mode::Normal), + "select" => Ok(Mode::Select), + "insert" => Ok(Mode::Insert), + _ => Err(anyhow!("Invalid mode '{}'", s)), + } + } +} + /// Like std::mem::replace() except it allows the replacement value to be mapped from the /// original value. fn take_with<T, F>(mut_ref: &mut T, closure: F) @@ -181,7 +170,7 @@ pub fn fold_home_dir(path: &Path) -> PathBuf { /// [`std::fs::canonicalize`] can be hard to use correctly, since it can often /// fail, or on Windows returns annoying device paths. This is a problem Cargo /// needs to improve on. -/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81 +/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81> pub fn normalize_path(path: &Path) -> PathBuf { let path = expand_tilde(path); let mut components = path.components().peekable(); @@ -253,7 +242,11 @@ impl Document { } // TODO: async fn? - pub fn load(path: PathBuf) -> Result<Self, Error> { + pub fn load( + path: PathBuf, + theme: Option<&Theme>, + config_loader: Option<&syntax::Loader>, + ) -> Result<Self, Error> { use std::{fs::File, io::BufReader}; let mut doc = if !path.exists() { @@ -277,6 +270,10 @@ impl Document { doc.detect_indent_style(); doc.set_line_ending(line_ending); + if let Some(loader) = config_loader { + doc.detect_language(theme, loader); + } + Ok(doc) } @@ -351,12 +348,10 @@ impl Document { } } - fn detect_language(&mut self) { - if let Some(path) = self.path() { - let loader = LOADER.get().unwrap(); - let language_config = loader.language_config_for_file_name(path); - let scopes = loader.scopes(); - self.set_language(language_config, scopes); + pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { + if let Some(path) = &self.path { + let language_config = config_loader.language_config_for_file_name(path); + self.set_language(theme, language_config); } } @@ -493,18 +488,16 @@ impl Document { // and error out when document is saved self.path = Some(path); - // try detecting the language based on filepath - self.detect_language(); - Ok(()) } pub fn set_language( &mut self, + theme: Option<&Theme>, language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>, - scopes: &[String], ) { if let Some(language_config) = language_config { + let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]); if let Some(highlight_config) = language_config.highlight_config(scopes) { let syntax = Syntax::new(&self.text, highlight_config); self.syntax = Some(syntax); @@ -518,12 +511,15 @@ impl Document { }; } - pub fn set_language2(&mut self, scope: &str) { - let loader = LOADER.get().unwrap(); - let language_config = loader.language_config_for_scope(scope); - let scopes = loader.scopes(); + pub fn set_language2( + &mut self, + scope: &str, + theme: Option<&Theme>, + config_loader: Arc<syntax::Loader>, + ) { + let language_config = config_loader.language_config_for_scope(scope); - self.set_language(language_config, scopes); + self.set_language(theme, language_config); } pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index fb2eb36d..839bcdcd 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,10 +1,15 @@ -use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId}; +use crate::clipboard::{get_clipboard_provider, ClipboardProvider}; +use crate::{ + theme::{self, Theme}, + tree::Tree, + Document, DocumentId, RegisterSelection, View, ViewId, +}; +use helix_core::syntax; use tui::layout::Rect; use tui::terminal::CursorKind; use futures_util::future; -use std::path::PathBuf; -use std::time::Duration; +use std::{path::PathBuf, sync::Arc, time::Duration}; use slotmap::SlotMap; @@ -23,6 +28,10 @@ pub struct Editor { pub registers: Registers, pub theme: Theme, pub language_servers: helix_lsp::Registry, + pub clipboard_provider: Box<dyn ClipboardProvider>, + + pub syn_loader: Arc<syntax::Loader>, + pub theme_loader: Arc<theme::Loader>, pub status_msg: Option<(String, Severity)>, } @@ -35,27 +44,11 @@ pub enum Action { } impl Editor { - pub fn new(mut area: tui::layout::Rect) -> Self { - use helix_core::config_dir; - let config = std::fs::read(config_dir().join("theme.toml")); - // load $HOME/.config/helix/theme.toml, fallback to default config - let toml = config - .as_deref() - .unwrap_or(include_bytes!("../../theme.toml")); - let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml"); - - // initialize language registry - use helix_core::syntax::{Loader, LOADER}; - - // load $HOME/.config/helix/languages.toml, fallback to default config - let config = std::fs::read(helix_core::config_dir().join("languages.toml")); - let toml = config - .as_deref() - .unwrap_or(include_bytes!("../../languages.toml")); - - let config = toml::from_slice(toml).expect("Could not parse languages.toml"); - LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec())); - + pub fn new( + mut area: tui::layout::Rect, + themes: Arc<theme::Loader>, + config_loader: Arc<syntax::Loader>, + ) -> Self { let language_servers = helix_lsp::Registry::new(); // HAXX: offset the render area height by 1 to account for prompt/commandline @@ -66,9 +59,12 @@ impl Editor { documents: SlotMap::with_key(), count: None, selected_register: RegisterSelection::default(), - theme, + theme: themes.default(), language_servers, + syn_loader: config_loader, + theme_loader: themes, registers: Registers::default(), + clipboard_provider: get_clipboard_provider(), status_msg: None, } } @@ -85,6 +81,32 @@ impl Editor { self.status_msg = Some((error, Severity::Error)); } + pub fn set_theme(&mut self, theme: Theme) { + let scopes = theme.scopes(); + for config in self + .syn_loader + .language_configs_iter() + .filter(|cfg| cfg.is_highlight_initialized()) + { + config.reconfigure(scopes); + } + + self.theme = theme; + self._refresh(); + } + + pub fn set_theme_from_name(&mut self, theme: &str) { + let theme = match self.theme_loader.load(theme.as_ref()) { + Ok(theme) => theme, + Err(e) => { + log::warn!("failed setting theme `{}` - {}", theme, e); + return; + } + }; + + self.set_theme(theme); + } + fn _refresh(&mut self) { for (view, _) in self.tree.views_mut() { let doc = &self.documents[view.doc]; @@ -168,7 +190,7 @@ impl Editor { let id = if let Some(id) = id { id } else { - let mut doc = Document::load(path)?; + let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?; // try to find a language server based on the language name let language_server = doc @@ -254,6 +276,10 @@ impl Editor { self.documents.iter().map(|(_id, doc)| doc) } + pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> { + self.documents.iter_mut().map(|(_id, doc)| doc) + } + // pub fn current_document(&self) -> Document { // let id = self.view().doc; // let doc = &mut editor.documents[id]; diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs deleted file mode 100644 index ab417819..00000000 --- a/helix-view/src/input.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Input event handling, currently backed by crossterm. -use anyhow::{anyhow, Error}; -use crossterm::event; -use serde::de::{self, Deserialize, Deserializer}; -use std::fmt; - -pub use crossterm::event::{KeyCode, KeyModifiers}; - -/// Represents a key event. -// We use a newtype here because we want to customize Deserialize and Display. -#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)] -pub struct KeyEvent { - pub code: KeyCode, - pub modifiers: KeyModifiers, -} - -impl fmt::Display for KeyEvent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "{}{}{}", - if self.modifiers.contains(KeyModifiers::SHIFT) { - "S-" - } else { - "" - }, - if self.modifiers.contains(KeyModifiers::ALT) { - "A-" - } else { - "" - }, - if self.modifiers.contains(KeyModifiers::CONTROL) { - "C-" - } else { - "" - }, - ))?; - match self.code { - KeyCode::Backspace => f.write_str("backspace")?, - KeyCode::Enter => f.write_str("ret")?, - KeyCode::Left => f.write_str("left")?, - KeyCode::Right => f.write_str("right")?, - KeyCode::Up => f.write_str("up")?, - KeyCode::Down => f.write_str("down")?, - KeyCode::Home => f.write_str("home")?, - KeyCode::End => f.write_str("end")?, - KeyCode::PageUp => f.write_str("pageup")?, - KeyCode::PageDown => f.write_str("pagedown")?, - KeyCode::Tab => f.write_str("tab")?, - KeyCode::BackTab => f.write_str("backtab")?, - KeyCode::Delete => f.write_str("del")?, - KeyCode::Insert => f.write_str("ins")?, - KeyCode::Null => f.write_str("null")?, - KeyCode::Esc => f.write_str("esc")?, - KeyCode::Char('<') => f.write_str("lt")?, - KeyCode::Char('>') => f.write_str("gt")?, - KeyCode::Char('+') => f.write_str("plus")?, - KeyCode::Char('-') => f.write_str("minus")?, - KeyCode::Char(';') => f.write_str("semicolon")?, - KeyCode::Char('%') => f.write_str("percent")?, - KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?, - KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, - }; - Ok(()) - } -} - -impl std::str::FromStr for KeyEvent { - type Err = Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - let mut tokens: Vec<_> = s.split('-').collect(); - let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? { - "backspace" => KeyCode::Backspace, - "space" => KeyCode::Char(' '), - "ret" => KeyCode::Enter, - "lt" => KeyCode::Char('<'), - "gt" => KeyCode::Char('>'), - "plus" => KeyCode::Char('+'), - "minus" => KeyCode::Char('-'), - "semicolon" => KeyCode::Char(';'), - "percent" => KeyCode::Char('%'), - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "up" => KeyCode::Down, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "tab" => KeyCode::Tab, - "backtab" => KeyCode::BackTab, - "del" => KeyCode::Delete, - "ins" => KeyCode::Insert, - "null" => KeyCode::Null, - "esc" => KeyCode::Esc, - single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), - function if function.len() > 1 && function.starts_with('F') => { - let function: String = function.chars().skip(1).collect(); - let function = str::parse::<u8>(&function)?; - (function > 0 && function < 13) - .then(|| KeyCode::F(function)) - .ok_or_else(|| anyhow!("Invalid function key '{}'", function))? - } - invalid => return Err(anyhow!("Invalid key code '{}'", invalid)), - }; - - let mut modifiers = KeyModifiers::empty(); - for token in tokens { - let flag = match token { - "S" => KeyModifiers::SHIFT, - "A" => KeyModifiers::ALT, - "C" => KeyModifiers::CONTROL, - _ => return Err(anyhow!("Invalid key modifier '{}-'", token)), - }; - - if modifiers.contains(flag) { - return Err(anyhow!("Repeated key modifier '{}-'", token)); - } - modifiers.insert(flag); - } - - Ok(KeyEvent { code, modifiers }) - } -} - -impl<'de> Deserialize<'de> for KeyEvent { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - s.parse().map_err(de::Error::custom) - } -} - -impl From<event::KeyEvent> for KeyEvent { - fn from(event::KeyEvent { code, modifiers }: event::KeyEvent) -> KeyEvent { - KeyEvent { code, modifiers } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn parsing_unmodified_keys() { - assert_eq!( - str::parse::<KeyEvent>("backspace").unwrap(), - KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE - } - ); - - assert_eq!( - str::parse::<KeyEvent>("left").unwrap(), - KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE - } - ); - - assert_eq!( - str::parse::<KeyEvent>(",").unwrap(), - KeyEvent { - code: KeyCode::Char(','), - modifiers: KeyModifiers::NONE - } - ); - - assert_eq!( - str::parse::<KeyEvent>("w").unwrap(), - KeyEvent { - code: KeyCode::Char('w'), - modifiers: KeyModifiers::NONE - } - ); - - assert_eq!( - str::parse::<KeyEvent>("F12").unwrap(), - KeyEvent { - code: KeyCode::F(12), - modifiers: KeyModifiers::NONE - } - ); - } - - #[test] - fn parsing_modified_keys() { - assert_eq!( - str::parse::<KeyEvent>("S-minus").unwrap(), - KeyEvent { - code: KeyCode::Char('-'), - modifiers: KeyModifiers::SHIFT - } - ); - - assert_eq!( - str::parse::<KeyEvent>("C-A-S-F12").unwrap(), - KeyEvent { - code: KeyCode::F(12), - modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT - } - ); - - assert_eq!( - str::parse::<KeyEvent>("S-C-2").unwrap(), - KeyEvent { - code: KeyCode::Char('2'), - modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL - } - ); - } - - #[test] - fn parsing_nonsensical_keys_fails() { - assert!(str::parse::<KeyEvent>("F13").is_err()); - assert!(str::parse::<KeyEvent>("F0").is_err()); - assert!(str::parse::<KeyEvent>("aaa").is_err()); - assert!(str::parse::<KeyEvent>("S-S-a").is_err()); - assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err()); - assert!(str::parse::<KeyEvent>("FU").is_err()); - assert!(str::parse::<KeyEvent>("123").is_err()); - assert!(str::parse::<KeyEvent>("S--").is_err()); - } -} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 8b635700..17f415fc 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -1,18 +1,17 @@ #[macro_use] pub mod macros; +pub mod clipboard; pub mod document; pub mod editor; -pub mod input; pub mod register_selection; pub mod theme; pub mod tree; pub mod view; -slotmap::new_key_type! { - pub struct DocumentId; - pub struct ViewId; -} +use slotmap::new_key_type; +new_key_type! { pub struct DocumentId; } +new_key_type! { pub struct ViewId; } pub use document::Document; pub use editor::Editor; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 51a21421..66b91294 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,6 +1,11 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; +use anyhow::Context; use log::warn; +use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; use toml::Value; @@ -86,7 +91,84 @@ pub use tui::style::{Color, Modifier, Style}; // } /// Color theme for syntax highlighting. -#[derive(Debug)] + +pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { + toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") +}); + +#[derive(Clone, Debug)] +pub struct Loader { + user_dir: PathBuf, + default_dir: PathBuf, +} +impl Loader { + /// Creates a new loader that can load themes from two directories. + pub fn new<P: AsRef<Path>>(user_dir: P, default_dir: P) -> Self { + Self { + user_dir: user_dir.as_ref().join("themes"), + default_dir: default_dir.as_ref().join("themes"), + } + } + + /// Loads a theme first looking in the `user_dir` then in `default_dir` + pub fn load(&self, name: &str) -> Result<Theme, anyhow::Error> { + if name == "default" { + return Ok(self.default()); + } + let filename = format!("{}.toml", name); + + let user_path = self.user_dir.join(&filename); + let path = if user_path.exists() { + user_path + } else { + self.default_dir.join(filename) + }; + + let data = std::fs::read(&path)?; + toml::from_slice(data.as_slice()).context("Failed to deserialize theme") + } + + pub fn read_names(path: &Path) -> Vec<String> { + std::fs::read_dir(path) + .map(|entries| { + entries + .filter_map(|entry| { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext != "toml" { + return None; + } + return Some( + entry + .file_name() + .to_string_lossy() + .trim_end_matches(".toml") + .to_owned(), + ); + } + } + None + }) + .collect() + }) + .unwrap_or_default() + } + + /// Lists all theme names available in default and user directory + pub fn names(&self) -> Vec<String> { + let mut names = Self::read_names(&self.user_dir); + names.extend(Self::read_names(&self.default_dir)); + names + } + + /// Returns the default theme + pub fn default(&self) -> Theme { + DEFAULT_THEME.clone() + } +} + +#[derive(Clone, Debug)] pub struct Theme { scopes: Vec<String>, styles: HashMap<String, Style>, diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index a0c466d9..f7d6c1f2 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -434,6 +434,10 @@ impl Tree { self.focus = key; } } + + pub fn area(&self) -> Rect { + self.area + } } #[derive(Debug)] |