aboutsummaryrefslogtreecommitdiff
path: root/helix-view/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view/src')
-rw-r--r--helix-view/src/clipboard.rs193
-rw-r--r--helix-view/src/document.rs106
-rw-r--r--helix-view/src/editor.rs78
-rw-r--r--helix-view/src/input.rs226
-rw-r--r--helix-view/src/lib.rs9
-rw-r--r--helix-view/src/theme.rs86
-rw-r--r--helix-view/src/tree.rs4
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)]