diff --git a/book/src/configuration.md b/book/src/configuration.md index 7514a3d0fcc3e..355e20566c473 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -28,6 +28,9 @@ hidden = false You may also specify a file to use for configuration with the `-c` or `--config` CLI argument: `hx -c path/to/custom-config.toml`. +Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository. +Its settings will be merged with the configuration directory `config.toml` and the built-in configuration. + It is also possible to trigger configuration file reloading by sending the `USR1` signal to the helix process, e.g. via `pkill -USR1 hx`. This is only supported on unix operating systems. @@ -57,6 +60,7 @@ on unix operating systems. | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | +| `workspace-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | ### `[editor.statusline]` Section diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 0ff501a33fdb3..c9e5fe56e2091 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -68,6 +68,7 @@ | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:config-reload` | Refresh user config. | | `:config-open` | Open the user config.toml file. | +| `:config-open-workspace` | Open the workspace config.toml file. | | `:log-open` | Open the helix log file. | | `:insert-output` | Run shell command, inserting output before each selection. | | `:append-output` | Run shell command, appending output after each selection. | diff --git a/book/src/languages.md b/book/src/languages.md index 0646b9af95809..a2b3e4507abeb 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -62,6 +62,7 @@ These configuration keys are available: | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `max-line-length` | Maximum line length. Used for the `:reflow` command and soft-wrapping | +| `workspace-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | `` | ### File-type detection and the `file-types` key diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index e3f862a6054c4..9ac0efdc4bf40 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -36,55 +36,12 @@ pub mod unicode { pub use unicode_width as width; } +pub use helix_loader::find_root; + pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option { line.chars().position(|ch| !ch.is_whitespace()) } -/// Find project root. -/// -/// Order of detection: -/// * Top-most folder containing a root marker in current git repository -/// * Git repository root if no marker detected -/// * Top-most folder containing a root marker if not git repository detected -/// * Current working directory as fallback -pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::PathBuf { - let current_dir = std::env::current_dir().expect("unable to determine current directory"); - - let root = match root { - Some(root) => { - let root = std::path::Path::new(root); - if root.is_absolute() { - root.to_path_buf() - } else { - current_dir.join(root) - } - } - None => current_dir.clone(), - }; - - let mut top_marker = None; - for ancestor in root.ancestors() { - if root_markers - .iter() - .any(|marker| ancestor.join(marker).exists()) - { - top_marker = Some(ancestor); - } - - if ancestor.join(".git").exists() { - // Top marker is repo root if not root marker was detected yet - if top_marker.is_none() { - top_marker = Some(ancestor); - } - // Don't go higher than repo if we're in one - break; - } - } - - // Return the found top marker or the current_dir as fallback - top_marker.map_or(current_dir, |a| a.to_path_buf()) -} - pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice}; // pub use tendril::StrTendril as Tendril; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ca4da3dcd56dc..8fa546cad3a37 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -20,7 +20,7 @@ use std::{ fmt, hash::{Hash, Hasher}, mem::{replace, transmute}, - path::Path, + path::{Path, PathBuf}, str::FromStr, sync::Arc, }; @@ -126,6 +126,9 @@ pub struct LanguageConfiguration { pub auto_pairs: Option, pub rulers: Option>, // if set, override editor's rulers + + /// Hardcoded LSP root direcotries relative to the workspace root like `examples` or `tools/fuzz` + pub workspace_roots: Option>, } #[derive(Debug, PartialEq, Eq, Hash)] diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 0f329d217c0f6..28c5df0625f80 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -9,37 +9,38 @@ pub fn default_lang_config() -> toml::Value { /// User configured languages.toml file, merged with the default config. pub fn user_lang_config() -> Result { - let config = crate::local_config_dirs() - .into_iter() - .chain([crate::config_dir()].into_iter()) - .map(|path| path.join("languages.toml")) - .filter_map(|file| { - std::fs::read_to_string(file) - .map(|config| toml::from_str(&config)) - .ok() - }) - .collect::, _>>()? - .into_iter() - .chain([default_lang_config()].into_iter()) - .fold(toml::Value::Table(toml::value::Table::default()), |a, b| { - // combines for example - // b: - // [[language]] - // name = "toml" - // language-server = { command = "taplo", args = ["lsp", "stdio"] } - // - // a: - // [[language]] - // language-server = { command = "/usr/bin/taplo" } - // - // into: - // [[language]] - // name = "toml" - // language-server = { command = "/usr/bin/taplo" } - // - // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values - crate::merge_toml_values(b, a, 3) - }); + let config = [ + crate::config_dir(), + crate::find_root(None, &[], &[]).join(".helix"), + ] + .into_iter() + .map(|path| path.join("languages.toml")) + .filter_map(|file| { + std::fs::read_to_string(file) + .map(|config| toml::from_str(&config)) + .ok() + }) + .collect::, _>>()? + .into_iter() + .fold(default_lang_config(), |a, b| { + // combines for example + // b: + // [[language]] + // name = "toml" + // language-server = { command = "taplo", args = ["lsp", "stdio"] } + // + // a: + // [[language]] + // language-server = { command = "/usr/bin/taplo" } + // + // into: + // [[language]] + // name = "toml" + // language-server = { command = "/usr/bin/taplo" } + // + // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values + crate::merge_toml_values(a, b, 3) + }); Ok(config) } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 8dc2928adc9f7..6ece7879d4d7e 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -32,7 +32,7 @@ pub fn runtime_dir() -> PathBuf { if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent - let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); + let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR); log::debug!("runtime dir: {}", path.to_string_lossy()); return path; } @@ -60,15 +60,6 @@ pub fn config_dir() -> PathBuf { path } -pub fn local_config_dirs() -> Vec { - let directories = find_local_config_dirs() - .into_iter() - .map(|path| path.join(".helix")) - .collect(); - log::debug!("Located configuration folders: {:?}", directories); - directories -} - pub fn cache_dir() -> PathBuf { // TODO: allow env var override let strategy = choose_base_strategy().expect("Unable to find the config directory!"); @@ -84,6 +75,10 @@ pub fn config_file() -> PathBuf { .unwrap_or_else(|| config_dir().join("config.toml")) } +pub fn workspace_config_file() -> PathBuf { + find_root(None, &[], &[]).join(".helix").join("config.toml") +} + pub fn lang_config_file() -> PathBuf { config_dir().join("languages.toml") } @@ -92,22 +87,6 @@ pub fn log_file() -> PathBuf { cache_dir().join("helix.log") } -pub fn find_local_config_dirs() -> Vec { - let current_dir = std::env::current_dir().expect("unable to determine current directory"); - let mut directories = Vec::new(); - - for ancestor in current_dir.ancestors() { - if ancestor.join(".git").exists() { - directories.push(ancestor.to_path_buf()); - // Don't go higher than repo if we're in one - break; - } else if ancestor.join(".helix").is_dir() { - directories.push(ancestor.to_path_buf()); - } - } - directories -} - /// Merge two TOML documents, merging values from `right` onto `left` /// /// When an array exists in both `left` and `right`, `right`'s array is @@ -249,3 +228,41 @@ mod merge_toml_tests { ) } } + +pub fn find_root(root: Option<&str>, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf { + let current_dir = std::env::current_dir().expect("unable to determine current directory"); + + let root = match root { + Some(root) => { + let root = std::path::Path::new(root); + if root.is_absolute() { + root.to_path_buf() + } else { + current_dir.join(root) + } + } + None => current_dir.clone(), + }; + + let mut top_marker = None; + for ancestor in root.ancestors() { + if root_markers + .iter() + .any(|marker| ancestor.join(marker).exists()) + { + top_marker = Some(ancestor); + } + + if ancestor.join(".git").is_dir() || root_dirs.iter().any(|dir| ancestor.ends_with(dir)) { + // Top marker is repo root if not root marker was detected yet + if top_marker.is_none() { + top_marker = Some(ancestor); + } + // Don't go higher than repo if we're in one + break; + } + } + + // Return the found top marker or the current_dir as fallback + top_marker.map_or(current_dir, |a| a.to_path_buf()) +} diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 6827f568d986a..892d1451e5c86 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -9,13 +9,13 @@ use helix_loader::{self, VERSION_AND_GIT_HASH}; use lsp_types as lsp; use serde::Deserialize; use serde_json::Value; -use std::collections::HashMap; use std::future::Future; use std::process::Stdio; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; +use std::{collections::HashMap, path::PathBuf}; use tokio::{ io::{BufReader, BufWriter}, process::{Child, Command}, @@ -49,6 +49,7 @@ impl Client { config: Option, server_environment: HashMap, root_markers: &[String], + manual_roots: &[PathBuf], id: usize, req_timeout: u64, doc_path: Option<&std::path::PathBuf>, @@ -79,6 +80,7 @@ impl Client { let root_path = find_root( doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())), root_markers, + manual_roots, ); let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 8418896cbb735..7621dff4dee5a 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -14,6 +14,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, + path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -398,6 +399,7 @@ impl Registry { &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -413,7 +415,7 @@ impl Registry { let id = self.counter.fetch_add(1, Ordering::Relaxed); let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path)?; + start_client(id, language_config, config, doc_path, root_dirs)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); let (_, old_client) = entry.insert((id, client.clone())); @@ -431,6 +433,7 @@ impl Registry { &mut self, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -444,7 +447,7 @@ impl Registry { let id = self.counter.fetch_add(1, Ordering::Relaxed); let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path)?; + start_client(id, language_config, config, doc_path, root_dirs)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); entry.insert((id, client.clone())); @@ -545,6 +548,7 @@ fn start_client( config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, + root_dirs: &[PathBuf], ) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, @@ -552,6 +556,7 @@ fn start_client( config.config.clone(), ls_config.environment.clone(), &config.roots, + config.workspace_roots.as_deref().unwrap_or(root_dirs), id, ls_config.timeout, doc_path, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d1dc92236cf0a..40aa7485a88dd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2397,7 +2397,7 @@ fn append_mode(cx: &mut Context) { fn file_picker(cx: &mut Context) { // We don't specify language markers, root will be the root of the current // git repo or the current dir if we're not in a repo - let root = find_root(None, &[]); + let root = find_root(None, &[], &[]); let picker = ui::file_picker(root, &cx.editor.config()); cx.push_layer(Box::new(overlayed(picker))); } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1fd11b65de5ee..de1f254903864 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1326,13 +1326,16 @@ fn lsp_restart( return Ok(()); } + let editor_config = cx.editor.config.load(); let (_view, doc) = current!(cx.editor); let config = doc .language_config() .context("LSP not defined for the current document")?; let scope = config.scope.clone(); - cx.editor.language_servers.restart(config, doc.path())?; + cx.editor + .language_servers + .restart(config, doc.path(), &editor_config.workspace_roots)?; // This collect is needed because refresh_language_server would need to re-borrow editor. let document_ids_to_refresh: Vec = cx @@ -1831,6 +1834,20 @@ fn open_config( Ok(()) } +fn open_workspace_config( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + cx.editor + .open(&helix_loader::workspace_config_file(), Action::Replace)?; + Ok(()) +} + fn open_log( cx: &mut compositor::Context, _args: &[Cow], @@ -2432,6 +2449,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: open_config, completer: None, }, + TypableCommand { + name: "config-open-workspace", + aliases: &[], + doc: "Open the workspace config.toml file.", + fun: open_workspace_config, + completer: None, + }, TypableCommand { name: "log-open", aliases: &[], diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f8388..9776ef7a461a5 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,27 +1,34 @@ -use crate::keymap::{default::default, merge_keys, Keymap}; +use crate::keymap; +use crate::keymap::{merge_keys, Keymap}; +use helix_loader::merge_toml_values; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; use std::fmt::Display; +use std::fs; use std::io::Error as IOError; -use std::path::PathBuf; use toml::de::Error as TomlError; -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Clone, PartialEq)] pub struct Config { pub theme: Option, - #[serde(default = "default")] pub keys: HashMap, - #[serde(default)] pub editor: helix_view::editor::Config, } +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigRaw { + pub theme: Option, + pub keys: Option>, + pub editor: Option, +} + impl Default for Config { fn default() -> Config { Config { theme: None, - keys: default(), + keys: keymap::default(), editor: helix_view::editor::Config::default(), } } @@ -33,6 +40,12 @@ pub enum ConfigLoadError { Error(IOError), } +impl Default for ConfigLoadError { + fn default() -> Self { + ConfigLoadError::Error(IOError::new(std::io::ErrorKind::NotFound, "place holder")) + } +} + impl Display for ConfigLoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -43,17 +56,72 @@ impl Display for ConfigLoadError { } impl Config { - pub fn load(config_path: PathBuf) -> Result { - match std::fs::read_to_string(config_path) { - Ok(config) => toml::from_str(&config) - .map(merge_keys) - .map_err(ConfigLoadError::BadConfig), - Err(err) => Err(ConfigLoadError::Error(err)), - } + pub fn load( + global: Result, + local: Result, + ) -> Result { + let global_config: Result = + global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let local_config: Result = + local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let res = match (global_config, local_config) { + (Ok(global), Ok(local)) => { + let mut keys = keymap::default(); + if let Some(global_keys) = global.keys { + merge_keys(&mut keys, global_keys) + } + if let Some(local_keys) = local.keys { + merge_keys(&mut keys, local_keys) + } + + let editor = match (global.editor, local.editor) { + (None, None) => helix_view::editor::Config::default(), + (None, Some(val)) | (Some(val), None) => { + val.try_into().map_err(ConfigLoadError::BadConfig)? + } + (Some(global), Some(local)) => merge_toml_values(global, local, 3) + .try_into() + .map_err(ConfigLoadError::BadConfig)?, + }; + + Config { + theme: local.theme.or(global.theme), + keys, + editor, + } + } + // if any configs are invalid return that first + (_, Err(ConfigLoadError::BadConfig(err))) + | (Err(ConfigLoadError::BadConfig(err)), _) => { + return Err(ConfigLoadError::BadConfig(err)) + } + (Ok(config), Err(_)) | (Err(_), Ok(config)) => { + let mut keys = keymap::default(); + if let Some(keymap) = config.keys { + merge_keys(&mut keys, keymap); + } + Config { + theme: config.theme, + keys, + editor: config.editor.map_or_else( + || Ok(helix_view::editor::Config::default()), + |val| val.try_into().map_err(ConfigLoadError::BadConfig), + )?, + } + } + // these are just two io errors return the one for the global config + (Err(err), Err(_)) => return Err(err), + }; + + Ok(res) } pub fn load_default() -> Result { - Config::load(helix_loader::config_file()) + let global_config = + fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error); + let local_config = fs::read_to_string(helix_loader::workspace_config_file()) + .map_err(ConfigLoadError::Error); + Config::load(global_config, local_config) } } @@ -61,6 +129,12 @@ impl Config { mod tests { use super::*; + impl Config { + fn load_test(config: &str) -> Config { + Config::load(Ok(config.to_owned()), Err(ConfigLoadError::default())).unwrap() + } + } + #[test] fn parsing_keymaps_config_file() { use crate::keymap; @@ -77,18 +151,24 @@ mod tests { A-F12 = "move_next_word_end" "#; + let mut keys = keymap::default(); + merge_keys( + &mut keys, + hashmap! { + Mode::Insert => Keymap::new(keymap!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + })), + Mode::Normal => Keymap::new(keymap!({ "Normal mode" + "A-F12" => move_next_word_end, + })), + }, + ); + assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), + Config::load_test(sample_keymaps), Config { - keys: hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" - "A-F12" => move_next_word_end, - })), - }, + keys, ..Default::default() } ); @@ -97,11 +177,11 @@ mod tests { #[test] fn keys_resolve_to_correct_defaults() { // From serde default - let default_keys = toml::from_str::("").unwrap().keys; - assert_eq!(default_keys, default()); + let default_keys = Config::load_test("").keys; + assert_eq!(default_keys, keymap::default()); // From the Default trait let default_keys = Config::default().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, keymap::default()); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e94a5f66b4470..3033c6a4883c6 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -2,7 +2,6 @@ pub mod default; pub mod macros; pub use crate::commands::MappableCommand; -use crate::config::Config; use arc_swap::{ access::{DynAccess, DynGuard}, ArcSwap, @@ -16,7 +15,7 @@ use std::{ sync::Arc, }; -use default::default; +pub use default::default; use macros::key; #[derive(Debug, Clone)] @@ -417,12 +416,10 @@ impl Default for Keymaps { } /// Merge default config keys with user overwritten keys for custom user config. -pub fn merge_keys(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default()); - for (mode, keys) in &mut config.keys { +pub fn merge_keys(dst: &mut HashMap, mut delta: HashMap) { + for (mode, keys) in dst { keys.merge(delta.remove(mode).unwrap_or_default()) } - config } #[cfg(test)] @@ -449,26 +446,24 @@ mod tests { #[test] fn merge_partial_keys() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "i" => normal_mode, - "无" => insert_mode, - "z" => jump_backward, - "g" => { "Merge into goto mode" - "$" => goto_line_end, - "g" => delete_char_forward, - }, - }) - ) - }, - ..Default::default() + let keymap = hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "i" => normal_mode, + "无" => insert_mode, + "z" => jump_backward, + "g" => { "Merge into goto mode" + "$" => goto_line_end, + "g" => delete_char_forward, + }, + }) + ) }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); + let mut merged_keyamp = default(); + merge_keys(&mut merged_keyamp, keymap.clone()); + assert_ne!(keymap, merged_keyamp); - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone()))); assert_eq!( keymap.get(Mode::Normal, key!('i')), KeymapResult::Matched(MappableCommand::normal_mode), @@ -486,7 +481,7 @@ mod tests { "Leaf should replace node" ); - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Assumes that `g` is a node in default keymap assert_eq!( keymap.root().search(&[key!('g'), key!('$')]).unwrap(), @@ -506,30 +501,28 @@ mod tests { "Old leaves in subnode should be present in merged node" ); - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0); } #[test] fn order_should_be_set() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, + let keymap = hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, }, - }) - ) - }, - ..Default::default() + }, + }) + ) }; - let mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + let mut merged_keyamp = default(); + merge_keys(&mut merged_keyamp, keymap.clone()); + assert_ne!(keymap, merged_keyamp); + let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( keymap diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c5379f37a..e0c3f6e701e07 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -3,7 +3,7 @@ use crossterm::event::EventStream; use helix_loader::VERSION_AND_GIT_HASH; use helix_term::application::Application; use helix_term::args::Args; -use helix_term::config::Config; +use helix_term::config::{Config, ConfigLoadError}; use std::path::PathBuf; fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { @@ -126,18 +126,19 @@ FLAGS: helix_loader::initialize_config_file(args.config_file.clone()); - let config = match std::fs::read_to_string(helix_loader::config_file()) { - Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::merge_keys) - .unwrap_or_else(|err| { - eprintln!("Bad config: {}", err); - eprintln!("Press to continue with default config"); - use std::io::Read; - let _ = std::io::stdin().read(&mut []); - Config::default() - }), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), - Err(err) => return Err(Error::new(err)), + let config = match Config::load_default() { + Ok(config) => config, + Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => { + Config::default() + } + Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)), + Err(ConfigLoadError::BadConfig(err)) => { + eprintln!("Bad config: {}", err); + eprintln!("Press to continue with default config"); + use std::io::Read; + let _ = std::io::stdin().read(&mut []); + Config::default() + } }; let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 8755b60f06cad..c4fbf2f108a3c 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -1,6 +1,7 @@ use std::{ fs::File, io::{Read, Write}, + mem::replace, path::PathBuf, time::Duration, }; @@ -164,7 +165,8 @@ pub async fn test_with_config>( test_case: T, ) -> anyhow::Result<()> { let test_case = test_case.into(); - config = helix_term::keymap::merge_keys(config); + let keys = replace(&mut config.keys, helix_term::keymap::default()); + merge_keys(&mut config.keys, keys); let app = Application::new(args, config, syn_conf)?; test_key_sequence_with_input_text( @@ -211,7 +213,7 @@ pub fn temp_file_with_contents>( /// Generates a config with defaults more suitable for integration tests pub fn test_config() -> Config { - merge_keys(Config { + Config { editor: helix_view::editor::Config { lsp: LspConfig { enable: false, @@ -219,8 +221,9 @@ pub fn test_config() -> Config { }, ..Default::default() }, + keys: helix_term::keymap::default(), ..Default::default() - }) + } } /// Replaces all LF chars with the system's appropriate line feed diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 042f5bdb42691..4e83e8df10852 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -274,6 +274,8 @@ pub struct Config { /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, pub soft_wrap: SoftWrap, + /// Workspace specific root dirs + pub workspace_roots: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -764,6 +766,7 @@ impl Default for Config { indent_guides: IndentGuidesConfig::default(), color_modes: false, soft_wrap: SoftWrap::default(), + workspace_roots: Vec::new(), } } } @@ -1090,15 +1093,14 @@ impl Editor { } // if doc doesn't have a URL it's a scratch buffer, ignore it - let (lang, path) = { - let doc = self.document(doc_id)?; - (doc.language.clone(), doc.path().cloned()) - }; + let doc = self.document(doc_id)?; + let (lang, path) = (doc.language.clone(), doc.path().cloned()); + let root_dirs = &doc.config.load().workspace_roots; // try to find a language server based on the language name let language_server = lang.as_ref().and_then(|language| { self.language_servers - .get(language, path.as_ref()) + .get(language, path.as_ref(), root_dirs) .map_err(|e| { log::error!( "Failed to initialize the LSP for `{}` {{ {} }}",