From f6627f59e530a192607cda78616eda807119c26e Mon Sep 17 00:00:00 2001 From: Shafkath Shuhan Date: Fri, 10 Dec 2021 18:52:04 -0500 Subject: [PATCH] support project-specific language configuration --- helix-core/src/lib.rs | 8 ++ helix-term/src/application.rs | 134 ++++++++++++++++++++-------------- 2 files changed, 88 insertions(+), 54 deletions(-) diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 4ae044ccee1c4..d82f0741bd1de 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -92,6 +92,14 @@ pub fn config_dir() -> std::path::PathBuf { path } +/// Searches for the local `.helix` directory by searching for the root `.git` directory, +/// using the CWD if it can't find one. +pub fn local_config_dir() -> std::path::PathBuf { + let root = find_root(None) + .unwrap_or_else(|| std::env::current_dir().expect("unable to determine current directory")); + root.join(".helix") +} + pub fn cache_dir() -> std::path::PathBuf { // TODO: allow env var override let strategy = choose_base_strategy().expect("Unable to find the config directory!"); diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 90330751a1454..66ae20b565ca2 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -52,77 +52,110 @@ pub struct Application { impl Application { pub fn new(args: Args, mut config: Config) -> Result { use helix_view::editor::Action; - let mut compositor = Compositor::new()?; - let size = compositor.size(); - - let conf_dir = helix_core::config_dir(); - let theme_loader = - std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir())); - - // load default and user config, and merge both - let builtin_err_msg = - "Could not parse built-in languages.toml, something must be very wrong"; - let def_lang_conf: toml::Value = - toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg); - let def_syn_loader_conf: helix_core::syntax::Configuration = - def_lang_conf.clone().try_into().expect(builtin_err_msg); - let user_lang_conf = std::fs::read(conf_dir.join("languages.toml")) - .ok() - .map(|raw| toml::from_slice(&raw)); - let lang_conf = match user_lang_conf { - Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)), - Some(err @ Err(_)) => err, - None => Ok(def_lang_conf), - }; - - let theme = if let Some(theme) = &config.theme { - match theme_loader.load(theme) { - Ok(theme) => theme, - Err(e) => { - log::warn!("failed to load theme `{}` - {}", theme, e); - theme_loader.default() - } - } - } else { - theme_loader.default() - }; + // These configuration directories can contain `config.toml` and `languages.toml`. + // `local_config_dir` is a `.helix` folder within the projec directory. + let config_dir = helix_core::config_dir(); + let local_config_dir = helix_core::local_config_dir(); + + // Config override order: local -> global -> default. + // Read and parse the `languages.toml` files as TOML objects. + let default_lang_config: toml::Value = + toml::from_slice(include_bytes!("../../languages.toml")) + .expect("failed to read the default `languages.toml`"); + let lang_config = + { + let local_config = match std::fs::read(local_config_dir.join("languages.toml")) { + Ok(config) => toml::from_slice(&config) + .expect("failed to read the local `languages.toml`"), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + toml::Value::Table(toml::value::Table::default()) + } + Err(err) => return Err(Error::new(err)), + }; + let global_config = match std::fs::read(config_dir.join("languages.toml")) { + Ok(config) => toml::from_slice(&config) + .expect("failed to read the global `languages.toml`"), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + toml::Value::Table(toml::value::Table::default()) + } + Err(err) => return Err(Error::new(err)), + }; - let syn_loader_conf: helix_core::syntax::Configuration = lang_conf - .and_then(|conf| conf.try_into()) - .unwrap_or_else(|err| { + merge_toml_values( + default_lang_config.clone(), + merge_toml_values(global_config, local_config), + ) + }; + + // Convert previous `toml::Value`s into the config type. + let default_syn_loader_config: helix_core::syntax::Configuration = default_lang_config + .try_into() + .expect("failed to parse the default `languages.toml`"); + let syn_loader_config: helix_core::syntax::Configuration = + lang_config.try_into().unwrap_or_else(|err| { eprintln!("Bad language config: {}", err); eprintln!("Press to continue with default language config"); use std::io::Read; // This waits for an enter press. let _ = std::io::stdin().read(&mut []); - def_syn_loader_conf + default_syn_loader_config }); - let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); + let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_config)); + // Initialize rendering. + let theme_loader = + std::sync::Arc::new(theme::Loader::new(&config_dir, &helix_core::runtime_dir())); + let mut compositor = Compositor::new()?; let mut editor = Editor::new( - size, + compositor.size(), theme_loader.clone(), syn_loader.clone(), config.editor.clone(), ); + // Initialize the UI. let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys))); compositor.push(editor_view); + // Grab and load the user's default theme. + let theme = if let Some(theme) = &config.theme { + match theme_loader.load(theme) { + Ok(theme) => theme, + Err(e) => { + log::warn!("failed to load theme `{}` - {}", theme, e); + theme_loader.default() + } + } + } else { + theme_loader.default() + }; + editor.set_theme(theme); + + #[cfg(windows)] + let signals = futures_util::stream::empty(); + #[cfg(not(windows))] + let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?; + + // Handle CLI arguments. if args.load_tutor { let path = helix_core::runtime_dir().join("tutor.txt"); editor.open(path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; } else if !args.files.is_empty() { - let first = &args.files[0]; // we know it's not empty + // File paths passed as e.g. `hx foo.rs bar.rs` + // SAFETY: The file count is already known to be non-zero. + let first = &args.files[0]; + + // If the first argument is a directory, then only the file picker at that + // path is opened. Otherwise, all files are opened in separate vertical splits. if first.is_dir() { std::env::set_current_dir(&first)?; editor.new_file(Action::VerticalSplit); compositor.push(Box::new(ui::file_picker(".".into(), &config.editor))); } else { - let nr_of_files = args.files.len(); + let file_count = args.files.len(); editor.open(first.to_path_buf(), Action::VerticalSplit)?; for file in args.files { if file.is_dir() { @@ -133,9 +166,11 @@ impl Application { editor.open(file.to_path_buf(), Action::Load)?; } } - editor.set_status(format!("Loaded {} files.", nr_of_files)); + editor.set_status(format!("Loaded {} files.", file_count)); } } else if stdin().is_tty() { + // If no arguments are passed and there is no stdin piping, then only a scratch + // buffer is opened. editor.new_file(Action::VerticalSplit); } else if cfg!(target_os = "macos") { // On Linux and Windows, we allow the output of a command to be piped into the new buffer. @@ -148,14 +183,7 @@ impl Application { .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } - editor.set_theme(theme); - - #[cfg(windows)] - let signals = futures_util::stream::empty(); - #[cfg(not(windows))] - let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?; - - let app = Self { + Ok(Self { compositor, editor, @@ -167,9 +195,7 @@ impl Application { signals, jobs: Jobs::new(), lsp_progress: LspProgressMap::new(), - }; - - Ok(app) + }) } fn render(&mut self) {