From 4e4873d59c41c0c72549c4eccd63b8a339f0aba7 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Thu, 28 Jul 2022 20:00:24 +0200 Subject: [PATCH 001/195] docs: add section about local config, written like the one for languages --- .helix/config.toml | 2 ++ .helix/languages.toml | 3 +++ book/src/configuration.md | 7 +++++++ 3 files changed, 12 insertions(+) create mode 100644 .helix/config.toml create mode 100644 .helix/languages.toml diff --git a/.helix/config.toml b/.helix/config.toml new file mode 100644 index 000000000000..3333b105d08e --- /dev/null +++ b/.helix/config.toml @@ -0,0 +1,2 @@ +theme = "gruvbox_light" +err = yes diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 000000000000..38f7db065ed0 --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,3 @@ +[[language]] +name = "rust" +#auto-format = false diff --git a/book/src/configuration.md b/book/src/configuration.md index affd497c7580..cf7d3cda71fa 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -25,8 +25,15 @@ select = "underline" hidden = false ``` +<<<<<<< HEAD You may also specify a file to use for configuration with the `-c` or `--config` CLI argument: `hx -c path/to/custom-config.toml`. +======= +Editor configuration may also be overridden local to a project by creating +a `config.toml` file under a `.helix` directory. Its settings will be merged +with the configuration directory `config.toml` and the built-in configuration. + +>>>>>>> a4a8e122 (docs: add section about local config, written like the one for languages) ## Editor From 1e9bee31eda2557d94f9c5b443c608a31a4c51f6 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Thu, 28 Jul 2022 20:04:02 +0200 Subject: [PATCH 002/195] fix: mistakes were made --- .helix/config.toml | 2 -- .helix/languages.toml | 3 --- 2 files changed, 5 deletions(-) delete mode 100644 .helix/config.toml delete mode 100644 .helix/languages.toml diff --git a/.helix/config.toml b/.helix/config.toml deleted file mode 100644 index 3333b105d08e..000000000000 --- a/.helix/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -theme = "gruvbox_light" -err = yes diff --git a/.helix/languages.toml b/.helix/languages.toml deleted file mode 100644 index 38f7db065ed0..000000000000 --- a/.helix/languages.toml +++ /dev/null @@ -1,3 +0,0 @@ -[[language]] -name = "rust" -#auto-format = false From 04da5ee20aa34e4b311b6be640c30ea59b2e2f9e Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Tue, 30 Aug 2022 01:11:43 +0200 Subject: [PATCH 003/195] fix: cleaner code --- helix-term/src/config.rs | 89 ++++++++++++++++++++++++++++++++++++++++ helix-term/src/main.rs | 14 +------ 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f838..c1dfe8b8ba1b 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -55,6 +55,95 @@ impl Config { pub fn load_default() -> Result { Config::load(helix_loader::config_file()) } + + // Load a merged config from configuration and $PWD/.helix/config.toml + pub fn load_merged_config() -> Config { + let root_config: Config = std::fs::read_to_string(helix_loader::config_file()) + .ok() + .and_then(|config| toml::from_str(&config).ok()) + .unwrap_or_else(|| { + eprintln!("Bad config: {:?}", helix_loader::config_file()); + Config::halt_and_confirm("default"); + Config::default() + }); + + // Load each config file + let local_config_values = helix_loader::local_config_dirs() + .into_iter() + .map(|path| path.join("config.toml")) + .chain([helix_loader::config_file()]) + .filter_map(|file| Config::load_config_toml_values(&root_config, file)); + + // Merge configs and return, or alert user of error and load default + match local_config_values + .fold(toml::Value::Table(toml::value::Table::default()), |a, b| { + helix_loader::merge_toml_values(b, a, 3) + }) + .try_into() + { + Ok(conf) => conf, + Err(_) => root_config, + } + } + + // Load a specific config file if allowed by config + // Stay with toml::Values as they can be merged + pub fn load_config_toml_values( + root_config: &Config, + config_path: std::path::PathBuf, + ) -> Option { + if config_path.exists() { + let mut confirmed = true; + if config_path != helix_loader::config_file() { + if root_config.editor.security.load_local_config { + if root_config.editor.security.confirm_local_config { + eprintln!( + "Type yes to continue with loading config: {:#?}", + config_path + ); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap_or_default(); + confirmed = input.contains("yes"); + } //else we still presume confirmed = true + } else { + // User denies local configs, do not load + confirmed = false; + } + } + if confirmed { + log::debug!("Load config: {:?}", config_path); + let bytes = std::fs::read(&config_path); + let cfg: Option = match bytes { + Ok(bytes) => { + let cfg = toml::from_slice(&bytes); + match cfg { + Ok(cfg) => Some(cfg), + Err(e) => { + eprintln!("Toml parse error for {:?}: {}", &config_path, e); + Config::halt_and_confirm("loaded"); + None + } + } + } + Err(e) => { + eprintln!("Could not read {:?}: {}", &config_path, e); + Config::halt_and_confirm("loaded"); + None + } + }; + return cfg; + } else { + return None; + } + } + None + } + + fn halt_and_confirm(config_type: &'static str) { + eprintln!("Press to continue with {} config", config_type); + let mut tmp = String::new(); + let _ = std::io::stdin().read_line(&mut tmp); + } } #[cfg(test)] diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 7f04f2014b95..91ee8d239095 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -122,19 +122,7 @@ 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: Config = Config::load_merged_config(); // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config).context("unable to create new application")?; From 6ad3fb333dcec2f7917f6106dea968d186b1b324 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Tue, 30 Aug 2022 08:52:17 +0200 Subject: [PATCH 004/195] fix: after rebase, reinstate editor.security --- helix-term/src/main.rs | 2 +- helix-view/src/editor.rs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 91ee8d239095..de80e29d037e 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -122,7 +122,7 @@ FLAGS: helix_loader::initialize_config_file(args.config_file.clone()); - let config: Config = Config::load_merged_config(); + let config = Config::load_merged_config(); // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config).context("unable to create new application")?; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 0bf7ebd02cc9..59fafd2650d5 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -156,6 +156,9 @@ pub struct Config { /// Search configuration. #[serde(default)] pub search: SearchConfig, + /// Security settings (i.e. loading TOML files from $PWD/.helix) + #[serde(default)] + pub security: SecurityConfig, pub lsp: LspConfig, pub terminal: Option, /// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers. @@ -168,6 +171,26 @@ pub struct Config { pub color_modes: bool, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +pub struct SecurityConfig { + pub load_local_config: bool, + pub confirm_local_config: bool, + //pub load_local_languages: bool, //TODO: implement + //pub confirm_local_languages: bool, //TODO: implement +} + +impl Default for SecurityConfig { + fn default() -> Self { + Self { + load_local_config: false, + confirm_local_config: true, + //load_local_languages: false, + //confirm_local_languages: true, + } + } +} + #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct TerminalConfig { @@ -550,6 +573,7 @@ impl Default for Config { cursor_shape: CursorShapeConfig::default(), true_color: false, search: SearchConfig::default(), + security: SecurityConfig::default(), lsp: LspConfig::default(), terminal: get_terminal_provider(), rulers: Vec::new(), From a53a008ce0382992164cc32018986f791688d6e2 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Tue, 30 Aug 2022 08:57:48 +0200 Subject: [PATCH 005/195] fix: configuration book entry after rebase --- book/src/configuration.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index cf7d3cda71fa..0f32ee96ddaa 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -25,15 +25,12 @@ select = "underline" hidden = false ``` -<<<<<<< HEAD You may also specify a file to use for configuration with the `-c` or -`--config` CLI argument: `hx -c path/to/custom-config.toml`. -======= -Editor configuration may also be overridden local to a project by creating -a `config.toml` file under a `.helix` directory. Its settings will be merged -with the configuration directory `config.toml` and the built-in configuration. +`--config` CLI argument: `hx -c path/to/custom-config.toml`. ->>>>>>> a4a8e122 (docs: add section about local config, written like the one for languages) +Finally, you can have a `config.toml` local to a project by it under a `.helix` directory in your repository. +Its settings will be merged with the configuration directory `config.toml` and the built-in configuration, +if you have enabled the feature under `[editor.security]` in your global configuration. ## Editor @@ -236,3 +233,13 @@ Example: render = true character = "╎" ``` + +### `[editor.security]` Section + +Opt in to features that may put you at risk. +It is possible to write malicious TOML files, so we suggest you keep the `confirm_*` options to their default value of `true`. + +| Key | Description | Default | +| --- | --- | --- | +| `load_local_config` | Load a `config.yaml` from `$PWD/.helix` that will merge with your global configuration. | `false` | +| `confirm_local_config` | Prompt the user at launch in order to accept the loading of a found local `$PWD/.helix` | `true` | \ No newline at end of file From f93e2d1f3ecdbdeb384387828efe32b0a4338d25 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Tue, 30 Aug 2022 11:08:42 +0200 Subject: [PATCH 006/195] fix: clippy from rebase --- helix-term/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index de80e29d037e..b7c80f30d65d 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Result}; use crossterm::event::EventStream; use helix_term::application::Application; use helix_term::args::Args; From e6e6ec3d911c239cd54dc6878fac19f31bb32378 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Thu, 1 Sep 2022 23:57:17 +0200 Subject: [PATCH 007/195] fix: use reduce and not fold --- helix-term/src/config.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index c1dfe8b8ba1b..269ebc793435 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -75,14 +75,9 @@ impl Config { .filter_map(|file| Config::load_config_toml_values(&root_config, file)); // Merge configs and return, or alert user of error and load default - match local_config_values - .fold(toml::Value::Table(toml::value::Table::default()), |a, b| { - helix_loader::merge_toml_values(b, a, 3) - }) - .try_into() - { - Ok(conf) => conf, - Err(_) => root_config, + match local_config_values.reduce(|a, b| helix_loader::merge_toml_values(b, a, 3)) { + Some(conf) => conf.try_into().unwrap_or_default(), + None => root_config, } } From ed9aa6020dcb9e7fb5f90e95c9b0473e159203c2 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Thu, 1 Sep 2022 23:59:03 +0200 Subject: [PATCH 008/195] fix: early exit --- helix-term/src/config.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 269ebc793435..22e3f261e21c 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -87,7 +87,9 @@ impl Config { root_config: &Config, config_path: std::path::PathBuf, ) -> Option { - if config_path.exists() { + if !config_path.exists() { + return None; + } else { let mut confirmed = true; if config_path != helix_loader::config_file() { if root_config.editor.security.load_local_config { @@ -131,7 +133,6 @@ impl Config { return None; } } - None } fn halt_and_confirm(config_type: &'static str) { From 3da3606b30d5c418ed061207f58ff9a55acb600f Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Fri, 2 Sep 2022 00:31:16 +0200 Subject: [PATCH 009/195] style: clippy ok --- helix-term/src/config.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 22e3f261e21c..fe05b0fc69e5 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -88,7 +88,7 @@ impl Config { config_path: std::path::PathBuf, ) -> Option { if !config_path.exists() { - return None; + None } else { let mut confirmed = true; if config_path != helix_loader::config_file() { @@ -128,9 +128,9 @@ impl Config { None } }; - return cfg; + cfg } else { - return None; + None } } } From 9359c4518185e1e8c97198a67a87b0b1a502da2e Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Fri, 9 Sep 2022 20:12:51 +0200 Subject: [PATCH 010/195] style: no need to else after early exit, golang style --- helix-term/src/config.rs | 81 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index fe05b0fc69e5..bf09da8f9c2d 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -88,50 +88,49 @@ impl Config { config_path: std::path::PathBuf, ) -> Option { if !config_path.exists() { - None - } else { - let mut confirmed = true; - if config_path != helix_loader::config_file() { - if root_config.editor.security.load_local_config { - if root_config.editor.security.confirm_local_config { - eprintln!( - "Type yes to continue with loading config: {:#?}", - config_path - ); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap_or_default(); - confirmed = input.contains("yes"); - } //else we still presume confirmed = true - } else { - // User denies local configs, do not load - confirmed = false; - } + return None; + } + let mut confirmed = true; + if config_path != helix_loader::config_file() { + if root_config.editor.security.load_local_config { + if root_config.editor.security.confirm_local_config { + eprintln!( + "Type yes to continue with loading config: {:#?}", + config_path + ); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap_or_default(); + confirmed = input.contains("yes"); + } //else we still presume confirmed = true + } else { + // User denies local configs, do not load + confirmed = false; } - if confirmed { - log::debug!("Load config: {:?}", config_path); - let bytes = std::fs::read(&config_path); - let cfg: Option = match bytes { - Ok(bytes) => { - let cfg = toml::from_slice(&bytes); - match cfg { - Ok(cfg) => Some(cfg), - Err(e) => { - eprintln!("Toml parse error for {:?}: {}", &config_path, e); - Config::halt_and_confirm("loaded"); - None - } + } + if confirmed { + log::debug!("Load config: {:?}", config_path); + let bytes = std::fs::read(&config_path); + let cfg: Option = match bytes { + Ok(bytes) => { + let cfg = toml::from_slice(&bytes); + match cfg { + Ok(cfg) => Some(cfg), + Err(e) => { + eprintln!("Toml parse error for {:?}: {}", &config_path, e); + Config::halt_and_confirm("loaded"); + None } } - Err(e) => { - eprintln!("Could not read {:?}: {}", &config_path, e); - Config::halt_and_confirm("loaded"); - None - } - }; - cfg - } else { - None - } + } + Err(e) => { + eprintln!("Could not read {:?}: {}", &config_path, e); + Config::halt_and_confirm("loaded"); + None + } + }; + cfg + } else { + None } } From a0f80cd1d7cbac0ed87c7176c93732f13dd6ec33 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Mon, 19 Sep 2022 22:56:32 +0200 Subject: [PATCH 011/195] fix: remove confirm_local_config --- book/src/configuration.md | 3 +-- helix-term/src/config.rs | 21 +++------------------ helix-view/src/editor.rs | 15 +-------------- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 0f32ee96ddaa..602805e0ebc0 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -241,5 +241,4 @@ It is possible to write malicious TOML files, so we suggest you keep the `confir | Key | Description | Default | | --- | --- | --- | -| `load_local_config` | Load a `config.yaml` from `$PWD/.helix` that will merge with your global configuration. | `false` | -| `confirm_local_config` | Prompt the user at launch in order to accept the loading of a found local `$PWD/.helix` | `true` | \ No newline at end of file +| `load-local-config` | Load `config.yaml` from `$PWD/.helix` that will merge with your global configuration. | `false` | diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index bf09da8f9c2d..ef428057f2fd 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -90,24 +90,9 @@ impl Config { if !config_path.exists() { return None; } - let mut confirmed = true; - if config_path != helix_loader::config_file() { - if root_config.editor.security.load_local_config { - if root_config.editor.security.confirm_local_config { - eprintln!( - "Type yes to continue with loading config: {:#?}", - config_path - ); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap_or_default(); - confirmed = input.contains("yes"); - } //else we still presume confirmed = true - } else { - // User denies local configs, do not load - confirmed = false; - } - } - if confirmed { + if config_path == helix_loader::config_file() + || root_config.editor.security.load_local_config + { log::debug!("Load config: {:?}", config_path); let bytes = std::fs::read(&config_path); let cfg: Option = match bytes { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 59fafd2650d5..eeb8b1eb6d97 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -171,24 +171,11 @@ pub struct Config { pub color_modes: bool, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct SecurityConfig { pub load_local_config: bool, - pub confirm_local_config: bool, //pub load_local_languages: bool, //TODO: implement - //pub confirm_local_languages: bool, //TODO: implement -} - -impl Default for SecurityConfig { - fn default() -> Self { - Self { - load_local_config: false, - confirm_local_config: true, - //load_local_languages: false, - //confirm_local_languages: true, - } - } } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] From 11aa4189bd928b5f23e61164717a3ed85f8efa07 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Fri, 23 Sep 2022 13:48:24 +0200 Subject: [PATCH 012/195] fix: remove confirm option from book --- book/src/configuration.md | 1 - 1 file changed, 1 deletion(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 602805e0ebc0..437d553cce23 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -237,7 +237,6 @@ character = "╎" ### `[editor.security]` Section Opt in to features that may put you at risk. -It is possible to write malicious TOML files, so we suggest you keep the `confirm_*` options to their default value of `true`. | Key | Description | Default | | --- | --- | --- | From 8acc1b95181f5961834b9016ed14ab75dde37481 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Fri, 23 Sep 2022 13:55:49 +0200 Subject: [PATCH 013/195] style: merged an if statement --- helix-term/src/config.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index ef428057f2fd..28e0f2836c3b 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -87,11 +87,9 @@ impl Config { root_config: &Config, config_path: std::path::PathBuf, ) -> Option { - if !config_path.exists() { - return None; - } - if config_path == helix_loader::config_file() - || root_config.editor.security.load_local_config + if config_path.exists() + && (config_path == helix_loader::config_file() + || root_config.editor.security.load_local_config) { log::debug!("Load config: {:?}", config_path); let bytes = std::fs::read(&config_path); From 0f311640c12b6fbdf8f0b1a3d7814208d9ac09fc Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Sat, 24 Sep 2022 08:48:25 +0200 Subject: [PATCH 014/195] style: ...and early return --- helix-term/src/config.rs | 49 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 28e0f2836c3b..437d557696eb 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -87,34 +87,33 @@ impl Config { root_config: &Config, config_path: std::path::PathBuf, ) -> Option { - if config_path.exists() - && (config_path == helix_loader::config_file() - || root_config.editor.security.load_local_config) + if !config_path.exists() + || (config_path != helix_loader::config_file() + && !root_config.editor.security.load_local_config) { - log::debug!("Load config: {:?}", config_path); - let bytes = std::fs::read(&config_path); - let cfg: Option = match bytes { - Ok(bytes) => { - let cfg = toml::from_slice(&bytes); - match cfg { - Ok(cfg) => Some(cfg), - Err(e) => { - eprintln!("Toml parse error for {:?}: {}", &config_path, e); - Config::halt_and_confirm("loaded"); - None - } + return None; + } + log::debug!("Load config: {:?}", config_path); + let bytes = std::fs::read(&config_path); + let cfg: Option = match bytes { + Ok(bytes) => { + let cfg = toml::from_slice(&bytes); + match cfg { + Ok(cfg) => Some(cfg), + Err(e) => { + eprintln!("Toml parse error for {:?}: {}", &config_path, e); + Config::halt_and_confirm("loaded"); + None } } - Err(e) => { - eprintln!("Could not read {:?}: {}", &config_path, e); - Config::halt_and_confirm("loaded"); - None - } - }; - cfg - } else { - None - } + } + Err(e) => { + eprintln!("Could not read {:?}: {}", &config_path, e); + Config::halt_and_confirm("loaded"); + None + } + }; + cfg } fn halt_and_confirm(config_type: &'static str) { From 43470ccaf382fae3b1ebc2fbc85c5c5dcdc75e56 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Mon, 26 Sep 2022 02:10:39 +0200 Subject: [PATCH 015/195] fix: default to loading configs --- book/src/configuration.md | 4 ++-- helix-view/src/editor.rs | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 437d553cce23..e6f8c0d1a38c 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -236,8 +236,8 @@ character = "╎" ### `[editor.security]` Section -Opt in to features that may put you at risk. +Features that the paranoid among us may choose to disable. | Key | Description | Default | | --- | --- | --- | -| `load-local-config` | Load `config.yaml` from `$PWD/.helix` that will merge with your global configuration. | `false` | +| `load-local-config` | Load `config.yaml` from `$PWD/.helix` that will merge with your global configuration. | `true` | diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index eeb8b1eb6d97..5aa062bf0a4d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -171,13 +171,21 @@ pub struct Config { pub color_modes: bool, } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct SecurityConfig { pub load_local_config: bool, //pub load_local_languages: bool, //TODO: implement } +impl Default for SecurityConfig { + fn default() -> Self { + Self { + load_local_config: true, + } + } +} + #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct TerminalConfig { From 671f297630d360cfa5d55dc69d4af98521a4b152 Mon Sep 17 00:00:00 2001 From: bb Date: Sun, 9 Oct 2022 17:33:16 +0200 Subject: [PATCH 016/195] Make stickiness configurable --- book/src/remapping.md | 2 ++ helix-term/src/keymap.rs | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/book/src/remapping.md b/book/src/remapping.md index bd4ac7f81c86..37fcba2c7140 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -50,5 +50,7 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes Keys can be disabled by binding them to the `no_op` command. +Making a mode "sticky" can be achieved by adding `sticky = true` to the mapping. + Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands. > Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`. diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 088b3b6d2083..dbf8e3472dd9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -200,11 +200,25 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { { let mut mapping = HashMap::new(); let mut order = Vec::new(); - while let Some((key, value)) = map.next_entry::()? { - mapping.insert(key, value); - order.push(key); + let mut is_sticky = false; + while let Some(toml_key) = map.next_key::()? { + let maybe_key_event = toml_key.clone().try_into::(); + match (maybe_key_event, toml_key.as_str()) { + (Ok(key_event), _) => { + mapping.insert(key_event, map.next_value::()?); + order.push(key_event); + } + (Err(_), Some("sticky")) => { + is_sticky = map.next_value::()?; + } + (Err(err), _) => { + return Err(serde::de::Error::custom(err.to_string())); + } + } } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order))) + let mut trie_node = KeyTrieNode::new("", mapping, order); + trie_node.is_sticky = is_sticky; + Ok(KeyTrie::Node(trie_node)) } } From 84b4f12194c6afe2a50f2d1ea44c3408cc499dc3 Mon Sep 17 00:00:00 2001 From: Alexander Brevig Date: Fri, 18 Nov 2022 09:14:45 +0100 Subject: [PATCH 017/195] docs: fix typos --- book/src/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index e6f8c0d1a38c..eba6f5f1856e 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -28,7 +28,7 @@ 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 it under a `.helix` directory in your repository. +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, if you have enabled the feature under `[editor.security]` in your global configuration. @@ -240,4 +240,4 @@ Features that the paranoid among us may choose to disable. | Key | Description | Default | | --- | --- | --- | -| `load-local-config` | Load `config.yaml` from `$PWD/.helix` that will merge with your global configuration. | `true` | +| `load-local-config` | Load `config.toml` from `$PWD/.helix` that will merge with your global configuration. | `true` | From 09ea56b234312c68e6b9830b07a3f50ce6c43df0 Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Fri, 23 Sep 2022 21:37:23 -0400 Subject: [PATCH 018/195] Add support for labels in custom menu keymaps --- helix-term/src/config.rs | 33 +++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 17 +++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f838..fbea75970582 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -94,6 +94,39 @@ mod tests { ); } + #[test] + fn parsing_menus() { + use crate::keymap; + use crate::keymap::Keymap; + use helix_core::hashmap; + use helix_view::document::Mode; + + let sample_keymaps = r#" + [keys.normal] + f = { f = "file_picker", c = "wclose" } + b = { label = "buffer", b = "buffer_picker", n = "goto_next_buffer" } + "#; + + assert_eq!( + toml::from_str::(sample_keymaps).unwrap(), + Config { + keys: hashmap! { + Mode::Normal => Keymap::new(keymap!({ "Normal mode" + "f" => { "" + "f" => file_picker, + "c" => wclose, + }, + "b" => { "buffer" + "b" => buffer_picker, + "n" => goto_next_buffer, + }, + })), + }, + ..Default::default() + } + ); + } + #[test] fn keys_resolve_to_correct_defaults() { // From serde default diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4a131f0a5217..c2d6cecbf1d7 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -198,13 +198,22 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { where M: serde::de::MapAccess<'de>, { + let mut name = ""; let mut mapping = HashMap::new(); let mut order = Vec::new(); - while let Some((key, value)) = map.next_entry::()? { - mapping.insert(key, value); - order.push(key); + + while let Some(key) = map.next_key::<&str>()? { + match key { + "label" => name = map.next_value::<&str>()?, + _ => { + let key_event = key.parse::().map_err(serde::de::Error::custom)?; + let key_trie = map.next_value::()?; + mapping.insert(key_event, key_trie); + order.push(key_event); + } + } } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order))) + Ok(KeyTrie::Node(KeyTrieNode::new(name, mapping, order))) } } From 608687da6008063db6721d57a74a1f88ccb0d2ae Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Fri, 23 Sep 2022 23:03:56 -0400 Subject: [PATCH 019/195] Add support for labels on typable commands --- helix-term/src/config.rs | 38 ++++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 28 +++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index fbea75970582..ac19cee106a9 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -127,6 +127,44 @@ mod tests { ); } + #[test] + fn parsing_typable_commands() { + use crate::keymap; + use crate::keymap::MappableCommand; + use helix_view::document::Mode; + use helix_view::input::KeyEvent; + use std::str::FromStr; + + let sample_keymaps = r#" + [keys.normal] + o = { label = "Edit Config", command = ":open ~/.config" } + c = ":buffer-close" + "#; + + let config = toml::from_str::(sample_keymaps).unwrap(); + + let tree = config.keys.get(&Mode::Normal).unwrap().root(); + + if let keymap::KeyTrie::Node(node) = tree { + let open_node = node.get(&KeyEvent::from_str("o").unwrap()).unwrap(); + + if let keymap::KeyTrie::Leaf(MappableCommand::Typable { doc, .. }) = open_node { + assert_eq!(doc, "Edit Config"); + } else { + panic!("Edit Config did not parse to typable command"); + } + + let close_node = node.get(&KeyEvent::from_str("c").unwrap()).unwrap(); + if let keymap::KeyTrie::Leaf(MappableCommand::Typable { doc, .. }) = close_node { + assert_eq!(doc, ":buffer-close []"); + } else { + panic!(":buffer-close command did not parse to typable command"); + } + } else { + panic!("Config did not parse to trie"); + } + } + #[test] fn keys_resolve_to_correct_defaults() { // From serde default diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c2d6cecbf1d7..5dff983952e0 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -198,13 +198,15 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { where M: serde::de::MapAccess<'de>, { - let mut name = ""; + let mut label = ""; + let mut command = None; let mut mapping = HashMap::new(); let mut order = Vec::new(); while let Some(key) = map.next_key::<&str>()? { match key { - "label" => name = map.next_value::<&str>()?, + "label" => label = map.next_value::<&str>()?, + "command" => command = Some(map.next_value::()?), _ => { let key_event = key.parse::().map_err(serde::de::Error::custom)?; let key_trie = map.next_value::()?; @@ -213,7 +215,27 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { } } } - Ok(KeyTrie::Node(KeyTrieNode::new(name, mapping, order))) + + match command { + None => Ok(KeyTrie::Node(KeyTrieNode::new(label, mapping, order))), + Some(cmd) => { + if label.is_empty() { + Ok(KeyTrie::Leaf(cmd)) + } else { + match cmd { + MappableCommand::Typable { name, args, .. } => { + Ok(MappableCommand::Typable { + name, + args, + doc: label.to_string(), + }) + .map(KeyTrie::Leaf) + } + MappableCommand::Static { .. } => Ok(KeyTrie::Leaf(cmd)), + } + } + } + } } } From 091b3f5df094e60295bcfce134fe94ce43d64b67 Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Sat, 24 Sep 2022 21:31:15 -0400 Subject: [PATCH 020/195] refactor keymap map visitor to reduce # of cases --- helix-term/src/keymap.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 5dff983952e0..8a4fa8337fb5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -219,20 +219,16 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { match command { None => Ok(KeyTrie::Node(KeyTrieNode::new(label, mapping, order))), Some(cmd) => { - if label.is_empty() { - Ok(KeyTrie::Leaf(cmd)) + let status = (cmd, label.is_empty()); + if let (MappableCommand::Typable { name, args, .. }, false) = status { + Ok(MappableCommand::Typable { + name, + args, + doc: label.to_string(), + }) + .map(KeyTrie::Leaf) } else { - match cmd { - MappableCommand::Typable { name, args, .. } => { - Ok(MappableCommand::Typable { - name, - args, - doc: label.to_string(), - }) - .map(KeyTrie::Leaf) - } - MappableCommand::Static { .. } => Ok(KeyTrie::Leaf(cmd)), - } + Ok(KeyTrie::Leaf(status.0)) } } } From 56d624ef5a8ebc3c48ff23a966ffe6b20ec19a9c Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Tue, 4 Oct 2022 19:18:22 -0400 Subject: [PATCH 021/195] Simplify labelled command pattern match Co-authored-by: Michael Davis --- helix-term/src/keymap.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 8a4fa8337fb5..82e1137fc4eb 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -218,19 +218,14 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { match command { None => Ok(KeyTrie::Node(KeyTrieNode::new(label, mapping, order))), - Some(cmd) => { - let status = (cmd, label.is_empty()); - if let (MappableCommand::Typable { name, args, .. }, false) = status { - Ok(MappableCommand::Typable { - name, - args, - doc: label.to_string(), - }) - .map(KeyTrie::Leaf) - } else { - Ok(KeyTrie::Leaf(status.0)) - } + Some(MappableCommand::Typable { name, args, .. }) if !label.is_empty() => { + Ok(KeyTrie::Leaf(MappableCommand::Typable { + name, + args, + doc: label.to_string(), + })) } + Some(command) => Ok(KeyTrie::Leaf(command)), } } } From 3642213127cacf0bcf67cabc2be920277c39eaa1 Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Mon, 17 Oct 2022 20:15:27 -0400 Subject: [PATCH 022/195] Add some basic docs --- book/src/remapping.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/book/src/remapping.md b/book/src/remapping.md index e89c66113d1b..657ae509bc7d 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -19,6 +19,13 @@ w = "move_line_up" # Maps the 'w' key move_line_up g = { a = "code_action" } # Maps `ga` to show possible code actions "ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode +# You can create labeled sub-menus and provide friendly labels for typeable commands +[keys.normal.space.f] # Registering multiple mappings under a single entry creates a sub-menu (accesed by 'space', 'f' in this case) +label = "File" # The menu is called file and within it: +f = "file_picker" # 'f' opens the file picker +s = { label = "Save", command = ":write" } # 's' saves the current file +c = { label = "Edit Config", command = ":open ~/.config/helix/config.toml" } # 'c' opens the helix config file + [keys.insert] "A-x" = "normal_mode" # Maps Alt-X to enter normal mode j = { k = "normal_mode" } # Maps `jk` to exit insert mode From 780457293485a8ded512db622b7f09d8b6b18505 Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Tue, 15 Nov 2022 18:42:36 -0500 Subject: [PATCH 023/195] fix typos in menu label docs --- book/src/remapping.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/remapping.md b/book/src/remapping.md index 657ae509bc7d..5676f96e12ee 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -20,8 +20,8 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions "ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode # You can create labeled sub-menus and provide friendly labels for typeable commands -[keys.normal.space.f] # Registering multiple mappings under a single entry creates a sub-menu (accesed by 'space', 'f' in this case) -label = "File" # The menu is called file and within it: +[keys.normal.space.f] # Registering multiple mappings under a single entry creates a sub-menu (accessed by 'space', 'f' in this case) +label = "File" # The menu is called file and within it: f = "file_picker" # 'f' opens the file picker s = { label = "Save", command = ":write" } # 's' saves the current file c = { label = "Edit Config", command = ":open ~/.config/helix/config.toml" } # 'c' opens the helix config file From 0e6c4af38bad31c344d416f2b5592facccc4f26c Mon Sep 17 00:00:00 2001 From: Matthew Cheely Date: Tue, 15 Nov 2022 21:20:39 -0500 Subject: [PATCH 024/195] return errors for ambiguous and unsupported labels in menus --- helix-term/src/keymap.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 82e1137fc4eb..a17e7a0ad446 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -218,6 +218,12 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { match command { None => Ok(KeyTrie::Node(KeyTrieNode::new(label, mapping, order))), + Some(_command) if !order.is_empty() => { + Err(serde::de::Error::custom("ambiguous mapping: 'command' is only valid with 'label', but I found other keys")) + } + Some(MappableCommand::Static { .. }) if !label.is_empty() => { + Err(serde::de::Error::custom("custom labels are only available for typable commands (the ones starting with ':')")) + } Some(MappableCommand::Typable { name, args, .. }) if !label.is_empty() => { Ok(KeyTrie::Leaf(MappableCommand::Typable { name, From 294d1a1c3989ccba51ec66313bd052b0499ba13f Mon Sep 17 00:00:00 2001 From: Paul Scott Date: Thu, 5 Jan 2023 21:51:38 +1100 Subject: [PATCH 025/195] Generalised to multiple runtime directories with priorities This is an implementation for #3346. Previously, one of the following runtime directories were used: 1. `$HELIX_RUNTIME` 2. sibling directory to `$CARGO_MANIFEST_DIR` 3. subdirectory of user config directory 4. subdirectory of path to helix executable The first directory provided / found to exist in this order was used as a root for all runtime file searches (grammars, themes, queries). This change lowers the priority of `$HELIX_RUNTIME` so that the user config runtime has higher priority. More significantly, all of these directories are now searched for runtime files, enabling a user to override default or system-level runtime files. If the same file name appears in multiple runtime directories, the following priority is now used: 1. sibling directory to `$CARGO_MANIFEST_DIR` 2. subdirectory of user config directory 3. `$HELIX_RUNTIME` 4. subdirectory of path to helix executable One exception to this rule is that a user can have a `themes` directory directly in the user config directory that has higher piority to `themes` directories in runtime directories. That behaviour has been preserved. As part of implementing this feature `theme::Loader` was simplified and the cycle detection logic of the theme inheritance was improved to cover more cases and to be more explicit. --- helix-loader/src/grammar.rs | 23 +++++---- helix-loader/src/lib.rs | 81 +++++++++++++++++++++++++------ helix-term/src/application.rs | 9 ++-- helix-term/src/commands/typed.rs | 2 +- helix-term/src/health.rs | 26 +++++----- helix-term/src/ui/mod.rs | 8 +-- helix-view/src/theme.rs | 83 +++++++++++++++++--------------- 7 files changed, 149 insertions(+), 83 deletions(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 2aa924755112..c7be502aa983 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -67,8 +67,9 @@ pub fn get_language(name: &str) -> Result { #[cfg(not(target_arch = "wasm32"))] pub fn get_language(name: &str) -> Result { use libloading::{Library, Symbol}; - let mut library_path = crate::runtime_dir().join("grammars").join(name); - library_path.set_extension(DYLIB_EXTENSION); + let mut rel_library_path = PathBuf::new().join("grammars").join(name); + rel_library_path.set_extension(DYLIB_EXTENSION); + let library_path = crate::runtime_file(rel_library_path); let library = unsafe { Library::new(&library_path) } .with_context(|| format!("Error opening dynamic library {:?}", library_path))?; @@ -252,7 +253,9 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result { remote, revision, .. } = grammar.source { - let grammar_dir = crate::runtime_dir() + let grammar_dir = crate::runtime_dirs() + .first() + .expect("No runtime directories provided") // guaranteed by post-condition .join("grammars") .join("sources") .join(&grammar.grammar_id); @@ -350,7 +353,9 @@ fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result< let grammar_dir = if let GrammarSource::Local { path } = &grammar.source { PathBuf::from(&path) } else { - crate::runtime_dir() + crate::runtime_dirs() + .first() + .expect("No runtime directories provided") // guaranteed by post-condition .join("grammars") .join("sources") .join(&grammar.grammar_id) @@ -401,7 +406,10 @@ fn build_tree_sitter_library( None } }; - let parser_lib_path = crate::runtime_dir().join("grammars"); + let parser_lib_path = crate::runtime_dirs() + .first() + .expect("No runtime directories provided") // guaranteed by post-condition + .join("grammars"); let mut library_path = parser_lib_path.join(&grammar.grammar_id); library_path.set_extension(DYLIB_EXTENSION); @@ -511,9 +519,6 @@ fn mtime(path: &Path) -> Result { /// Gives the contents of a file from a language's `runtime/queries/` /// directory pub fn load_runtime_file(language: &str, filename: &str) -> Result { - let path = crate::RUNTIME_DIR - .join("queries") - .join(language) - .join(filename); + let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename)); std::fs::read_to_string(&path) } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 80d44a8264b8..6dc0013d1d6d 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -2,11 +2,12 @@ pub mod config; pub mod grammar; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); -pub static RUNTIME_DIR: once_cell::sync::Lazy = once_cell::sync::Lazy::new(runtime_dir); +static RUNTIME_DIRS: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(prioritize_runtime_dirs); static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -25,31 +26,83 @@ pub fn initialize_config_file(specified_file: Option) { CONFIG_FILE.set(config_file).ok(); } -pub fn runtime_dir() -> PathBuf { - if let Ok(dir) = std::env::var("HELIX_RUNTIME") { - return dir.into(); - } - +/// A list of runtime directories from highest to lowest priority +/// +/// The priority is: +/// +/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set) +/// 2. subdirectory of user config directory (always included) +/// 3. `HELIX_RUNTIME` (if environment variable is set) +/// 4. subdirectory of path to helix executable (always included) +/// +/// Postcondition: returns at least two paths (they might not exist). +fn prioritize_runtime_dirs() -> Vec { + const RT_DIR: &str = "runtime"; + // Adding higher priority first + let mut rt_dirs = Vec::new(); 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); log::debug!("runtime dir: {}", path.to_string_lossy()); - return path; + rt_dirs.push(path); } - const RT_DIR: &str = "runtime"; - let conf_dir = config_dir().join(RT_DIR); - if conf_dir.exists() { - return conf_dir; + let conf_rt_dir = config_dir().join(RT_DIR); + rt_dirs.push(conf_rt_dir); + + if let Ok(dir) = std::env::var("HELIX_RUNTIME") { + rt_dirs.push(dir.into()); } // fallback to location of the executable being run // canonicalize the path in case the executable is symlinked - std::env::current_exe() + let exe_rt_dir = std::env::current_exe() .ok() .and_then(|path| std::fs::canonicalize(path).ok()) .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR))) - .unwrap() + .unwrap(); + rt_dirs.push(exe_rt_dir); + rt_dirs +} + +/// Runtime directories ordered from highest to lowest priority +/// +/// All directories should be checked when looking for files. +/// +/// Postcondition: returns at least one path (it might not exist). +pub fn runtime_dirs() -> &'static [PathBuf] { + &RUNTIME_DIRS +} + +/// Find file with path relative to runtime directory +/// +/// `rel_path` should be the relative path from within the `runtime/` directory. +/// The valid runtime directories are searched in priority order and the first +/// file found to exist is returned, otherwise None. +fn find_runtime_file>(rel_path: P) -> Option { + RUNTIME_DIRS.iter().find_map(|rt_dir| { + let path = rt_dir.join(rel_path.as_ref()); + if path.exists() { + Some(path) + } else { + None + } + }) +} + +/// Find file with path relative to runtime directory +/// +/// `rel_path` should be the relative path from within the `runtime/` directory. +/// The valid runtime directories are searched in priority order and the first +/// file found to exist is returned, otherwise the path to the final attempt +/// that failed. +pub fn runtime_file>(rel_path: P) -> PathBuf { + find_runtime_file(&rel_path).unwrap_or_else(|| { + RUNTIME_DIRS + .last() + .map(|dir| dir.join(rel_path.as_ref())) + .unwrap_or_default() + }) } pub fn config_dir() -> PathBuf { diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c0cbc2451483..569afa59f6d8 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -135,10 +135,9 @@ impl Application { use helix_view::editor::Action; - let theme_loader = std::sync::Arc::new(theme::Loader::new( - &helix_loader::config_dir(), - &helix_loader::runtime_dir(), - )); + let mut theme_parent_dirs = vec![helix_loader::config_dir()]; + theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); + let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); let true_color = config.editor.true_color || crate::true_color(); let theme = config @@ -184,7 +183,7 @@ impl Application { compositor.push(editor_view); if args.load_tutor { - let path = helix_loader::runtime_dir().join("tutor"); + let path = helix_loader::runtime_file("tutor"); editor.open(&path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index de24c4fba5ca..3fc47f20cc8e 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1406,7 +1406,7 @@ fn tutor( return Ok(()); } - let path = helix_loader::runtime_dir().join("tutor"); + let path = helix_loader::runtime_file("tutor"); cx.editor.open(&path, Action::Replace)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(cx.editor).set_path(None)?; diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 6558fe19fb4c..995acdd8cffb 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -52,7 +52,7 @@ pub fn general() -> std::io::Result<()> { let config_file = helix_loader::config_file(); let lang_file = helix_loader::lang_config_file(); let log_file = helix_loader::log_file(); - let rt_dir = helix_loader::runtime_dir(); + let rt_dirs = helix_loader::runtime_dirs(); let clipboard_provider = get_clipboard_provider(); if config_file.exists() { @@ -66,17 +66,19 @@ pub fn general() -> std::io::Result<()> { writeln!(stdout, "Language file: default")?; } writeln!(stdout, "Log file: {}", log_file.display())?; - writeln!(stdout, "Runtime directory: {}", rt_dir.display())?; - - if let Ok(path) = std::fs::read_link(&rt_dir) { - let msg = format!("Runtime directory is symlinked to {}", path.display()); - writeln!(stdout, "{}", msg.yellow())?; - } - if !rt_dir.exists() { - writeln!(stdout, "{}", "Runtime directory does not exist.".red())?; - } - if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { - writeln!(stdout, "{}", "Runtime directory is empty.".red())?; + writeln!(stdout, "Number of Runtime directories: {}", rt_dirs.len())?; + for (i, rt_dir) in rt_dirs.iter().enumerate() { + writeln!(stdout, "Runtime directory {}: {}", i + 1, rt_dir.display())?; + if let Ok(path) = std::fs::read_link(&rt_dir) { + let msg = format!("Runtime directory is symlinked to {}", path.display()); + writeln!(stdout, "{}", msg.yellow())?; + } + if !rt_dir.exists() { + writeln!(stdout, "{}", "Runtime directory does not exist.".red())?; + } + if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { + writeln!(stdout, "{}", "Runtime directory is empty.".red())?; + } } writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index eb4807581e8c..a7c2dbb38fbe 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -281,10 +281,10 @@ pub mod completers { } pub fn theme(_editor: &Editor, input: &str) -> Vec { - let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes")); - names.extend(theme::Loader::read_names( - &helix_loader::config_dir().join("themes"), - )); + 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"))); + } names.push("default".into()); names.push("base16_default".into()); names.sort(); diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index cb0d3ac46d34..f1dc6fb876d4 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, path::{Path, PathBuf}, }; @@ -35,19 +35,21 @@ pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| Theme { #[derive(Clone, Debug)] pub struct Loader { - user_dir: PathBuf, - default_dir: PathBuf, + /// Theme directories to search from highest to lowest priority + theme_dirs: Vec, } impl Loader { - /// Creates a new loader that can load themes from two directories. - pub fn new>(user_dir: P, default_dir: P) -> Self { + /// Creates a new loader that can load themes from multiple directories. + /// + /// The provided directories should be ordered from highest to lowest priority. + /// The directories will have their "themes" subdirectory searched. + pub fn new(dirs: &[PathBuf]) -> Self { Self { - user_dir: user_dir.as_ref().join("themes"), - default_dir: default_dir.as_ref().join("themes"), + theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(), } } - /// Loads a theme first looking in the `user_dir` then in `default_dir` + /// Loads a theme searching directories in priority order. pub fn load(&self, name: &str) -> Result { if name == "default" { return Ok(self.default()); @@ -56,7 +58,8 @@ impl Loader { return Ok(self.base16_default()); } - let theme = self.load_theme(name, name, false).map(Theme::from)?; + let mut visited_paths = HashSet::new(); + let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?; Ok(Theme { name: name.into(), @@ -64,16 +67,19 @@ impl Loader { }) } - // load the theme and its parent recursively and merge them - // `base_theme_name` is the theme from the config.toml, - // used to prevent some circular loading scenarios - fn load_theme( - &self, - name: &str, - base_them_name: &str, - only_default_dir: bool, - ) -> Result { - let path = self.path(name, only_default_dir); + /// Recursively load a theme, merging with any inherited parent themes. + /// + /// The paths that have been visited in the inheritance hierarchy are tracked + /// to detect and avoid cycling. + /// + /// It is possible for one file to inherit from another file with the same name + /// so long as the second file is in a themes directory with lower priority. + /// However, it is not recommended that users do this as it will make tracing + /// errors more difficult. + fn load_theme(&self, name: &str, visited_paths: &mut HashSet) -> Result { + let path = self + .path(name, visited_paths) + .ok_or_else(|| anyhow!("Theme: not found or recursively inheriting: {}", name))?; let theme_toml = self.load_toml(path)?; let inherits = theme_toml.get("inherits"); @@ -90,11 +96,7 @@ impl Loader { // load default themes's toml from const. "default" => DEFAULT_THEME_DATA.clone(), "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(), - _ => self.load_theme( - parent_theme_name, - base_them_name, - base_them_name == parent_theme_name, - )?, + _ => self.load_theme(parent_theme_name, visited_paths)?, }; self.merge_themes(parent_theme_toml, theme_toml) @@ -146,7 +148,7 @@ impl Loader { merge_toml_values(theme, palette.into(), 1) } - // Loads the theme data as `toml::Value` first from the user_dir then in default_dir + // Loads the theme data as `toml::Value` fn load_toml(&self, path: PathBuf) -> Result { let data = std::fs::read(&path)?; let value = toml::from_slice(data.as_slice())?; @@ -154,24 +156,29 @@ impl Loader { Ok(value) } - // Returns the path to the theme with the name - // With `only_default_dir` as false the path will first search for the user path - // disabled it ignores the user path and returns only the default path - fn path(&self, name: &str, only_default_dir: bool) -> PathBuf { + /// Returns the path to the theme with the given name + /// + /// Ignores paths already visited and follows directory priority order. + fn path(&self, name: &str, visited_paths: &mut HashSet) -> Option { let filename = format!("{}.toml", name); - let user_path = self.user_dir.join(&filename); - if !only_default_dir && user_path.exists() { - user_path - } else { - self.default_dir.join(filename) - } + self.theme_dirs.iter().find_map(|dir| { + let path = dir.join(&filename); + if path.exists() && !visited_paths.contains(&path) { + visited_paths.insert(path.clone()); + Some(path) + } else { + None + } + }) } - /// Lists all theme names available in default and user directory + /// Lists all theme names available in all directories pub fn names(&self) -> Vec { - let mut names = Self::read_names(&self.user_dir); - names.extend(Self::read_names(&self.default_dir)); + let mut names = Vec::new(); + for dir in &self.theme_dirs { + names.extend(Self::read_names(dir)); + } names } From b642e905d561abcc09725dde56667df0e3f20ec4 Mon Sep 17 00:00:00 2001 From: Paul Scott Date: Wed, 11 Jan 2023 21:03:41 +1100 Subject: [PATCH 026/195] Removed AsRef usage to avoid binary growth --- helix-loader/src/grammar.rs | 2 +- helix-loader/src/lib.rs | 10 +++++----- helix-term/src/application.rs | 3 ++- helix-term/src/commands/typed.rs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index c7be502aa983..cd6b07aa434e 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -69,7 +69,7 @@ pub fn get_language(name: &str) -> Result { use libloading::{Library, Symbol}; let mut rel_library_path = PathBuf::new().join("grammars").join(name); rel_library_path.set_extension(DYLIB_EXTENSION); - let library_path = crate::runtime_file(rel_library_path); + let library_path = crate::runtime_file(&rel_library_path); let library = unsafe { Library::new(&library_path) } .with_context(|| format!("Error opening dynamic library {:?}", library_path))?; diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 6dc0013d1d6d..08760818b97e 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -79,9 +79,9 @@ pub fn runtime_dirs() -> &'static [PathBuf] { /// `rel_path` should be the relative path from within the `runtime/` directory. /// The valid runtime directories are searched in priority order and the first /// file found to exist is returned, otherwise None. -fn find_runtime_file>(rel_path: P) -> Option { +fn find_runtime_file(rel_path: &Path) -> Option { RUNTIME_DIRS.iter().find_map(|rt_dir| { - let path = rt_dir.join(rel_path.as_ref()); + let path = rt_dir.join(rel_path); if path.exists() { Some(path) } else { @@ -96,11 +96,11 @@ fn find_runtime_file>(rel_path: P) -> Option { /// The valid runtime directories are searched in priority order and the first /// file found to exist is returned, otherwise the path to the final attempt /// that failed. -pub fn runtime_file>(rel_path: P) -> PathBuf { - find_runtime_file(&rel_path).unwrap_or_else(|| { +pub fn runtime_file(rel_path: &Path) -> PathBuf { + find_runtime_file(rel_path).unwrap_or_else(|| { RUNTIME_DIRS .last() - .map(|dir| dir.join(rel_path.as_ref())) + .map(|dir| dir.join(rel_path)) .unwrap_or_default() }) } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 569afa59f6d8..045229c9f302 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -31,6 +31,7 @@ use crate::{ use log::{debug, error, warn}; use std::{ io::{stdin, stdout, Write}, + path::Path, sync::Arc, time::{Duration, Instant}, }; @@ -183,7 +184,7 @@ impl Application { compositor.push(editor_view); if args.load_tutor { - let path = helix_loader::runtime_file("tutor"); + let path = helix_loader::runtime_file(Path::new("tutor")); editor.open(&path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 3fc47f20cc8e..aed32671190a 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1406,7 +1406,7 @@ fn tutor( return Ok(()); } - let path = helix_loader::runtime_file("tutor"); + let path = helix_loader::runtime_file(Path::new("tutor")); cx.editor.open(&path, Action::Replace)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(cx.editor).set_path(None)?; From c4620ff9526b4a11dce3e56fe606250ce748ef29 Mon Sep 17 00:00:00 2001 From: Paul Scott Date: Wed, 18 Jan 2023 21:23:19 +1100 Subject: [PATCH 027/195] Health displaying ;-separated runtime dirs --- helix-term/src/health.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 995acdd8cffb..fbbaa3d6a3b9 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -66,18 +66,26 @@ pub fn general() -> std::io::Result<()> { writeln!(stdout, "Language file: default")?; } writeln!(stdout, "Log file: {}", log_file.display())?; - writeln!(stdout, "Number of Runtime directories: {}", rt_dirs.len())?; - for (i, rt_dir) in rt_dirs.iter().enumerate() { - writeln!(stdout, "Runtime directory {}: {}", i + 1, rt_dir.display())?; + writeln!( + stdout, + "Runtime directories: {}", + rt_dirs + .iter() + .map(|d| d.to_string_lossy()) + .collect::>() + .join(";") + )?; + for rt_dir in rt_dirs.iter() { if let Ok(path) = std::fs::read_link(&rt_dir) { - let msg = format!("Runtime directory is symlinked to {}", path.display()); + let msg = format!("Runtime directory is symlinked to: {}", path.display()); writeln!(stdout, "{}", msg.yellow())?; } if !rt_dir.exists() { - writeln!(stdout, "{}", "Runtime directory does not exist.".red())?; - } - if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { - writeln!(stdout, "{}", "Runtime directory is empty.".red())?; + let msg = format!("Runtime directory does not exist: {}", rt_dir.display()); + writeln!(stdout, "{}", msg.yellow())?; + } else if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { + let msg = format!("Runtime directory is empty: {}", rt_dir.display()); + writeln!(stdout, "{}", msg.yellow())?; } } writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?; From 5a0a1db7d661900eb5d6214c82f2ac24535cec21 Mon Sep 17 00:00:00 2001 From: Paul Scott Date: Wed, 18 Jan 2023 21:49:30 +1100 Subject: [PATCH 028/195] Changed HELIX_RUNTIME build from src instructions --- book/src/install.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/book/src/install.md b/book/src/install.md index bf0ff079c40e..507fd4ada49a 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -100,8 +100,9 @@ cargo install --path helix-term --locked This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`. Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the -config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden -via the `HELIX_RUNTIME` environment variable. +config directory (for example `~/.config/helix/runtime` on Linux/macOS). An alternative runtime directory can +be used by setting the `HELIX_RUNTIME` environment variable. Both runtime directories can be used at the same +time, with the files residing under the config runtime directory given priority. | OS | Command | | -------------------- | ------------------------------------------------ | @@ -125,11 +126,6 @@ cd %appdata%\helix mklink /D runtime "\runtime" ``` -The runtime location can be overridden via the `HELIX_RUNTIME` environment variable. - -> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term --locked`, -> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`. - If you plan on keeping the repo locally, an alternative to copying/symlinking runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime` (`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory). From 6d50d7fd68a5e16acba42b7b910c4b68cbdb471f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 15:33:55 +0100 Subject: [PATCH 029/195] remove one-liner solely used for a test --- helix-term/src/keymap.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4a131f0a5217..60b9ff1c31c8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -112,10 +112,6 @@ impl KeyTrieNode { } Info::from_keymap(self.name(), body) } - /// Get a reference to the key trie node's order. - pub fn order(&self) -> &[KeyEvent] { - self.order.as_slice() - } } impl Default for KeyTrieNode { @@ -541,7 +537,7 @@ mod tests { ); // Make sure an order was set during merge let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order().is_empty()) + assert!(!node.node().unwrap().order.as_slice().is_empty()) } #[test] From 37f49f6838e95b23ee64d5b63c827c8cfd9b469a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 19:55:30 +0100 Subject: [PATCH 030/195] Inline helper fuction of singular reference --- helix-term/src/config.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f838..f7cac6a36d10 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -28,7 +28,7 @@ impl Default for Config { } #[derive(Debug)] -pub enum ConfigLoadError { +pub enum ConfigLoadError { BadConfig(TomlError), Error(IOError), } @@ -43,18 +43,15 @@ impl Display for ConfigLoadError { } impl Config { - pub fn load(config_path: PathBuf) -> Result { - match std::fs::read_to_string(config_path) { + // REFACTOR? code similar to config assignment in main.rs, + pub fn load_default() -> Result { + match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) .map(merge_keys) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } } - - pub fn load_default() -> Result { - Config::load(helix_loader::config_file()) - } } #[cfg(test)] From c748f7cb1d88d9652b45d8edb82343fa0adb1073 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 21:55:05 +0100 Subject: [PATCH 031/195] Remove order property of KeyTrie struct: Was not implemented correctly as the order was in the most cases assigned from the values of an HashMap, which does not guarantee order. This applied to when a keymap was both deserialized and merged. Info box body was then being sorted as a function of the fallacious `order` property, and by a method that yielded at worst a time complexity of at least n^2. Furthermore, the body contents were inerted at first by the order of the hash map keys, in which `order` itself was based on. A more reliable predifined sorting order is to be implemented. --- helix-term/src/keymap.rs | 29 ++++------------------------- helix-term/src/keymap/macros.rs | 4 +--- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 60b9ff1c31c8..03ac5ce18269 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -24,7 +24,6 @@ pub struct KeyTrieNode { /// A label for keys coming under this node, like "Goto mode" name: String, map: HashMap, - order: Vec, pub is_sticky: bool, } @@ -33,22 +32,18 @@ impl<'de> Deserialize<'de> for KeyTrieNode { where D: serde::Deserializer<'de>, { - let map = HashMap::::deserialize(deserializer)?; - let order = map.keys().copied().collect::>(); // NOTE: map.keys() has arbitrary order Ok(Self { - map, - order, + map: HashMap::::deserialize(deserializer)?, ..Default::default() }) } } impl KeyTrieNode { - pub fn new(name: &str, map: HashMap, order: Vec) -> Self { + pub fn new(name: &str, map: HashMap) -> Self { Self { name: name.to_string(), map, - order, is_sticky: false, } } @@ -70,11 +65,6 @@ impl KeyTrieNode { } self.map.insert(key, trie); } - for &key in self.map.keys() { - if !self.order.contains(&key) { - self.order.push(key); - } - } } pub fn infobox(&self) -> Info { @@ -97,12 +87,6 @@ impl KeyTrieNode { None => body.push((desc, BTreeSet::from([key]))), } } - body.sort_unstable_by_key(|(_, keys)| { - self.order - .iter() - .position(|&k| k == *keys.iter().next().unwrap()) - .unwrap() - }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { body = body @@ -116,7 +100,7 @@ impl KeyTrieNode { impl Default for KeyTrieNode { fn default() -> Self { - Self::new("", HashMap::new(), Vec::new()) + Self::new("", HashMap::new()) } } @@ -195,12 +179,10 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { M: serde::de::MapAccess<'de>, { let mut mapping = HashMap::new(); - let mut order = Vec::new(); while let Some((key, value)) = map.next_entry::()? { mapping.insert(key, value); - order.push(key); } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order))) + Ok(KeyTrie::Node(KeyTrieNode::new("", mapping))) } } @@ -535,9 +517,6 @@ mod tests { &KeyTrie::Leaf(MappableCommand::vsplit), "Leaf should be present in merged subnode" ); - // Make sure an order was set during merge - let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order.as_slice().is_empty()) } #[test] diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index c4a1bfbb3064..1cf4151c9a38 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -101,7 +101,6 @@ macro_rules! keymap { { let _cap = hashmap!(@count $($($key),+),*); let mut _map = ::std::collections::HashMap::with_capacity(_cap); - let mut _order = ::std::vec::Vec::with_capacity(_cap); $( $( let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); @@ -110,10 +109,9 @@ macro_rules! keymap { keymap!(@trie $value) ); assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); - _order.push(_key); )+ )* - let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); + let mut _node = $crate::keymap::KeyTrieNode::new($label, _map); $( _node.is_sticky = $sticky; )? $crate::keymap::KeyTrie::Node(_node) } From 2c7c4d943bb4bb52225af218a60bdb62573420d4 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 22:57:28 +0100 Subject: [PATCH 032/195] Infobox: Remove superflous command descripition pruning: Exist under the wrong (possibly just outdated) assumption that command descriptions are written with their KeyTrie name prefixed (Space, View, Goto etc.). For examle: The command `file_picker` is assumed to have the description "Space Open file picker", which is not the case , nor for any other command description. --- helix-term/src/keymap.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 03ac5ce18269..938e781210f9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -87,13 +87,7 @@ impl KeyTrieNode { None => body.push((desc, BTreeSet::from([key]))), } } - let prefix = format!("{} ", self.name()); - if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { - body = body - .into_iter() - .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) - .collect(); - } + Info::from_keymap(self.name(), body) } } From 8d35e64eea2235aca513aafe73178bb5a55467d9 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 23:42:10 +0100 Subject: [PATCH 033/195] Keymap infobox: Use Vec in place of BTree: BTree was being used to store a list of keyevents for a given command. This list was only iterated over twice to in the end be converted to a Vec. Better to just use a Vec from start given the use- case. Temporalily reverts #952. --- helix-term/src/keymap.rs | 8 ++++---- helix-view/src/info.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 938e781210f9..b575ee97e9ff 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -11,7 +11,7 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ borrow::Cow, - collections::{BTreeSet, HashMap}, + collections::HashMap, ops::{Deref, DerefMut}, sync::Arc, }; @@ -68,7 +68,7 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); + let mut body: Vec<(&str, Vec)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => { @@ -82,9 +82,9 @@ impl KeyTrieNode { }; match body.iter().position(|(d, _)| d == &desc) { Some(pos) => { - body[pos].1.insert(key); + body[pos].1.push(key); } - None => body.push((desc, BTreeSet::from([key]))), + None => body.push((desc, Vec::from([key]))), } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 3080cf8e1b2f..26dc844d75b1 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,6 +1,6 @@ use crate::input::KeyEvent; use helix_core::{register::Registers, unicode::width::UnicodeWidthStr}; -use std::{collections::BTreeSet, fmt::Write}; +use std::fmt::Write; #[derive(Debug)] /// Info box used in editor. Rendering logic will be in other crate. @@ -55,7 +55,7 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet)>) -> Self { + pub fn from_keymap(title: &str, body: Vec<(&str, Vec)>) -> Self { let body: Vec<_> = body .into_iter() .map(|(desc, events)| { From f187f2d5942c541c5a605136223b50b5fa6a08cd Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 23:53:07 +0100 Subject: [PATCH 034/195] Keymap infobox: Place in correct order from start: Infobox body was being filled with description then KeyEvent list to only later be iterated over for the purpose of flippingin this order. --- helix-term/src/keymap.rs | 8 ++++---- helix-view/src/info.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b575ee97e9ff..1a91d997e862 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -68,7 +68,7 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, Vec)> = Vec::with_capacity(self.len()); + let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => { @@ -80,11 +80,11 @@ impl KeyTrieNode { KeyTrie::Node(n) => n.name(), KeyTrie::Sequence(_) => "[Multiple commands]", }; - match body.iter().position(|(d, _)| d == &desc) { + match body.iter().position(|(_, d)| d == &desc) { Some(pos) => { - body[pos].1.push(key); + body[pos].0.push(key); } - None => body.push((desc, Vec::from([key]))), + None => body.push((Vec::from([key]), desc)), } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 26dc844d75b1..fea8f535cd59 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -55,10 +55,10 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(&str, Vec)>) -> Self { + pub fn from_keymap(title: &str, body: Vec<(Vec, &str)>) -> Self { let body: Vec<_> = body .into_iter() - .map(|(desc, events)| { + .map(|(events, desc)| { let events = events.iter().map(ToString::to_string).collect::>(); (events.join(", "), desc) }) From 2a0e9d20bf003f75cc34677d9d8d610e9fbe4c51 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 18 Dec 2022 00:56:15 +0100 Subject: [PATCH 035/195] Keymap infobox: Format body from start Vec was being created as an intermediary into Info.rs for it to be converted into a comma separated string. This commit removes this intermediate step but also the single purpose from_keymap public function in Info.rs, as it is no longer needed. --- helix-term/src/keymap.rs | 22 ++++++++++------------ helix-view/src/info.rs | 13 ------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 1a91d997e862..7ff4f83764f9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -68,27 +68,25 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); + let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { - let desc = match trie { - KeyTrie::Leaf(cmd) => { - if cmd.name() == "no_op" { + let description: &str = match trie { + KeyTrie::Leaf(command) => { + if command.name() == "no_op" { continue; } - cmd.doc() + command.doc() } - KeyTrie::Node(n) => n.name(), + KeyTrie::Node(key_trie) => key_trie.name(), KeyTrie::Sequence(_) => "[Multiple commands]", }; - match body.iter().position(|(_, d)| d == &desc) { - Some(pos) => { - body[pos].0.push(key); - } - None => body.push((Vec::from([key]), desc)), + match body.iter().position(|(_, existing_description)| &description == existing_description) { + Some(pos) => body[pos].0 += &format!(", {}", &key.to_string()), + None => body.push((key.to_string(), description)), } } - Info::from_keymap(self.name(), body) + Info::new(self.name(), &body) } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index fea8f535cd59..1503e855e362 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,4 +1,3 @@ -use crate::input::KeyEvent; use helix_core::{register::Registers, unicode::width::UnicodeWidthStr}; use std::fmt::Write; @@ -55,18 +54,6 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(Vec, &str)>) -> Self { - let body: Vec<_> = body - .into_iter() - .map(|(events, desc)| { - let events = events.iter().map(ToString::to_string).collect::>(); - (events.join(", "), desc) - }) - .collect(); - - Self::new(title, &body) - } - pub fn from_registers(registers: &Registers) -> Self { let body: Vec<_> = registers .inner() From faec82729a50236f394b046b5e4c11453b247a99 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 26 Dec 2022 17:19:33 +0100 Subject: [PATCH 036/195] keymap.rs file splitting, cleanup and descriptive naming: * Descriptive naming and correct use of data structure terminology example translations: keymap.reverse_map -> keymap.command_list keytrienode -> keytrie (struct) search->traverse merge->merge_keytrie keytrie -> keytrienode (enum describing node variants) Leaf -> MappableCommand Sequence -> CommandSequence Node -> KeyTrie (A leaf is a node, and the sequence was also a node. So old naming made absolutely no sense whatsoever.) * Splitting parts of the keymap.rs file into {keytrienode, keytrie, keymaps, tests}.rs. (Should be self-explanatory by looking at the old keymap.rs.) * Removed KeytrieNode enum functions node(&self) -> Option<&KeyTrie> and node_mut(&mut self) -> Option<&mut KeyTrie> from KeyTrieNode as they served no purpose that could not be used from elsewhere. Was also a bit strange to return a "parent struct" from an enum "building block". * Removed getters that could be achieved by making fields public for now. (E.g .root(), .map(), .name()) * Removed keymap.merge() and keytrienode.merge_nodes() All merging could be handled by keytrie.merge() and keymaps.merge_with_default(), unnecessary to have a second unused system that does about the same thing. We also don't want functions that can cause panics as merge_nodes() could. * Initial simplification and formatting of command palette. Formatting is done during the creation of the command lists, removes the need for the creation and and traversal of an intermediary Vec>. Keyevent formatting changed from "(w)" to ["space>w>C-q"]. Clarifying how commands are triggered by moving through submenus/keytries. --- helix-term/src/application.rs | 2 +- helix-term/src/commands.rs | 36 +- helix-term/src/config.rs | 4 +- helix-term/src/keymap.rs | 591 +++------------------------ helix-term/src/keymap.rs:41:29 | 0 helix-term/src/keymap/default.rs | 12 +- helix-term/src/keymap/keymaps.rs | 121 ++++++ helix-term/src/keymap/keytrie.rs | 129 ++++++ helix-term/src/keymap/keytrienode.rs | 72 ++++ helix-term/src/keymap/macros.rs | 44 +- helix-term/src/keymap/tests.rs | 181 ++++++++ helix-term/src/lib.rs | 1 + helix-term/src/ui/editor.rs | 6 +- 13 files changed, 602 insertions(+), 597 deletions(-) create mode 100644 helix-term/src/keymap.rs:41:29 create mode 100644 helix-term/src/keymap/keymaps.rs create mode 100644 helix-term/src/keymap/keytrie.rs create mode 100644 helix-term/src/keymap/keytrienode.rs create mode 100644 helix-term/src/keymap/tests.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c0cbc2451483..42daaa64d44a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -24,7 +24,7 @@ use crate::{ compositor::{Compositor, Event}, config::Config, job::Jobs, - keymap::Keymaps, + keymap::keymaps::Keymaps, ui::{self, overlay::overlayed}, }; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e773a2c789d8..7e892d8606f6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -46,7 +46,7 @@ use crate::{ args, compositor::{self, Component, Compositor}, job::Callback, - keymap::ReverseKeymap, + keymap::CommandList, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; @@ -2450,30 +2450,21 @@ fn jumplist_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } -impl ui::menu::Item for MappableCommand { - type Data = ReverseKeymap; - fn format(&self, keymap: &Self::Data) -> Row { - let fmt_binding = |bindings: &Vec>| -> String { - bindings.iter().fold(String::new(), |mut acc, bind| { - if !acc.is_empty() { - acc.push(' '); - } - for key in bind { - acc.push_str(&key.key_sequence_format()); - } - acc - }) - }; + +impl ui::menu::Item for MappableCommand { + type Data = CommandList; + + fn label(&self, keymap: &Self::Data) -> Spans { match self { MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { - Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [:{}]", doc, name).into(), + Some(key_events) => format!("{} {:?} ':{}'", doc, key_events, name).into(), + None => format!("{} ':{}'", doc, name).into(), }, MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [{}]", doc, name).into(), + Some(key_events) => format!("{} {:?} '{}'", doc, key_events, name).into(), + None => format!("{} '{}'", doc, name).into(), }, } } @@ -2482,9 +2473,9 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap = compositor.find::().unwrap().keymaps.map() + let keymap_command_lists = compositor.find::().unwrap().keymaps.load_keymaps() [&cx.editor.mode] - .reverse_map(); + .command_list(); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2495,7 +2486,7 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let picker = Picker::new(commands, keymap_command_lists, move |cx, command, _action| { let mut ctx = Context { register: None, count: std::num::NonZeroUsize::new(1), @@ -2524,6 +2515,7 @@ pub fn command_palette(cx: &mut Context) { compositor.push(Box::new(overlayed(picker))); }, )); + } fn last_picker(cx: &mut Context) { diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index f7cac6a36d10..471b8c168a79 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,4 +1,4 @@ -use crate::keymap::{default::default, merge_keys, Keymap}; +use crate::keymap::{default::default, Keymap, keymaps::Keymaps}; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; @@ -47,7 +47,7 @@ impl Config { pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(merge_keys) + .map(Keymaps::merge_with_default) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 7ff4f83764f9..edfa41df2705 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,570 +1,83 @@ +pub mod keymaps; +pub mod keytrienode; pub mod default; pub mod macros; +pub mod keytrie; +pub mod tests; -pub use crate::commands::MappableCommand; -use crate::config::Config; -use arc_swap::{ - access::{DynAccess, DynGuard}, - ArcSwap, -}; -use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; -use std::{ - borrow::Cow, - collections::HashMap, - ops::{Deref, DerefMut}, - sync::Arc, -}; - -use default::default; -use macros::key; - -#[derive(Debug, Clone)] -pub struct KeyTrieNode { - /// A label for keys coming under this node, like "Goto mode" - name: String, - map: HashMap, - pub is_sticky: bool, -} - -impl<'de> Deserialize<'de> for KeyTrieNode { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - Ok(Self { - map: HashMap::::deserialize(deserializer)?, - ..Default::default() - }) - } -} - -impl KeyTrieNode { - pub fn new(name: &str, map: HashMap) -> Self { - Self { - name: name.to_string(), - map, - is_sticky: false, - } - } - - pub fn name(&self) -> &str { - &self.name - } - - /// Merge another Node in. Leaves and subnodes from the other node replace - /// corresponding keyevent in self, except when both other and self have - /// subnodes for same key. In that case the merge is recursive. - pub fn merge(&mut self, mut other: Self) { - for (key, trie) in std::mem::take(&mut other.map) { - if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) { - if let KeyTrie::Node(other_node) = trie { - node.merge(other_node); - continue; - } - } - self.map.insert(key, trie); - } - } - - pub fn infobox(&self) -> Info { - let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); - for (&key, trie) in self.iter() { - let description: &str = match trie { - KeyTrie::Leaf(command) => { - if command.name() == "no_op" { - continue; - } - command.doc() - } - KeyTrie::Node(key_trie) => key_trie.name(), - KeyTrie::Sequence(_) => "[Multiple commands]", - }; - match body.iter().position(|(_, existing_description)| &description == existing_description) { - Some(pos) => body[pos].0 += &format!(", {}", &key.to_string()), - None => body.push((key.to_string(), description)), - } - } - - Info::new(self.name(), &body) - } -} - -impl Default for KeyTrieNode { - fn default() -> Self { - Self::new("", HashMap::new()) - } -} - -impl PartialEq for KeyTrieNode { - fn eq(&self, other: &Self) -> bool { - self.map == other.map - } -} - -impl Deref for KeyTrieNode { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.map - } -} - -impl DerefMut for KeyTrieNode { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum KeyTrie { - Leaf(MappableCommand), - Sequence(Vec), - Node(KeyTrieNode), -} - -impl<'de> Deserialize<'de> for KeyTrie { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(KeyTrieVisitor) - } -} - -struct KeyTrieVisitor; - -impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { - type Value = KeyTrie; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a command, list of commands, or sub-keymap") - } - - fn visit_str(self, command: &str) -> Result - where - E: serde::de::Error, - { - command - .parse::() - .map(KeyTrie::Leaf) - .map_err(E::custom) - } - - fn visit_seq(self, mut seq: S) -> Result - where - S: serde::de::SeqAccess<'de>, - { - let mut commands = Vec::new(); - while let Some(command) = seq.next_element::<&str>()? { - commands.push( - command - .parse::() - .map_err(serde::de::Error::custom)?, - ) - } - Ok(KeyTrie::Sequence(commands)) - } - - fn visit_map(self, mut map: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - let mut mapping = HashMap::new(); - while let Some((key, value)) = map.next_entry::()? { - mapping.insert(key, value); - } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping))) - } -} - -impl KeyTrie { - pub fn node(&self) -> Option<&KeyTrieNode> { - match *self { - KeyTrie::Node(ref node) => Some(node), - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - } - } - - pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { - match *self { - KeyTrie::Node(ref mut node) => Some(node), - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - } - } - - /// Merge another KeyTrie in, assuming that this KeyTrie and the other - /// are both Nodes. Panics otherwise. - pub fn merge_nodes(&mut self, mut other: Self) { - let node = std::mem::take(other.node_mut().unwrap()); - self.node_mut().unwrap().merge(node); - } - - pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> { - let mut trie = self; - for key in keys { - trie = match trie { - KeyTrie::Node(map) => map.get(key), - // leaf encountered while keys left to process - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - }? - } - Some(trie) +use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use crate::{ + commands::MappableCommand, + keymap::{ + keytrie::KeyTrie, + keytrienode::KeyTrieNode } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum KeymapResult { - /// Needs more keys to execute a command. Contains valid keys for next keystroke. - Pending(KeyTrieNode), - Matched(MappableCommand), - /// Matched a sequence of commands to execute. - MatchedSequence(Vec), - /// Key was not found in the root keymap - NotFound, - /// Key is invalid in combination with previous keys. Contains keys leading upto - /// and including current (invalid) key. - Cancelled(Vec), -} +}; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] +/// KeyTrie starting point. pub struct Keymap { - /// Always a Node - root: KeyTrie, + pub root_node: KeyTrie } -/// A map of command names to keybinds that will execute the command. -pub type ReverseKeymap = HashMap>>; - +pub type CommandList = HashMap>; impl Keymap { - pub fn new(root: KeyTrie) -> Self { - Keymap { root } - } - - pub fn reverse_map(&self) -> ReverseKeymap { - // recursively visit all nodes in keymap - fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec) { + pub fn new(root_node: KeyTrie) -> Self { + Keymap { root_node } + } + + /// Returns a key-value list of all commands associated to a given Keymap. + /// Keys are the node names (see KeyTrieNode documentation) + /// Values are lists of stringified KeyEvents that triger the command. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". + /// Ancestor KeyEvents are in this case "space" and "w". + pub fn command_list(&self) -> CommandList { + let mut list = HashMap::new(); + _command_list(&mut list, &KeyTrieNode::KeyTrie(self.root_node.clone()), &mut String::new()); + return list; + + fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { match node { - KeyTrie::Leaf(cmd) => match cmd { - MappableCommand::Typable { name, .. } => { - cmd_map.entry(name.into()).or_default().push(keys.clone()) + KeyTrieNode::KeyTrie(trie_node) => { + for (key_event, subtrie_node) in trie_node.deref() { + let mut temp_prefix: String = prefix.to_string(); + if &temp_prefix != "" { + temp_prefix.push_str(">"); + } + temp_prefix.push_str(&key_event.to_string()); + _command_list(list, subtrie_node, &mut temp_prefix); } - MappableCommand::Static { name, .. } => cmd_map - .entry(name.to_string()) - .or_default() - .push(keys.clone()), }, - KeyTrie::Node(next) => { - for (key, trie) in &next.map { - keys.push(*key); - map_node(cmd_map, trie, keys); - keys.pop(); - } - } - KeyTrie::Sequence(_) => {} + KeyTrieNode::MappableCommand(mappable_command) => { + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + }, + KeyTrieNode::CommandSequence(_) => {} }; } - - let mut res = HashMap::new(); - map_node(&mut res, &self.root, &mut Vec::new()); - res - } - - pub fn root(&self) -> &KeyTrie { - &self.root - } - - pub fn merge(&mut self, other: Self) { - self.root.merge_nodes(other.root); - } -} - -impl Deref for Keymap { - type Target = KeyTrieNode; - - fn deref(&self) -> &Self::Target { - self.root.node().unwrap() } } impl Default for Keymap { fn default() -> Self { - Self::new(KeyTrie::Node(KeyTrieNode::default())) + Self::new(KeyTrie::default()) } } -pub struct Keymaps { - pub map: Box>>, - /// Stores pending keys waiting for the next key. This is relative to a - /// sticky node if one is in use. - state: Vec, - /// Stores the sticky node if one is activated. - pub sticky: Option, -} - -impl Keymaps { - pub fn new(map: Box>>) -> Self { - Self { - map, - state: Vec::new(), - sticky: None, - } - } - - pub fn map(&self) -> DynGuard> { - self.map.load() - } - - /// Returns list of keys waiting to be disambiguated in current mode. - pub fn pending(&self) -> &[KeyEvent] { - &self.state - } - - pub fn sticky(&self) -> Option<&KeyTrieNode> { - self.sticky.as_ref() - } - - /// Lookup `key` in the keymap to try and find a command to execute. Escape - /// key cancels pending keystrokes. If there are no pending keystrokes but a - /// sticky node is in use, it will be cleared. - pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { - // TODO: remove the sticky part and look up manually - let keymaps = &*self.map(); - let keymap = &keymaps[&mode]; - - if key!(Esc) == key { - if !self.state.is_empty() { - // Note that Esc is not included here - return KeymapResult::Cancelled(self.state.drain(..).collect()); - } - self.sticky = None; - } - - let first = self.state.get(0).unwrap_or(&key); - let trie_node = match self.sticky { - Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), - None => Cow::Borrowed(&keymap.root), - }; - - let trie = match trie_node.search(&[*first]) { - Some(KeyTrie::Leaf(ref cmd)) => { - return KeymapResult::Matched(cmd.clone()); - } - Some(KeyTrie::Sequence(ref cmds)) => { - return KeymapResult::MatchedSequence(cmds.clone()); - } - None => return KeymapResult::NotFound, - Some(t) => t, - }; - - self.state.push(key); - match trie.search(&self.state[1..]) { - Some(KeyTrie::Node(map)) => { - if map.is_sticky { - self.state.clear(); - self.sticky = Some(map.clone()); - } - KeymapResult::Pending(map.clone()) - } - Some(KeyTrie::Leaf(cmd)) => { - self.state.clear(); - KeymapResult::Matched(cmd.clone()) - } - Some(KeyTrie::Sequence(cmds)) => { - self.state.clear(); - KeymapResult::MatchedSequence(cmds.clone()) - } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), - } - } -} - -impl Default for Keymaps { - fn default() -> Self { - Self::new(Box::new(ArcSwap::new(Arc::new(default())))) - } -} +/// Returns the Keymap root KeyTrie node. +impl Deref for Keymap { + type Target = KeyTrie; -/// 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 { - keys.merge(delta.remove(mode).unwrap_or_default()) + fn deref(&self) -> &Self::Target { + &self.root_node } - config } -#[cfg(test)] -mod tests { - use super::macros::keymap; - use super::*; - use arc_swap::access::Constant; - use helix_core::hashmap; - - #[test] - #[should_panic] - fn duplicate_keys_should_panic() { - keymap!({ "Normal mode" - "i" => normal_mode, - "i" => goto_definition, - }); - } - - #[test] - fn check_duplicate_keys_in_default_keymap() { - // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro - Keymaps::default(); - } - - #[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 mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); - assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "Leaf should replace leaf" - ); - assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New leaf should be present in merged keymap" - ); - // Assumes that z is a node in the default keymap - assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "Leaf should replace node" - ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_line_end), - "Leaf should be present in merged subnode" - ); - // Assumes that `gg` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::delete_char_forward), - "Leaf should replace old leaf in merged subnode" - ); - // Assumes that `ge` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_last_line), - "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); - } - - #[test] - fn order_should_be_set() { - let config = Config { - keys: 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(); - // Make sure mapping works - assert_eq!( - keymap - .root() - .search(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - &KeyTrie::Leaf(MappableCommand::vsplit), - "Leaf should be present in merged subnode" - ); - } - - #[test] - fn aliased_modes_are_same_in_default_keymap() { - let keymaps = Keymaps::default().map(); - let root = keymaps.get(&Mode::Normal).unwrap().root(); - assert_eq!( - root.search(&[key!(' '), key!('w')]).unwrap(), - root.search(&["C-w".parse::().unwrap()]).unwrap(), - "Mismatch for window mode on `Space-w` and `Ctrl-w`" - ); - assert_eq!( - root.search(&[key!('z')]).unwrap(), - root.search(&[key!('Z')]).unwrap(), - "Mismatch for view mode on `z` and `Z`" - ); - } - - #[test] - fn reverse_map() { - let normal_mode = keymap!({ "Normal mode" - "i" => insert_mode, - "g" => { "Goto" - "g" => goto_file_start, - "e" => goto_file_end, - }, - "j" | "k" => move_line_down, - }); - let keymap = Keymap::new(normal_mode); - let mut reverse_map = keymap.reverse_map(); - - // sort keybindings in order to have consistent tests - // HashMaps can be compared but we can still get different ordering of bindings - // for commands that have multiple bindings assigned - for v in reverse_map.values_mut() { - v.sort() - } - - assert_eq!( - reverse_map, - HashMap::from([ - ("insert_mode".to_string(), vec![vec![key!('i')]]), - ( - "goto_file_start".to_string(), - vec![vec![key!('g'), key!('g')]] - ), - ( - "goto_file_end".to_string(), - vec![vec![key!('g'), key!('e')]] - ), - ( - "move_line_down".to_string(), - vec![vec![key!('j')], vec![key!('k')]] - ), - ]), - "Mismatch" - ) +/// Returns the Keymap root KeyTrie node. +impl DerefMut for Keymap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.root_node } } diff --git a/helix-term/src/keymap.rs:41:29 b/helix-term/src/keymap.rs:41:29 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index ef93dee08a77..7ce2736021b4 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,11 +1,13 @@ use std::collections::HashMap; use super::macros::keymap; -use super::{Keymap, Mode}; +use super::Keymap; +use super::keytrie::KeyTrie; +use helix_view::document::Mode; use helix_core::hashmap; pub fn default() -> HashMap { - let normal = keymap!({ "Normal mode" + let normal: KeyTrie = keymap!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, "k" | "up" => move_line_up, @@ -317,8 +319,8 @@ pub fn default() -> HashMap { "C-a" => increment, "C-x" => decrement, }); - let mut select = normal.clone(); - select.merge_nodes(keymap!({ "Select mode" + let mut select: KeyTrie = normal.clone(); + select.merge_keytrie(keymap!({ "Select mode" "h" | "left" => extend_char_left, "j" | "down" => extend_line_down, "k" | "up" => extend_line_up, @@ -345,7 +347,7 @@ pub fn default() -> HashMap { "v" => normal_mode, })); - let insert = keymap!({ "Insert mode" + let insert: KeyTrie = keymap!({ "Insert mode" "esc" => normal_mode, "C-s" => commit_undo_checkpoint, diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs new file mode 100644 index 000000000000..fe03af6d48c6 --- /dev/null +++ b/helix-term/src/keymap/keymaps.rs @@ -0,0 +1,121 @@ +use std::{sync::Arc, collections::HashMap}; +use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use helix_view::{document::Mode, input::KeyEvent}; +use crate::commands::MappableCommand; +use crate::config::Config; +use super::{macros::key, keytrienode::KeyTrieNode, default}; +use super::keytrie::KeyTrie; +use super::Keymap; +use super::default::default; + +#[derive(Debug, Clone, PartialEq)] +pub enum KeymapResult { + Pending(KeyTrie), + Matched(MappableCommand), + MatchedCommandSequence(Vec), + NotFound, + /// Contains pressed KeyEvents leading up to the cancellation. + Cancelled(Vec), +} + +pub struct Keymaps { + pub keymaps: Box>>, + /// Relative to a sticky node if Some. + pending_keys: Vec, + pub sticky_keytrie: Option, +} + +impl Keymaps { + pub fn new(keymaps: Box>>) -> Self { + Self { + keymaps, + pending_keys: Vec::new(), + sticky_keytrie: None, + } + } + + pub fn load_keymaps(&self) -> DynGuard> { + self.keymaps.load() + } + + /// Returns list of keys waiting to be disambiguated in current mode. + pub fn pending(&self) -> &[KeyEvent] { + &self.pending_keys + } + + pub fn sticky_keytrie(&self) -> Option<&KeyTrie> { + self.sticky_keytrie.as_ref() + } + + pub fn merge_with_default(mut config: Config) -> Config { + let mut delta = std::mem::replace(&mut config.keys, default::default()); + for (mode, keys) in &mut config.keys { + keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) + } + config + } + + /// Lookup `key` in the keymap to try and find a command to execute. + /// Escape key represents cancellation. + /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. + pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + // TODO: remove the sticky part and look up manually + let keymaps = &*self.load_keymaps(); + let active_keymap = &keymaps[&mode]; + + if key == key!(Esc) { + if !self.pending_keys.is_empty() { + // NOTE: Esc is not included here + return KeymapResult::Cancelled(self.pending_keys.drain(..).collect()); + } + // TODO: Shouldn't we return here also? + self.sticky_keytrie = None; + } + + // Check if sticky keytrie is to be used. + let starting_keytrie = match self.sticky_keytrie { + None => &active_keymap.root_node, + Some(ref active_sticky_keytrie) => active_sticky_keytrie, + }; + + // TODO: why check either pending or regular key? + let first_key = self.pending_keys.get(0).unwrap_or(&key); + + let pending_keytrie: KeyTrie = match starting_keytrie.traverse(&[*first_key]) { + Some(KeyTrieNode::KeyTrie(sub_keytrie)) => sub_keytrie, + Some(KeyTrieNode::MappableCommand(cmd)) => { + return KeymapResult::Matched(cmd.clone()); + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + return KeymapResult::MatchedCommandSequence(cmds.clone()); + } + None => return KeymapResult::NotFound, + }; + + self.pending_keys.push(key); + match pending_keytrie.traverse(&self.pending_keys[1..]) { + Some(KeyTrieNode::KeyTrie(map)) => { + if map.is_sticky { + self.pending_keys.clear(); + self.sticky_keytrie = Some(map.clone()); + } + KeymapResult::Pending(map.clone()) + } + Some(KeyTrieNode::MappableCommand(cmd)) => { + self.pending_keys.clear(); + KeymapResult::Matched(cmd.clone()) + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + self.pending_keys.clear(); + KeymapResult::MatchedCommandSequence(cmds.clone()) + } + None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), + } + } +} + +impl Default for Keymaps { + fn default() -> Self { + Self::new(Box::new(ArcSwap::new(Arc::new(default())))) + } +} diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs new file mode 100644 index 000000000000..ae5ecb58f12d --- /dev/null +++ b/helix-term/src/keymap/keytrie.rs @@ -0,0 +1,129 @@ +use serde::Deserialize; +use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use helix_view::{info::Info, input::KeyEvent}; +use super::keytrienode::KeyTrieNode; + +/// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode +#[derive(Debug, Clone)] +pub struct KeyTrie { + documentation: String, + children: HashMap, + pub is_sticky: bool, +} + +impl KeyTrie { + pub fn new(documentation: &str, children: HashMap) -> Self { + Self { + documentation: documentation.to_string(), + children, + is_sticky: false, + } + } + + // None symbolizes NotFound + pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { + return _traverse(self, key_events, 0); + + fn _traverse(keytrie: &KeyTrie, key_events: &[KeyEvent], mut depth: usize) -> Option { + if depth == key_events.len() { + return Some(KeyTrieNode::KeyTrie(keytrie.clone())); + } + else if let Some(found_child) = keytrie.get(&key_events[depth]) { + match found_child { + KeyTrieNode::KeyTrie(sub_keytrie) => { + depth += 1; + return _traverse(sub_keytrie, key_events, depth) + }, + _ => return Some(found_child.clone()) + } + } + return None; + } + } + + pub fn merge_keytrie(&mut self, mut other_keytrie: Self) { + for (other_key_event, other_child_node) in std::mem::take(&mut other_keytrie.children) { + match other_child_node { + KeyTrieNode::KeyTrie(other_child_key_trie) => { + if let Some(KeyTrieNode::KeyTrie(self_clashing_child_key_trie)) = self.children.get_mut(&other_key_event) { + self_clashing_child_key_trie.merge_keytrie(other_child_key_trie); + } + else { + self.children.insert(other_key_event, KeyTrieNode::KeyTrie(other_child_key_trie)); + } + } + KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { + self.children.insert(other_key_event, other_child_node); + } + } + } + } + + /// Open an Info box for a given KeyTrie + /// Shows the children listed by possible KeyEvents + /// and thier associated documentation. + pub fn infobox(&self) -> Info { + let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); + for (&key_event, key_trie) in self.iter() { + let documentation: &str = match key_trie { + KeyTrieNode::MappableCommand(command) => { + if command.name() == "no_op" { + continue; + } + command.doc() + }, + KeyTrieNode::KeyTrie(key_trie) => &key_trie.documentation, + // FIX: default to a join of all command names + // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. + // Regardless if the command sequence is different. + KeyTrieNode::CommandSequence(_) => "[Multiple commands]", + }; + match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { + Some(position) => body[position].0 += &format!(", {}", &key_event.to_string()), + None => body.push((key_event.to_string(), documentation)), + } + } + + Info::new(&self.documentation, &body) + } +} + +impl Default for KeyTrie { + fn default() -> Self { + Self::new("", HashMap::new()) + } +} + +impl PartialEq for KeyTrie { + fn eq(&self, other: &Self) -> bool { + self.children == other.children + } +} + +/// Returns the children of the KeyTrie +impl Deref for KeyTrie { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.children + } +} + +/// Returns the children of the KeyTrie +impl DerefMut for KeyTrie { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.children + } +} + +impl<'de> Deserialize<'de> for KeyTrie { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self { + children: HashMap::::deserialize(deserializer)?, + ..Default::default() + }) + } +} diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs new file mode 100644 index 000000000000..a8a0ebba384d --- /dev/null +++ b/helix-term/src/keymap/keytrienode.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; +use super::MappableCommand; +use serde::Deserialize; +use serde::de::Visitor; +use super::keytrie::KeyTrie; +use helix_view::input::KeyEvent; + +/// Each variant includes a documentaion property. +/// For the MappableCommand and CommandSequence variants, the property is self explanatory. +/// For KeyTrie, the documentation is used for respective infobox titles, +/// or infobox KeyEvent descriptions that in themselves trigger the opening of another infobox. +#[derive(Debug, Clone, PartialEq)] +pub enum KeyTrieNode { + MappableCommand(MappableCommand), + CommandSequence(Vec), + KeyTrie(KeyTrie), +} + +impl<'de> Deserialize<'de> for KeyTrieNode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(KeyTrieNodeVisitor) + } +} + +struct KeyTrieNodeVisitor; + +impl<'de> Visitor<'de> for KeyTrieNodeVisitor { + type Value = KeyTrieNode; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a KeyTrieNode") + } + + fn visit_str(self, command: &str) -> Result + where + E: serde::de::Error, + { + command + .parse::() + .map(KeyTrieNode::MappableCommand) + .map_err(E::custom) + } + + fn visit_seq(self, mut seq: S) -> Result + where + S: serde::de::SeqAccess<'de>, + { + let mut commands = Vec::new(); + while let Some(command) = seq.next_element::<&str>()? { + commands.push( + command + .parse::() + .map_err(serde::de::Error::custom)?, + ) + } + Ok(KeyTrieNode::CommandSequence(commands)) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut sub_key_trie = HashMap::new(); + while let Some((key_event, key_trie_node)) = map.next_entry::()? { + sub_key_trie.insert(key_event, key_trie_node); + } + Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", sub_key_trie))) + } +} \ No newline at end of file diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 1cf4151c9a38..4d81d220ffb9 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -80,42 +80,36 @@ macro_rules! alt { /// ``` #[macro_export] macro_rules! keymap { - (@trie $cmd:ident) => { - $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd) - }; - - (@trie - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } - ) => { - keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) - }; - - (@trie [$($cmd:ident),* $(,)?]) => { - $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*]) - }; - - ( - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } - ) => { + ({ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { // modified from the hashmap! macro { let _cap = hashmap!(@count $($($key),+),*); - let mut _map = ::std::collections::HashMap::with_capacity(_cap); + let mut _map: ::std::collections::HashMap<::helix_view::input::KeyEvent, $crate::keymap::keytrienode::KeyTrieNode> = + ::std::collections::HashMap::with_capacity(_cap); $( $( let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); - let _duplicate = _map.insert( - _key, - keymap!(@trie $value) - ); - assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); + let _potential_duplicate = _map.insert(_key,keymap!(@trie $value)); + assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); )+ )* - let mut _node = $crate::keymap::KeyTrieNode::new($label, _map); + let mut _node = $crate::keymap::keytrie::KeyTrie::new($label, _map); $( _node.is_sticky = $sticky; )? - $crate::keymap::KeyTrie::Node(_node) + _node } }; + + (@trie {$label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { + $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) + }; + + (@trie $cmd:ident) => { + $crate::keymap::keytrienode::KeyTrieNode::MappableCommand($crate::commands::MappableCommand::$cmd) + }; + + (@trie [$($cmd:ident),* $(,)?]) => { + $crate::keymap::keytrienode::KeyTrieNode::CommandSequence(vec![$($crate::commands::Command::$cmd),*]) + }; } pub use alt; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs new file mode 100644 index 000000000000..55420d1f5f14 --- /dev/null +++ b/helix-term/src/keymap/tests.rs @@ -0,0 +1,181 @@ +#[macro_use] +#[cfg(test)] +mod tests { + use arc_swap::access::Constant; + use helix_core::hashmap; + use helix_view::{document::Mode, input::KeyEvent}; + use crate::config::Config; + use crate::commands::MappableCommand; + use crate::keymap::*; + use keymaps::{Keymaps, KeymapResult}; + use macros::*; + + #[test] + #[should_panic] + fn duplicate_keys_should_panic() { + keymap!({ "Normal mode" + "i" => normal_mode, + "i" => goto_definition, + }); + } + + #[test] + fn check_duplicate_keys_in_default_keymap() { + // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro + Keymaps::default(); + } + + #[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 mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + + let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + assert_eq!( + keymap.get(Mode::Normal, key!('i')), + KeymapResult::Matched(MappableCommand::normal_mode), + "New mappable command should ovveride default." + ); + assert_eq!( + keymap.get(Mode::Normal, key!('无')), + KeymapResult::Matched(MappableCommand::insert_mode), + "New mappable command should be present in merged keymap." + ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(Mode::Normal, key!('z')), + KeymapResult::Matched(MappableCommand::jump_backward), + "New Mappable command should replace default sub keytrie." + ); + + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), + "Mappable command should be present in merged keytrie." + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), + "Mappable command should replace default in merged keytrie." + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), + "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." + ); + + assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_config.keys.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, + }, + }, + }) + ) + }, + ..Default::default() + }; + let mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Make sure mapping works + assert_eq!( + keymap_normal + .root_node + .traverse(&[key!(' '), key!('s'), key!('v')]) + .unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::vsplit), + "Mappable command should be present in merged keytrie." + ); + } + + #[test] + fn aliased_modes_are_same_in_default_keymap() { + let keymaps = Keymaps::default().keymaps; + let root = keymaps.load().get(&Mode::Normal).unwrap().root_node.clone(); + assert_eq!( + root.traverse(&[key!(' '), key!('w')]).unwrap(), + root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + "Mismatch for window mode on `Space-w` and `Ctrl-w`." + ); + assert_eq!( + root.traverse(&[key!('z')]).unwrap(), + root.traverse(&[key!('Z')]).unwrap(), + "Mismatch for view mode on `z` and `Z`." + ); + } + + #[test] + fn command_list() { + let normal_mode = keymap!({ "Normal mode" + "i" => insert_mode, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_file_end, + }, + "j" | "k" => move_line_down, + }); + let keymap = Keymap::new(normal_mode); + let mut command_list = keymap.command_list(); + + // sort keybindings in order to have consistent tests + // HashMaps can be compared but we can still get different ordering of bindings + // for commands that have multiple bindings assigned + for v in command_list.values_mut() { + v.sort() + } + + assert_eq!( + command_list, + HashMap::from([ + ( + "insert_mode".to_string(), + vec![key!('i').to_string()] + ), + ( + "goto_file_start".to_string(), + vec![format!("{}>{}", key!('g'), key!('g'))] + ), + ( + "goto_file_end".to_string(), + vec![format!("{}>{}", key!('g'), key!('e'))] + ), + ( + "move_line_down".to_string(), + vec![key!('j').to_string(), key!('k').to_string()] + ) + ]), + "Mismatch" + ) + } +} \ No newline at end of file diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index a945b20dedaf..58ae12c2e120 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -11,6 +11,7 @@ pub mod job; pub mod keymap; pub mod ui; pub use keymap::macros::*; +pub use keymap::tests; #[cfg(not(windows))] fn true_color() -> bool { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a0518964e2a7..bd5d99fbcbaa 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::{KeymapResult, Keymaps}, + keymap::keymaps::{KeymapResult, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -936,7 +936,7 @@ impl EditorView { let mut last_mode = mode; self.pseudo_pending.extend(self.keymaps.pending()); let key_result = self.keymaps.get(mode, event); - cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); + cxt.editor.autoinfo = self.keymaps.sticky_keytrie().map(|node| node.infobox()); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); @@ -976,7 +976,7 @@ impl EditorView { execute_command(command); } KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), - KeymapResult::MatchedSequence(commands) => { + KeymapResult::MatchedCommandSequence(commands) => { for command in commands { execute_command(command); } From d5520e84c7d4ab1c8265dbcbb000ee35d0ebea7b Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 26 Dec 2022 19:08:52 +0100 Subject: [PATCH 037/195] Rename MappableCommand field doc to description: For it not to be confused with the upcoming ":help" feature. --- helix-term/src/commands.rs | 24 ++++++++++++------------ helix-term/src/keymap/keytrie.rs | 5 ++--- helix-term/src/main.rs | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7e892d8606f6..b4f64296852c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -133,23 +133,23 @@ pub enum MappableCommand { Typable { name: String, args: Vec, - doc: String, + description: String, }, Static { name: &'static str, fun: fn(cx: &mut Context), - doc: &'static str, + description: &'static str, }, } macro_rules! static_commands { - ( $($name:ident, $doc:literal,)* ) => { + ( $($name:ident, $description:literal,)* ) => { $( #[allow(non_upper_case_globals)] pub const $name: Self = Self::Static { name: stringify!($name), fun: $name, - doc: $doc + description: $description }; )* @@ -162,7 +162,7 @@ macro_rules! static_commands { impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { - Self::Typable { name, args, doc: _ } => { + Self::Typable { name, args, description: _ } => { let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { @@ -186,10 +186,10 @@ impl MappableCommand { } } - pub fn doc(&self) -> &str { + pub fn description(&self) -> &str { match &self { - Self::Typable { doc, .. } => doc, - Self::Static { doc, .. } => doc, + Self::Typable { description, .. } => description, + Self::Static { description, .. } => description, } } @@ -476,7 +476,7 @@ impl std::str::FromStr for MappableCommand { .get(name) .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), - doc: format!(":{} {:?}", cmd.name, args), + description: format!(":{} {:?}", cmd.name, args), args, }) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) @@ -2458,11 +2458,11 @@ impl ui::menu::Item for MappableCommand { fn label(&self, keymap: &Self::Data) -> Spans { match self { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { + MappableCommand::Typable { description: doc, name, .. } => match keymap.get(name as &String) { Some(key_events) => format!("{} {:?} ':{}'", doc, key_events, name).into(), None => format!("{} ':{}'", doc, name).into(), }, - MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { + MappableCommand::Static { description: doc, name, .. } => match keymap.get(*name) { Some(key_events) => format!("{} {:?} '{}'", doc, key_events, name).into(), None => format!("{} '{}'", doc, name).into(), }, @@ -2481,7 +2481,7 @@ pub fn command_palette(cx: &mut Context) { commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { MappableCommand::Typable { name: cmd.name.to_owned(), - doc: cmd.doc.to_owned(), + description: cmd.doc.to_owned(), args: Vec::new(), } })); diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index ae5ecb58f12d..25ab0b196afe 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -60,8 +60,7 @@ impl KeyTrie { } /// Open an Info box for a given KeyTrie - /// Shows the children listed by possible KeyEvents - /// and thier associated documentation. + /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self) -> Info { let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); for (&key_event, key_trie) in self.iter() { @@ -70,7 +69,7 @@ impl KeyTrie { if command.name() == "no_op" { continue; } - command.doc() + command.description() }, KeyTrieNode::KeyTrie(key_trie) => &key_trie.documentation, // FIX: default to a join of all command names diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c5379f37..bae827e30402 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -128,7 +128,7 @@ FLAGS: let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::merge_keys) + .map(helix_term::keymap::keymaps::Keymaps::merge_with_default) .unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); From aea10f70513f155135725853dbf984e2e9f7e8e2 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 26 Dec 2022 22:44:50 +0100 Subject: [PATCH 038/195] Initial sorting of keymap info window --- helix-term/src/keymap/keytrie.rs | 39 ++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 25ab0b196afe..8020b6caafbf 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -62,7 +62,7 @@ impl KeyTrie { /// Open an Info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self) -> Info { - let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); + let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); for (&key_event, key_trie) in self.iter() { let documentation: &str = match key_trie { KeyTrieNode::MappableCommand(command) => { @@ -78,12 +78,43 @@ impl KeyTrie { KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { - Some(position) => body[position].0 += &format!(", {}", &key_event.to_string()), - None => body.push((key_event.to_string(), documentation)), + Some(position) => body[position].0.push(key_event.to_string()), + None => { + let mut temp_vec: Vec = Vec::new(); + temp_vec.push(key_event.to_string()); + body.push((temp_vec, documentation)) + }, } } + // OPTIMIZATIONS? + // Change the children hashmap to an ordered datastructure? Like a regulap map maybe? + // Add sorted conditional? + // Add body as a keytrie field and reload it in keytrie merge? + + // Make the shortest keyevents appear first + let mut sorted_body = body + .iter() + .map(|(key_events, description)| { + let mut temp_key_events = key_events.clone(); + temp_key_events.sort_unstable_by(|a, b| a.len().cmp(&b.len())); + (temp_key_events, *description) + }) + .collect::, &str)>>(); + sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + + let stringified_key_events_body: Vec<(String, &str)> = sorted_body + .iter() + .map(|(key_events, description)| { + let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { + if acc.is_empty() { acc.push_str(key_event); } + else { acc.push_str(&format!(", {}", key_event)) } + acc + }); + (key_events_string, *description) + }) + .collect(); - Info::new(&self.documentation, &body) + Info::new(&self.documentation, &stringified_key_events_body) } } From 7f86d01407840a7e1d9a9fda62279af16c80df88 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 12:08:12 +0100 Subject: [PATCH 039/195] Infobox: Consistently place lowercase equivalents first This, along with previous commit reverts regression in #952 --- helix-term/src/keymap/keytrie.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 8020b6caafbf..e49b33d501b1 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -91,7 +91,7 @@ impl KeyTrie { // Add sorted conditional? // Add body as a keytrie field and reload it in keytrie merge? - // Make the shortest keyevents appear first + // Make the shortest keyevent appear first let mut sorted_body = body .iter() .map(|(key_events, description)| { @@ -102,6 +102,27 @@ impl KeyTrie { .collect::, &str)>>(); sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + // Consistently place lowercase before uppercase of the same letter. + if sorted_body.len() > 1 { + let mut x_index = 0; + let mut y_index = 1; + + while y_index < sorted_body.len() { + let x = &sorted_body[x_index].0[0]; + let y = &sorted_body[y_index].0[0]; + if x.to_lowercase() == y.to_lowercase() { + // Uppercase regarded as lower value. + if x < y { + let temp_holder = sorted_body[x_index].clone(); + sorted_body[x_index] = sorted_body[y_index].clone(); + sorted_body[y_index] = temp_holder; + } + } + x_index = y_index; + y_index += 1; + } + } + let stringified_key_events_body: Vec<(String, &str)> = sorted_body .iter() .map(|(key_events, description)| { From 7935f77a98afcfe913b98148cc6c219532a6e18d Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 13:15:46 +0100 Subject: [PATCH 040/195] Remove infobox optimization suggestion comment: Sort duration calculations averaged to about 0.04 ms when opening the larger infoboxes. --- helix-term/src/keymap/keytrie.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index e49b33d501b1..fa115770ed83 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -86,10 +86,6 @@ impl KeyTrie { }, } } - // OPTIMIZATIONS? - // Change the children hashmap to an ordered datastructure? Like a regulap map maybe? - // Add sorted conditional? - // Add body as a keytrie field and reload it in keytrie merge? // Make the shortest keyevent appear first let mut sorted_body = body @@ -101,7 +97,6 @@ impl KeyTrie { }) .collect::, &str)>>(); sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); - // Consistently place lowercase before uppercase of the same letter. if sorted_body.len() > 1 { let mut x_index = 0; From 0482d097337220d78f496ce7e28810ca5b501bf4 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 13:50:05 +0100 Subject: [PATCH 041/195] keymap testing touchups --- helix-term/src/keymap/keytrie.rs | 3 ++- helix-term/src/keymap/tests.rs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index fa115770ed83..89c441e2974a 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -87,7 +87,8 @@ impl KeyTrie { } } - // Make the shortest keyevent appear first + // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent + // Those events will always be placed after the one letter KeyEvent let mut sorted_body = body .iter() .map(|(key_events, description)| { diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 55420d1f5f14..557debb94cb1 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -89,7 +89,7 @@ mod tests { } #[test] - fn order_should_be_set() { + fn merges_with_default_keymap_config() { let config = Config { keys: hashmap! { Mode::Normal => Keymap::new( @@ -105,10 +105,9 @@ mod tests { }, ..Default::default() }; - let mut merged_config = merge_keys(config.clone()); + let mut merged_config = Keymaps::merge_with_default(config.clone()); assert_ne!(config, merged_config); let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Make sure mapping works assert_eq!( keymap_normal .root_node From 6b01af59ee6d8648857db40fe0b75984ec092a65 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 14:33:32 +0100 Subject: [PATCH 042/195] Exclude config no_op bindings in command palette. --- helix-term/src/keymap.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index edfa41df2705..58433629f09b 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -52,7 +52,8 @@ impl Keymap { } }, KeyTrieNode::MappableCommand(mappable_command) => { - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + if mappable_command.name() == "no_op" { return } + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); }, KeyTrieNode::CommandSequence(_) => {} }; From 7a8397c061e977001af135ba20f2aaa5b55e31a4 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 20:22:59 +0100 Subject: [PATCH 043/195] Cleaner infobox join operation --- helix-term/src/keymap/keytrie.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 89c441e2974a..227a0a5b7d9c 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -123,8 +123,8 @@ impl KeyTrie { .iter() .map(|(key_events, description)| { let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { - if acc.is_empty() { acc.push_str(key_event); } - else { acc.push_str(&format!(", {}", key_event)) } + if !acc.is_empty() { acc.push_str(", "); } + acc.push_str(key_event); acc }); (key_events_string, *description) From 546010bde7e2ae121b75ae3bd79f1452677a484e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 28 Dec 2022 17:48:59 +0100 Subject: [PATCH 044/195] use statement cleanups --- helix-term/src/application.rs | 15 ++++++--------- helix-term/src/commands.rs | 28 +++++++++++----------------- helix-term/src/config.rs | 7 ++----- helix-term/src/keymap.rs | 9 +++++---- helix-term/src/keymap.rs:41:29 | 0 helix-term/src/keymap/default.rs | 7 ++----- helix-term/src/keymap/keymaps.rs | 12 ++++-------- helix-term/src/keymap/keytrie.rs | 6 +++--- helix-term/src/keymap/keytrienode.rs | 8 +++----- helix-term/src/keymap/tests.rs | 8 ++------ 10 files changed, 38 insertions(+), 62 deletions(-) delete mode 100644 helix-term/src/keymap.rs:41:29 diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 42daaa64d44a..b614f5395c12 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,5 +1,3 @@ -use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -15,9 +13,6 @@ use helix_view::{ tree::Layout, Align, Editor, }; -use serde_json::json; -use tui::backend::Backend; - use crate::{ args::Args, commands::apply_workspace_edit, @@ -27,16 +22,17 @@ use crate::{ keymap::keymaps::Keymaps, ui::{self, overlay::overlayed}, }; - -use log::{debug, error, warn}; use std::{ io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; - +use arc_swap::{access::Map, ArcSwap}; +use futures_util::Stream; +use log::{debug, error, warn}; use anyhow::{Context, Error}; - +use serde_json::json; +use tui::backend::Backend; use crossterm::{ event::{ DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, @@ -45,6 +41,7 @@ use crossterm::{ execute, terminal, tty::IsTty, }; + #[cfg(not(windows))] use { signal_hook::{consts::signal, low_level}, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b4f64296852c..7255995186cf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,11 +3,11 @@ pub(crate) mod lsp; pub(crate) mod typed; pub use dap::*; -use helix_vcs::Hunk; pub use lsp::*; use tui::widgets::Row; pub use typed::*; +use helix_vcs::Hunk; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, @@ -15,7 +15,7 @@ use helix_core::{ indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, - movement::{self, Direction}, + movement::{self, Direction, Movement}, object, pos_at_coords, pos_at_visual_coords, regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, @@ -36,33 +36,27 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, }; - -use anyhow::{anyhow, bail, ensure, Context as _}; -use fuzzy_matcher::FuzzyMatcher; -use insert::*; -use movement::Movement; - use crate::{ + commands::insert::*, args, compositor::{self, Component, Compositor}, - job::Callback, + job::{Callback, self, Jobs}, keymap::CommandList, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; - -use crate::job::{self, Jobs}; -use futures_util::StreamExt; -use std::{collections::HashMap, fmt, future::Future}; -use std::{collections::HashSet, num::NonZeroUsize}; - use std::{ + collections::{HashMap, HashSet}, + num::NonZeroUsize, + future::Future, borrow::Cow, path::{Path, PathBuf}, + fmt, }; - +use anyhow::{anyhow, bail, ensure, Context as _}; +use fuzzy_matcher::FuzzyMatcher; +use futures_util::StreamExt; use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; - use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 471b8c168a79..d924bff79c90 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,11 +1,8 @@ use crate::keymap::{default::default, Keymap, keymaps::Keymaps}; use helix_view::document::Mode; -use serde::Deserialize; -use std::collections::HashMap; -use std::fmt::Display; -use std::io::Error as IOError; -use std::path::PathBuf; +use std::{fmt::Display, collections::HashMap, io::Error as IOError}; use toml::de::Error as TomlError; +use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 58433629f09b..f5cbcec212a5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,12 +1,11 @@ pub mod keymaps; -pub mod keytrienode; pub mod default; pub mod macros; -pub mod keytrie; pub mod tests; -use serde::Deserialize; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; +mod keytrie; +mod keytrienode; + use crate::{ commands::MappableCommand, keymap::{ @@ -14,6 +13,8 @@ use crate::{ keytrienode::KeyTrieNode } }; +use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] diff --git a/helix-term/src/keymap.rs:41:29 b/helix-term/src/keymap.rs:41:29 deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 7ce2736021b4..b74ae84c7b63 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,10 +1,7 @@ -use std::collections::HashMap; - -use super::macros::keymap; -use super::Keymap; -use super::keytrie::KeyTrie; +use super::{macros::keymap, Keymap, keytrie::KeyTrie}; use helix_view::document::Mode; use helix_core::hashmap; +use std::collections::HashMap; pub fn default() -> HashMap { let normal: KeyTrie = keymap!({ "Normal mode" diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index fe03af6d48c6..ec9ecbce8eb7 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -1,12 +1,8 @@ +use super::*; +use crate::{commands::MappableCommand, config::Config}; +use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; -use helix_view::{document::Mode, input::KeyEvent}; -use crate::commands::MappableCommand; -use crate::config::Config; -use super::{macros::key, keytrienode::KeyTrieNode, default}; -use super::keytrie::KeyTrie; -use super::Keymap; -use super::default::default; #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { @@ -116,6 +112,6 @@ impl Keymaps { impl Default for Keymaps { fn default() -> Self { - Self::new(Box::new(ArcSwap::new(Arc::new(default())))) + Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) } } diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 227a0a5b7d9c..0706182db228 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,7 +1,7 @@ -use serde::Deserialize; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; -use helix_view::{info::Info, input::KeyEvent}; use super::keytrienode::KeyTrieNode; +use helix_view::{info::Info, input::KeyEvent}; +use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; +use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index a8a0ebba384d..41b5a7cd3343 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,9 +1,7 @@ -use std::collections::HashMap; -use super::MappableCommand; -use serde::Deserialize; -use serde::de::Visitor; -use super::keytrie::KeyTrie; +use super::{MappableCommand, keytrie::KeyTrie}; use helix_view::input::KeyEvent; +use std::collections::HashMap; +use serde::{Deserialize, de::Visitor}; /// Each variant includes a documentaion property. /// For the MappableCommand and CommandSequence variants, the property is self explanatory. diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 557debb94cb1..cc06e9992b30 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,14 +1,10 @@ #[macro_use] #[cfg(test)] mod tests { - use arc_swap::access::Constant; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::config::Config; - use crate::commands::MappableCommand; - use crate::keymap::*; - use keymaps::{Keymaps, KeymapResult}; - use macros::*; + use crate::{config::Config, commands::MappableCommand, keymap::*}; + use arc_swap::access::Constant; #[test] #[should_panic] From 036ce4abea1fd6d4deab00e5fb4172f7f9f00d80 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 28 Dec 2022 18:44:26 +0100 Subject: [PATCH 045/195] Move Config related tests from keymap tests Config tests --- helix-term/src/config.rs | 127 +++++++++++++++++++++++++++++---- helix-term/src/keymap.rs | 5 +- helix-term/src/keymap/tests.rs | 97 +------------------------ 3 files changed, 118 insertions(+), 111 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index d924bff79c90..21fbb60ea016 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,8 +1,8 @@ -use crate::keymap::{default::default, Keymap, keymaps::Keymaps}; +use crate::keymap::{default::default, keymaps::Keymaps, Keymap}; use helix_view::document::Mode; -use std::{fmt::Display, collections::HashMap, io::Error as IOError}; -use toml::de::Error as TomlError; use serde::Deserialize; +use std::{collections::HashMap, fmt::Display, io::Error as IOError}; +use toml::de::Error as TomlError; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] @@ -25,7 +25,7 @@ impl Default for Config { } #[derive(Debug)] -pub enum ConfigLoadError { +pub enum ConfigLoadError { BadConfig(TomlError), Error(IOError), } @@ -40,7 +40,7 @@ impl Display for ConfigLoadError { } impl Config { - // REFACTOR? code similar to config assignment in main.rs, + // REFACTOR? code similar to config assignment in main.rs, pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) @@ -53,15 +53,23 @@ impl Config { #[cfg(test)] mod tests { - use super::*; + use crate::{ + commands::MappableCommand, + config::Config, + keymap::{ + default, + keymaps::{KeymapResult, Keymaps}, + keytrienode::KeyTrieNode, + macros::*, + Keymap, + }, + }; + use arc_swap::access::Constant; + use helix_core::hashmap; + use helix_view::document::Mode; #[test] fn parsing_keymaps_config_file() { - use crate::keymap; - use crate::keymap::Keymap; - use helix_core::hashmap; - use helix_view::document::Mode; - let sample_keymaps = r#" [keys.insert] y = "move_line_down" @@ -92,10 +100,103 @@ mod tests { fn keys_resolve_to_correct_defaults() { // From serde default let default_keys = toml::from_str::("").unwrap().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, default::default()); // From the Default trait let default_keys = Config::default().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, default::default()); + } + + #[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 mut merged_config = Keymaps::merge_with_default(config.clone()); + assert_ne!(config, merged_config); + + let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + assert_eq!( + keymap.get(Mode::Normal, key!('i')), + KeymapResult::Matched(MappableCommand::normal_mode), + "New mappable command should ovveride default." + ); + assert_eq!( + keymap.get(Mode::Normal, key!('无')), + KeymapResult::Matched(MappableCommand::insert_mode), + "New mappable command should be present in merged keymap." + ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(Mode::Normal, key!('z')), + KeymapResult::Matched(MappableCommand::jump_backward), + "New Mappable command should replace default sub keytrie." + ); + + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), + "Mappable command should be present in merged keytrie." + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), + "Mappable command should replace default in merged keytrie." + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), + "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." + ); + + assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + } + + #[test] + fn merges_with_default_keymap_config() { + let config = Config { + keys: hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, + }, + }, + }) + ) + }, + ..Default::default() + }; + let mut merged_config = Keymaps::merge_with_default(config.clone()); + assert_ne!(config, merged_config); + let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + assert_eq!( + keymap_normal + .root_node + .traverse(&[key!(' '), key!('s'), key!('v')]) + .unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::vsplit), + "Mappable command should be present in merged keytrie." + ); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f5cbcec212a5..601046600b5e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -2,9 +2,8 @@ pub mod keymaps; pub mod default; pub mod macros; pub mod tests; - -mod keytrie; -mod keytrienode; +pub mod keytrienode; +pub mod keytrie; use crate::{ commands::MappableCommand, diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index cc06e9992b30..157230d816d3 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,8 +3,8 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::{config::Config, commands::MappableCommand, keymap::*}; - use arc_swap::access::Constant; + use crate::keymap::*; + use std::collections::HashMap; #[test] #[should_panic] @@ -21,99 +21,6 @@ mod tests { Keymaps::default(); } - #[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 mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); - assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "New mappable command should ovveride default." - ); - assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New mappable command should be present in merged keymap." - ); - // Assumes that z is a node in the default keymap - assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "New Mappable command should replace default sub keytrie." - ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap - assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), - "Mappable command should be present in merged keytrie." - ); - // Assumes that `gg` is in default keymap - assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), - "Mappable command should replace default in merged keytrie." - ); - // Assumes that `ge` is in default keymap - assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), - "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." - ); - - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); - } - - #[test] - fn merges_with_default_keymap_config() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, - }, - }) - ) - }, - ..Default::default() - }; - let mut merged_config = Keymaps::merge_with_default(config.clone()); - assert_ne!(config, merged_config); - let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - assert_eq!( - keymap_normal - .root_node - .traverse(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::vsplit), - "Mappable command should be present in merged keytrie." - ); - } - #[test] fn aliased_modes_are_same_in_default_keymap() { let keymaps = Keymaps::default().keymaps; From ac98d82cd738ca7e4448634f496adbcaae8fba33 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 28 Dec 2022 19:35:34 +0100 Subject: [PATCH 046/195] Config test cleanup * Elaborated on test descriptions * Removed duplicate unit test * Unified keytrie traversal in tests --- helix-term/src/config.rs | 84 ++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 21fbb60ea016..40d98378da10 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -58,18 +58,17 @@ mod tests { config::Config, keymap::{ default, - keymaps::{KeymapResult, Keymaps}, + keymaps::Keymaps, keytrienode::KeyTrieNode, macros::*, Keymap, }, }; - use arc_swap::access::Constant; use helix_core::hashmap; use helix_view::document::Mode; #[test] - fn parsing_keymaps_config_file() { + fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] y = "move_line_down" @@ -108,8 +107,8 @@ mod tests { } #[test] - fn merge_partial_keys() { - let config = Config { + fn user_config_merges_with_default() { + let user_config = Config { keys: hashmap! { Mode::Normal => Keymap::new( keymap!({ "Normal mode" @@ -125,78 +124,51 @@ mod tests { }, ..Default::default() }; - let mut merged_config = Keymaps::merge_with_default(config.clone()); - assert_ne!(config, merged_config); + let mut merged_config = Keymaps::merge_with_default(user_config.clone()); + assert_ne!( + user_config, + merged_config, + "Merged user keymap with default should differ from user keymap." + ); - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + let keymap_normal_root_key_trie = &merged_config.keys.get_mut(&Mode::Normal).unwrap().root_node; assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "New mappable command should ovveride default." + keymap_normal_root_key_trie.traverse(&[key!('i')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::normal_mode), + "User supplied mappable command should ovveride default mappable command bound to the same key event." ); assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New mappable command should be present in merged keymap." + keymap_normal_root_key_trie.traverse(&[key!('无')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::insert_mode), + "User supplied mappable command of new key event should be present in merged keymap." ); // Assumes that z is a node in the default keymap assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "New Mappable command should replace default sub keytrie." + keymap_normal_root_key_trie.traverse(&[key!('z')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::jump_backward), + "User supplied mappable command should replace a sub keytrie from default keymap bound to the same key event." ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap + // Assumes that `g` is a sub key trie in default keymap assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), + keymap_normal_root_key_trie.traverse(&[key!('g'), key!('$')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), - "Mappable command should be present in merged keytrie." + "User supplied mappable command should be inserted under the correct sub keytrie." ); // Assumes that `gg` is in default keymap assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), + keymap_normal_root_key_trie.traverse(&[key!('g'), key!('g')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), - "Mappable command should replace default in merged keytrie." + "User supplied mappable command should replace default even in sub keytries." ); // Assumes that `ge` is in default keymap assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), + keymap_normal_root_key_trie.traverse(&[key!('g'), key!('e')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), - "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." + "Default mappable commands that aren't ovveridden should exist in merged keymap." ); + // Huh? assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); } - - #[test] - fn merges_with_default_keymap_config() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, - }, - }) - ) - }, - ..Default::default() - }; - let mut merged_config = Keymaps::merge_with_default(config.clone()); - assert_ne!(config, merged_config); - let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - assert_eq!( - keymap_normal - .root_node - .traverse(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::vsplit), - "Mappable command should be present in merged keytrie." - ); - } } From 109228bd89ce7225a576ab8973a37d627c8ff906 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 08:38:17 +0100 Subject: [PATCH 047/195] Load keymap config consistently * Moved config merging Keymaps to Config * Removed unused default EditorView function --- helix-term/src/config.rs | 18 +++++++++++++----- helix-term/src/keymap/keymaps.rs | 11 ++--------- helix-term/src/main.rs | 2 +- helix-term/src/ui/editor.rs | 6 ------ 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 40d98378da10..eae1da41fc94 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,4 +1,4 @@ -use crate::keymap::{default::default, keymaps::Keymaps, Keymap}; +use crate::keymap::{default, Keymap}; use helix_view::document::Mode; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, io::Error as IOError}; @@ -8,7 +8,7 @@ use toml::de::Error as TomlError; #[serde(deny_unknown_fields)] pub struct Config { pub theme: Option, - #[serde(default = "default")] + #[serde(default = "default::default")] pub keys: HashMap, #[serde(default)] pub editor: helix_view::editor::Config, @@ -18,7 +18,7 @@ impl Default for Config { fn default() -> Config { Config { theme: None, - keys: default(), + keys: default::default(), editor: helix_view::editor::Config::default(), } } @@ -44,11 +44,19 @@ impl Config { pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(Keymaps::merge_with_default) + .map(self::Config::merge_in_default_keymap) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } } + + pub fn merge_in_default_keymap(mut config: Config) -> Config { + let mut delta = std::mem::replace(&mut config.keys, default::default()); + for (mode, keys) in &mut config.keys { + keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) + } + config + } } #[cfg(test)] @@ -124,7 +132,7 @@ mod tests { }, ..Default::default() }; - let mut merged_config = Keymaps::merge_with_default(user_config.clone()); + let mut merged_config = Keymaps::merge_in_default_keymap(user_config.clone()); assert_ne!( user_config, merged_config, diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index ec9ecbce8eb7..5d899f49c7a2 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{commands::MappableCommand, config::Config}; +use crate::commands::MappableCommand; use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; @@ -43,14 +43,6 @@ impl Keymaps { self.sticky_keytrie.as_ref() } - pub fn merge_with_default(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default::default()); - for (mode, keys) in &mut config.keys { - keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) - } - config - } - /// Lookup `key` in the keymap to try and find a command to execute. /// Escape key represents cancellation. /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. @@ -110,6 +102,7 @@ impl Keymaps { } } +// NOTE: Only used for testing purposes impl Default for Keymaps { fn default() -> Self { Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index bae827e30402..f3cfd0800ed0 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -128,7 +128,7 @@ FLAGS: let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::keymaps::Keymaps::merge_with_default) + .map(Config::merge_in_default_keymap) .unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index bd5d99fbcbaa..98da59c95097 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -47,12 +47,6 @@ pub enum InsertEvent { TriggerCompletion, } -impl Default for EditorView { - fn default() -> Self { - Self::new(Keymaps::default()) - } -} - impl EditorView { pub fn new(keymaps: Keymaps) -> Self { Self { From 527198a6ffc2886130d1b1b9d4d31b1b322fd4f7 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 18:58:53 +0100 Subject: [PATCH 048/195] Make use of new self context in keymap config load --- helix-term/src/config.rs | 13 ++++++------- helix-term/src/main.rs | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index eae1da41fc94..57fe28a84045 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -44,18 +44,18 @@ impl Config { pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(self::Config::merge_in_default_keymap) + .map(|config: Config| config.merge_in_default_keymap()) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } } - pub fn merge_in_default_keymap(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default::default()); - for (mode, keys) in &mut config.keys { + pub fn merge_in_default_keymap(mut self) -> Config { + let mut delta = std::mem::replace(&mut self.keys, default::default()); + for (mode, keys) in &mut self.keys { keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) } - config + self } } @@ -66,7 +66,6 @@ mod tests { config::Config, keymap::{ default, - keymaps::Keymaps, keytrienode::KeyTrieNode, macros::*, Keymap, @@ -132,7 +131,7 @@ mod tests { }, ..Default::default() }; - let mut merged_config = Keymaps::merge_in_default_keymap(user_config.clone()); + let mut merged_config = user_config.clone().merge_in_default_keymap(); assert_ne!( user_config, merged_config, diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index f3cfd0800ed0..67e4f4a0cd55 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -128,7 +128,7 @@ FLAGS: let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(Config::merge_in_default_keymap) + .map(|config: Config| config.merge_in_default_keymap()) .unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); From 917ca0d52289db71f26b427656baa3df89e54e98 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 21:27:37 +0100 Subject: [PATCH 049/195] Minor keymap module visibilyti cleanup --- helix-term/src/keymap.rs | 3 ++- helix-term/src/keymap/keymaps.rs | 1 - helix-term/src/lib.rs | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 601046600b5e..880b405f604e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,9 +1,10 @@ pub mod keymaps; pub mod default; pub mod macros; -pub mod tests; +// NOTE: Only pub becuase of their use in macros pub mod keytrienode; pub mod keytrie; +mod tests; use crate::{ commands::MappableCommand, diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index 5d899f49c7a2..f824401b1833 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -102,7 +102,6 @@ impl Keymaps { } } -// NOTE: Only used for testing purposes impl Default for Keymaps { fn default() -> Self { Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index 58ae12c2e120..fc8e934e1a7d 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -10,8 +10,6 @@ pub mod health; pub mod job; pub mod keymap; pub mod ui; -pub use keymap::macros::*; -pub use keymap::tests; #[cfg(not(windows))] fn true_color() -> bool { From 8b8fadb695733ce5961dec896091697cc4c9c985 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 21:54:41 +0100 Subject: [PATCH 050/195] Renamed the keymap! macro to keytrie!: It produces, after all, a Keytrie. It would be a bit like saying: let engine = car!() --- helix-term/src/config.rs | 6 +++--- helix-term/src/keymap/default.rs | 10 +++++----- helix-term/src/keymap/macros.rs | 18 +++++++++++++----- helix-term/src/keymap/tests.rs | 4 ++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 57fe28a84045..fc724d818ff0 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -89,11 +89,11 @@ mod tests { toml::from_str::(sample_keymaps).unwrap(), Config { keys: hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" + Mode::Insert => Keymap::new(keytrie!({ "Insert mode" "y" => move_line_down, "S-C-a" => delete_selection, })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" + Mode::Normal => Keymap::new(keytrie!({ "Normal mode" "A-F12" => move_next_word_end, })), }, @@ -118,7 +118,7 @@ mod tests { let user_config = Config { keys: hashmap! { Mode::Normal => Keymap::new( - keymap!({ "Normal mode" + keytrie!({ "Normal mode" "i" => normal_mode, "无" => insert_mode, "z" => jump_backward, diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index b74ae84c7b63..551cf1a79bed 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,10 +1,10 @@ -use super::{macros::keymap, Keymap, keytrie::KeyTrie}; +use super::{macros::keytrie, Keymap}; use helix_view::document::Mode; use helix_core::hashmap; use std::collections::HashMap; pub fn default() -> HashMap { - let normal: KeyTrie = keymap!({ "Normal mode" + let normal = keytrie!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, "k" | "up" => move_line_up, @@ -316,8 +316,8 @@ pub fn default() -> HashMap { "C-a" => increment, "C-x" => decrement, }); - let mut select: KeyTrie = normal.clone(); - select.merge_keytrie(keymap!({ "Select mode" + let mut select = normal.clone(); + select.merge_keytrie(keytrie!({ "Select mode" "h" | "left" => extend_char_left, "j" | "down" => extend_line_down, "k" | "up" => extend_line_up, @@ -344,7 +344,7 @@ pub fn default() -> HashMap { "v" => normal_mode, })); - let insert: KeyTrie = keymap!({ "Insert mode" + let insert = keytrie!({ "Insert mode" "esc" => normal_mode, "C-s" => commit_undo_checkpoint, diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 4d81d220ffb9..66fa78f7a34b 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -66,9 +66,8 @@ macro_rules! alt { /// /// ``` /// # use helix_core::hashmap; -/// # use helix_term::keymap; -/// # use helix_term::keymap::Keymap; -/// let normal_mode = keymap!({ "Normal mode" +/// # use helix_term::keymap::{Keymap, macros::keytrie}; +/// let normal_mode = keytrie!({ "Normal mode" /// "i" => insert_mode, /// "g" => { "Goto" /// "g" => goto_file_start, @@ -79,7 +78,7 @@ macro_rules! alt { /// let keymap = Keymap::new(normal_mode); /// ``` #[macro_export] -macro_rules! keymap { +macro_rules! keytrie { ({ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { // modified from the hashmap! macro { @@ -88,8 +87,13 @@ macro_rules! keymap { ::std::collections::HashMap::with_capacity(_cap); $( $( +<<<<<<< HEAD let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); let _potential_duplicate = _map.insert(_key,keymap!(@trie $value)); +======= + let _key = $key.parse::().unwrap(); + let _potential_duplicate = _map.insert(_key,keytrie!(@trie $value)); +>>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); )+ )* @@ -100,7 +104,7 @@ macro_rules! keymap { }; (@trie {$label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { - $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) + $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keytrie!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) }; (@trie $cmd:ident) => { @@ -113,7 +117,11 @@ macro_rules! keymap { } pub use alt; +<<<<<<< HEAD pub use ctrl; pub use key; pub use keymap; pub use shift; +======= +pub use keytrie; +>>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 157230d816d3..4570ac2e50eb 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -9,7 +9,7 @@ mod tests { #[test] #[should_panic] fn duplicate_keys_should_panic() { - keymap!({ "Normal mode" + keytrie!({ "Normal mode" "i" => normal_mode, "i" => goto_definition, }); @@ -39,7 +39,7 @@ mod tests { #[test] fn command_list() { - let normal_mode = keymap!({ "Normal mode" + let normal_mode = keytrie!({ "Normal mode" "i" => insert_mode, "g" => { "Goto" "g" => goto_file_start, From 7747777d68a4d199e2db6edf6bb704c5a6cbaa0f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 19 Jan 2023 15:37:43 +0100 Subject: [PATCH 051/195] Fix failed cherry picks --- helix-term/src/commands.rs | 45 ++++++++++++++++++++++---------- helix-term/src/commands/typed.rs | 3 +-- helix-term/src/keymap.rs | 2 +- helix-term/src/keymap/keymaps.rs | 1 + helix-term/src/keymap/keytrie.rs | 2 +- helix-term/src/keymap/macros.rs | 9 ------- helix-term/src/keymap/tests.rs | 9 +++---- 7 files changed, 39 insertions(+), 32 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7255995186cf..bfd7e475ef3c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,7 +4,7 @@ pub(crate) mod typed; pub use dap::*; pub use lsp::*; -use tui::widgets::Row; +use tui::widgets::{Row, Cell}; pub use typed::*; use helix_vcs::Hunk; @@ -34,7 +34,7 @@ use helix_view::{ keyboard::KeyCode, tree, view::View, - Document, DocumentId, Editor, ViewId, + Document, DocumentId, Editor, ViewId, apply_transaction, }; use crate::{ commands::insert::*, @@ -2444,22 +2444,40 @@ fn jumplist_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } - - - +// NOTE: does not present aliases impl ui::menu::Item for MappableCommand { type Data = CommandList; - fn label(&self, keymap: &Self::Data) -> Spans { + fn format(&self, command_list: &Self::Data) -> Row { match self { - MappableCommand::Typable { description: doc, name, .. } => match keymap.get(name as &String) { - Some(key_events) => format!("{} {:?} ':{}'", doc, key_events, name).into(), - None => format!("{} ':{}'", doc, name).into(), - }, - MappableCommand::Static { description: doc, name, .. } => match keymap.get(*name) { - Some(key_events) => format!("{} {:?} '{}'", doc, key_events, name).into(), - None => format!("{} '{}'", doc, name).into(), + MappableCommand::Typable { description: doc, name, .. } => { + let mut row: Vec = vec![Cell::from(&*name.as_str()), Cell::from(""), Cell::from(&*doc.as_str())]; + match command_list.get(name as &String) { + Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + None => {} + } + return Row::new(row); }, + MappableCommand::Static { description: doc, name, .. } => { + let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; + match command_list.get(*name) { + Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + None => {} + } + return Row::new(row) + } + } + + // TODO: Generalize into a Vec Display implemention? + fn format_key_events(key_events: &Vec) -> String { + let mut result_string: String = String::new(); + for key_event in key_events { + if !result_string.is_empty() { + result_string.push_str(", "); + } + result_string.push_str(key_event); + } + result_string } } } @@ -2509,7 +2527,6 @@ pub fn command_palette(cx: &mut Context) { compositor.push(Box::new(overlayed(picker))); }, )); - } fn last_picker(cx: &mut Context) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index bd91df5ae62a..deb5f7bc88a8 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,8 +1,7 @@ use std::ops::Deref; -use crate::job::Job; - use super::*; +use crate::job::*; use helix_view::editor::{Action, CloseError, ConfigEvent}; use ui::completers::{self, Completer}; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 880b405f604e..6b1b56a80f1a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -26,7 +26,7 @@ pub struct Keymap { pub type CommandList = HashMap>; impl Keymap { pub fn new(root_node: KeyTrie) -> Self { - Keymap { root_node } + Self { root_node } } /// Returns a key-value list of all commands associated to a given Keymap. diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index f824401b1833..773b6213003f 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -1,4 +1,5 @@ use super::*; +use crate::keymap::macros::*; use crate::commands::MappableCommand; use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 0706182db228..cb7798971246 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,6 +1,6 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; +use std::{collections::HashMap, ops::{Deref, DerefMut}}; use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 66fa78f7a34b..24fc7de212d8 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -87,13 +87,8 @@ macro_rules! keytrie { ::std::collections::HashMap::with_capacity(_cap); $( $( -<<<<<<< HEAD let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); - let _potential_duplicate = _map.insert(_key,keymap!(@trie $value)); -======= - let _key = $key.parse::().unwrap(); let _potential_duplicate = _map.insert(_key,keytrie!(@trie $value)); ->>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); )+ )* @@ -117,11 +112,7 @@ macro_rules! keytrie { } pub use alt; -<<<<<<< HEAD pub use ctrl; pub use key; -pub use keymap; pub use shift; -======= pub use keytrie; ->>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 4570ac2e50eb..a4cded6fb645 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,7 +3,7 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::keymap::*; + use crate::keymap::{*, macros::*}; use std::collections::HashMap; #[test] @@ -17,14 +17,13 @@ mod tests { #[test] fn check_duplicate_keys_in_default_keymap() { - // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro - Keymaps::default(); + // will panic on duplicate keys, assumes that `Keymap` uses keymap! macro + Keymap::default(); } #[test] fn aliased_modes_are_same_in_default_keymap() { - let keymaps = Keymaps::default().keymaps; - let root = keymaps.load().get(&Mode::Normal).unwrap().root_node.clone(); + let root = Keymap::default().get(&Mode::Normal).unwrap().root_node.clone(); assert_eq!( root.traverse(&[key!(' '), key!('w')]).unwrap(), root.traverse(&["C-w".parse::().unwrap()]).unwrap(), From 342f794b22ae9c9ff09567ee014c8444f73716c5 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 23:29:00 +0100 Subject: [PATCH 052/195] Removed keymap::Keymap: By the end of this refactroring it became clear that it was mostly acting as a useless wrapper for KeyTrie. --- helix-term/src/commands.rs | 21 ++++--- helix-term/src/config.rs | 22 ++++--- helix-term/src/keymap.rs | 85 ---------------------------- helix-term/src/keymap/default.rs | 10 ++-- helix-term/src/keymap/keymaps.rs | 48 ++++++++++++++-- helix-term/src/keymap/keytrienode.rs | 3 +- helix-term/src/keymap/macros.rs | 6 +- helix-term/src/keymap/tests.rs | 19 ++++++- 8 files changed, 91 insertions(+), 123 deletions(-) delete mode 100644 helix-term/src/keymap.rs diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index bfd7e475ef3c..55b4e2b3009a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -7,6 +7,15 @@ pub use lsp::*; use tui::widgets::{Row, Cell}; pub use typed::*; +use crate::{ + commands::insert::*, + args, + keymap::keymaps::CommandList, + compositor::{self, Component, Compositor}, + job::{Callback, self, Jobs}, + ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, +}; + use helix_vcs::Hunk; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, @@ -36,14 +45,6 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, apply_transaction, }; -use crate::{ - commands::insert::*, - args, - compositor::{self, Component, Compositor}, - job::{Callback, self, Jobs}, - keymap::CommandList, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, -}; use std::{ collections::{HashMap, HashSet}, num::NonZeroUsize, @@ -2485,9 +2486,7 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymaps.load_keymaps() - [&cx.editor.mode] - .command_list(); + let keymap_command_lists = compositor.find::().unwrap().keymaps.command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index fc724d818ff0..8f7649578c70 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,4 +1,4 @@ -use crate::keymap::{default, Keymap}; +use crate::keymap::{default, keytrie::KeyTrie}; use helix_view::document::Mode; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, io::Error as IOError}; @@ -9,7 +9,7 @@ use toml::de::Error as TomlError; pub struct Config { pub theme: Option, #[serde(default = "default::default")] - pub keys: HashMap, + pub keys: HashMap, #[serde(default)] pub editor: helix_view::editor::Config, } @@ -53,7 +53,7 @@ impl Config { pub fn merge_in_default_keymap(mut self) -> Config { let mut delta = std::mem::replace(&mut self.keys, default::default()); for (mode, keys) in &mut self.keys { - keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) + keys.merge_keytrie(delta.remove(mode).unwrap_or_default()) } self } @@ -68,7 +68,6 @@ mod tests { default, keytrienode::KeyTrieNode, macros::*, - Keymap, }, }; use helix_core::hashmap; @@ -89,13 +88,13 @@ mod tests { toml::from_str::(sample_keymaps).unwrap(), Config { keys: hashmap! { - Mode::Insert => Keymap::new(keytrie!({ "Insert mode" + Mode::Insert => keytrie!({ "Insert mode" "y" => move_line_down, "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keytrie!({ "Normal mode" + }), + Mode::Normal => keytrie!({ "Normal mode" "A-F12" => move_next_word_end, - })), + }), }, ..Default::default() } @@ -117,8 +116,7 @@ mod tests { fn user_config_merges_with_default() { let user_config = Config { keys: hashmap! { - Mode::Normal => Keymap::new( - keytrie!({ "Normal mode" + Mode::Normal => keytrie!({ "Normal mode" "i" => normal_mode, "无" => insert_mode, "z" => jump_backward, @@ -127,7 +125,7 @@ mod tests { "g" => delete_char_forward, }, }) - ) + }, ..Default::default() }; @@ -138,7 +136,7 @@ mod tests { "Merged user keymap with default should differ from user keymap." ); - let keymap_normal_root_key_trie = &merged_config.keys.get_mut(&Mode::Normal).unwrap().root_node; + let keymap_normal_root_key_trie = &merged_config.keys.get_mut(&Mode::Normal).unwrap(); assert_eq!( keymap_normal_root_key_trie.traverse(&[key!('i')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::normal_mode), diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs deleted file mode 100644 index 6b1b56a80f1a..000000000000 --- a/helix-term/src/keymap.rs +++ /dev/null @@ -1,85 +0,0 @@ -pub mod keymaps; -pub mod default; -pub mod macros; -// NOTE: Only pub becuase of their use in macros -pub mod keytrienode; -pub mod keytrie; -mod tests; - -use crate::{ - commands::MappableCommand, - keymap::{ - keytrie::KeyTrie, - keytrienode::KeyTrieNode - } -}; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; -use serde::Deserialize; - -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(transparent)] -/// KeyTrie starting point. -pub struct Keymap { - pub root_node: KeyTrie -} - -pub type CommandList = HashMap>; -impl Keymap { - pub fn new(root_node: KeyTrie) -> Self { - Self { root_node } - } - - /// Returns a key-value list of all commands associated to a given Keymap. - /// Keys are the node names (see KeyTrieNode documentation) - /// Values are lists of stringified KeyEvents that triger the command. - /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. - /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". - /// Ancestor KeyEvents are in this case "space" and "w". - pub fn command_list(&self) -> CommandList { - let mut list = HashMap::new(); - _command_list(&mut list, &KeyTrieNode::KeyTrie(self.root_node.clone()), &mut String::new()); - return list; - - fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { - match node { - KeyTrieNode::KeyTrie(trie_node) => { - for (key_event, subtrie_node) in trie_node.deref() { - let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { - temp_prefix.push_str(">"); - } - temp_prefix.push_str(&key_event.to_string()); - _command_list(list, subtrie_node, &mut temp_prefix); - } - }, - KeyTrieNode::MappableCommand(mappable_command) => { - if mappable_command.name() == "no_op" { return } - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); - }, - KeyTrieNode::CommandSequence(_) => {} - }; - } - } -} - -impl Default for Keymap { - fn default() -> Self { - Self::new(KeyTrie::default()) - } -} - -/// Returns the Keymap root KeyTrie node. -impl Deref for Keymap { - type Target = KeyTrie; - - fn deref(&self) -> &Self::Target { - &self.root_node - } -} - -/// Returns the Keymap root KeyTrie node. -impl DerefMut for Keymap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.root_node - } -} diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 551cf1a79bed..5ee0706622e1 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,9 +1,9 @@ -use super::{macros::keytrie, Keymap}; +use super::{macros::keytrie, keytrie::KeyTrie}; use helix_view::document::Mode; use helix_core::hashmap; use std::collections::HashMap; -pub fn default() -> HashMap { +pub fn default() -> HashMap { let normal = keytrie!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, @@ -370,8 +370,8 @@ pub fn default() -> HashMap { "end" => goto_line_end_newline, }); hashmap!( - Mode::Normal => Keymap::new(normal), - Mode::Select => Keymap::new(select), - Mode::Insert => Keymap::new(insert), + Mode::Normal => normal, + Mode::Select => select, + Mode::Insert => insert, ) } diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index 773b6213003f..f99cfe9bdebe 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -5,6 +5,8 @@ use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use std::ops::Deref; + #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { Pending(KeyTrie), @@ -16,14 +18,15 @@ pub enum KeymapResult { } pub struct Keymaps { - pub keymaps: Box>>, + pub keymaps: Box>>, /// Relative to a sticky node if Some. pending_keys: Vec, pub sticky_keytrie: Option, } +pub type CommandList = HashMap>; impl Keymaps { - pub fn new(keymaps: Box>>) -> Self { + pub fn new(keymaps: Box>>) -> Self { Self { keymaps, pending_keys: Vec::new(), @@ -31,7 +34,7 @@ impl Keymaps { } } - pub fn load_keymaps(&self) -> DynGuard> { + pub fn load_keymaps(&self) -> DynGuard> { self.keymaps.load() } @@ -63,7 +66,7 @@ impl Keymaps { // Check if sticky keytrie is to be used. let starting_keytrie = match self.sticky_keytrie { - None => &active_keymap.root_node, + None => &active_keymap, Some(ref active_sticky_keytrie) => active_sticky_keytrie, }; @@ -101,6 +104,43 @@ impl Keymaps { None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), } } + + fn get_keytrie(&self, mode: &Mode) -> KeyTrie { + // HELP: Unsure how I should handle this Option + self.keymaps.load().get(mode).unwrap().clone() + } + + /// Returns a key-value list of all commands associated to a given Keymap. + /// Keys are the node names (see KeyTrieNode documentation) + /// Values are lists of stringified KeyEvents that triger the command. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". + /// Ancestor KeyEvents are in this case "space" and "w". + pub fn command_list(&self, mode: &Mode) -> CommandList { + let mut list = HashMap::new(); + _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + return list; + + fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { + match node { + KeyTrieNode::KeyTrie(trie_node) => { + for (key_event, subtrie_node) in trie_node.deref() { + let mut temp_prefix: String = prefix.to_string(); + if &temp_prefix != "" { + temp_prefix.push_str(">"); + } + temp_prefix.push_str(&key_event.to_string()); + _command_list(list, subtrie_node, &mut temp_prefix); + } + }, + KeyTrieNode::MappableCommand(mappable_command) => { + if mappable_command.name() == "no_op" { return } + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + }, + KeyTrieNode::CommandSequence(_) => {} + }; + } + } } impl Default for Keymaps { diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 41b5a7cd3343..433372377787 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,4 +1,5 @@ -use super::{MappableCommand, keytrie::KeyTrie}; +use super::keytrie::KeyTrie; +use crate::commands::MappableCommand; use helix_view::input::KeyEvent; use std::collections::HashMap; use serde::{Deserialize, de::Visitor}; diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 24fc7de212d8..e17bad91d48d 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -62,11 +62,11 @@ macro_rules! alt { }; } -/// Macro for defining the root of a `Keymap` object. Example: +/// Macro for defining the root of a `KeyTrie` object. Example: /// /// ``` /// # use helix_core::hashmap; -/// # use helix_term::keymap::{Keymap, macros::keytrie}; +/// # use helix_term::keymap::{keytrie::KeyTrie, macros::keytrie}; /// let normal_mode = keytrie!({ "Normal mode" /// "i" => insert_mode, /// "g" => { "Goto" @@ -75,7 +75,7 @@ macro_rules! alt { /// }, /// "j" | "down" => move_line_down, /// }); -/// let keymap = Keymap::new(normal_mode); +/// let keymap = normal_mode; /// ``` #[macro_export] macro_rules! keytrie { diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index a4cded6fb645..6d566c91f7ad 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,8 +3,17 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; +<<<<<<< HEAD use crate::keymap::{*, macros::*}; use std::collections::HashMap; +======= + use crate::{ + keymap::macros::*, + keymap::keymaps::Keymaps, + }; + use std::{sync::Arc, collections::HashMap}; + use arc_swap::ArcSwap; +>>>>>>> adb17e18 (Removed keymap::Keymap:) #[test] #[should_panic] @@ -23,7 +32,12 @@ mod tests { #[test] fn aliased_modes_are_same_in_default_keymap() { +<<<<<<< HEAD let root = Keymap::default().get(&Mode::Normal).unwrap().root_node.clone(); +======= + let keymaps = Keymaps::default().keymaps; + let root = keymaps.load().get(&Mode::Normal).unwrap().clone(); +>>>>>>> adb17e18 (Removed keymap::Keymap:) assert_eq!( root.traverse(&[key!(' '), key!('w')]).unwrap(), root.traverse(&["C-w".parse::().unwrap()]).unwrap(), @@ -46,8 +60,9 @@ mod tests { }, "j" | "k" => move_line_down, }); - let keymap = Keymap::new(normal_mode); - let mut command_list = keymap.command_list(); + + let keymap = Keymaps::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests // HashMaps can be compared but we can still get different ordering of bindings From 020c53af9739629d1be6cc7e8e0fc093b43660e3 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Fri, 30 Dec 2022 10:59:05 +0100 Subject: [PATCH 053/195] Switched keymap::keymaps::Keymaps to keymap::Keymap Root keymap module was empty as old Keymap could be removed. And there was no point anymore in differentiating Keymaps and Keymap. --- helix-term/src/application.rs | 4 +- helix-term/src/commands.rs | 5 +- helix-term/src/keymap.rs | 161 +++++++++++++++++++++++++++++++++ helix-term/src/keymap/tests.rs | 26 ++---- helix-term/src/ui/editor.rs | 22 ++--- 5 files changed, 182 insertions(+), 36 deletions(-) create mode 100644 helix-term/src/keymap.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b614f5395c12..81fa20951122 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -18,8 +18,8 @@ use crate::{ commands::apply_workspace_edit, compositor::{Compositor, Event}, config::Config, + keymap::Keymap, job::Jobs, - keymap::keymaps::Keymaps, ui::{self, overlay::overlayed}, }; use std::{ @@ -177,7 +177,7 @@ impl Application { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + let editor_view = Box::new(ui::EditorView::new(Keymap::new(keys))); compositor.push(editor_view); if args.load_tutor { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 55b4e2b3009a..6d013456a29f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,13 +4,12 @@ pub(crate) mod typed; pub use dap::*; pub use lsp::*; -use tui::widgets::{Row, Cell}; pub use typed::*; use crate::{ commands::insert::*, args, - keymap::keymaps::CommandList, + keymap::CommandList, compositor::{self, Component, Compositor}, job::{Callback, self, Jobs}, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, @@ -2486,7 +2485,7 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymaps.command_list(&cx.editor.mode); + let keymap_command_lists = compositor.find::().unwrap().keymap.command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs new file mode 100644 index 000000000000..780b7e6531aa --- /dev/null +++ b/helix-term/src/keymap.rs @@ -0,0 +1,161 @@ +pub mod default; +pub mod macros; +// NOTE: Only pub becuase of their use in macros +pub mod keytrienode; +pub mod keytrie; +mod tests; + +use self::{ + keytrienode::KeyTrieNode, + keytrie::KeyTrie, + macros::key, +}; + +use crate::commands::MappableCommand; +use helix_view::{document::Mode, input::KeyEvent}; +use std::{sync::Arc, collections::HashMap}; +use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; + +use std::ops::Deref; + +#[derive(Debug, Clone, PartialEq)] +pub enum KeymapResult { + Pending(KeyTrie), + Matched(MappableCommand), + MatchedCommandSequence(Vec), + NotFound, + /// Contains pressed KeyEvents leading up to the cancellation. + Cancelled(Vec), +} + +pub struct Keymap { + pub keytries: Box>>, + /// Relative to a sticky node if Some. + pending_keys: Vec, + pub sticky_keytrie: Option, +} + +pub type CommandList = HashMap>; +impl Keymap { + pub fn new(keymaps: Box>>) -> Self { + Self { + keytries: keymaps, + pending_keys: Vec::new(), + sticky_keytrie: None, + } + } + + pub fn load_keymaps(&self) -> DynGuard> { + self.keytries.load() + } + + /// Returns list of keys waiting to be disambiguated in current mode. + pub fn pending(&self) -> &[KeyEvent] { + &self.pending_keys + } + + pub fn sticky_keytrie(&self) -> Option<&KeyTrie> { + self.sticky_keytrie.as_ref() + } + + /// Lookup `key` in the keymap to try and find a command to execute. + /// Escape key represents cancellation. + /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. + pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + // TODO: remove the sticky part and look up manually + let keymaps = &*self.load_keymaps(); + let active_keymap = &keymaps[&mode]; + + if key == key!(Esc) { + if !self.pending_keys.is_empty() { + // NOTE: Esc is not included here + return KeymapResult::Cancelled(self.pending_keys.drain(..).collect()); + } + // TODO: Shouldn't we return here also? + self.sticky_keytrie = None; + } + + // Check if sticky keytrie is to be used. + let starting_keytrie = match self.sticky_keytrie { + None => &active_keymap, + Some(ref active_sticky_keytrie) => active_sticky_keytrie, + }; + + // TODO: why check either pending or regular key? + let first_key = self.pending_keys.get(0).unwrap_or(&key); + + let pending_keytrie: KeyTrie = match starting_keytrie.traverse(&[*first_key]) { + Some(KeyTrieNode::KeyTrie(sub_keytrie)) => sub_keytrie, + Some(KeyTrieNode::MappableCommand(cmd)) => { + return KeymapResult::Matched(cmd.clone()); + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + return KeymapResult::MatchedCommandSequence(cmds.clone()); + } + None => return KeymapResult::NotFound, + }; + + self.pending_keys.push(key); + match pending_keytrie.traverse(&self.pending_keys[1..]) { + Some(KeyTrieNode::KeyTrie(map)) => { + if map.is_sticky { + self.pending_keys.clear(); + self.sticky_keytrie = Some(map.clone()); + } + KeymapResult::Pending(map.clone()) + } + Some(KeyTrieNode::MappableCommand(cmd)) => { + self.pending_keys.clear(); + KeymapResult::Matched(cmd.clone()) + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + self.pending_keys.clear(); + KeymapResult::MatchedCommandSequence(cmds.clone()) + } + None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), + } + } + + fn get_keytrie(&self, mode: &Mode) -> KeyTrie { + // HELP: Unsure how I should handle this Option + self.keytries.load().get(mode).unwrap().clone() + } + + /// Returns a key-value list of all commands associated to a given Keymap. + /// Keys are the node names (see KeyTrieNode documentation) + /// Values are lists of stringified KeyEvents that triger the command. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". + /// Ancestor KeyEvents are in this case "space" and "w". + pub fn command_list(&self, mode: &Mode) -> CommandList { + let mut list = HashMap::new(); + _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + return list; + + fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { + match node { + KeyTrieNode::KeyTrie(trie_node) => { + for (key_event, subtrie_node) in trie_node.deref() { + let mut temp_prefix: String = prefix.to_string(); + if &temp_prefix != "" { + temp_prefix.push_str(">"); + } + temp_prefix.push_str(&key_event.to_string()); + _command_list(list, subtrie_node, &mut temp_prefix); + } + }, + KeyTrieNode::MappableCommand(mappable_command) => { + if mappable_command.name() == "no_op" { return } + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + }, + KeyTrieNode::CommandSequence(_) => {} + }; + } + } +} + +impl Default for Keymap { + fn default() -> Self { + Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) + } +} diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 6d566c91f7ad..07145bdf310a 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,17 +3,8 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; -<<<<<<< HEAD use crate::keymap::{*, macros::*}; use std::collections::HashMap; -======= - use crate::{ - keymap::macros::*, - keymap::keymaps::Keymaps, - }; - use std::{sync::Arc, collections::HashMap}; - use arc_swap::ArcSwap; ->>>>>>> adb17e18 (Removed keymap::Keymap:) #[test] #[should_panic] @@ -32,20 +23,15 @@ mod tests { #[test] fn aliased_modes_are_same_in_default_keymap() { -<<<<<<< HEAD - let root = Keymap::default().get(&Mode::Normal).unwrap().root_node.clone(); -======= - let keymaps = Keymaps::default().keymaps; - let root = keymaps.load().get(&Mode::Normal).unwrap().clone(); ->>>>>>> adb17e18 (Removed keymap::Keymap:) + let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); assert_eq!( - root.traverse(&[key!(' '), key!('w')]).unwrap(), - root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!(' '), key!('w')]).unwrap(), + normal_mode_keytrie_root.traverse(&["C-w".parse::().unwrap()]).unwrap(), "Mismatch for window mode on `Space-w` and `Ctrl-w`." ); assert_eq!( - root.traverse(&[key!('z')]).unwrap(), - root.traverse(&[key!('Z')]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!('z')]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!('Z')]).unwrap(), "Mismatch for view mode on `z` and `Z`." ); } @@ -61,7 +47,7 @@ mod tests { "j" | "k" => move_line_down, }); - let keymap = Keymaps::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 98da59c95097..a4f3f182d9c2 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::keymaps::{KeymapResult, Keymaps}, + keymap::{KeymapResult, Keymap}, ui::{Completion, ProgressSpinners}, }; @@ -32,7 +32,7 @@ use super::lsp::SignatureHelp; use super::statusline; pub struct EditorView { - pub keymaps: Keymaps, + pub keymap: Keymap, on_next_key: Option>, pseudo_pending: Vec, last_insert: (commands::MappableCommand, Vec), @@ -48,9 +48,9 @@ pub enum InsertEvent { } impl EditorView { - pub fn new(keymaps: Keymaps) -> Self { + pub fn new(keymap: Keymap) -> Self { Self { - keymaps, + keymap, on_next_key: None, pseudo_pending: Vec::new(), last_insert: (commands::MappableCommand::normal_mode, Vec::new()), @@ -928,9 +928,9 @@ impl EditorView { event: KeyEvent, ) -> Option { let mut last_mode = mode; - self.pseudo_pending.extend(self.keymaps.pending()); - let key_result = self.keymaps.get(mode, event); - cxt.editor.autoinfo = self.keymaps.sticky_keytrie().map(|node| node.infobox()); + self.pseudo_pending.extend(self.keymap.pending()); + let key_result = self.keymap.get(mode, event); + cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox()); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); @@ -994,7 +994,7 @@ impl EditorView { Some(ch) => commands::insert::insert_char(cx, ch), None => { if let KeymapResult::Matched(command) = - self.keymaps.get(Mode::Insert, ev) + self.keymap.get(Mode::Insert, ev) { command.execute(cx); } @@ -1016,7 +1016,7 @@ impl EditorView { std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); } // special handling for repeat operator - (key!('.'), _) if self.keymaps.pending().is_empty() => { + (key!('.'), _) if self.keymap.pending().is_empty() => { for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) { // first execute whatever put us into insert mode self.last_insert.0.execute(cxt); @@ -1063,7 +1063,7 @@ impl EditorView { cxt.register = cxt.editor.selected_register.take(); self.handle_keymap_event(mode, cxt, event); - if self.keymaps.pending().is_empty() { + if self.keymap.pending().is_empty() { cxt.editor.count = None } } @@ -1521,7 +1521,7 @@ impl Component for EditorView { if let Some(count) = cx.editor.count { disp.push_str(&count.to_string()) } - for key in self.keymaps.pending() { + for key in self.keymap.pending() { disp.push_str(&key.key_sequence_format()); } for key in &self.pseudo_pending { From bda06bcd32d6e24bcee219d7715b64566b4aa743 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Fri, 20 Jan 2023 17:34:59 +0100 Subject: [PATCH 054/195] Re-implemented pre-defined orders in keytrie * Fixed aliased_modes_are_same_in_default_keymap to check for order too. It failed to detect a bug in which the swap_view_* were ordered differently under space-w and c-w. --- helix-term/src/config.rs | 11 +- helix-term/src/keymap.rs | 9 +- helix-term/src/keymap/default.rs | 4 +- helix-term/src/keymap/keytrie.rs | 162 ++++++++++++++++----------- helix-term/src/keymap/keytrienode.rs | 27 ++++- helix-term/src/keymap/macros.rs | 38 ++++--- helix-term/src/keymap/tests.rs | 4 +- 7 files changed, 150 insertions(+), 105 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 8f7649578c70..3ed2fa52323b 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -103,11 +103,8 @@ 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::default()); - - // From the Default trait + let serde_default = toml::from_str::("").unwrap().keys; + assert_eq!(serde_default, default::default()); let default_keys = Config::default().keys; assert_eq!(default_keys, default::default()); } @@ -173,7 +170,7 @@ mod tests { ); // Huh? - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + assert!(merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1); + assert!(merged_config.keys.get(&Mode::Insert).unwrap().get_children().len() > 0); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 780b7e6531aa..9bd04222e39f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -16,8 +16,6 @@ use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; -use std::ops::Deref; - #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { Pending(KeyTrie), @@ -135,13 +133,14 @@ impl Keymap { fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { match node { KeyTrieNode::KeyTrie(trie_node) => { - for (key_event, subtrie_node) in trie_node.deref() { + for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); if &temp_prefix != "" { - temp_prefix.push_str(">"); + temp_prefix.push_str("→"); } temp_prefix.push_str(&key_event.to_string()); - _command_list(list, subtrie_node, &mut temp_prefix); + _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); + } }, KeyTrieNode::MappableCommand(mappable_command) => { diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5ee0706622e1..3fffaa08c517 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -187,10 +187,10 @@ pub fn default() -> HashMap { "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, "C-l" | "l" | "right" => jump_view_right, - "L" => swap_view_right, - "K" => swap_view_up, "H" => swap_view_left, "J" => swap_view_down, + "K" => swap_view_up, + "L" => swap_view_right, "n" => { "New split scratch buffer" "C-s" | "s" => hsplit_new, "C-v" | "v" => vsplit_new, diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index cb7798971246..8d97c7a8ca5b 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,25 +1,36 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] pub struct KeyTrie { documentation: String, - children: HashMap, + /// Used for pre-defined order in infoboxes, values represent the index of the key tries children. + child_order: HashMap, + children: Vec, pub is_sticky: bool, } impl KeyTrie { - pub fn new(documentation: &str, children: HashMap) -> Self { + pub fn new(documentation: &str, child_order: HashMap, children: Vec) -> Self { Self { documentation: documentation.to_string(), + child_order, children, is_sticky: false, } } + pub fn get_child_order(&self) -> &HashMap { + &self.child_order + } + + pub fn get_children(&self) -> &Vec { + &self.children + } + // None symbolizes NotFound pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { return _traverse(self, key_events, 0); @@ -28,13 +39,13 @@ impl KeyTrie { if depth == key_events.len() { return Some(KeyTrieNode::KeyTrie(keytrie.clone())); } - else if let Some(found_child) = keytrie.get(&key_events[depth]) { - match found_child { + else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { + match &keytrie.children[*found_index] { KeyTrieNode::KeyTrie(sub_keytrie) => { depth += 1; return _traverse(sub_keytrie, key_events, depth) }, - _ => return Some(found_child.clone()) + _found_child => return Some(_found_child.clone()) } } return None; @@ -42,84 +53,81 @@ impl KeyTrie { } pub fn merge_keytrie(&mut self, mut other_keytrie: Self) { - for (other_key_event, other_child_node) in std::mem::take(&mut other_keytrie.children) { - match other_child_node { - KeyTrieNode::KeyTrie(other_child_key_trie) => { - if let Some(KeyTrieNode::KeyTrie(self_clashing_child_key_trie)) = self.children.get_mut(&other_key_event) { - self_clashing_child_key_trie.merge_keytrie(other_child_key_trie); + for (other_key_event, other_index) in other_keytrie.get_child_order() { + let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; + match other_child_keytrie_node { + KeyTrieNode::KeyTrie(ref other_child_keytrie) => { + if let Some(self_index) = self.child_order.get(&other_key_event) { + if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { + self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); + } } else { - self.children.insert(other_key_event, KeyTrieNode::KeyTrie(other_child_key_trie)); + self.child_order.insert(*other_key_event, self.children.len()); + self.children.push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); } } KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { - self.children.insert(other_key_event, other_child_node); + if let Some(existing_index) = self.child_order.get(other_key_event) { + self.children[*existing_index] = other_child_keytrie_node.clone(); + } + else { + self.child_order.insert(*other_key_event, self.children.len()); + self.children.push(other_child_keytrie_node.clone()); + } } } } } - + /// Open an Info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self) -> Info { - let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); - for (&key_event, key_trie) in self.iter() { + let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); + let mut key_event_order = Vec::with_capacity(self.children.len()); + // child_order and children is of same length + unsafe { key_event_order.set_len(self.children.len()); } + for (key_event, index) in &self.child_order { + key_event_order[*index] = key_event.clone(); + } + + for (index, key_trie) in self.children.iter().enumerate() { let documentation: &str = match key_trie { - KeyTrieNode::MappableCommand(command) => { + KeyTrieNode::MappableCommand(ref command) => { if command.name() == "no_op" { continue; } command.description() }, - KeyTrieNode::KeyTrie(key_trie) => &key_trie.documentation, + KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, // FIX: default to a join of all command names // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. // Regardless if the command sequence is different. KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; + let key_event = key_event_order[index]; match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { Some(position) => body[position].0.push(key_event.to_string()), None => { - let mut temp_vec: Vec = Vec::new(); - temp_vec.push(key_event.to_string()); - body.push((temp_vec, documentation)) + body.push((vec![key_event.to_string()], documentation)) }, } } // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent // Those events will always be placed after the one letter KeyEvent - let mut sorted_body = body - .iter() - .map(|(key_events, description)| { - let mut temp_key_events = key_events.clone(); - temp_key_events.sort_unstable_by(|a, b| a.len().cmp(&b.len())); - (temp_key_events, *description) - }) - .collect::, &str)>>(); - sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); - // Consistently place lowercase before uppercase of the same letter. - if sorted_body.len() > 1 { - let mut x_index = 0; - let mut y_index = 1; - - while y_index < sorted_body.len() { - let x = &sorted_body[x_index].0[0]; - let y = &sorted_body[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() { - // Uppercase regarded as lower value. - if x < y { - let temp_holder = sorted_body[x_index].clone(); - sorted_body[x_index] = sorted_body[y_index].clone(); - sorted_body[y_index] = temp_holder; - } + for (key_events, _) in body.iter_mut() { + key_events.sort_unstable_by(|a, b| { + if a.len() == 1 { return Ordering::Less } + if b.len() > a.len() && b.starts_with("C-") { + return Ordering::Greater } - x_index = y_index; - y_index += 1; - } + a.len().cmp(&b.len()) + }); } - let stringified_key_events_body: Vec<(String, &str)> = sorted_body + // TODO: conditional sort added here by calling infobox_sort(body) + let stringified_key_events_body: Vec<(String, &str)> = body .iter() .map(|(key_events, description)| { let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { @@ -137,7 +145,7 @@ impl KeyTrie { impl Default for KeyTrie { fn default() -> Self { - Self::new("", HashMap::new()) + Self::new("", HashMap::new(), Vec::new()) } } @@ -147,30 +155,50 @@ impl PartialEq for KeyTrie { } } -/// Returns the children of the KeyTrie -impl Deref for KeyTrie { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.children - } -} - -/// Returns the children of the KeyTrie -impl DerefMut for KeyTrie { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.children - } -} - impl<'de> Deserialize<'de> for KeyTrie { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { + // NOTE: no assumption of pre-defined order in config + let child_collection = HashMap::::deserialize(deserializer)?; + let mut child_order = HashMap::::new(); + let mut children = Vec::new(); + for (key_event, keytrie_node) in child_collection { + child_order.insert(key_event, children.len()); + children.push(keytrie_node); + } + Ok(Self { - children: HashMap::::deserialize(deserializer)?, + child_order, + children, ..Default::default() }) } } + +type InfoBoxBody<'a> = Vec<(Vec, &'a str)>; +fn infobox_sort(mut body: InfoBoxBody) -> InfoBoxBody { + body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + // Consistently place lowercase before uppercase of the same letter. + if body.len() > 1 { + let mut x_index = 0; + let mut y_index = 1; + + while y_index < body.len() { + let x = &body[x_index].0[0]; + let y = &body[y_index].0[0]; + if x.to_lowercase() == y.to_lowercase() { + // Uppercase regarded as lower value. + if x < y { + let temp_holder = body[x_index].clone(); + body[x_index] = body[y_index].clone(); + body[y_index] = temp_holder; + } + } + x_index = y_index; + y_index += 1; + } + } + body +} diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 433372377787..66a642ac902d 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, de::Visitor}; /// For the MappableCommand and CommandSequence variants, the property is self explanatory. /// For KeyTrie, the documentation is used for respective infobox titles, /// or infobox KeyEvent descriptions that in themselves trigger the opening of another infobox. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub enum KeyTrieNode { MappableCommand(MappableCommand), CommandSequence(Vec), @@ -24,6 +24,23 @@ impl<'de> Deserialize<'de> for KeyTrieNode { } } +impl PartialEq for KeyTrieNode { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (KeyTrieNode::MappableCommand(_self), KeyTrieNode::MappableCommand(_other)) => { + _self == _other + }, + (KeyTrieNode::CommandSequence(_self), KeyTrieNode::CommandSequence(_other)) => { + _self == _other + }, + (KeyTrieNode::KeyTrie(_self), KeyTrieNode::KeyTrie(_other)) => { + _self.get_children() == _other.get_children() + }, + _ => false + } + } +} + struct KeyTrieNodeVisitor; impl<'de> Visitor<'de> for KeyTrieNodeVisitor { @@ -62,10 +79,12 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { where M: serde::de::MapAccess<'de>, { - let mut sub_key_trie = HashMap::new(); + let mut children = Vec::new(); + let mut child_order = HashMap::new(); while let Some((key_event, key_trie_node)) = map.next_entry::()? { - sub_key_trie.insert(key_event, key_trie_node); + child_order.insert(key_event, children.len()); + children.push(key_trie_node); } - Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", sub_key_trie))) + Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", child_order, children))) } } \ No newline at end of file diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index e17bad91d48d..b834c43e56aa 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -1,8 +1,8 @@ #[macro_export] macro_rules! key { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; @@ -16,9 +16,9 @@ macro_rules! key { #[macro_export] macro_rules! shift { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, } }; @@ -32,9 +32,9 @@ macro_rules! shift { #[macro_export] macro_rules! ctrl { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, } }; @@ -48,9 +48,9 @@ macro_rules! ctrl { #[macro_export] macro_rules! alt { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::ALT, } }; @@ -79,27 +79,29 @@ macro_rules! alt { /// ``` #[macro_export] macro_rules! keytrie { - ({ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { - // modified from the hashmap! macro + // Sub key_trie + ({ $label:literal $(sticky=$sticky:literal)? $($($key_event:literal)|+ => $value:tt,)+ }) => { { - let _cap = hashmap!(@count $($($key),+),*); - let mut _map: ::std::collections::HashMap<::helix_view::input::KeyEvent, $crate::keymap::keytrienode::KeyTrieNode> = - ::std::collections::HashMap::with_capacity(_cap); + let _cap = hashmap!(@count $($($key_event),+),*); + let mut _children: Vec<$crate::keymap::keytrienode::KeyTrieNode> = ::std::vec::Vec::new(); + let mut _child_order: ::std::collections::HashMap<::helix_view::input::KeyEvent, usize> = ::std::collections::HashMap::with_capacity(_cap); $( $( - let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); - let _potential_duplicate = _map.insert(_key,keytrie!(@trie $value)); + let _key_event = $key_event.parse::<::helix_view::input::KeyEvent>().unwrap(); + let _potential_duplicate = _child_order.insert(_key_event, _children.len()); assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); + _children.push(keytrie!(@trie $value)); )+ )* - let mut _node = $crate::keymap::keytrie::KeyTrie::new($label, _map); + + let mut _node = $crate::keymap::keytrie::KeyTrie::new($label, _child_order, _children); $( _node.is_sticky = $sticky; )? _node } }; - (@trie {$label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { - $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keytrie!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) + (@trie {$label:literal $(sticky=$sticky:literal)? $($($key_event:literal)|+ => $value:tt,)+ }) => { + $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keytrie!({ $label $(sticky=$sticky)? $($($key_event)|+ => $value,)+ })) }; (@trie $cmd:ident) => { diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 07145bdf310a..4ab431c6e31d 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -66,11 +66,11 @@ mod tests { ), ( "goto_file_start".to_string(), - vec![format!("{}>{}", key!('g'), key!('g'))] + vec![format!("{}→{}", key!('g'), key!('g'))] ), ( "goto_file_end".to_string(), - vec![format!("{}>{}", key!('g'), key!('e'))] + vec![format!("{}→{}", key!('g'), key!('e'))] ), ( "move_line_down".to_string(), From 87fc5714039b5eaa47ee6d9c905b968d5ef28f7b Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 21 Jan 2023 16:20:05 +0100 Subject: [PATCH 055/195] Add sort_infobox editor config option --- book/src/configuration.md | 1 + helix-term/src/keymap/keytrie.rs | 7 ++++--- helix-term/src/ui/editor.rs | 5 +++-- helix-view/src/editor.rs | 3 +++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index ab229f772a1c..9e9c07451c31 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -53,6 +53,7 @@ on unix operating systems. | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `auto-info` | Whether to display infoboxes | `true` | +| `sorted-infobox` | Sort infoboxes by key event category rather than by predefined command categories | `false` | | `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | | `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` | diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 8d97c7a8ca5b..d2b9734f1cf2 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -82,7 +82,7 @@ impl KeyTrie { /// Open an Info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. - pub fn infobox(&self) -> Info { + pub fn infobox(&self, alphabetical_sort: bool) -> Info { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length @@ -126,7 +126,8 @@ impl KeyTrie { }); } - // TODO: conditional sort added here by calling infobox_sort(body) + if alphabetical_sort { body = alphabetially_sort_infobox(body); } + let stringified_key_events_body: Vec<(String, &str)> = body .iter() .map(|(key_events, description)| { @@ -178,7 +179,7 @@ impl<'de> Deserialize<'de> for KeyTrie { } type InfoBoxBody<'a> = Vec<(Vec, &'a str)>; -fn infobox_sort(mut body: InfoBoxBody) -> InfoBoxBody { +fn alphabetially_sort_infobox(mut body: InfoBoxBody) -> InfoBoxBody { body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); // Consistently place lowercase before uppercase of the same letter. if body.len() > 1 { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a4f3f182d9c2..94ead3f0f0e6 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -930,7 +930,8 @@ impl EditorView { let mut last_mode = mode; self.pseudo_pending.extend(self.keymap.pending()); let key_result = self.keymap.get(mode, event); - cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox()); + let sort_infobox = cxt.editor.config.load().sorted_infobox; + cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox(sort_infobox)); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); @@ -969,7 +970,7 @@ impl EditorView { KeymapResult::Matched(command) => { execute_command(command); } - KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), + KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox(sort_infobox)), KeymapResult::MatchedCommandSequence(commands) => { for command in commands { execute_command(command); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9af8e4c34175..c54f8be94248 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -246,6 +246,8 @@ pub struct Config { pub completion_trigger_len: u8, /// Whether to display infoboxes. Defaults to true. pub auto_info: bool, + /// Sort infoboxes alphabetically rather than by predefined categories. Defaults to `false`. + pub sorted_infobox: bool, pub file_picker: FilePickerConfig, /// Configuration of the statusline elements pub statusline: StatusLineConfig, @@ -717,6 +719,7 @@ impl Default for Config { bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), color_modes: false, + sorted_infobox: false, } } } From ecf5d61a75c10b602e22ad7fa0d899e1cc083a82 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 16:45:50 +0100 Subject: [PATCH 056/195] Refine sorting behavior Sorts by modifier, then by KeyCode category, then by each KeyEvent. Single character alphas are placed before the rest (ex. "space") in KeyCode::Char. --- helix-term/Cargo.toml | 1 - helix-term/src/keymap/keytrie.rs | 126 +++++++++++++++++++++++++------ helix-view/src/keyboard.rs | 59 +++++---------- 3 files changed, 120 insertions(+), 66 deletions(-) diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 895a08828a98..fd009aa776ed 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -36,7 +36,6 @@ helix-loader = { version = "0.6", path = "../helix-loader" } anyhow = "1" once_cell = "1.17" - which = "4.2" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index d2b9734f1cf2..7fe5df9a8610 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -80,9 +80,9 @@ impl KeyTrie { } } - /// Open an Info box for a given KeyTrie + /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. - pub fn infobox(&self, alphabetical_sort: bool) -> Info { + pub fn infobox(&self, sort_infobox: bool) -> Info { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length @@ -114,6 +114,7 @@ impl KeyTrie { } } + // TODO: Add "A-" aknowledgement? // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent // Those events will always be placed after the one letter KeyEvent for (key_events, _) in body.iter_mut() { @@ -126,7 +127,7 @@ impl KeyTrie { }); } - if alphabetical_sort { body = alphabetially_sort_infobox(body); } + if sort_infobox { body = keyevent_sort_infobox(body); } let stringified_key_events_body: Vec<(String, &str)> = body .iter() @@ -178,28 +179,107 @@ impl<'de> Deserialize<'de> for KeyTrie { } } -type InfoBoxBody<'a> = Vec<(Vec, &'a str)>; -fn alphabetially_sort_infobox(mut body: InfoBoxBody) -> InfoBoxBody { - body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); - // Consistently place lowercase before uppercase of the same letter. - if body.len() > 1 { - let mut x_index = 0; - let mut y_index = 1; - - while y_index < body.len() { - let x = &body[x_index].0[0]; - let y = &body[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() { - // Uppercase regarded as lower value. - if x < y { - let temp_holder = body[x_index].clone(); - body[x_index] = body[y_index].clone(); - body[y_index] = temp_holder; +// (KeyEvents, Description) +type InfoBoxRow<'a> = (Vec, &'a str); +type InfoBoxBody<'a> = Vec>; +/// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. +/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are +/// placed together, and alphas are placed before the rest. +fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { + use std::str::FromStr; + use std::collections::BTreeMap; + use helix_view::keyboard::{KeyCode, KeyModifiers, MediaKeyCode}; + + let mut category_holder: BTreeMap>> = BTreeMap::new(); + let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); + for infobox_row in body { + let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); + if !category_holder.contains_key(&first_keyevent.modifiers) { + category_holder.insert(first_keyevent.modifiers.clone(), BTreeMap::new()); + } + + // HACK: inserting by variant not by variant value. + // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant + // Hence the use of mock Variant values + let keycode_category = match first_keyevent.code { + KeyCode::Char(_) => { + KeyCode::Char('a') + } + KeyCode::F(_) => { + KeyCode::F(0) + } + KeyCode::Media(_) => { + KeyCode::Media(MediaKeyCode::Play) + } + other_keycode => { + other_keycode + } + }; + + + let modifier_category = category_holder.get_mut(&first_keyevent.modifiers) + .expect("keycode category existence should be checked."); + if !modifier_category.contains_key(&keycode_category) { + modifier_category.insert(keycode_category.clone(), Vec::new()); + } + modifier_category.get_mut(&keycode_category) + .expect("key existence should be checked") + .push(infobox_row); + } + + for (_, keycode_categories) in category_holder { + for (keycode_category, mut infobox_rows) in keycode_categories { + if infobox_rows.len() > 1 { + match keycode_category { + KeyCode::Char(_) => { + infobox_rows.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + + // Consistently place lowercase before uppercase of the same letter. + let mut x_index = 0; + let mut y_index = 1; + while y_index < infobox_rows.len() { + let x = &infobox_rows[x_index].0[0]; + let y = &infobox_rows[y_index].0[0]; + if x.to_lowercase() == y.to_lowercase() { + if x < y { + infobox_rows.swap(x_index, y_index); + } + } + x_index = y_index; + y_index += 1; + } + + // TEMP: until drain_filter becomes stable. Migth also be worth implementing + // FromIterator on InfoboxBody by then, last two for loops could then also be replaced by Iter::chain + let mut alphas = Vec::new(); + let mut misc = Vec::new(); + for infobox_row in infobox_rows { + if ('a'..='z') + .map(|char| char.to_string()) + .find(|alpha_char| *alpha_char == infobox_row.0[0].to_lowercase()) + .is_some() + { + alphas.push(infobox_row); + } + else { + misc.push(infobox_row); + } + } + infobox_rows = Vec::with_capacity(alphas.len() + misc.len()); + for alpha_row in alphas { + infobox_rows.push(alpha_row); + } + for misc_row in misc { + infobox_rows.push(misc_row); + } + }, + _ => { + infobox_rows.sort_unstable(); + } } } - x_index = y_index; - y_index += 1; + sorted_body.append(infobox_rows.as_mut()); } } - body + sorted_body } diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index cf673e113ce2..44813235b8c9 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -212,63 +212,38 @@ impl From for ModifierKeyCode { } /// Represents a key. +/// Variant order determines order in keymap infobox if sorted_infobox is set to true. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum KeyCode { - /// Backspace key. - Backspace, - /// Enter key. - Enter, - /// Left arrow key. - Left, - /// Right arrow key. - Right, - /// Up arrow key. + /// Character key. + /// Ex: `KeyCode::Char('c')` represents the `c` character. + Char(char), + /// Function key. + /// Ex: `KeyCode::F(1)` represents the F1 key. + F(u8), Up, - /// Down arrow key. Down, - /// Home key. + Left, + Right, + Enter, + Esc, + Tab, + Backspace, + Insert, + Delete, Home, - /// End key. End, - /// Page up key. PageUp, - /// Page down key. PageDown, - /// Tab key. - Tab, - /// Delete key. - Delete, - /// Insert key. - Insert, - /// F key. - /// - /// `KeyCode::F(1)` represents F1 key, etc. - F(u8), - /// A character. - /// - /// `KeyCode::Char('c')` represents `c` character, etc. - Char(char), - /// Null. Null, - /// Escape key. - Esc, - /// CapsLock key. CapsLock, - /// ScrollLock key. ScrollLock, - /// NumLock key. NumLock, - /// PrintScreen key. - PrintScreen, - /// Pause key. - Pause, - /// Menu key. Menu, - /// KeypadBegin key. + Pause, + PrintScreen, KeypadBegin, - /// A media key. Media(MediaKeyCode), - /// A modifier key. Modifier(ModifierKeyCode), } From 5e7f4a0972ce0debdaf7f1b075970e1f664a2bf6 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 16:53:05 +0100 Subject: [PATCH 057/195] Removed warnings --- helix-term/src/keymap/keytrie.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 7fe5df9a8610..04a2424dcec2 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,6 +1,6 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; +use std::{collections::HashMap, cmp::Ordering}; use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode @@ -52,7 +52,7 @@ impl KeyTrie { } } - pub fn merge_keytrie(&mut self, mut other_keytrie: Self) { + pub fn merge_keytrie(&mut self, other_keytrie: Self) { for (other_key_event, other_index) in other_keytrie.get_child_order() { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; match other_child_keytrie_node { From c88a817da4e9934604e6074ed96b82240c98c2eb Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 17:01:47 +0100 Subject: [PATCH 058/195] Use .join(", ") rather that own implementation --- helix-term/src/keymap/keytrie.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 04a2424dcec2..01c03ec9d1b1 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -80,6 +80,7 @@ impl KeyTrie { } } + // IMPROVEMENT: cache sorting and update cache only when config is updated /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self, sort_infobox: bool) -> Info { @@ -130,16 +131,9 @@ impl KeyTrie { if sort_infobox { body = keyevent_sort_infobox(body); } let stringified_key_events_body: Vec<(String, &str)> = body - .iter() - .map(|(key_events, description)| { - let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { - if !acc.is_empty() { acc.push_str(", "); } - acc.push_str(key_event); - acc - }); - (key_events_string, *description) - }) - .collect(); + .iter().map(|(key_events, description)| { + (key_events.join(", "), *description) + }).collect(); Info::new(&self.documentation, &stringified_key_events_body) } From 2f0fa30aef5dbd383388db249f50b2f7cd4d5e9e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 18:05:24 +0100 Subject: [PATCH 059/195] Fix apply_trasaction rebase fail --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6d013456a29f..7a79fd45b488 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -42,7 +42,7 @@ use helix_view::{ keyboard::KeyCode, tree, view::View, - Document, DocumentId, Editor, ViewId, apply_transaction, + Document, DocumentId, Editor, ViewId, }; use std::{ collections::{HashMap, HashSet}, From f3de1fbe39daaac0d914f05847b4270f4f3397d2 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 19:28:59 +0100 Subject: [PATCH 060/195] Configuration -> LanguageConfigurations * Renamed helix_core::syntax::Configuration to LanguageConfigurations to dinstiuish it from the other types of configurations (theme, keys, editor...), and to illustate that it is actually a collection of LanguageConfigurations * (Would similarly want the the LanguagesConfiguarions field 'language' was renamed to 'language_configurations' but that wold interfere with the languages.toml file. --- helix-core/src/config.rs | 4 ++-- helix-core/src/syntax.rs | 16 ++++++++-------- helix-term/src/application.rs | 2 +- xtask/src/helpers.rs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/helix-core/src/config.rs b/helix-core/src/config.rs index 2076fc2244df..27828b2fd203 100644 --- a/helix-core/src/config.rs +++ b/helix-core/src/config.rs @@ -1,10 +1,10 @@ /// Syntax configuration loader based on built-in languages.toml. -pub fn default_syntax_loader() -> crate::syntax::Configuration { +pub fn default_syntax_loader() -> crate::syntax::LanguageConfigurations { helix_loader::config::default_lang_config() .try_into() .expect("Could not serialize built-in languages.toml") } /// Syntax configuration loader based on user configured languages.toml. -pub fn user_syntax_loader() -> Result { +pub fn user_syntax_loader() -> Result { helix_loader::config::user_lang_config()?.try_into() } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 41ab23e1343a..c164d32fb763 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -60,11 +60,11 @@ fn default_timeout() -> u64 { } #[derive(Debug, Serialize, Deserialize)] -pub struct Configuration { +pub struct LanguageConfigurations { pub language: Vec, } -impl Default for Configuration { +impl Default for LanguageConfigurations { fn default() -> Self { crate::config::default_syntax_loader() } @@ -75,11 +75,11 @@ impl Default for Configuration { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub language_id: String, // c-sharp, rust + pub language_id: String, // c-sharp, rust pub scope: String, // source.rust pub file_types: Vec, // filename extension or ends_with? #[serde(default)] - pub shebangs: Vec, // interpreter(s) associated with language + pub shebangs: Vec, // interpreter(s) associated with language pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, pub max_line_length: Option, @@ -561,7 +561,7 @@ pub struct Loader { } impl Loader { - pub fn new(config: Configuration) -> Self { + pub fn new(config: LanguageConfigurations) -> Self { let mut loader = Self { language_configs: Vec::new(), language_config_ids_by_extension: HashMap::new(), @@ -2278,7 +2278,7 @@ mod test { "#, ); - let loader = Loader::new(Configuration { language: vec![] }); + let loader = Loader::new(LanguageConfigurations { language: vec![] }); let language = get_language("rust").unwrap(); let query = Query::new(language, query_str).unwrap(); @@ -2337,7 +2337,7 @@ mod test { .map(String::from) .collect(); - let loader = Loader::new(Configuration { language: vec![] }); + let loader = Loader::new(LanguageConfigurations { language: vec![] }); let language = get_language("rust").unwrap(); let config = HighlightConfiguration::new( @@ -2440,7 +2440,7 @@ mod test { ) { let source = Rope::from_str(source); - let loader = Loader::new(Configuration { language: vec![] }); + let loader = Loader::new(LanguageConfigurations { language: vec![] }); let language = get_language(language_name).unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap(); diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c0cbc2451483..5892379582b5 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -128,7 +128,7 @@ impl Application { pub fn new( args: Args, config: Config, - syn_loader_conf: syntax::Configuration, + syn_loader_conf: syntax::LanguageConfigurations, ) -> Result { #[cfg(feature = "integration")] setup_integration_logging(); diff --git a/xtask/src/helpers.rs b/xtask/src/helpers.rs index 4f759e74f725..eca88f4696f9 100644 --- a/xtask/src/helpers.rs +++ b/xtask/src/helpers.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use crate::path; -use helix_core::syntax::Configuration as LangConfig; +use helix_core::syntax::LanguageConfigurations as LangConfig; use helix_term::health::TsFeature; /// Get the list of languages that support a particular tree-sitter From 55fb3e18e142be75715f0bd3affe17de8d419a0d Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 19:36:40 +0100 Subject: [PATCH 061/195] user_lang_config -> merged_lang_config * Renamed user_lang_config to merged_lang_config, as it includes the default language config. --- helix-core/src/config.rs | 2 +- helix-loader/src/config.rs | 2 +- helix-loader/src/grammar.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-core/src/config.rs b/helix-core/src/config.rs index 27828b2fd203..1a375e29e092 100644 --- a/helix-core/src/config.rs +++ b/helix-core/src/config.rs @@ -6,5 +6,5 @@ pub fn default_syntax_loader() -> crate::syntax::LanguageConfigurations { } /// Syntax configuration loader based on user configured languages.toml. pub fn user_syntax_loader() -> Result { - helix_loader::config::user_lang_config()?.try_into() + helix_loader::config::merged_lang_config()?.try_into() } diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index 259b1318ea00..b71d895715bd 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -5,7 +5,7 @@ pub fn default_lang_config() -> toml::Value { } /// User configured languages.toml file, merged with the default config. -pub fn user_lang_config() -> Result { +pub fn merged_lang_config() -> Result { let config = crate::local_config_dirs() .into_iter() .chain([crate::config_dir()].into_iter()) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 2aa924755112..fadb39499b82 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -191,7 +191,7 @@ pub fn build_grammars(target: Option) -> Result<()> { // merged. The `grammar_selection` key of the config is then used to filter // down all grammars into a subset of the user's choosing. fn get_grammar_configs() -> Result> { - let config: Configuration = crate::config::user_lang_config() + let config: Configuration = crate::config::merged_lang_config() .context("Could not parse languages.toml")? .try_into()?; From 34f43806ae8095813fdb39a81f15965bd94ed643 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 20:32:01 +0100 Subject: [PATCH 062/195] Enforce separation of concerns in config loading * Moved the functions of helix_loader::config (relate only to langage config) to helix-loader/lib.rs, and removed the file. Mostly to aid in comparison in how the config is loaded. * Moved the functions of helix_core::config to helix_core::syntax. This way they're loaded the same way as the config.toml. Forces a consistency initialsing language config with LanguageConfigurations::default() and ::merged() respectively. helix_core::config.rs was then removed. --- helix-core/src/config.rs | 10 --------- helix-core/src/lib.rs | 1 - helix-core/src/syntax.rs | 10 ++++++++- helix-loader/src/config.rs | 42 ----------------------------------- helix-loader/src/grammar.rs | 2 +- helix-loader/src/lib.rs | 41 +++++++++++++++++++++++++++++++++- helix-term/src/application.rs | 4 ++-- helix-term/src/health.rs | 10 ++++----- helix-term/src/main.rs | 6 +++-- 9 files changed, 61 insertions(+), 65 deletions(-) delete mode 100644 helix-core/src/config.rs delete mode 100644 helix-loader/src/config.rs diff --git a/helix-core/src/config.rs b/helix-core/src/config.rs deleted file mode 100644 index 1a375e29e092..000000000000 --- a/helix-core/src/config.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Syntax configuration loader based on built-in languages.toml. -pub fn default_syntax_loader() -> crate::syntax::LanguageConfigurations { - helix_loader::config::default_lang_config() - .try_into() - .expect("Could not serialize built-in languages.toml") -} -/// Syntax configuration loader based on user configured languages.toml. -pub fn user_syntax_loader() -> Result { - helix_loader::config::merged_lang_config()?.try_into() -} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index ee174e69d1cd..4253a9a5092f 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -3,7 +3,6 @@ pub use encoding_rs as encoding; pub mod auto_pairs; pub mod chars; pub mod comment; -pub mod config; pub mod diagnostic; pub mod diff; pub mod graphemes; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index c164d32fb763..eb1288e86ed2 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -64,9 +64,17 @@ pub struct LanguageConfigurations { pub language: Vec, } +impl LanguageConfigurations { + /// Attemps to deserialize a merged user configured languages.toml with the repository languages.toml file. + pub fn merged() -> Result { + helix_loader::merged_lang_config()?.try_into() + } +} impl Default for LanguageConfigurations { fn default() -> Self { - crate::config::default_syntax_loader() + helix_loader::default_lang_config() + .try_into() + .expect("Failed to deserialize built-in languages.toml into LanguageConfigurations") } } diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs deleted file mode 100644 index b71d895715bd..000000000000 --- a/helix-loader/src/config.rs +++ /dev/null @@ -1,42 +0,0 @@ -/// Default built-in languages.toml. -pub fn default_lang_config() -> toml::Value { - toml::from_slice(include_bytes!("../../languages.toml")) - .expect("Could not parse built-in languages.toml to valid toml") -} - -/// User configured languages.toml file, merged with the default config. -pub fn merged_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(&file) - .map(|config| toml::from_slice(&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) - }); - - Ok(config) -} diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index fadb39499b82..4a68fe3fb7c1 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -191,7 +191,7 @@ pub fn build_grammars(target: Option) -> Result<()> { // merged. The `grammar_selection` key of the config is then used to filter // down all grammars into a subset of the user's choosing. fn get_grammar_configs() -> Result> { - let config: Configuration = crate::config::merged_lang_config() + let config: Configuration = crate::merged_lang_config() .context("Could not parse languages.toml")? .try_into()?; diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 80d44a8264b8..8952183ac22d 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,4 +1,3 @@ -pub mod config; pub mod grammar; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; @@ -107,7 +106,47 @@ pub fn find_local_config_dirs() -> Vec { } directories } +/// Default built-in languages.toml. +pub fn default_lang_config() -> toml::Value { + toml::from_slice(include_bytes!("../../languages.toml")) + .expect("Could not parse built-in languages.toml to valid toml") +} +pub fn merged_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(&file) + .map(|config| toml::from_slice(&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) + }); + + Ok(config) +} /// Merge two TOML documents, merging values from `right` onto `left` /// /// When an array exists in both `left` and `right`, `right`'s array is diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 5892379582b5..b352d5355ce1 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -3,7 +3,7 @@ use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, - pos_at_coords, syntax, Selection, + pos_at_coords, syntax::{self, LanguageConfigurations}, Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ @@ -399,7 +399,7 @@ impl Application { /// refresh language config after config change fn refresh_language_config(&mut self) -> Result<(), Error> { - let syntax_config = helix_core::config::user_syntax_loader() + let syntax_config = LanguageConfigurations::merged() .map_err(|err| anyhow::anyhow!("Failed to load language config: {}", err))?; self.syn_loader = std::sync::Arc::new(syntax::Loader::new(syntax_config)); diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 6558fe19fb4c..96d58114dae3 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -2,7 +2,7 @@ use crossterm::{ style::{Color, Print, Stylize}, tty::IsTty, }; -use helix_core::config::{default_syntax_loader, user_syntax_loader}; +use helix_core::syntax::LanguageConfigurations; use helix_loader::grammar::load_runtime_file; use helix_view::clipboard::get_clipboard_provider; use std::io::Write; @@ -114,7 +114,7 @@ pub fn languages_all() -> std::io::Result<()> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - let mut syn_loader_conf = match user_syntax_loader() { + let mut syn_loader_conf = match LanguageConfigurations::merged() { Ok(conf) => conf, Err(err) => { let stderr = std::io::stderr(); @@ -127,7 +127,7 @@ pub fn languages_all() -> std::io::Result<()> { err )?; writeln!(stderr, "{}", "Using default language config".yellow())?; - default_syntax_loader() + LanguageConfigurations::default() } }; @@ -206,7 +206,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); - let syn_loader_conf = match user_syntax_loader() { + let syn_loader_conf = match helix_loader::merged_lang_config()?.try_into() { Ok(conf) => conf, Err(err) => { let stderr = std::io::stderr(); @@ -219,7 +219,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { err )?; writeln!(stderr, "{}", "Using default language config".yellow())?; - default_syntax_loader() + LanguageConfigurations::default() } }; diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c5379f37..095e42fe8bbb 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -140,13 +140,15 @@ FLAGS: Err(err) => return Err(Error::new(err)), }; - let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| { + let syn_loader_conf = helix_loader::default_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 []); - helix_core::config::default_syntax_loader() + helix_loader::default_lang_config() + .try_into() + .expect("Could not serialize built-in languages.toml") }); // TODO: use the thread local executor to spawn the application task separately from the work pool From e3ec215aedc455e4848ca492df781d95816957e9 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 21:24:40 +0100 Subject: [PATCH 063/195] Move --help message to separate file. --- helix-term/src/help.rs | 35 +++++++++++++++++++++++++++++++++++ helix-term/src/main.rs | 38 +++----------------------------------- 2 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 helix-term/src/help.rs diff --git a/helix-term/src/help.rs b/helix-term/src/help.rs new file mode 100644 index 000000000000..6a03ad57f19b --- /dev/null +++ b/helix-term/src/help.rs @@ -0,0 +1,35 @@ + pub fn help() -> String { + format!( + "\ +{} {} +{} +{} + +USAGE: + hx [FLAGS] [files]... + +ARGS: + ... Sets the input file to use, position can also be specified via file[:row[:col]] + +FLAGS: + -h, --help Prints help information + --tutor Loads the tutorial + --health [CATEGORY] Checks for potential errors in editor setup + CATEGORY can be a language or one of 'clipboard', 'languages' + or 'all'. 'all' is the default if not specified. + -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml + -c, --config Specifies a file to use for configuration + -v Increases logging verbosity each use for up to 3 times + --log Specifies a file to use for logging + (default file: {}) + -V, --version Prints version information + --vsplit Splits all given files vertically into different windows + --hsplit Splits all given files horizontally into different windows +", + env!("CARGO_PKG_NAME"), + helix_loader::VERSION_AND_GIT_HASH, + env!("CARGO_PKG_AUTHORS"), + env!("CARGO_PKG_DESCRIPTION"), + helix_loader::log_file().display(), +) +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 095e42fe8bbb..f7660ae0793c 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,3 +1,5 @@ +mod help; + use anyhow::{Context, Error, Result}; use crossterm::event::EventStream; use helix_loader::VERSION_AND_GIT_HASH; @@ -47,45 +49,11 @@ async fn main_impl() -> Result { std::fs::create_dir_all(parent).ok(); } - let help = format!( - "\ -{} {} -{} -{} - -USAGE: - hx [FLAGS] [files]... - -ARGS: - ... Sets the input file to use, position can also be specified via file[:row[:col]] - -FLAGS: - -h, --help Prints help information - --tutor Loads the tutorial - --health [CATEGORY] Checks for potential errors in editor setup - CATEGORY can be a language or one of 'clipboard', 'languages' - or 'all'. 'all' is the default if not specified. - -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml - -c, --config Specifies a file to use for configuration - -v Increases logging verbosity each use for up to 3 times - --log Specifies a file to use for logging - (default file: {}) - -V, --version Prints version information - --vsplit Splits all given files vertically into different windows - --hsplit Splits all given files horizontally into different windows -", - env!("CARGO_PKG_NAME"), - VERSION_AND_GIT_HASH, - env!("CARGO_PKG_AUTHORS"), - env!("CARGO_PKG_DESCRIPTION"), - logpath.display(), - ); - let args = Args::parse_args().context("could not parse arguments")?; // Help has a higher priority and should be handled separately. if args.display_help { - print!("{}", help); + print!("{}", help::help()); std::process::exit(0); } From cab5aac9c8d6fb90783fd2439b3ec1ad86a90a5b Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 21:32:36 +0100 Subject: [PATCH 064/195] help.rs: use named parameters --- helix-term/src/help.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/helix-term/src/help.rs b/helix-term/src/help.rs index 6a03ad57f19b..1f1226b1847b 100644 --- a/helix-term/src/help.rs +++ b/helix-term/src/help.rs @@ -1,9 +1,8 @@ - pub fn help() -> String { - format!( - "\ -{} {} -{} -{} +pub fn help() -> String { + format!("\ +{pkg_name} {version} +{authors} +{description} USAGE: hx [FLAGS] [files]... @@ -21,15 +20,15 @@ FLAGS: -c, --config Specifies a file to use for configuration -v Increases logging verbosity each use for up to 3 times --log Specifies a file to use for logging - (default file: {}) + (default file: {log_file_path}) -V, --version Prints version information --vsplit Splits all given files vertically into different windows --hsplit Splits all given files horizontally into different windows ", - env!("CARGO_PKG_NAME"), - helix_loader::VERSION_AND_GIT_HASH, - env!("CARGO_PKG_AUTHORS"), - env!("CARGO_PKG_DESCRIPTION"), - helix_loader::log_file().display(), -) -} + pkg_name = env!("CARGO_PKG_NAME"), + version = helix_loader::VERSION_AND_GIT_HASH, + authors = env!("CARGO_PKG_AUTHORS"), + description = env!("CARGO_PKG_DESCRIPTION"), + log_file_path = helix_loader::log_file().display(), + ) +} \ No newline at end of file From 88bd9cc568e0db87fb5793a326840f594117d2ab Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 22:11:45 +0100 Subject: [PATCH 065/195] Fix error prone config setup in main * Removed config dir check and creation from helix-term main.rs, it was being done in the proceeding line with helix_loader::initialize_config_file() * Log path setup moved to helix_loader, and setup not done before setting up logging. It would create the default log folder even if user specifies thier own log file. * Fixed helix_loader::config_file() and log_file() assumption of existing parent directory. --- helix-loader/src/lib.rs | 44 +++++++++++++++++-------- helix-term/src/main.rs | 73 +++++++++++++++++------------------------ 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 8952183ac22d..0c59aab957d1 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -8,20 +8,28 @@ pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); pub static RUNTIME_DIR: once_cell::sync::Lazy = once_cell::sync::Lazy::new(runtime_dir); static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +static LOG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); -pub fn initialize_config_file(specified_file: Option) { +pub fn setup_log_file(specified_file: Option) { + let log_file = specified_file.unwrap_or_else(|| { + let log_dir = cache_dir(); + if !log_dir.exists() { + std::fs::create_dir_all(&log_dir).ok(); + } + log_dir.join("helix.log") + }); + LOG_FILE.set(log_file).ok(); +} + +pub fn setup_config_file(specified_file: Option) { let config_file = specified_file.unwrap_or_else(|| { let config_dir = config_dir(); - if !config_dir.exists() { std::fs::create_dir_all(&config_dir).ok(); } - config_dir.join("config.toml") }); - - // We should only initialize this value once. - CONFIG_FILE.set(config_file).ok(); + CONFIG_FILE.set(config_file).unwrap(); } pub fn runtime_dir() -> PathBuf { @@ -76,21 +84,29 @@ pub fn cache_dir() -> PathBuf { path } +pub fn log_file() -> PathBuf { + match LOG_FILE.get() { + Some(log_path) => log_path.to_path_buf(), + None => { + setup_log_file(None); + log_file() + } + } +} pub fn config_file() -> PathBuf { - CONFIG_FILE - .get() - .map(|path| path.to_path_buf()) - .unwrap_or_else(|| config_dir().join("config.toml")) + match CONFIG_FILE.get() { + Some(config_path) => config_path.to_path_buf(), + None => { + setup_config_file(None); + config_file() + } + } } pub fn lang_config_file() -> PathBuf { config_dir().join("languages.toml") } -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(); diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index f7660ae0793c..4f1d6a9345a9 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -8,34 +8,6 @@ use helix_term::args::Args; use helix_term::config::Config; use std::path::PathBuf; -fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { - let mut base_config = fern::Dispatch::new(); - - base_config = match verbosity { - 0 => base_config.level(log::LevelFilter::Warn), - 1 => base_config.level(log::LevelFilter::Info), - 2 => base_config.level(log::LevelFilter::Debug), - _3_or_more => base_config.level(log::LevelFilter::Trace), - }; - - // Separate file config so we can include year, month and day in file logs - let file_config = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} {} [{}] {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"), - record.target(), - record.level(), - message - )) - }) - .chain(fern::log_file(logpath)?); - - base_config.chain(file_config).apply()?; - - Ok(()) -} - fn main() -> Result<()> { let exit_code = main_impl()?; std::process::exit(exit_code); @@ -43,13 +15,8 @@ fn main() -> Result<()> { #[tokio::main] async fn main_impl() -> Result { - let logpath = helix_loader::log_file(); - let parent = logpath.parent().unwrap(); - if !parent.exists() { - std::fs::create_dir_all(parent).ok(); - } - - let args = Args::parse_args().context("could not parse arguments")?; + let args = Args::parse_args().context("failed to parse arguments")?; + setup_logging(args.log_file.clone(), args.verbosity).context("failed to initialize logging")?; // Help has a higher priority and should be handled separately. if args.display_help { @@ -84,15 +51,12 @@ async fn main_impl() -> Result { return Ok(0); } - let logpath = args.log_file.as_ref().cloned().unwrap_or(logpath); - setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; - let config_dir = helix_loader::config_dir(); if !config_dir.exists() { std::fs::create_dir_all(&config_dir).ok(); } - helix_loader::initialize_config_file(args.config_file.clone()); + helix_loader::setup_config_file(args.config_file.clone()); let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) @@ -114,9 +78,7 @@ async fn main_impl() -> Result { use std::io::Read; // This waits for an enter press. let _ = std::io::stdin().read(&mut []); - helix_loader::default_lang_config() - .try_into() - .expect("Could not serialize built-in languages.toml") + helix_core::syntax::LanguageConfigurations::default() }); // TODO: use the thread local executor to spawn the application task separately from the work pool @@ -127,3 +89,30 @@ async fn main_impl() -> Result { Ok(exit_code) } + +fn setup_logging(logpath: Option, verbosity: u64) -> Result<()> { + helix_loader::setup_log_file(logpath); + + let mut base_config = fern::Dispatch::new(); + base_config = match verbosity { + 0 => base_config.level(log::LevelFilter::Warn), + 1 => base_config.level(log::LevelFilter::Info), + 2 => base_config.level(log::LevelFilter::Debug), + _3_or_more => base_config.level(log::LevelFilter::Trace), + }; + + // Separate file config so we can include year, month and day in file logs + let file_config = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"), + record.target(), + record.level(), + message + )) + }) + .chain(fern::log_file(helix_loader::log_file())?); + base_config.chain(file_config).apply()?; + Ok(()) +} \ No newline at end of file From 11248c192b7eefa39c263b5f973991d0456f4b64 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 22:17:36 +0100 Subject: [PATCH 066/195] Consistent exit handling Return Ok(0) to main instead of directly doing process::exit(0) on only some of the args. Makes it easier to implement special exit handling before quitting. --- helix-term/src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 4f1d6a9345a9..ecee8321b046 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -21,12 +21,12 @@ async fn main_impl() -> Result { // Help has a higher priority and should be handled separately. if args.display_help { print!("{}", help::help()); - std::process::exit(0); + return Ok(0); } if args.display_version { println!("helix {}", VERSION_AND_GIT_HASH); - std::process::exit(0); + return Ok(0); } if args.health { @@ -37,8 +37,7 @@ async fn main_impl() -> Result { return Err(err.into()); } } - - std::process::exit(0); + return Ok(0); } if args.fetch_grammars { From 3cd0042751e19cdf2a5267bbe7a7c5b982458127 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 22:30:27 +0100 Subject: [PATCH 067/195] Missed dual config setup in main. --- helix-term/src/main.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index ecee8321b046..dcda44dda27b 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -18,17 +18,14 @@ async fn main_impl() -> Result { let args = Args::parse_args().context("failed to parse arguments")?; setup_logging(args.log_file.clone(), args.verbosity).context("failed to initialize logging")?; - // Help has a higher priority and should be handled separately. if args.display_help { print!("{}", help::help()); return Ok(0); } - if args.display_version { println!("helix {}", VERSION_AND_GIT_HASH); return Ok(0); } - if args.health { if let Err(err) = helix_term::health::print_health(args.health_arg) { // Piping to for example `head -10` requires special handling: @@ -39,22 +36,15 @@ async fn main_impl() -> Result { } return Ok(0); } - if args.fetch_grammars { helix_loader::grammar::fetch_grammars()?; return Ok(0); } - if args.build_grammars { helix_loader::grammar::build_grammars(None)?; return Ok(0); } - let config_dir = helix_loader::config_dir(); - if !config_dir.exists() { - std::fs::create_dir_all(&config_dir).ok(); - } - helix_loader::setup_config_file(args.config_file.clone()); let config = match std::fs::read_to_string(helix_loader::config_file()) { From 8c5700d295abc8ec6e8fdc5de5a703f97d774ea0 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 14 Jan 2023 23:58:21 +0100 Subject: [PATCH 068/195] X Setup coherent config loading API Does not put project in working state, rebase with #5635 --- helix-term/src/application.rs | 6 ++--- helix-term/src/config.rs | 50 +++++++++-------------------------- helix-term/src/main.rs | 21 ++++++++++----- 3 files changed, 30 insertions(+), 47 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b352d5355ce1..2a21025d407b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -432,12 +432,12 @@ impl Application { fn refresh_config(&mut self) { let mut refresh_config = || -> Result<(), Error> { - let default_config = Config::load_default() + let merged_user_config = Config::merged() .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; self.refresh_language_config()?; - self.refresh_theme(&default_config)?; + self.refresh_theme(&merged_user_config)?; // Store new config - self.config.store(Arc::new(default_config)); + self.config.store(Arc::new(merged_user_config)); Ok(()) }; diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f838..c4517ffe4138 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,11 +1,8 @@ -use crate::keymap::{default::default, merge_keys, Keymap}; +use crate::keymap::{default::{default, self}, merge_keys, Keymap}; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; -use std::fmt::Display; -use std::io::Error as IOError; -use std::path::PathBuf; -use toml::de::Error as TomlError; +use anyhow::{Error, anyhow}; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] @@ -17,46 +14,25 @@ pub struct Config { pub editor: helix_view::editor::Config, } +impl Config { + pub fn merged() -> Result { + let config_string = std::fs::read_to_string(helix_loader::config_file())?; + toml::from_str(&config_string) + .map(|config: Config| config.merge_in_default_keymap()) + .map_err(|error| anyhow!("{}", error)) + } +} + impl Default for Config { fn default() -> Config { Config { theme: None, - keys: default(), + keys: default::default(), editor: helix_view::editor::Config::default(), } } } - -#[derive(Debug)] -pub enum ConfigLoadError { - BadConfig(TomlError), - Error(IOError), -} - -impl Display for ConfigLoadError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConfigLoadError::BadConfig(err) => err.fmt(f), - ConfigLoadError::Error(err) => err.fmt(f), - } - } -} - -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_default() -> Result { - Config::load(helix_loader::config_file()) - } -} - + #[cfg(test)] mod tests { use super::*; diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index dcda44dda27b..afd98b4c2f66 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -2,6 +2,7 @@ mod help; use anyhow::{Context, Error, Result}; use crossterm::event::EventStream; +use helix_core::syntax::LanguageConfigurations; use helix_loader::VERSION_AND_GIT_HASH; use helix_term::application::Application; use helix_term::args::Args; @@ -46,7 +47,14 @@ async fn main_impl() -> Result { } helix_loader::setup_config_file(args.config_file.clone()); + let config = Config::merged().unwrap_or_else(|err| { + eprintln!("Bad config: {}", err); + eprintln!("Press to continue with default config"); + let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut[]); + Config::default() + }); +<<<<<<< HEAD let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) .map(helix_term::keymap::merge_keys) @@ -62,20 +70,19 @@ async fn main_impl() -> Result { }; let syn_loader_conf = helix_loader::default_lang_config().try_into().unwrap_or_else(|err| { +======= + let language_configurations = LanguageConfigurations::merged().unwrap_or_else(|err| { +>>>>>>> 4731861d (Unify config loading) 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 []); - helix_core::syntax::LanguageConfigurations::default() + let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut[]); + LanguageConfigurations::default() }); // TODO: use the thread local executor to spawn the application task separately from the work pool - let mut app = Application::new(args, config, syn_loader_conf) + let mut app = Application::new(args, config, language_configurations) .context("unable to create new application")?; - let exit_code = app.run(&mut EventStream::new()).await?; - Ok(exit_code) } From e6c49adb31ff399069f93dbf5ff1833559552915 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 18:43:26 +0100 Subject: [PATCH 069/195] Fix failing test --- helix-term/src/config.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 3ed2fa52323b..354940ee174f 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -84,21 +84,24 @@ mod tests { A-F12 = "move_next_word_end" "#; - assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), - Config { - keys: hashmap! { - Mode::Insert => keytrie!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - }), - Mode::Normal => keytrie!({ "Normal mode" - "A-F12" => move_next_word_end, - }), - }, - ..Default::default() - } - ); + let config = Config { + keys: hashmap! { + Mode::Insert => keytrie!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + }), + Mode::Normal => keytrie!({ "Normal mode" + "A-F12" => move_next_word_end, + }), + }, + ..Default::default() + }; + for mode in config.keys.keys() { + assert_eq!( + config.keys.get(mode).unwrap().get_children(), + toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + ); + } } #[test] From 3d52c7a4f9ff607dd66ae69cb71d989b040d9b2f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 18:43:26 +0100 Subject: [PATCH 070/195] Fix failing test --- helix-term/src/config.rs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 3ed2fa52323b..a4905a3c7673 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -77,28 +77,31 @@ mod tests { fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] - y = "move_line_down" S-C-a = "delete_selection" + y = "move_line_down" [keys.normal] A-F12 = "move_next_word_end" "#; - assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), - Config { - keys: hashmap! { - Mode::Insert => keytrie!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - }), - Mode::Normal => keytrie!({ "Normal mode" - "A-F12" => move_next_word_end, - }), - }, - ..Default::default() - } - ); + let config = Config { + keys: hashmap! { + Mode::Insert => keytrie!({ "Insert mode" + "y" => move_line_down, + "S-C-a" => delete_selection, + }), + Mode::Normal => keytrie!({ "Normal mode" + "A-F12" => move_next_word_end, + }), + }, + ..Default::default() + }; + for mode in config.keys.keys() { + assert_eq!( + config.keys.get(mode).unwrap().get_children(), + toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + ); + } } #[test] From 1eaaf2df588c87fef22f4e421da32b3e8d06cb34 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 18:43:26 +0100 Subject: [PATCH 071/195] Fix failing test --- helix-term/src/config.rs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 3ed2fa52323b..4838e2d79c26 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -77,28 +77,31 @@ mod tests { fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] - y = "move_line_down" S-C-a = "delete_selection" + y = "move_line_down" [keys.normal] A-F12 = "move_next_word_end" "#; - assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), - Config { - keys: hashmap! { - Mode::Insert => keytrie!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - }), - Mode::Normal => keytrie!({ "Normal mode" - "A-F12" => move_next_word_end, - }), - }, - ..Default::default() - } - ); + let config = Config { + keys: hashmap! { + Mode::Insert => keytrie!({ "Insert mode" + "S-C-a" => delete_selection, + "y" => move_line_down, + }), + Mode::Normal => keytrie!({ "Normal mode" + "A-F12" => move_next_word_end, + }), + }, + ..Default::default() + }; + for mode in config.keys.keys() { + assert_eq!( + config.keys.get(mode).unwrap().get_children(), + toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + ); + } } #[test] From 6aec856710087e28ffc99cc150cae3c4c18c2381 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 15 Jan 2023 09:37:24 +0100 Subject: [PATCH 072/195] lib.rs cleanup * RUNTIME_DIR now behaves similar to the other runtime files * merge_toml_values: Moved function explanation comment in a function call to its documentation. --- helix-core/src/syntax.rs | 2 +- helix-loader/src/grammar.rs | 2 +- helix-loader/src/lib.rs | 180 +++++++++++++++++++----------------- helix-term/src/config.rs | 2 +- helix-term/src/health.rs | 2 +- helix-term/src/main.rs | 2 +- 6 files changed, 98 insertions(+), 92 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index eb1288e86ed2..3c67e7e5dad4 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -72,7 +72,7 @@ impl LanguageConfigurations { } impl Default for LanguageConfigurations { fn default() -> Self { - helix_loader::default_lang_config() + helix_loader::default_lang_configs() .try_into() .expect("Failed to deserialize built-in languages.toml into LanguageConfigurations") } diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 4a68fe3fb7c1..b29cd935e0c2 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -511,7 +511,7 @@ fn mtime(path: &Path) -> Result { /// Gives the contents of a file from a language's `runtime/queries/` /// directory pub fn load_runtime_file(language: &str, filename: &str) -> Result { - let path = crate::RUNTIME_DIR + let path = crate::runtime_dir() .join("queries") .join(language) .join(filename); diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 0c59aab957d1..9b58bdf0b671 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,24 +1,33 @@ pub mod grammar; +pub mod repo_paths; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; use std::path::PathBuf; pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); -pub static RUNTIME_DIR: once_cell::sync::Lazy = once_cell::sync::Lazy::new(runtime_dir); - +static RUNTIME_DIR: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); static LOG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); -pub fn setup_log_file(specified_file: Option) { - let log_file = specified_file.unwrap_or_else(|| { - let log_dir = cache_dir(); - if !log_dir.exists() { - std::fs::create_dir_all(&log_dir).ok(); +pub fn config_file() -> PathBuf { + match CONFIG_FILE.get() { + Some(config_path) => config_path.to_path_buf(), + None => { + setup_config_file(None); + config_file() } - log_dir.join("helix.log") - }); - LOG_FILE.set(log_file).ok(); + } +} + +pub fn log_file() -> PathBuf { + match LOG_FILE.get() { + Some(log_path) => log_path.to_path_buf(), + None => { + setup_log_file(None); + log_file() + } + } } pub fn setup_config_file(specified_file: Option) { @@ -32,102 +41,98 @@ pub fn setup_config_file(specified_file: Option) { CONFIG_FILE.set(config_file).unwrap(); } -pub fn runtime_dir() -> PathBuf { - if let Ok(dir) = std::env::var("HELIX_RUNTIME") { - return dir.into(); - } - - 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); - log::debug!("runtime dir: {}", path.to_string_lossy()); - return path; - } - - const RT_DIR: &str = "runtime"; - let conf_dir = config_dir().join(RT_DIR); - if conf_dir.exists() { - return conf_dir; - } - - // fallback to location of the executable being run - // canonicalize the path in case the executable is symlinked - std::env::current_exe() - .ok() - .and_then(|path| std::fs::canonicalize(path).ok()) - .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR))) - .unwrap() +pub fn setup_log_file(specified_file: Option) { + let log_file = specified_file.unwrap_or_else(|| { + let log_dir = cache_dir(); + if !log_dir.exists() { + std::fs::create_dir_all(&log_dir).ok(); + } + log_dir.join("helix.log") + }); + LOG_FILE.set(log_file).ok(); } pub fn config_dir() -> PathBuf { // TODO: allow env var override - let strategy = choose_base_strategy().expect("Unable to find the config directory!"); + let strategy = choose_base_strategy().expect("Unable to determine system base directory specification!"); let mut path = strategy.config_dir(); path.push("helix"); 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!"); + let strategy = choose_base_strategy().expect("Unable to determine system base directory specification!"); let mut path = strategy.cache_dir(); path.push("helix"); path } -pub fn log_file() -> PathBuf { - match LOG_FILE.get() { - Some(log_path) => log_path.to_path_buf(), - None => { - setup_log_file(None); - log_file() - } +pub fn runtime_dir() -> PathBuf { + if let Some(runtime_dir) = RUNTIME_DIR.get() { + return runtime_dir.to_path_buf(); } -} -pub fn config_file() -> PathBuf { - match CONFIG_FILE.get() { - Some(config_path) => config_path.to_path_buf(), - None => { - setup_config_file(None); - config_file() - } + else { + RUNTIME_DIR.set(_runtime_dir()).unwrap(); + runtime_dir() } } -pub fn lang_config_file() -> PathBuf { - config_dir().join("languages.toml") +/// $HELIX_RUNTIME || config_dir/runtime || repo/runtime (if run by cargo) || executable location +fn _runtime_dir() -> PathBuf { + // TODO: shouldn't it also look for XDG_RUNTIME_DIR? + if let Ok(dir) = std::env::var("HELIX_RUNTIME") { + return dir.into(); + } + + const RT_DIR: &str = "runtime"; + if std::env::var("CARGO_MANIFEST_DIR").is_ok() { + let path = repo_paths::project_root().join(RT_DIR); + log::debug!("runtime dir: {}", path.to_string_lossy()); + return path; + } + + let conf_dir = config_dir().join(RT_DIR); + if conf_dir.exists() { + return conf_dir; + } + + std::env::current_exe().ok() + .and_then(|path| std::fs::canonicalize(path).ok()) + .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR))) + .unwrap() } -pub fn find_local_config_dirs() -> Vec { - let current_dir = std::env::current_dir().expect("unable to determine current directory"); +// NOTE: only used for languages.toml files right now +pub fn 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()); + directories.push(ancestor.to_path_buf().join(".helix")); // 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.push(ancestor.to_path_buf().join(".helix")); } } + log::debug!("Located langauge configuration folders: {:?}", directories); directories } + +pub fn user_lang_config_file() -> PathBuf { + config_dir().join("languages.toml") +} + /// Default built-in languages.toml. -pub fn default_lang_config() -> toml::Value { - toml::from_slice(include_bytes!("../../languages.toml")) +pub fn default_lang_configs() -> toml::Value { + toml::from_slice(ineclude_bytes!("../..languages.toml")).unwrap()) .expect("Could not parse built-in languages.toml to valid toml") } +/// Searces for language.toml in config path (user config) and in 'helix' directories +/// in opened git repository (local). Merge order: +/// local -> user config -> default pub fn merged_lang_config() -> Result { let config = crate::local_config_dirs() .into_iter() @@ -140,29 +145,14 @@ pub fn merged_lang_config() -> Result { }) .collect::, _>>()? .into_iter() - .chain([default_lang_config()].into_iter()) + .chain([default_lang_configs()].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) }); Ok(config) } + /// Merge two TOML documents, merging values from `right` onto `left` /// /// When an array exists in both `left` and `right`, `right`'s array is @@ -176,6 +166,22 @@ pub fn merged_lang_config() -> Result { /// documents that use a top-level array of values like the `languages.toml`, /// where one usually wants to override or add to the array instead of /// replacing it altogether. +/// +/// For example: +/// +/// left: +/// [[language]] +/// name = "toml" +/// language-server = { command = "taplo", args = ["lsp", "stdio"] } +/// +/// right: +/// [[language]] +/// language-server = { command = "/usr/bin/taplo" } +/// +/// result: +/// [[language]] +/// name = "toml" +/// language-server = { command = "/usr/bin/taplo" } pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value { use toml::Value; diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index cfbbd1ec1ac6..1fbefc514b86 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -22,7 +22,7 @@ impl Config { .map_err(|error| anyhow!("{}", error)) } - pub fn merge_in_default_keymap(mut self) -> Config { + pub fn merge_in_default_keymap(mut self) -> Self { let mut delta = std::mem::replace(&mut self.keys, default::default()); for (mode, keys) in &mut self.keys { keys.merge_keytrie(delta.remove(mode).unwrap_or_default()) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 96d58114dae3..c7304c2b67bc 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -50,7 +50,7 @@ pub fn general() -> std::io::Result<()> { let mut stdout = stdout.lock(); let config_file = helix_loader::config_file(); - let lang_file = helix_loader::lang_config_file(); + let lang_file = helix_loader::user_lang_config_file(); let log_file = helix_loader::log_file(); let rt_dir = helix_loader::runtime_dir(); let clipboard_provider = get_clipboard_provider(); diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index bbb330b9b8c1..d23c53418a5e 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,6 +1,6 @@ mod help; -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Result}; use crossterm::event::EventStream; use helix_core::syntax::LanguageConfigurations; use helix_loader::VERSION_AND_GIT_HASH; From 5331bb61175cfd05052f4c2590348cfde47510f3 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 17 Jan 2023 11:58:30 +0100 Subject: [PATCH 073/195] Unify tasks from xtask and helix_loader * Wider use relative path replacements by moving xtask::pash to helix_loader::repo_paths.rs. * Wider use of new LanguageConfig load API * Minor appalication.rs cleanup --- helix-loader/src/lib.rs | 4 +- helix-loader/src/repo_paths.rs | 35 +++++++++++++++++ helix-term/src/application.rs | 66 ++++++++++++-------------------- helix-term/src/commands/lsp.rs | 4 +- helix-term/src/commands/typed.rs | 8 ++-- helix-term/src/ui/completion.rs | 8 ++-- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 2 +- helix-view/src/editor.rs | 8 ++-- helix-view/src/theme.rs | 7 ++-- xtask/src/docgen.rs | 11 +++--- xtask/src/helpers.rs | 11 +----- xtask/src/main.rs | 1 - xtask/src/path.rs | 24 ------------ xtask/src/querycheck.rs | 5 ++- xtask/src/themelint.rs | 6 +-- 16 files changed, 96 insertions(+), 106 deletions(-) create mode 100644 helix-loader/src/repo_paths.rs delete mode 100644 xtask/src/path.rs diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 9b58bdf0b671..82407b35fb63 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -126,8 +126,8 @@ pub fn user_lang_config_file() -> PathBuf { /// Default built-in languages.toml. pub fn default_lang_configs() -> toml::Value { - toml::from_slice(ineclude_bytes!("../..languages.toml")).unwrap()) - .expect("Could not parse built-in languages.toml to valid toml") + toml::from_slice(&std::fs::read(repo_paths::default_lang_configs()).unwrap()) + .expect("Could not parse built-in languages.toml to valid toml") } /// Searces for language.toml in config path (user config) and in 'helix' directories diff --git a/helix-loader/src/repo_paths.rs b/helix-loader/src/repo_paths.rs new file mode 100644 index 000000000000..0c40181a5b27 --- /dev/null +++ b/helix-loader/src/repo_paths.rs @@ -0,0 +1,35 @@ +use std::path::{Path, PathBuf}; + +pub fn project_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap().to_path_buf() +} + +pub fn book_gen() -> PathBuf { + project_root().join("book/src/generated/") +} + +pub fn ts_queries() -> PathBuf { + project_root().join("runtime/queries") +} + +pub fn themes() -> PathBuf { + project_root().join("runtime/themes") +} + +pub fn default_config_dir() -> PathBuf { + // TODO: would be nice to move config files away from project root folder + project_root() +} + +pub fn default_lang_configs() -> PathBuf { + default_config_dir().join("languages.toml") +} + +pub fn default_theme() -> PathBuf { + default_config_dir().join("theme.toml") +} + +pub fn default_base16_theme() -> PathBuf { + default_config_dir().join("base16_theme.toml") +} \ No newline at end of file diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index fb1ec0a8fbcb..a3c2ec97f136 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -74,7 +74,7 @@ pub struct Application { #[allow(dead_code)] theme_loader: Arc, #[allow(dead_code)] - syn_loader: Arc, + lang_configs_loader: Arc, signals: Signals, jobs: Jobs, @@ -125,7 +125,7 @@ impl Application { pub fn new( args: Args, config: Config, - syn_loader_conf: syntax::LanguageConfigurations, + langauge_configurations: syntax::LanguageConfigurations, ) -> Result { #[cfg(feature = "integration")] setup_integration_logging(); @@ -153,7 +153,7 @@ impl Application { }) .unwrap_or_else(|| theme_loader.default_theme(true_color)); - let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); + let lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(langauge_configurations)); #[cfg(not(feature = "integration"))] let backend = CrosstermBackend::new(stdout()); @@ -168,7 +168,7 @@ impl Application { let mut editor = Editor::new( area, theme_loader.clone(), - syn_loader.clone(), + lang_configs_loader.clone(), Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor })), @@ -263,7 +263,7 @@ impl Application { config, theme_loader, - syn_loader, + lang_configs_loader, signals, jobs: Jobs::new(), @@ -396,32 +396,13 @@ impl Application { /// refresh language config after config change fn refresh_language_config(&mut self) -> Result<(), Error> { - let syntax_config = LanguageConfigurations::merged() + let language_configs = LanguageConfigurations::merged() .map_err(|err| anyhow::anyhow!("Failed to load language config: {}", err))?; - self.syn_loader = std::sync::Arc::new(syntax::Loader::new(syntax_config)); - self.editor.syn_loader = self.syn_loader.clone(); + self.lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(language_configs)); + self.editor.lang_configs_loader = self.lang_configs_loader.clone(); for document in self.editor.documents.values_mut() { - document.detect_language(self.syn_loader.clone()); - } - - Ok(()) - } - - /// Refresh theme after config change - fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> { - if let Some(theme) = config.theme.clone() { - let true_color = self.true_color(); - let theme = self - .theme_loader - .load(&theme) - .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; - - if true_color || theme.is_16_color() { - self.editor.set_theme(theme); - } else { - anyhow::bail!("theme requires truecolor support, which is not available") - } + document.detect_language(self.lang_configs_loader.clone()); } Ok(()) @@ -432,26 +413,29 @@ impl Application { let merged_user_config = Config::merged() .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; self.refresh_language_config()?; - self.refresh_theme(&merged_user_config)?; - // Store new config + + if let Some(theme) = &self.config.load().theme { + let true_color = self.config.load().editor.true_color || crate::true_color(); + let theme = self.theme_loader.load(theme) + .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; + + if true_color || theme.is_16_color() { + self.editor.set_theme(theme); + } else { + anyhow::bail!("Theme requires truecolor support, which is not available!") + } + } + self.config.store(Arc::new(merged_user_config)); Ok(()) }; match refresh_config() { - Ok(_) => { - self.editor.set_status("Config refreshed"); - } - Err(err) => { - self.editor.set_error(err.to_string()); - } + Ok(_) => { self.editor.set_status("Config refreshed"); }, + Err(err) => { self.editor.set_error(err.to_string()); } } } - fn true_color(&self) -> bool { - self.config.load().editor.true_color || crate::true_color() - } - #[cfg(windows)] // no signal handling available on windows pub async fn handle_signals(&mut self, _signal: ()) {} @@ -542,7 +526,7 @@ impl Application { return; } - let loader = self.editor.syn_loader.clone(); + let loader = self.editor.lang_configs_loader.clone(); // borrowing the same doc again to get around the borrow checker let doc = doc_mut!(self.editor, &doc_save_event.doc_id); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 578eb6084196..41d079f85b4e 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1097,7 +1097,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { let mut contents = SignatureHelp::new( signature.label.clone(), language.to_string(), - Arc::clone(&editor.syn_loader), + Arc::clone(&editor.lang_configs_loader), ); let signature_doc = if config.lsp.display_signature_help_docs { @@ -1193,7 +1193,7 @@ pub fn hover(cx: &mut Context) { // skip if contents empty - let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); + let contents = ui::Markdown::new(contents, editor.lang_configs_loader.clone()); let popup = Popup::new("hover", contents).auto_close(true); compositor.replace_or_push("hover", popup); } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index deb5f7bc88a8..28063cc23452 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1242,7 +1242,7 @@ fn tree_sitter_scopes( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); + let contents = ui::Markdown::new(contents, editor.lang_configs_loader.clone()); let popup = Popup::new("hover", contents).auto_close(true); compositor.replace_or_push("hover", popup); }, @@ -1535,7 +1535,7 @@ fn language( if args[0] == "text" { doc.set_language(None, None) } else { - doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone())?; + doc.set_language_by_language_id(&args[0], cx.editor.lang_configs_loader.clone())?; } doc.detect_indent_and_line_ending(); @@ -1671,7 +1671,7 @@ fn tree_sitter_subtree( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); + let contents = ui::Markdown::new(contents, editor.lang_configs_loader.clone()); let popup = Popup::new("hover", contents).auto_close(true); compositor.replace_or_push("hover", popup); }, @@ -1804,7 +1804,7 @@ fn run_shell_command( move |editor: &mut Editor, compositor: &mut Compositor| { let contents = ui::Markdown::new( format!("```sh\n{}\n```", output), - editor.syn_loader.clone(), + editor.lang_configs_loader.clone(), ); let popup = Popup::new("shell", contents).position(Some( helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2eca709d181b..b2d82ca54b02 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -408,7 +408,7 @@ impl Component for Completion { option.detail.as_deref().unwrap_or_default(), contents ), - cx.editor.syn_loader.clone(), + cx.editor.lang_configs_loader.clone(), ) } Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { @@ -419,10 +419,10 @@ impl Component for Completion { if let Some(detail) = &option.detail.as_deref() { Markdown::new( format!("```{}\n{}\n```\n{}", language, detail, contents), - cx.editor.syn_loader.clone(), + cx.editor.lang_configs_loader.clone(), ) } else { - Markdown::new(contents.to_string(), cx.editor.syn_loader.clone()) + Markdown::new(contents.to_string(), cx.editor.lang_configs_loader.clone()) } } None if option.detail.is_some() => { @@ -435,7 +435,7 @@ impl Component for Completion { language, option.detail.as_deref().unwrap_or_default(), ), - cx.editor.syn_loader.clone(), + cx.editor.lang_configs_loader.clone(), ) } None => return, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index eb4807581e8c..0a2a8b0bd18d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -368,7 +368,7 @@ pub mod completers { let text: String = "text".into(); let language_ids = editor - .syn_loader + .lang_configs_loader .language_configs() .map(|config| &config.language_id) .chain(std::iter::once(&text)); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ccf37eb2bac5..abc002a37646 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -211,7 +211,7 @@ impl FilePicker { // Then attempt to highlight it if it has no language set if let Some(doc) = doc { if doc.language_config().is_none() { - let loader = cx.editor.syn_loader.clone(); + let loader = cx.editor.lang_configs_loader.clone(); doc.detect_language(loader); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c54f8be94248..3eccb4811ae1 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -788,7 +788,7 @@ pub struct Editor { pub clipboard_provider: Box, - pub syn_loader: Arc, + pub lang_configs_loader: Arc, pub theme_loader: Arc, /// last_theme is used for theme previews. We store the current theme here, /// and if previewing is cancelled, we can return to it. @@ -896,7 +896,7 @@ impl Editor { debugger: None, debugger_events: SelectAll::new(), breakpoints: HashMap::new(), - syn_loader, + lang_configs_loader: syn_loader, theme_loader, last_theme: None, last_line_number: None, @@ -1003,7 +1003,7 @@ impl Editor { } let scopes = theme.scopes(); - self.syn_loader.set_scopes(scopes.to_vec()); + self.lang_configs_loader.set_scopes(scopes.to_vec()); match preview { ThemeAction::Preview => { @@ -1223,7 +1223,7 @@ impl Editor { let id = if let Some(id) = id { id } else { - let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?; + let mut doc = Document::open(&path, None, Some(self.lang_configs_loader.clone()))?; let _ = Self::launch_language_server(&mut self.language_servers, &mut doc); if let Some(diff_base) = self.diff_providers.get_diff_base(&path) { diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index cb0d3ac46d34..47bc67eade5e 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -5,7 +5,7 @@ use std::{ use anyhow::{anyhow, Context, Result}; use helix_core::hashmap; -use helix_loader::merge_toml_values; +use helix_loader::{merge_toml_values, repo_paths}; use log::warn; use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; @@ -15,11 +15,12 @@ use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME_DATA: Lazy = Lazy::new(|| { - toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") + toml::from_slice(&std::fs::read(repo_paths::default_theme()).unwrap()) + .expect("Failed to parse default theme") }); pub static BASE16_DEFAULT_THEME_DATA: Lazy = Lazy::new(|| { - toml::from_slice(include_bytes!("../../base16_theme.toml")) + toml::from_slice(&std::fs::read(repo_paths::default_base16_theme()).unwrap()) .expect("Failed to parse base 16 default theme") }); diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index 473882f3ee65..c33e60008a66 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -1,7 +1,8 @@ use crate::helpers; -use crate::path; use crate::DynError; +use helix_core::syntax::LanguageConfigurations; +use helix_loader::repo_paths; use helix_term::commands::TYPABLE_COMMAND_LIST; use helix_term::health::TsFeature; use std::fs; @@ -62,9 +63,9 @@ pub fn lang_features() -> Result { cols.push("Default LSP".to_owned()); md.push_str(&md_table_heading(&cols)); - let config = helpers::lang_config(); + let lang_configs = LanguageConfigurations::default(); - let mut langs = config + let mut langs = lang_configs .language .iter() .map(|l| l.language_id.clone()) @@ -78,7 +79,7 @@ pub fn lang_features() -> Result { let mut row = Vec::new(); for lang in langs { - let lc = config + let lc = lang_configs .language .iter() .find(|l| l.language_id == lang) @@ -112,6 +113,6 @@ pub fn lang_features() -> Result { pub fn write(filename: &str, data: &str) { let error = format!("Could not write to {}", filename); - let path = path::book_gen().join(filename); + let path = repo_paths::book_gen().join(filename); fs::write(path, data).expect(&error); } diff --git a/xtask/src/helpers.rs b/xtask/src/helpers.rs index eca88f4696f9..812f17584656 100644 --- a/xtask/src/helpers.rs +++ b/xtask/src/helpers.rs @@ -1,13 +1,11 @@ use std::path::{Path, PathBuf}; - -use crate::path; -use helix_core::syntax::LanguageConfigurations as LangConfig; +use helix_loader::repo_paths; use helix_term::health::TsFeature; /// Get the list of languages that support a particular tree-sitter /// based feature. pub fn ts_lang_support(feat: TsFeature) -> Vec { - let queries_dir = path::ts_queries(); + let queries_dir = repo_paths::ts_queries(); find_files(&queries_dir, feat.runtime_filename()) .iter() @@ -37,8 +35,3 @@ pub fn find_files(dir: &Path, filename: &str) -> Vec { .flatten() .collect() } - -pub fn lang_config() -> LangConfig { - let bytes = std::fs::read(path::lang_config()).unwrap(); - toml::from_slice(&bytes).unwrap() -} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1421fd1a1de6..556e247d463e 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,5 @@ mod docgen; mod helpers; -mod path; mod querycheck; mod themelint; diff --git a/xtask/src/path.rs b/xtask/src/path.rs deleted file mode 100644 index 6f4545c274aa..000000000000 --- a/xtask/src/path.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::path::{Path, PathBuf}; - -pub fn project_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .to_path_buf() -} - -pub fn book_gen() -> PathBuf { - project_root().join("book/src/generated/") -} - -pub fn ts_queries() -> PathBuf { - project_root().join("runtime/queries") -} - -pub fn lang_config() -> PathBuf { - project_root().join("languages.toml") -} - -pub fn themes() -> PathBuf { - project_root().join("runtime/themes") -} diff --git a/xtask/src/querycheck.rs b/xtask/src/querycheck.rs index 454d0e5cd9cb..cb88f488443a 100644 --- a/xtask/src/querycheck.rs +++ b/xtask/src/querycheck.rs @@ -1,7 +1,8 @@ +use helix_core::syntax::LanguageConfigurations; + use crate::DynError; pub fn query_check() -> Result<(), DynError> { - use crate::helpers::lang_config; use helix_core::{syntax::read_query, tree_sitter::Query}; use helix_loader::grammar::get_language; @@ -13,7 +14,7 @@ pub fn query_check() -> Result<(), DynError> { "indents.scm", ]; - for language in lang_config().language { + for language in LanguageConfigurations::default().language { let language_name = &language.language_id; let grammar_name = language.grammar.as_ref().unwrap_or(language_name); for query_file in query_files { diff --git a/xtask/src/themelint.rs b/xtask/src/themelint.rs index 06dfae407a23..3f9821ee30b7 100644 --- a/xtask/src/themelint.rs +++ b/xtask/src/themelint.rs @@ -1,5 +1,5 @@ -use crate::path; use crate::DynError; +use helix_loader::repo_paths; use helix_view::theme::Loader; use helix_view::theme::Modifier; use helix_view::Theme; @@ -155,7 +155,7 @@ pub fn lint(file: String) -> Result<(), DynError> { println!("Skipping base16: {}", file); return Ok(()); } - let path = path::themes().join(file.clone() + ".toml"); + let path = repo_paths::themes().join(file.clone() + ".toml"); let theme = std::fs::read(&path).unwrap(); let theme: Theme = toml::from_slice(&theme).expect("Failed to parse theme"); @@ -178,7 +178,7 @@ pub fn lint(file: String) -> Result<(), DynError> { } pub fn lint_all() -> Result<(), DynError> { - let files = Loader::read_names(path::themes().as_path()); + let files = Loader::read_names(repo_paths::themes().as_path()); let files_count = files.len(); let ok_files_count = files .into_iter() From 3be034c41d6bf32fbfae36e7831ba6bb5295c345 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 17 Jan 2023 14:57:40 +0100 Subject: [PATCH 074/195] Use unified config loading in docgen and health health.rs * Removed duplicate clipboard provider writeout. * Removed 'all' flag option but added a 'paths' option. * Removed check if user config exist or show default part as default is always merged with user config. docgen: * removed xtask::helper::lang_config(), using LanguageConfiguarions::default() instead. * moved xtask::query_check to xtask::main and made it use TSFeature.runtime_filename() --- helix-loader/src/lib.rs | 1 + helix-loader/src/ts_probe.rs | 39 ++++ helix-term/src/health.rs | 371 +++++++++++------------------------ helix-term/src/help.rs | 6 +- xtask/src/docgen.rs | 18 +- xtask/src/helpers.rs | 37 ---- xtask/src/main.rs | 25 ++- xtask/src/querycheck.rs | 40 ---- 8 files changed, 189 insertions(+), 348 deletions(-) create mode 100644 helix-loader/src/ts_probe.rs delete mode 100644 xtask/src/helpers.rs delete mode 100644 xtask/src/querycheck.rs diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 82407b35fb63..3b3e5abd42d4 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,4 +1,5 @@ pub mod grammar; +pub mod ts_probe; pub mod repo_paths; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; diff --git a/helix-loader/src/ts_probe.rs b/helix-loader/src/ts_probe.rs new file mode 100644 index 000000000000..f80900958ae7 --- /dev/null +++ b/helix-loader/src/ts_probe.rs @@ -0,0 +1,39 @@ +// NOTE: currently not making use of folds, injections, locals, tags. +// (fd --hidden --glob *.scm --exec basename {} \; | sort | uniq) +/// Helper functions for probing Tree-sitter language support in Helix +#[derive(Copy, Clone)] +pub enum TsFeature { + Highlight, + TextObject, + AutoIndent, +} + +impl TsFeature { + pub fn all() -> &'static [Self] { + &[Self::Highlight, Self::TextObject, Self::AutoIndent] + } + + pub fn runtime_filename(&self) -> &'static str { + match *self { + Self::Highlight => "highlights.scm", + Self::TextObject => "textobjects.scm", + Self::AutoIndent => "indents.scm", + } + } + + pub fn long_title(&self) -> &'static str { + match *self { + Self::Highlight => "Syntax Highlighting", + Self::TextObject => "Treesitter Textobjects", + Self::AutoIndent => "Auto Indent", + } + } + + pub fn short_title(&self) -> &'static str { + match *self { + Self::Highlight => "Highlight", + Self::TextObject => "Textobject", + Self::AutoIndent => "Indent", + } + } +} \ No newline at end of file diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index c7304c2b67bc..95d43b72352b 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -1,73 +1,36 @@ -use crossterm::{ - style::{Color, Print, Stylize}, - tty::IsTty, -}; -use helix_core::syntax::LanguageConfigurations; -use helix_loader::grammar::load_runtime_file; -use helix_view::clipboard::get_clipboard_provider; +use crossterm::{style::{Color, Print, Stylize}, tty::IsTty}; +use helix_loader::ts_probe::TsFeature; +use helix_loader::grammar; +use helix_core::syntax::{LanguageConfigurations, LanguageConfiguration}; +use helix_view::clipboard; use std::io::Write; -#[derive(Copy, Clone)] -pub enum TsFeature { - Highlight, - TextObject, - AutoIndent, -} - -impl TsFeature { - pub fn all() -> &'static [Self] { - &[Self::Highlight, Self::TextObject, Self::AutoIndent] - } - - pub fn runtime_filename(&self) -> &'static str { - match *self { - Self::Highlight => "highlights.scm", - Self::TextObject => "textobjects.scm", - Self::AutoIndent => "indents.scm", - } - } - - pub fn long_title(&self) -> &'static str { - match *self { - Self::Highlight => "Syntax Highlighting", - Self::TextObject => "Treesitter Textobjects", - Self::AutoIndent => "Auto Indent", - } - } - - pub fn short_title(&self) -> &'static str { - match *self { - Self::Highlight => "Highlight", - Self::TextObject => "Textobject", - Self::AutoIndent => "Indent", +pub fn print_health(health_arg: Option) -> std::io::Result<()> { + match health_arg.as_deref() { + None => { + display_paths()?; + display_clipboard()?; + writeln!(std::io::stdout().lock())?; + display_all_languages()?; } + Some("paths") => display_paths()?, + Some("clipboard") => display_clipboard()?, + Some("languages") => display_all_languages()?, + Some(lang) => display_language(lang.to_string())?, } + Ok(()) } -/// Display general diagnostics. -pub fn general() -> std::io::Result<()> { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); +fn display_paths() -> std::io::Result<()> { + let mut stdout = std::io::stdout().lock(); - let config_file = helix_loader::config_file(); - let lang_file = helix_loader::user_lang_config_file(); - let log_file = helix_loader::log_file(); - let rt_dir = helix_loader::runtime_dir(); - let clipboard_provider = get_clipboard_provider(); + writeln!(stdout, "Default config merged with user preferences supplied in:")?; + writeln!(stdout, "Config: {}", helix_loader::config_file().display())?; + writeln!(stdout, "Language config: {}", helix_loader::user_lang_config_file().display())?; + writeln!(stdout, "Log file: {}", helix_loader::log_file().display())?; - if config_file.exists() { - writeln!(stdout, "Config file: {}", config_file.display())?; - } else { - writeln!(stdout, "Config file: default")?; - } - if lang_file.exists() { - writeln!(stdout, "Language file: {}", lang_file.display())?; - } else { - writeln!(stdout, "Language file: default")?; - } - writeln!(stdout, "Log file: {}", log_file.display())?; + let rt_dir = helix_loader::runtime_dir(); writeln!(stdout, "Runtime directory: {}", rt_dir.display())?; - if let Ok(path) = std::fs::read_link(&rt_dir) { let msg = format!("Runtime directory is symlinked to {}", path.display()); writeln!(stdout, "{}", msg.yellow())?; @@ -78,245 +41,137 @@ pub fn general() -> std::io::Result<()> { if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { writeln!(stdout, "{}", "Runtime directory is empty.".red())?; } - writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?; Ok(()) } -pub fn clipboard() -> std::io::Result<()> { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - - let board = get_clipboard_provider(); - match board.name().as_ref() { +fn display_clipboard() -> std::io::Result<()> { + let mut stdout = std::io::stdout().lock(); + let clipboard = clipboard::get_clipboard_provider(); + match clipboard.name().as_ref() { "none" => { - writeln!( - stdout, - "{}", - "System clipboard provider: Not installed".red() - )?; - writeln!( - stdout, - " {}", - "For troubleshooting system clipboard issues, refer".red() - )?; - writeln!(stdout, " {}", - "https://github.com/helix-editor/helix/wiki/Troubleshooting#copypaste-fromto-system-clipboard-not-working" - .red().underlined())?; + writeln!(stdout, "{}", "No system clipboard provider installed, refer to:".red())?; + let link = "https://github.com/helix-editor/helix/wiki/Troubleshooting#copypaste-fromto-system-clipboard-not-working"; + writeln!(stdout, "{}", link.red().underlined())?; } name => writeln!(stdout, "System clipboard provider: {}", name)?, } - Ok(()) } -pub fn languages_all() -> std::io::Result<()> { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); +fn load_merged_language_configurations() -> std::io::Result> { + LanguageConfigurations::merged().or_else(|err| { + let mut stderr = std::io::stderr().lock(); + writeln!(stderr,"{}: {}","Error parsing user language config".red(),err)?; + writeln!(stderr, "{}", "Using default language config".yellow())?; + Ok(LanguageConfigurations::default()) + }) + .map(|lang_configs| lang_configs.language) +} - let mut syn_loader_conf = match LanguageConfigurations::merged() { - Ok(conf) => conf, - Err(err) => { - let stderr = std::io::stderr(); - let mut stderr = stderr.lock(); +fn display_language(lang_str: String) -> std::io::Result<()> { + let mut stdout = std::io::stdout().lock(); - writeln!( - stderr, - "{}: {}", - "Error parsing user language config".red(), - err - )?; - writeln!(stderr, "{}", "Using default language config".yellow())?; - LanguageConfigurations::default() + let language_configurations = load_merged_language_configurations()?; + let lang = match language_configurations.iter().find(|l| l.language_id == lang_str) { + Some(found_language) => found_language, + None => { + writeln!(stdout, "{}", format!("Language '{lang_str}' not found").red())?; + let suggestions: Vec<&str> = language_configurations.iter() + .filter(|l| l.language_id.starts_with(lang_str.chars().next().unwrap())) + .map(|l| l.language_id.as_str()) + .collect(); + if !suggestions.is_empty() { + let suggestions = suggestions.join(", "); + writeln!(stdout,"Did you mean one of these: {} ?",suggestions.yellow())?; + } + return Ok(()); } }; - let mut headings = vec!["Language", "LSP", "DAP"]; + let probe_protocol = |protocol_name: &str, server_cmd: Option| -> std::io::Result<()> { + let mut stdout = std::io::stdout().lock(); + match server_cmd { + Some(server_cmd) => { + writeln!(stdout, "Configured {protocol_name}: {}", server_cmd.clone().green())?; + let result = match which::which(&server_cmd) { + Ok(path) => path.display().to_string().green(), + Err(_) => format!("Not found in $PATH").red() + }; + writeln!(stdout, "Binary for {server_cmd}: {result}")? + }, + None => writeln!(stdout, "Configured {protocol_name}: {}", "None".yellow())? + }; + Ok(()) + }; + + probe_protocol("language server",lang.language_server.as_ref() + .map(|lsp| lsp.command.to_string()))?; + probe_protocol("debug adapter",lang.debugger.as_ref() + .map(|dap| dap.command.to_string()))?; - for feat in TsFeature::all() { - headings.push(feat.short_title()) + for feature in TsFeature::all() { + let supported = match grammar::load_runtime_file(&lang.language_id, feature.runtime_filename()).is_ok() { + true => "✓".green(), + false => "✗".red(), + }; + writeln!(stdout, "{} queries: {supported}", feature.short_title())?; + } + Ok(()) +} + +fn display_all_languages() -> std::io::Result<()> { + let mut stdout = std::io::stdout().lock(); + + let mut column_headers = vec!["Language", "LSP", "DAP"]; + for treesitter_feature in TsFeature::all() { + column_headers.push(treesitter_feature.short_title()) } - let terminal_cols = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80); - let column_width = terminal_cols as usize / headings.len(); - let is_terminal = std::io::stdout().is_tty(); + let column_width = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80) as usize / column_headers.len(); + let print_column = |item: &str, color: Color| { + let mut data = format!("{:column_width$}", item + .get(..column_width - 2) + .map(|s| format!("{}…", s)) + .unwrap_or_else(|| item.to_string())); - let column = |item: &str, color: Color| { - let mut data = format!( - "{:width$}", - item.get(..column_width - 2) - .map(|s| format!("{}…", s)) - .unwrap_or_else(|| item.to_string()), - width = column_width, - ); - if is_terminal { + if std::io::stdout().is_tty() { data = data.stylize().with(color).to_string(); } - - // We can't directly use println!() because of // https://github.com/crossterm-rs/crossterm/issues/589 let _ = crossterm::execute!(std::io::stdout(), Print(data)); }; - for heading in headings { - column(heading, Color::White); + for header in column_headers { + print_column(header, Color::White); } writeln!(stdout)?; - syn_loader_conf - .language - .sort_unstable_by_key(|l| l.language_id.clone()); - let check_binary = |cmd: Option| match cmd { Some(cmd) => match which::which(&cmd) { - Ok(_) => column(&format!("✓ {}", cmd), Color::Green), - Err(_) => column(&format!("✘ {}", cmd), Color::Red), + Ok(_) => print_column(&format!("✓ {}", cmd), Color::Green), + Err(_) => print_column(&format!("✗ {}", cmd), Color::Red), }, - None => column("None", Color::Yellow), + None => print_column("None", Color::Yellow), }; - for lang in &syn_loader_conf.language { - column(&lang.language_id, Color::Reset); + let mut language_configurations = load_merged_language_configurations()?; + language_configurations.sort_unstable_by_key(|l| l.language_id.clone()); + for lang in &language_configurations { + print_column(&lang.language_id, Color::Reset); - let lsp = lang - .language_server - .as_ref() - .map(|lsp| lsp.command.to_string()); + let lsp = lang.language_server.as_ref().map(|lsp| lsp.command.to_string()); check_binary(lsp); - let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string()); check_binary(dap); for ts_feat in TsFeature::all() { - match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() { - true => column("✓", Color::Green), - false => column("✘", Color::Red), + match grammar::load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() { + true => print_column("✓", Color::Green), + false => print_column("✗", Color::Red), } } - writeln!(stdout)?; } - Ok(()) -} - -/// Display diagnostics pertaining to a particular language (LSP, -/// highlight queries, etc). -pub fn language(lang_str: String) -> std::io::Result<()> { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - - let syn_loader_conf = match helix_loader::merged_lang_config()?.try_into() { - Ok(conf) => conf, - Err(err) => { - let stderr = std::io::stderr(); - let mut stderr = stderr.lock(); - - writeln!( - stderr, - "{}: {}", - "Error parsing user language config".red(), - err - )?; - writeln!(stderr, "{}", "Using default language config".yellow())?; - LanguageConfigurations::default() - } - }; - - let lang = match syn_loader_conf - .language - .iter() - .find(|l| l.language_id == lang_str) - { - Some(l) => l, - None => { - let msg = format!("Language '{}' not found", lang_str); - writeln!(stdout, "{}", msg.red())?; - let suggestions: Vec<&str> = syn_loader_conf - .language - .iter() - .filter(|l| l.language_id.starts_with(lang_str.chars().next().unwrap())) - .map(|l| l.language_id.as_str()) - .collect(); - if !suggestions.is_empty() { - let suggestions = suggestions.join(", "); - writeln!( - stdout, - "Did you mean one of these: {} ?", - suggestions.yellow() - )?; - } - return Ok(()); - } - }; - - probe_protocol( - "language server", - lang.language_server - .as_ref() - .map(|lsp| lsp.command.to_string()), - )?; - - probe_protocol( - "debug adapter", - lang.debugger.as_ref().map(|dap| dap.command.to_string()), - )?; - - for ts_feat in TsFeature::all() { - probe_treesitter_feature(&lang_str, *ts_feat)? - } - - Ok(()) -} - -/// Display diagnostics about LSP and DAP. -fn probe_protocol(protocol_name: &str, server_cmd: Option) -> std::io::Result<()> { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - - let cmd_name = match server_cmd { - Some(ref cmd) => cmd.as_str().green(), - None => "None".yellow(), - }; - writeln!(stdout, "Configured {}: {}", protocol_name, cmd_name)?; - - if let Some(cmd) = server_cmd { - let path = match which::which(&cmd) { - Ok(path) => path.display().to_string().green(), - Err(_) => format!("'{}' not found in $PATH", cmd).red(), - }; - writeln!(stdout, "Binary for {}: {}", protocol_name, path)?; - } - - Ok(()) -} - -/// Display diagnostics about a feature that requires tree-sitter -/// query files (highlights, textobjects, etc). -fn probe_treesitter_feature(lang: &str, feature: TsFeature) -> std::io::Result<()> { - let stdout = std::io::stdout(); - let mut stdout = stdout.lock(); - - let found = match load_runtime_file(lang, feature.runtime_filename()).is_ok() { - true => "✓".green(), - false => "✘".red(), - }; - writeln!(stdout, "{} queries: {}", feature.short_title(), found)?; - - Ok(()) -} - -pub fn print_health(health_arg: Option) -> std::io::Result<()> { - match health_arg.as_deref() { - Some("languages") => languages_all()?, - Some("clipboard") => clipboard()?, - None | Some("all") => { - general()?; - clipboard()?; - writeln!(std::io::stdout().lock())?; - languages_all()?; - } - Some(lang) => language(lang.to_string())?, - } - Ok(()) -} +} \ No newline at end of file diff --git a/helix-term/src/help.rs b/helix-term/src/help.rs index 1f1226b1847b..f210be03cf18 100644 --- a/helix-term/src/help.rs +++ b/helix-term/src/help.rs @@ -13,9 +13,9 @@ ARGS: FLAGS: -h, --help Prints help information --tutor Loads the tutorial - --health [CATEGORY] Checks for potential errors in editor setup - CATEGORY can be a language or one of 'clipboard', 'languages' - or 'all'. 'all' is the default if not specified. + --health [SECTION] Displays potential errors in editor setup. + Optional SECTION can 'paths', 'clipboard', 'languages' or a + singular language name. -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml -c, --config Specifies a file to use for configuration -v Increases logging verbosity each use for up to 3 times diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index c33e60008a66..ff4f3efa07e9 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -1,10 +1,9 @@ -use crate::helpers; use crate::DynError; use helix_core::syntax::LanguageConfigurations; use helix_loader::repo_paths; +use helix_loader::ts_probe::TsFeature; use helix_term::commands::TYPABLE_COMMAND_LIST; -use helix_term::health::TsFeature; use std::fs; pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md"; @@ -72,9 +71,16 @@ pub fn lang_features() -> Result { .collect::>(); langs.sort_unstable(); - let mut ts_features_to_langs = Vec::new(); - for &feat in ts_features { - ts_features_to_langs.push((feat, helpers::ts_lang_support(feat))); + let mut ts_support_by_feature = Vec::with_capacity(TsFeature::all().len()); + for feature in TsFeature::all() { + let mut langs_with_ts_support: Vec = Vec::new(); + for lang in LanguageConfigurations::default().language { + if helix_loader::grammar::load_runtime_file(&lang.language_id, feature.runtime_filename()).is_ok() { + langs_with_ts_support.push(lang.language_id.clone()); + } + } + + ts_support_by_feature.push(langs_with_ts_support); } let mut row = Vec::new(); @@ -86,7 +92,7 @@ pub fn lang_features() -> Result { .unwrap(); // lang comes from config row.push(lc.language_id.clone()); - for (_feat, support_list) in &ts_features_to_langs { + for support_list in &ts_support_by_feature { row.push( if support_list.contains(&lang) { "✓" diff --git a/xtask/src/helpers.rs b/xtask/src/helpers.rs deleted file mode 100644 index 812f17584656..000000000000 --- a/xtask/src/helpers.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::path::{Path, PathBuf}; -use helix_loader::repo_paths; -use helix_term::health::TsFeature; - -/// Get the list of languages that support a particular tree-sitter -/// based feature. -pub fn ts_lang_support(feat: TsFeature) -> Vec { - let queries_dir = repo_paths::ts_queries(); - - find_files(&queries_dir, feat.runtime_filename()) - .iter() - .map(|f| { - // .../helix/runtime/queries/python/highlights.scm - let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm - let lang = tail.components().next().unwrap(); // python - lang.as_os_str().to_string_lossy().to_string() - }) - .collect() -} - -// naive implementation, but suffices for our needs -pub fn find_files(dir: &Path, filename: &str) -> Vec { - std::fs::read_dir(dir) - .unwrap() - .filter_map(|entry| { - let path = entry.ok()?.path(); - if path.is_dir() { - Some(find_files(&path, filename)) - } else if path.file_name()?.to_string_lossy() == filename { - Some(vec![path]) - } else { - None - } - }) - .flatten() - .collect() -} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 556e247d463e..f4d96e348d1f 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,4 @@ mod docgen; -mod helpers; -mod querycheck; mod themelint; use std::{env, error::Error}; @@ -10,7 +8,6 @@ type DynError = Box; pub mod tasks { use crate::docgen::{lang_features, typable_commands, write}; use crate::docgen::{LANG_SUPPORT_MD_OUTPUT, TYPABLE_COMMANDS_MD_OUTPUT}; - use crate::querycheck::query_check; use crate::themelint::{lint, lint_all}; use crate::DynError; @@ -28,7 +25,27 @@ pub mod tasks { } pub fn querycheck() -> Result<(), DynError> { - query_check() + use helix_core::{syntax, tree_sitter::Query}; + use helix_loader::grammar::get_language; + use helix_loader::ts_probe::TsFeature; + for language_config in syntax::LanguageConfigurations::default().language { + for ts_feature in TsFeature::all() { + // TODO: do language name and grammar name discrepancies exist? + let language_name = &language_config.language_id; + let grammar_name = language_config.grammar.as_ref().unwrap_or(language_name); + if let Ok(treesitter_parser) = get_language(grammar_name) { + let query_feature_file_name = ts_feature.runtime_filename(); + let query_file_text_contents = syntax::read_query(language_name, query_feature_file_name); + if !query_file_text_contents.is_empty() { + if let Err(err) = Query::new(treesitter_parser, &query_file_text_contents) { + return Err(format!("Failed to parse {query_feature_file_name} queries for {language_name}: {err}").into()); + } + } + } + } + } + println!("Query check succeeded"); + Ok(()) } pub fn print_help() { diff --git a/xtask/src/querycheck.rs b/xtask/src/querycheck.rs deleted file mode 100644 index cb88f488443a..000000000000 --- a/xtask/src/querycheck.rs +++ /dev/null @@ -1,40 +0,0 @@ -use helix_core::syntax::LanguageConfigurations; - -use crate::DynError; - -pub fn query_check() -> Result<(), DynError> { - use helix_core::{syntax::read_query, tree_sitter::Query}; - use helix_loader::grammar::get_language; - - let query_files = [ - "highlights.scm", - "locals.scm", - "injections.scm", - "textobjects.scm", - "indents.scm", - ]; - - for language in LanguageConfigurations::default().language { - let language_name = &language.language_id; - let grammar_name = language.grammar.as_ref().unwrap_or(language_name); - for query_file in query_files { - let language = get_language(grammar_name); - let query_text = read_query(language_name, query_file); - if let Ok(lang) = language { - if !query_text.is_empty() { - if let Err(reason) = Query::new(lang, &query_text) { - return Err(format!( - "Failed to parse {} queries for {}: {}", - query_file, language_name, reason - ) - .into()); - } - } - } - } - } - - println!("Query check succeeded"); - - Ok(()) -} From d814356b7ff8c843f66129dd1ffc4ccb96e6eb5d Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 17:07:27 +0100 Subject: [PATCH 075/195] Touchoups * Fix failing integration testing * Fix bug in which which local directories were being tested for their existence. * Fixes #5144 * Ran cargo fmt --- helix-core/src/syntax.rs | 9 +- helix-loader/src/lib.rs | 30 +++--- helix-loader/src/repo_paths.rs | 6 +- helix-loader/src/ts_probe.rs | 2 +- helix-term/src/application.rs | 60 ++++++------ helix-term/src/commands.rs | 127 +++++++++++++++---------- helix-term/src/commands/typed.rs | 3 +- helix-term/src/config.rs | 61 ++++++++---- helix-term/src/health.rs | 123 +++++++++++++++++------- helix-term/src/help.rs | 5 +- helix-term/src/keymap.rs | 40 ++++---- helix-term/src/keymap/default.rs | 4 +- helix-term/src/keymap/keytrie.rs | 134 +++++++++++++++------------ helix-term/src/keymap/keytrienode.rs | 22 +++-- helix-term/src/keymap/macros.rs | 2 +- helix-term/src/keymap/tests.rs | 21 +++-- helix-term/src/main.rs | 8 +- helix-term/src/ui/editor.rs | 7 +- helix-term/tests/test/helpers.rs | 16 ++-- xtask/src/docgen.rs | 7 +- xtask/src/main.rs | 7 +- 21 files changed, 428 insertions(+), 266 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 6a3205a34979..98dfec899e25 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -7,8 +7,8 @@ use crate::{ Rope, RopeSlice, Tendril, }; -use anyhow; use ahash::RandomState; +use anyhow; use arc_swap::{ArcSwap, Guard}; use bitflags::bitflags; use hashbrown::raw::RawTable; @@ -69,7 +69,8 @@ impl LanguageConfigurations { // Local, user config, and system language configs pub fn merged() -> Result { let merged_lang_configs = helix_loader::merged_lang_config()?; - merged_lang_configs.try_into() + merged_lang_configs + .try_into() .map_err(|error| anyhow::anyhow!("{}", error)) } } @@ -86,11 +87,11 @@ impl Default for LanguageConfigurations { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub language_id: String, // c-sharp, rust + pub language_id: String, // c-sharp, rust pub scope: String, // source.rust pub file_types: Vec, // filename extension or ends_with? #[serde(default)] - pub shebangs: Vec, // interpreter(s) associated with language + pub shebangs: Vec, // interpreter(s) associated with language pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, pub max_line_length: Option, diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 7e87fdb7fe9f..9ed456f55072 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -1,6 +1,6 @@ pub mod grammar; -pub mod ts_probe; pub mod repo_paths; +pub mod ts_probe; use anyhow::Error; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; @@ -60,7 +60,8 @@ pub fn setup_log_file(specified_file: Option) { pub fn user_config_dir() -> PathBuf { // TODO: allow env var override - let strategy = choose_base_strategy().expect("Unable to determine system base directory specification!"); + let strategy = + choose_base_strategy().expect("Unable to determine system base directory specification!"); let mut path = strategy.config_dir(); path.push("helix"); path @@ -68,7 +69,8 @@ pub fn user_config_dir() -> PathBuf { pub fn cache_dir() -> PathBuf { // TODO: allow env var override - let strategy = choose_base_strategy().expect("Unable to determine system base directory specification!"); + let strategy = + choose_base_strategy().expect("Unable to determine system base directory specification!"); let mut path = strategy.cache_dir(); path.push("helix"); path @@ -77,8 +79,7 @@ pub fn cache_dir() -> PathBuf { pub fn runtime_dir() -> PathBuf { if let Some(runtime_dir) = RUNTIME_DIR.get() { return runtime_dir.to_path_buf(); - } - else { + } else { RUNTIME_DIR.set(_runtime_dir()).unwrap(); runtime_dir() } @@ -86,7 +87,7 @@ pub fn runtime_dir() -> PathBuf { /// $HELIX_RUNTIME || config_dir/runtime || repo/runtime (if run by cargo) || executable location fn _runtime_dir() -> PathBuf { - // TODO: shouldn't it also look for XDG_RUNTIME_DIR? + // TODO: shouldn't it also look for XDG_RUNTIME_DIR? if let Ok(dir) = std::env::var("HELIX_RUNTIME") { return dir.into(); } @@ -103,7 +104,8 @@ fn _runtime_dir() -> PathBuf { return conf_dir; } - std::env::current_exe().ok() + std::env::current_exe() + .ok() .and_then(|path| std::fs::canonicalize(path).ok()) .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR))) .unwrap() @@ -134,12 +136,12 @@ pub fn 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() { + let potential_dir = ancestor.to_path_buf().join(".helix"); + if potential_dir.is_dir() { + directories.push(potential_dir); + } if ancestor.join(".git").exists() { - directories.push(ancestor.to_path_buf().join(".helix")); - // 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().join(".helix")); } } log::debug!("Located local configuration folders: {:?}", directories); @@ -147,7 +149,6 @@ pub fn local_config_dirs() -> Vec { } fn merge_toml_by_config_paths(config_paths: Vec) -> Result { - println!("{config_paths:#?}"); let mut configs: Vec = Vec::with_capacity(config_paths.len()); for config_path in config_paths { if config_path.exists() { @@ -157,7 +158,10 @@ fn merge_toml_by_config_paths(config_paths: Vec) -> Result PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap().to_path_buf() + .parent() + .unwrap() + .to_path_buf() } pub fn book_gen() -> PathBuf { @@ -32,4 +34,4 @@ pub fn default_theme() -> PathBuf { pub fn default_base16_theme() -> PathBuf { default_config_dir().join("base16_theme.toml") -} \ No newline at end of file +} diff --git a/helix-loader/src/ts_probe.rs b/helix-loader/src/ts_probe.rs index f80900958ae7..3d7190453f4b 100644 --- a/helix-loader/src/ts_probe.rs +++ b/helix-loader/src/ts_probe.rs @@ -36,4 +36,4 @@ impl TsFeature { Self::AutoIndent => "Indent", } } -} \ No newline at end of file +} diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 7c410384348e..a241332eb175 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,7 +1,29 @@ +use crate::{ + args::Args, + commands::apply_workspace_edit, + compositor::{Compositor, Event}, + config::Config, + job::Jobs, + keymap::Keymap, + ui::{self, overlay::overlayed}, +}; +use anyhow::{Context, Error}; +use arc_swap::{access::Map, ArcSwap}; +use crossterm::{ + event::{ + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, + }, + execute, terminal, + tty::IsTty, +}; +use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, - pos_at_coords, syntax::{self, LanguageConfigurations}, Selection, + pos_at_coords, + syntax::{self, LanguageConfigurations}, + Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ @@ -13,34 +35,14 @@ use helix_view::{ tree::Layout, Align, Editor, }; -use crate::{ - args::Args, - commands::apply_workspace_edit, - compositor::{Compositor, Event}, - config::Config, - keymap::Keymap, - job::Jobs, - ui::{self, overlay::overlayed}, -}; +use log::{debug, error, warn}; +use serde_json::json; use std::{ io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; -use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; -use log::{debug, error, warn}; -use anyhow::{Context, Error}; -use serde_json::json; use tui::backend::Backend; -use crossterm::{ - event::{ - DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, - }, - execute, terminal, - tty::IsTty, -}; #[cfg(not(windows))] use { @@ -416,7 +418,9 @@ impl Application { if let Some(theme) = &self.config.load().theme { let true_color = self.config.load().editor.true_color || crate::true_color(); - let theme = self.theme_loader.load(theme) + let theme = self + .theme_loader + .load(theme) .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; if true_color || theme.is_16_color() { @@ -431,8 +435,12 @@ impl Application { }; match refresh_config() { - Ok(_) => { self.editor.set_status("Config refreshed"); }, - Err(err) => { self.editor.set_error(err.to_string()); } + Ok(_) => { + self.editor.set_status("Config refreshed"); + } + Err(err) => { + self.editor.set_error(err.to_string()); + } } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7a79fd45b488..d9d784fc820b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -7,15 +7,24 @@ pub use lsp::*; pub use typed::*; use crate::{ - commands::insert::*, args, - keymap::CommandList, + commands::insert::*, compositor::{self, Component, Compositor}, - job::{Callback, self, Jobs}, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, + job::{self, Callback, Jobs}, + keymap::CommandList, + ui::{ + self, + menu::{Cell, Row}, + overlay::overlayed, + FilePicker, Picker, Popup, Prompt, PromptEvent, + }, }; -use helix_vcs::Hunk; +use anyhow::{anyhow, bail, ensure, Context as _}; +use futures_util::StreamExt; +use fuzzy_matcher::FuzzyMatcher; +use grep_regex::RegexMatcherBuilder; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, @@ -33,6 +42,7 @@ use helix_core::{ visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; +use helix_vcs::Hunk; use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, @@ -44,22 +54,17 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, }; +use ignore::{DirEntry, WalkBuilder, WalkState}; +use once_cell::sync::Lazy; +use serde::de::{self, Deserialize, Deserializer}; use std::{ + borrow::Cow, collections::{HashMap, HashSet}, - num::NonZeroUsize, + fmt, future::Future, - borrow::Cow, + num::NonZeroUsize, path::{Path, PathBuf}, - fmt, }; -use anyhow::{anyhow, bail, ensure, Context as _}; -use fuzzy_matcher::FuzzyMatcher; -use futures_util::StreamExt; -use once_cell::sync::Lazy; -use serde::de::{self, Deserialize, Deserializer}; -use grep_regex::RegexMatcherBuilder; -use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; -use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; pub struct Context<'a> { @@ -156,7 +161,11 @@ macro_rules! static_commands { impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { - Self::Typable { name, args, description: _ } => { + Self::Typable { + name, + args, + description: _, + } => { let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { @@ -2450,22 +2459,38 @@ impl ui::menu::Item for MappableCommand { fn format(&self, command_list: &Self::Data) -> Row { match self { - MappableCommand::Typable { description: doc, name, .. } => { - let mut row: Vec = vec![Cell::from(&*name.as_str()), Cell::from(""), Cell::from(&*doc.as_str())]; + MappableCommand::Typable { + description: doc, + name, + .. + } => { + let mut row: Vec = vec![ + Cell::from(&*name.as_str()), + Cell::from(""), + Cell::from(&*doc.as_str()), + ]; match command_list.get(name as &String) { - Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + Some(key_events) => { + row[1] = Cell::from(format_key_events(key_events)); + } None => {} } return Row::new(row); - }, - MappableCommand::Static { description: doc, name, .. } => { + } + MappableCommand::Static { + description: doc, + name, + .. + } => { let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; match command_list.get(*name) { - Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + Some(key_events) => { + row[1] = Cell::from(format_key_events(key_events)); + } None => {} } - return Row::new(row) - } + return Row::new(row); + } } // TODO: Generalize into a Vec Display implemention? @@ -2485,7 +2510,11 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymap.command_list(&cx.editor.mode); + let keymap_command_lists = compositor + .find::() + .unwrap() + .keymap + .command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2496,32 +2525,36 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap_command_lists, move |cx, command, _action| { - let mut ctx = Context { - register: None, - count: std::num::NonZeroUsize::new(1), - editor: cx.editor, - callback: None, - on_next_key_callback: None, - jobs: cx.jobs, - }; - let focus = view!(ctx.editor).id; + let picker = Picker::new( + commands, + keymap_command_lists, + move |cx, command, _action| { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: None, + on_next_key_callback: None, + jobs: cx.jobs, + }; + let focus = view!(ctx.editor).id; - command.execute(&mut ctx); + command.execute(&mut ctx); - if ctx.editor.tree.contains(focus) { - let config = ctx.editor.config(); - let mode = ctx.editor.mode(); - let view = view_mut!(ctx.editor, focus); - let doc = doc_mut!(ctx.editor, &view.doc); + if ctx.editor.tree.contains(focus) { + let config = ctx.editor.config(); + let mode = ctx.editor.mode(); + let view = view_mut!(ctx.editor, focus); + let doc = doc_mut!(ctx.editor, &view.doc); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff); - if mode != Mode::Insert { - doc.append_changes_to_history(view); + if mode != Mode::Insert { + doc.append_changes_to_history(view); + } } - } - }); + }, + ); compositor.push(Box::new(overlayed(picker))); }, )); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 28063cc23452..f389a4c53a18 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1671,7 +1671,8 @@ fn tree_sitter_subtree( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - let contents = ui::Markdown::new(contents, editor.lang_configs_loader.clone()); + let contents = + ui::Markdown::new(contents, editor.lang_configs_loader.clone()); let popup = Popup::new("hover", contents).auto_close(true); compositor.replace_or_push("hover", popup); }, diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 72e9d6f62b74..abca7eebc42d 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,8 +1,8 @@ use crate::keymap::{default, keytrie::KeyTrie}; +use anyhow::{anyhow, Error}; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; -use anyhow::{Error, anyhow}; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] @@ -19,14 +19,15 @@ impl Config { pub fn merged() -> Result { let config_string = std::fs::read(helix_loader::config_file())?; toml::from_slice(&config_string) - .map(|config: Config| config.merge_in_default_keymap()) + .map(|config: Config| config.merge_in_default_keymap()) .map_err(|error| anyhow!("{}", error)) } /// Merge local config with user config and system keymap pub fn merged_local_config() -> Result { - helix_loader::merged_config()?.try_into() - .map(|config: Config| config.merge_in_default_keymap()) + helix_loader::merged_config()? + .try_into() + .map(|config: Config| config.merge_in_default_keymap()) .map_err(|error| anyhow!("{}", error)) } @@ -54,11 +55,7 @@ mod tests { use crate::{ commands::MappableCommand, config::Config, - keymap::{ - default, - keytrienode::KeyTrieNode, - macros::*, - }, + keymap::{default, keytrienode::KeyTrieNode, macros::*}, }; use helix_core::hashmap; use helix_view::document::Mode; @@ -69,8 +66,8 @@ mod tests { // into a hashmap first let sample_keymaps = r#" [keys.insert] - S-C-a = "delete_selection" y = "move_line_down" + S-C-a = "delete_selection" [keys.normal] A-F12 = "move_next_word_end" @@ -91,7 +88,12 @@ mod tests { for mode in config.keys.keys() { assert_eq!( config.keys.get(mode).unwrap().get_children(), - toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + toml::from_str::(sample_keymaps) + .unwrap() + .keys + .get(mode) + .unwrap() + .get_children() ); } } @@ -117,14 +119,13 @@ mod tests { "g" => delete_char_forward, }, }) - + }, ..Default::default() }; let mut merged_config = user_config.clone().merge_in_default_keymap(); assert_ne!( - user_config, - merged_config, + user_config, merged_config, "Merged user keymap with default should differ from user keymap." ); @@ -147,25 +148,47 @@ mod tests { ); // Assumes that `g` is a sub key trie in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('$')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('$')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), "User supplied mappable command should be inserted under the correct sub keytrie." ); // Assumes that `gg` is in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('g')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('g')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), "User supplied mappable command should replace default even in sub keytries." ); // Assumes that `ge` is in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('e')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('e')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), "Default mappable commands that aren't ovveridden should exist in merged keymap." ); // Huh? - assert!(merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().get_children().len() > 0); + assert!( + merged_config + .keys + .get(&Mode::Normal) + .unwrap() + .get_children() + .len() + > 1 + ); + assert!( + merged_config + .keys + .get(&Mode::Insert) + .unwrap() + .get_children() + .len() + > 0 + ); } } diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 95d43b72352b..bd5539b937d7 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -1,7 +1,10 @@ -use crossterm::{style::{Color, Print, Stylize}, tty::IsTty}; -use helix_loader::ts_probe::TsFeature; +use crossterm::{ + style::{Color, Print, Stylize}, + tty::IsTty, +}; +use helix_core::syntax::{LanguageConfiguration, LanguageConfigurations}; use helix_loader::grammar; -use helix_core::syntax::{LanguageConfigurations, LanguageConfiguration}; +use helix_loader::ts_probe::TsFeature; use helix_view::clipboard; use std::io::Write; @@ -24,9 +27,16 @@ pub fn print_health(health_arg: Option) -> std::io::Result<()> { fn display_paths() -> std::io::Result<()> { let mut stdout = std::io::stdout().lock(); - writeln!(stdout, "Default config merged with user preferences supplied in:")?; + writeln!( + stdout, + "Default config merged with user preferences supplied in:" + )?; writeln!(stdout, "Config: {}", helix_loader::config_file().display())?; - writeln!(stdout, "Language config: {}", helix_loader::user_lang_config_file().display())?; + writeln!( + stdout, + "Language config: {}", + helix_loader::user_lang_config_file().display() + )?; writeln!(stdout, "Log file: {}", helix_loader::log_file().display())?; let rt_dir = helix_loader::runtime_dir(); @@ -50,7 +60,11 @@ fn display_clipboard() -> std::io::Result<()> { let clipboard = clipboard::get_clipboard_provider(); match clipboard.name().as_ref() { "none" => { - writeln!(stdout, "{}", "No system clipboard provider installed, refer to:".red())?; + writeln!( + stdout, + "{}", + "No system clipboard provider installed, refer to:".red() + )?; let link = "https://github.com/helix-editor/helix/wiki/Troubleshooting#copypaste-fromto-system-clipboard-not-working"; writeln!(stdout, "{}", link.red().underlined())?; } @@ -59,31 +73,49 @@ fn display_clipboard() -> std::io::Result<()> { Ok(()) } -fn load_merged_language_configurations() -> std::io::Result> { - LanguageConfigurations::merged().or_else(|err| { +fn load_merged_language_configurations() -> std::io::Result> { + LanguageConfigurations::merged() + .or_else(|err| { let mut stderr = std::io::stderr().lock(); - writeln!(stderr,"{}: {}","Error parsing user language config".red(),err)?; + writeln!( + stderr, + "{}: {}", + "Error parsing user language config".red(), + err + )?; writeln!(stderr, "{}", "Using default language config".yellow())?; Ok(LanguageConfigurations::default()) - }) - .map(|lang_configs| lang_configs.language) + }) + .map(|lang_configs| lang_configs.language) } fn display_language(lang_str: String) -> std::io::Result<()> { let mut stdout = std::io::stdout().lock(); let language_configurations = load_merged_language_configurations()?; - let lang = match language_configurations.iter().find(|l| l.language_id == lang_str) { + let lang = match language_configurations + .iter() + .find(|l| l.language_id == lang_str) + { Some(found_language) => found_language, None => { - writeln!(stdout, "{}", format!("Language '{lang_str}' not found").red())?; - let suggestions: Vec<&str> = language_configurations.iter() + writeln!( + stdout, + "{}", + format!("Language '{lang_str}' not found").red() + )?; + let suggestions: Vec<&str> = language_configurations + .iter() .filter(|l| l.language_id.starts_with(lang_str.chars().next().unwrap())) .map(|l| l.language_id.as_str()) .collect(); if !suggestions.is_empty() { let suggestions = suggestions.join(", "); - writeln!(stdout,"Did you mean one of these: {} ?",suggestions.yellow())?; + writeln!( + stdout, + "Did you mean one of these: {} ?", + suggestions.yellow() + )?; } return Ok(()); } @@ -92,29 +124,41 @@ fn display_language(lang_str: String) -> std::io::Result<()> { let probe_protocol = |protocol_name: &str, server_cmd: Option| -> std::io::Result<()> { let mut stdout = std::io::stdout().lock(); match server_cmd { - Some(server_cmd) => { - writeln!(stdout, "Configured {protocol_name}: {}", server_cmd.clone().green())?; + Some(server_cmd) => { + writeln!( + stdout, + "Configured {protocol_name}: {}", + server_cmd.clone().green() + )?; let result = match which::which(&server_cmd) { Ok(path) => path.display().to_string().green(), - Err(_) => format!("Not found in $PATH").red() + Err(_) => format!("Not found in $PATH").red(), }; writeln!(stdout, "Binary for {server_cmd}: {result}")? - }, - None => writeln!(stdout, "Configured {protocol_name}: {}", "None".yellow())? + } + None => writeln!(stdout, "Configured {protocol_name}: {}", "None".yellow())?, }; Ok(()) }; - probe_protocol("language server",lang.language_server.as_ref() - .map(|lsp| lsp.command.to_string()))?; - probe_protocol("debug adapter",lang.debugger.as_ref() - .map(|dap| dap.command.to_string()))?; + probe_protocol( + "language server", + lang.language_server + .as_ref() + .map(|lsp| lsp.command.to_string()), + )?; + probe_protocol( + "debug adapter", + lang.debugger.as_ref().map(|dap| dap.command.to_string()), + )?; for feature in TsFeature::all() { - let supported = match grammar::load_runtime_file(&lang.language_id, feature.runtime_filename()).is_ok() { - true => "✓".green(), - false => "✗".red(), - }; + let supported = + match grammar::load_runtime_file(&lang.language_id, feature.runtime_filename()).is_ok() + { + true => "✓".green(), + false => "✗".red(), + }; writeln!(stdout, "{} queries: {supported}", feature.short_title())?; } Ok(()) @@ -128,12 +172,15 @@ fn display_all_languages() -> std::io::Result<()> { column_headers.push(treesitter_feature.short_title()) } - let column_width = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80) as usize / column_headers.len(); + let column_width = + crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80) as usize / column_headers.len(); let print_column = |item: &str, color: Color| { - let mut data = format!("{:column_width$}", item - .get(..column_width - 2) - .map(|s| format!("{}…", s)) - .unwrap_or_else(|| item.to_string())); + let mut data = format!( + "{:column_width$}", + item.get(..column_width - 2) + .map(|s| format!("{}…", s)) + .unwrap_or_else(|| item.to_string()) + ); if std::io::stdout().is_tty() { data = data.stylize().with(color).to_string(); @@ -160,13 +207,17 @@ fn display_all_languages() -> std::io::Result<()> { for lang in &language_configurations { print_column(&lang.language_id, Color::Reset); - let lsp = lang.language_server.as_ref().map(|lsp| lsp.command.to_string()); + let lsp = lang + .language_server + .as_ref() + .map(|lsp| lsp.command.to_string()); check_binary(lsp); let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string()); check_binary(dap); for ts_feat in TsFeature::all() { - match grammar::load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() { + match grammar::load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() + { true => print_column("✓", Color::Green), false => print_column("✗", Color::Red), } @@ -174,4 +225,4 @@ fn display_all_languages() -> std::io::Result<()> { writeln!(stdout)?; } Ok(()) -} \ No newline at end of file +} diff --git a/helix-term/src/help.rs b/helix-term/src/help.rs index f210be03cf18..18b5f34fa4ca 100644 --- a/helix-term/src/help.rs +++ b/helix-term/src/help.rs @@ -1,5 +1,6 @@ pub fn help() -> String { - format!("\ + format!( + "\ {pkg_name} {version} {authors} {description} @@ -31,4 +32,4 @@ FLAGS: description = env!("CARGO_PKG_DESCRIPTION"), log_file_path = helix_loader::log_file().display(), ) -} \ No newline at end of file +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 9bd04222e39f..c6b3f75b56c8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,20 +1,19 @@ pub mod default; pub mod macros; // NOTE: Only pub becuase of their use in macros -pub mod keytrienode; pub mod keytrie; +pub mod keytrienode; mod tests; -use self::{ - keytrienode::KeyTrieNode, - keytrie::KeyTrie, - macros::key, -}; +use self::{keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::key}; use crate::commands::MappableCommand; +use arc_swap::{ + access::{DynAccess, DynGuard}, + ArcSwap, +}; use helix_view::{document::Mode, input::KeyEvent}; -use std::{sync::Arc, collections::HashMap}; -use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { @@ -57,7 +56,7 @@ impl Keymap { } /// Lookup `key` in the keymap to try and find a command to execute. - /// Escape key represents cancellation. + /// Escape key represents cancellation. /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { // TODO: remove the sticky part and look up manually @@ -122,12 +121,16 @@ impl Keymap { /// Returns a key-value list of all commands associated to a given Keymap. /// Keys are the node names (see KeyTrieNode documentation) /// Values are lists of stringified KeyEvents that triger the command. - /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". /// Ancestor KeyEvents are in this case "space" and "w". pub fn command_list(&self, mode: &Mode) -> CommandList { let mut list = HashMap::new(); - _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + _command_list( + &mut list, + &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), + &mut String::new(), + ); return list; fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { @@ -135,18 +138,21 @@ impl Keymap { KeyTrieNode::KeyTrie(trie_node) => { for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { + if &temp_prefix != "" { temp_prefix.push_str("→"); } temp_prefix.push_str(&key_event.to_string()); _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); - } - }, + } KeyTrieNode::MappableCommand(mappable_command) => { - if mappable_command.name() == "no_op" { return } - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); - }, + if mappable_command.name() == "no_op" { + return; + } + list.entry(mappable_command.name().to_string()) + .or_default() + .push(prefix.to_string()); + } KeyTrieNode::CommandSequence(_) => {} }; } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 3fffaa08c517..24d53b8565d0 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,6 +1,6 @@ -use super::{macros::keytrie, keytrie::KeyTrie}; -use helix_view::document::Mode; +use super::{keytrie::KeyTrie, macros::keytrie}; use helix_core::hashmap; +use helix_view::document::Mode; use std::collections::HashMap; pub fn default() -> HashMap { diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 01c03ec9d1b1..d40002d09245 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,7 +1,7 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, cmp::Ordering}; use serde::Deserialize; +use std::{cmp::Ordering, collections::HashMap}; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] @@ -14,7 +14,11 @@ pub struct KeyTrie { } impl KeyTrie { - pub fn new(documentation: &str, child_order: HashMap, children: Vec) -> Self { + pub fn new( + documentation: &str, + child_order: HashMap, + children: Vec, + ) -> Self { Self { documentation: documentation.to_string(), child_order, @@ -35,17 +39,20 @@ impl KeyTrie { pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { return _traverse(self, key_events, 0); - fn _traverse(keytrie: &KeyTrie, key_events: &[KeyEvent], mut depth: usize) -> Option { + fn _traverse( + keytrie: &KeyTrie, + key_events: &[KeyEvent], + mut depth: usize, + ) -> Option { if depth == key_events.len() { return Some(KeyTrieNode::KeyTrie(keytrie.clone())); - } - else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { + } else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { match &keytrie.children[*found_index] { KeyTrieNode::KeyTrie(sub_keytrie) => { depth += 1; - return _traverse(sub_keytrie, key_events, depth) - }, - _found_child => return Some(_found_child.clone()) + return _traverse(sub_keytrie, key_events, depth); + } + _found_child => return Some(_found_child.clone()), } } return None; @@ -58,28 +65,31 @@ impl KeyTrie { match other_child_keytrie_node { KeyTrieNode::KeyTrie(ref other_child_keytrie) => { if let Some(self_index) = self.child_order.get(&other_key_event) { - if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { + if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = + self.children[*self_index] + { self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); } - } - else { - self.child_order.insert(*other_key_event, self.children.len()); - self.children.push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); + self.children + .push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); } } KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { if let Some(existing_index) = self.child_order.get(other_key_event) { self.children[*existing_index] = other_child_keytrie_node.clone(); - } - else { - self.child_order.insert(*other_key_event, self.children.len()); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); self.children.push(other_child_keytrie_node.clone()); } } } } } - + // IMPROVEMENT: cache sorting and update cache only when config is updated /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. @@ -87,7 +97,9 @@ impl KeyTrie { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length - unsafe { key_event_order.set_len(self.children.len()); } + unsafe { + key_event_order.set_len(self.children.len()); + } for (key_event, index) in &self.child_order { key_event_order[*index] = key_event.clone(); } @@ -99,7 +111,7 @@ impl KeyTrie { continue; } command.description() - }, + } KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, // FIX: default to a join of all command names // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. @@ -107,11 +119,12 @@ impl KeyTrie { KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; let key_event = key_event_order[index]; - match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { - Some(position) => body[position].0.push(key_event.to_string()), - None => { - body.push((vec![key_event.to_string()], documentation)) - }, + match body + .iter() + .position(|(_, existing_documentation)| &documentation == existing_documentation) + { + Some(position) => body[position].0.push(key_event.to_string()), + None => body.push((vec![key_event.to_string()], documentation)), } } @@ -120,20 +133,24 @@ impl KeyTrie { // Those events will always be placed after the one letter KeyEvent for (key_events, _) in body.iter_mut() { key_events.sort_unstable_by(|a, b| { - if a.len() == 1 { return Ordering::Less } + if a.len() == 1 { + return Ordering::Less; + } if b.len() > a.len() && b.starts_with("C-") { - return Ordering::Greater + return Ordering::Greater; } a.len().cmp(&b.len()) }); } - if sort_infobox { body = keyevent_sort_infobox(body); } + if sort_infobox { + body = keyevent_sort_infobox(body); + } let stringified_key_events_body: Vec<(String, &str)> = body - .iter().map(|(key_events, description)| { - (key_events.join(", "), *description) - }).collect(); + .iter() + .map(|(key_events, description)| (key_events.join(", "), *description)) + .collect(); Info::new(&self.documentation, &stringified_key_events_body) } @@ -165,7 +182,7 @@ impl<'de> Deserialize<'de> for KeyTrie { children.push(keytrie_node); } - Ok(Self { + Ok(Self { child_order, children, ..Default::default() @@ -177,14 +194,15 @@ impl<'de> Deserialize<'de> for KeyTrie { type InfoBoxRow<'a> = (Vec, &'a str); type InfoBoxBody<'a> = Vec>; /// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. -/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are +/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are /// placed together, and alphas are placed before the rest. fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { - use std::str::FromStr; - use std::collections::BTreeMap; use helix_view::keyboard::{KeyCode, KeyModifiers, MediaKeyCode}; + use std::collections::BTreeMap; + use std::str::FromStr; - let mut category_holder: BTreeMap>> = BTreeMap::new(); + let mut category_holder: BTreeMap>> = + BTreeMap::new(); let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); @@ -196,27 +214,20 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant // Hence the use of mock Variant values let keycode_category = match first_keyevent.code { - KeyCode::Char(_) => { - KeyCode::Char('a') - } - KeyCode::F(_) => { - KeyCode::F(0) - } - KeyCode::Media(_) => { - KeyCode::Media(MediaKeyCode::Play) - } - other_keycode => { - other_keycode - } + KeyCode::Char(_) => KeyCode::Char('a'), + KeyCode::F(_) => KeyCode::F(0), + KeyCode::Media(_) => KeyCode::Media(MediaKeyCode::Play), + other_keycode => other_keycode, }; - - let modifier_category = category_holder.get_mut(&first_keyevent.modifiers) + let modifier_category = category_holder + .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); if !modifier_category.contains_key(&keycode_category) { modifier_category.insert(keycode_category.clone(), Vec::new()); } - modifier_category.get_mut(&keycode_category) + modifier_category + .get_mut(&keycode_category) .expect("key existence should be checked") .push(infobox_row); } @@ -225,12 +236,14 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { for (keycode_category, mut infobox_rows) in keycode_categories { if infobox_rows.len() > 1 { match keycode_category { - KeyCode::Char(_) => { - infobox_rows.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + KeyCode::Char(_) => { + infobox_rows.sort_unstable_by(|a, b| { + a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase()) + }); // Consistently place lowercase before uppercase of the same letter. let mut x_index = 0; - let mut y_index = 1; + let mut y_index = 1; while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; @@ -238,9 +251,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { if x < y { infobox_rows.swap(x_index, y_index); } - } - x_index = y_index; - y_index += 1; + } + x_index = y_index; + y_index += 1; } // TEMP: until drain_filter becomes stable. Migth also be worth implementing @@ -254,19 +267,18 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { .is_some() { alphas.push(infobox_row); - } - else { + } else { misc.push(infobox_row); } } infobox_rows = Vec::with_capacity(alphas.len() + misc.len()); for alpha_row in alphas { - infobox_rows.push(alpha_row); + infobox_rows.push(alpha_row); } for misc_row in misc { - infobox_rows.push(misc_row); + infobox_rows.push(misc_row); } - }, + } _ => { infobox_rows.sort_unstable(); } diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 66a642ac902d..18696b1eccb8 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,8 +1,8 @@ use super::keytrie::KeyTrie; use crate::commands::MappableCommand; use helix_view::input::KeyEvent; +use serde::{de::Visitor, Deserialize}; use std::collections::HashMap; -use serde::{Deserialize, de::Visitor}; /// Each variant includes a documentaion property. /// For the MappableCommand and CommandSequence variants, the property is self explanatory. @@ -29,14 +29,14 @@ impl PartialEq for KeyTrieNode { match (self, other) { (KeyTrieNode::MappableCommand(_self), KeyTrieNode::MappableCommand(_other)) => { _self == _other - }, + } (KeyTrieNode::CommandSequence(_self), KeyTrieNode::CommandSequence(_other)) => { - _self == _other - }, + _self == _other + } (KeyTrieNode::KeyTrie(_self), KeyTrieNode::KeyTrie(_other)) => { _self.get_children() == _other.get_children() - }, - _ => false + } + _ => false, } } } @@ -65,7 +65,7 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { S: serde::de::SeqAccess<'de>, { let mut commands = Vec::new(); - while let Some(command) = seq.next_element::<&str>()? { + while let Some(command) = seq.next_element::()? { commands.push( command .parse::() @@ -85,6 +85,10 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { child_order.insert(key_event, children.len()); children.push(key_trie_node); } - Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", child_order, children))) + Ok(KeyTrieNode::KeyTrie(KeyTrie::new( + "", + child_order, + children, + ))) } -} \ No newline at end of file +} diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index b834c43e56aa..c8f52128e3e4 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -116,5 +116,5 @@ macro_rules! keytrie { pub use alt; pub use ctrl; pub use key; -pub use shift; pub use keytrie; +pub use shift; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 4ab431c6e31d..bc5e061892bb 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,9 +1,9 @@ #[macro_use] #[cfg(test)] mod tests { + use crate::keymap::{macros::*, *}; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::keymap::{*, macros::*}; use std::collections::HashMap; #[test] @@ -25,8 +25,12 @@ mod tests { fn aliased_modes_are_same_in_default_keymap() { let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); assert_eq!( - normal_mode_keytrie_root.traverse(&[key!(' '), key!('w')]).unwrap(), - normal_mode_keytrie_root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + normal_mode_keytrie_root + .traverse(&[key!(' '), key!('w')]) + .unwrap(), + normal_mode_keytrie_root + .traverse(&["C-w".parse::().unwrap()]) + .unwrap(), "Mismatch for window mode on `Space-w` and `Ctrl-w`." ); assert_eq!( @@ -47,7 +51,9 @@ mod tests { "j" | "k" => move_line_down, }); - let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( + hashmap!(Mode::Normal => normal_mode), + )))); let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests @@ -60,10 +66,7 @@ mod tests { assert_eq!( command_list, HashMap::from([ - ( - "insert_mode".to_string(), - vec![key!('i').to_string()] - ), + ("insert_mode".to_string(), vec![key!('i').to_string()]), ( "goto_file_start".to_string(), vec![format!("{}→{}", key!('g'), key!('g'))] @@ -80,4 +83,4 @@ mod tests { "Mismatch" ) } -} \ No newline at end of file +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 60ea8edb8446..f74376914114 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -49,7 +49,7 @@ async fn main_impl() -> Result { let mut config = Config::merged().unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); - let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut[]); + let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut []); Config::default() }); if config.editor.load_local_config { @@ -57,7 +57,7 @@ async fn main_impl() -> Result { config = Config::merged_local_config().unwrap_or_else(|err| { eprintln!("Bad local config: {}", err); eprintln!("Press to continue with default and user config"); - let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut[]); + let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut []); config }); } @@ -65,7 +65,7 @@ async fn main_impl() -> Result { let language_configurations = LanguageConfigurations::merged().unwrap_or_else(|err| { eprintln!("Bad language config: {}", err); eprintln!("Press to continue with default language config"); - let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut[]); + let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut []); LanguageConfigurations::default() }); @@ -77,7 +77,7 @@ async fn main_impl() -> Result { } fn setup_logging(logpath: Option, verbosity: u64) -> Result<()> { - helix_loader::setup_log_file(logpath); + helix_loader::setup_log_file(logpath); let mut base_config = fern::Dispatch::new(); base_config = match verbosity { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 94ead3f0f0e6..f4cfd51cf429 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::{KeymapResult, Keymap}, + keymap::{Keymap, KeymapResult}, ui::{Completion, ProgressSpinners}, }; @@ -931,7 +931,10 @@ impl EditorView { self.pseudo_pending.extend(self.keymap.pending()); let key_result = self.keymap.get(mode, event); let sort_infobox = cxt.editor.config.load().sorted_infobox; - cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox(sort_infobox)); + cxt.editor.autoinfo = self + .keymap + .sticky_keytrie() + .map(|node| node.infobox(sort_infobox)); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index a0f3a32e4cba..ee2b41517a63 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -7,7 +7,9 @@ use std::{ use anyhow::bail; use crossterm::event::{Event, KeyEvent}; -use helix_core::{diagnostic::Severity, test, Selection, Transaction}; +use helix_core::{ + diagnostic::Severity, syntax::LanguageConfigurations, test, Selection, Transaction, +}; use helix_term::{application::Application, args::Args, config::Config}; use helix_view::{doc, input::parse_macro, Editor}; use tempfile::NamedTempFile; @@ -147,8 +149,10 @@ pub async fn test_key_sequence_with_input_text>( /// By default, language server configuration is dropped from the languages.toml /// document. If a language-server is necessary for a test, it must be explicitly /// added in `overrides`. -pub fn test_syntax_conf(overrides: Option) -> helix_core::syntax::Configuration { - let mut lang = helix_loader::config::default_lang_config(); +pub fn test_syntax_conf(overrides: Option) -> LanguageConfigurations { + let mut lang: toml::Value = + toml::from_slice(&std::fs::read(helix_loader::repo_paths::default_lang_configs()).unwrap()) + .unwrap(); for lang_config in lang .as_table_mut() @@ -178,7 +182,7 @@ pub fn test_syntax_conf(overrides: Option) -> helix_core::syntax::Config pub async fn test_with_config>( args: Args, config: Config, - syn_conf: helix_core::syntax::Configuration, + syn_conf: LanguageConfigurations, test_case: T, ) -> anyhow::Result<()> { let test_case = test_case.into(); @@ -257,7 +261,7 @@ pub fn new_readonly_tempfile() -> anyhow::Result { pub struct AppBuilder { args: Args, config: Config, - syn_conf: helix_core::syntax::Configuration, + syn_conf: LanguageConfigurations, input: Option<(String, Selection)>, } @@ -298,7 +302,7 @@ impl AppBuilder { self } - pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self { + pub fn with_lang_config(mut self, syn_conf: LanguageConfigurations) -> Self { self.syn_conf = syn_conf; self } diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index ff4f3efa07e9..55af6a97ffe3 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -75,7 +75,12 @@ pub fn lang_features() -> Result { for feature in TsFeature::all() { let mut langs_with_ts_support: Vec = Vec::new(); for lang in LanguageConfigurations::default().language { - if helix_loader::grammar::load_runtime_file(&lang.language_id, feature.runtime_filename()).is_ok() { + if helix_loader::grammar::load_runtime_file( + &lang.language_id, + feature.runtime_filename(), + ) + .is_ok() + { langs_with_ts_support.push(lang.language_id.clone()); } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index f4d96e348d1f..6f1abd01dd5b 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -30,12 +30,13 @@ pub mod tasks { use helix_loader::ts_probe::TsFeature; for language_config in syntax::LanguageConfigurations::default().language { for ts_feature in TsFeature::all() { - // TODO: do language name and grammar name discrepancies exist? + // TODO: do language name and grammar name discrepancies exist? let language_name = &language_config.language_id; let grammar_name = language_config.grammar.as_ref().unwrap_or(language_name); if let Ok(treesitter_parser) = get_language(grammar_name) { let query_feature_file_name = ts_feature.runtime_filename(); - let query_file_text_contents = syntax::read_query(language_name, query_feature_file_name); + let query_file_text_contents = + syntax::read_query(language_name, query_feature_file_name); if !query_file_text_contents.is_empty() { if let Err(err) = Query::new(treesitter_parser, &query_file_text_contents) { return Err(format!("Failed to parse {query_feature_file_name} queries for {language_name}: {err}").into()); @@ -45,7 +46,7 @@ pub mod tasks { } } println!("Query check succeeded"); - Ok(()) + Ok(()) } pub fn print_help() { From ae6790c48d1c7672d121ba46393bf761e48a2d81 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 17:28:18 +0100 Subject: [PATCH 076/195] Run cargo fmt --- helix-term/src/application.rs | 44 ++++----- helix-term/src/commands.rs | 127 +++++++++++++++---------- helix-term/src/config.rs | 50 +++++++--- helix-term/src/keymap.rs | 40 ++++---- helix-term/src/keymap/default.rs | 4 +- helix-term/src/keymap/keytrie.rs | 134 +++++++++++++++------------ helix-term/src/keymap/keytrienode.rs | 20 ++-- helix-term/src/keymap/macros.rs | 2 +- helix-term/src/keymap/tests.rs | 21 +++-- helix-term/src/ui/editor.rs | 7 +- 10 files changed, 266 insertions(+), 183 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 81fa20951122..adaed7530bcf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,3 +1,23 @@ +use crate::{ + args::Args, + commands::apply_workspace_edit, + compositor::{Compositor, Event}, + config::Config, + job::Jobs, + keymap::Keymap, + ui::{self, overlay::overlayed}, +}; +use anyhow::{Context, Error}; +use arc_swap::{access::Map, ArcSwap}; +use crossterm::{ + event::{ + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, + }, + execute, terminal, + tty::IsTty, +}; +use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -13,34 +33,14 @@ use helix_view::{ tree::Layout, Align, Editor, }; -use crate::{ - args::Args, - commands::apply_workspace_edit, - compositor::{Compositor, Event}, - config::Config, - keymap::Keymap, - job::Jobs, - ui::{self, overlay::overlayed}, -}; +use log::{debug, error, warn}; +use serde_json::json; use std::{ io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; -use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; -use log::{debug, error, warn}; -use anyhow::{Context, Error}; -use serde_json::json; use tui::backend::Backend; -use crossterm::{ - event::{ - DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, - }, - execute, terminal, - tty::IsTty, -}; #[cfg(not(windows))] use { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7a79fd45b488..d9d784fc820b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -7,15 +7,24 @@ pub use lsp::*; pub use typed::*; use crate::{ - commands::insert::*, args, - keymap::CommandList, + commands::insert::*, compositor::{self, Component, Compositor}, - job::{Callback, self, Jobs}, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, + job::{self, Callback, Jobs}, + keymap::CommandList, + ui::{ + self, + menu::{Cell, Row}, + overlay::overlayed, + FilePicker, Picker, Popup, Prompt, PromptEvent, + }, }; -use helix_vcs::Hunk; +use anyhow::{anyhow, bail, ensure, Context as _}; +use futures_util::StreamExt; +use fuzzy_matcher::FuzzyMatcher; +use grep_regex::RegexMatcherBuilder; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, @@ -33,6 +42,7 @@ use helix_core::{ visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; +use helix_vcs::Hunk; use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, @@ -44,22 +54,17 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, }; +use ignore::{DirEntry, WalkBuilder, WalkState}; +use once_cell::sync::Lazy; +use serde::de::{self, Deserialize, Deserializer}; use std::{ + borrow::Cow, collections::{HashMap, HashSet}, - num::NonZeroUsize, + fmt, future::Future, - borrow::Cow, + num::NonZeroUsize, path::{Path, PathBuf}, - fmt, }; -use anyhow::{anyhow, bail, ensure, Context as _}; -use fuzzy_matcher::FuzzyMatcher; -use futures_util::StreamExt; -use once_cell::sync::Lazy; -use serde::de::{self, Deserialize, Deserializer}; -use grep_regex::RegexMatcherBuilder; -use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; -use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; pub struct Context<'a> { @@ -156,7 +161,11 @@ macro_rules! static_commands { impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { - Self::Typable { name, args, description: _ } => { + Self::Typable { + name, + args, + description: _, + } => { let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { @@ -2450,22 +2459,38 @@ impl ui::menu::Item for MappableCommand { fn format(&self, command_list: &Self::Data) -> Row { match self { - MappableCommand::Typable { description: doc, name, .. } => { - let mut row: Vec = vec![Cell::from(&*name.as_str()), Cell::from(""), Cell::from(&*doc.as_str())]; + MappableCommand::Typable { + description: doc, + name, + .. + } => { + let mut row: Vec = vec![ + Cell::from(&*name.as_str()), + Cell::from(""), + Cell::from(&*doc.as_str()), + ]; match command_list.get(name as &String) { - Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + Some(key_events) => { + row[1] = Cell::from(format_key_events(key_events)); + } None => {} } return Row::new(row); - }, - MappableCommand::Static { description: doc, name, .. } => { + } + MappableCommand::Static { + description: doc, + name, + .. + } => { let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; match command_list.get(*name) { - Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + Some(key_events) => { + row[1] = Cell::from(format_key_events(key_events)); + } None => {} } - return Row::new(row) - } + return Row::new(row); + } } // TODO: Generalize into a Vec Display implemention? @@ -2485,7 +2510,11 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymap.command_list(&cx.editor.mode); + let keymap_command_lists = compositor + .find::() + .unwrap() + .keymap + .command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2496,32 +2525,36 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap_command_lists, move |cx, command, _action| { - let mut ctx = Context { - register: None, - count: std::num::NonZeroUsize::new(1), - editor: cx.editor, - callback: None, - on_next_key_callback: None, - jobs: cx.jobs, - }; - let focus = view!(ctx.editor).id; + let picker = Picker::new( + commands, + keymap_command_lists, + move |cx, command, _action| { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: None, + on_next_key_callback: None, + jobs: cx.jobs, + }; + let focus = view!(ctx.editor).id; - command.execute(&mut ctx); + command.execute(&mut ctx); - if ctx.editor.tree.contains(focus) { - let config = ctx.editor.config(); - let mode = ctx.editor.mode(); - let view = view_mut!(ctx.editor, focus); - let doc = doc_mut!(ctx.editor, &view.doc); + if ctx.editor.tree.contains(focus) { + let config = ctx.editor.config(); + let mode = ctx.editor.mode(); + let view = view_mut!(ctx.editor, focus); + let doc = doc_mut!(ctx.editor, &view.doc); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff); - if mode != Mode::Insert { - doc.append_changes_to_history(view); + if mode != Mode::Insert { + doc.append_changes_to_history(view); + } } - } - }); + }, + ); compositor.push(Box::new(overlayed(picker))); }, )); diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4838e2d79c26..296526aeb71e 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -64,11 +64,7 @@ mod tests { use crate::{ commands::MappableCommand, config::Config, - keymap::{ - default, - keytrienode::KeyTrieNode, - macros::*, - }, + keymap::{default, keytrienode::KeyTrieNode, macros::*}, }; use helix_core::hashmap; use helix_view::document::Mode; @@ -99,7 +95,12 @@ mod tests { for mode in config.keys.keys() { assert_eq!( config.keys.get(mode).unwrap().get_children(), - toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + toml::from_str::(sample_keymaps) + .unwrap() + .keys + .get(mode) + .unwrap() + .get_children() ); } } @@ -125,14 +126,13 @@ mod tests { "g" => delete_char_forward, }, }) - + }, ..Default::default() }; let mut merged_config = user_config.clone().merge_in_default_keymap(); assert_ne!( - user_config, - merged_config, + user_config, merged_config, "Merged user keymap with default should differ from user keymap." ); @@ -155,25 +155,47 @@ mod tests { ); // Assumes that `g` is a sub key trie in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('$')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('$')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), "User supplied mappable command should be inserted under the correct sub keytrie." ); // Assumes that `gg` is in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('g')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('g')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), "User supplied mappable command should replace default even in sub keytries." ); // Assumes that `ge` is in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('e')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('e')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), "Default mappable commands that aren't ovveridden should exist in merged keymap." ); // Huh? - assert!(merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().get_children().len() > 0); + assert!( + merged_config + .keys + .get(&Mode::Normal) + .unwrap() + .get_children() + .len() + > 1 + ); + assert!( + merged_config + .keys + .get(&Mode::Insert) + .unwrap() + .get_children() + .len() + > 0 + ); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 9bd04222e39f..c6b3f75b56c8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,20 +1,19 @@ pub mod default; pub mod macros; // NOTE: Only pub becuase of their use in macros -pub mod keytrienode; pub mod keytrie; +pub mod keytrienode; mod tests; -use self::{ - keytrienode::KeyTrieNode, - keytrie::KeyTrie, - macros::key, -}; +use self::{keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::key}; use crate::commands::MappableCommand; +use arc_swap::{ + access::{DynAccess, DynGuard}, + ArcSwap, +}; use helix_view::{document::Mode, input::KeyEvent}; -use std::{sync::Arc, collections::HashMap}; -use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { @@ -57,7 +56,7 @@ impl Keymap { } /// Lookup `key` in the keymap to try and find a command to execute. - /// Escape key represents cancellation. + /// Escape key represents cancellation. /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { // TODO: remove the sticky part and look up manually @@ -122,12 +121,16 @@ impl Keymap { /// Returns a key-value list of all commands associated to a given Keymap. /// Keys are the node names (see KeyTrieNode documentation) /// Values are lists of stringified KeyEvents that triger the command. - /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". /// Ancestor KeyEvents are in this case "space" and "w". pub fn command_list(&self, mode: &Mode) -> CommandList { let mut list = HashMap::new(); - _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + _command_list( + &mut list, + &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), + &mut String::new(), + ); return list; fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { @@ -135,18 +138,21 @@ impl Keymap { KeyTrieNode::KeyTrie(trie_node) => { for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { + if &temp_prefix != "" { temp_prefix.push_str("→"); } temp_prefix.push_str(&key_event.to_string()); _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); - } - }, + } KeyTrieNode::MappableCommand(mappable_command) => { - if mappable_command.name() == "no_op" { return } - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); - }, + if mappable_command.name() == "no_op" { + return; + } + list.entry(mappable_command.name().to_string()) + .or_default() + .push(prefix.to_string()); + } KeyTrieNode::CommandSequence(_) => {} }; } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 3fffaa08c517..24d53b8565d0 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,6 +1,6 @@ -use super::{macros::keytrie, keytrie::KeyTrie}; -use helix_view::document::Mode; +use super::{keytrie::KeyTrie, macros::keytrie}; use helix_core::hashmap; +use helix_view::document::Mode; use std::collections::HashMap; pub fn default() -> HashMap { diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 01c03ec9d1b1..d40002d09245 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,7 +1,7 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, cmp::Ordering}; use serde::Deserialize; +use std::{cmp::Ordering, collections::HashMap}; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] @@ -14,7 +14,11 @@ pub struct KeyTrie { } impl KeyTrie { - pub fn new(documentation: &str, child_order: HashMap, children: Vec) -> Self { + pub fn new( + documentation: &str, + child_order: HashMap, + children: Vec, + ) -> Self { Self { documentation: documentation.to_string(), child_order, @@ -35,17 +39,20 @@ impl KeyTrie { pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { return _traverse(self, key_events, 0); - fn _traverse(keytrie: &KeyTrie, key_events: &[KeyEvent], mut depth: usize) -> Option { + fn _traverse( + keytrie: &KeyTrie, + key_events: &[KeyEvent], + mut depth: usize, + ) -> Option { if depth == key_events.len() { return Some(KeyTrieNode::KeyTrie(keytrie.clone())); - } - else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { + } else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { match &keytrie.children[*found_index] { KeyTrieNode::KeyTrie(sub_keytrie) => { depth += 1; - return _traverse(sub_keytrie, key_events, depth) - }, - _found_child => return Some(_found_child.clone()) + return _traverse(sub_keytrie, key_events, depth); + } + _found_child => return Some(_found_child.clone()), } } return None; @@ -58,28 +65,31 @@ impl KeyTrie { match other_child_keytrie_node { KeyTrieNode::KeyTrie(ref other_child_keytrie) => { if let Some(self_index) = self.child_order.get(&other_key_event) { - if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { + if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = + self.children[*self_index] + { self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); } - } - else { - self.child_order.insert(*other_key_event, self.children.len()); - self.children.push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); + self.children + .push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); } } KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { if let Some(existing_index) = self.child_order.get(other_key_event) { self.children[*existing_index] = other_child_keytrie_node.clone(); - } - else { - self.child_order.insert(*other_key_event, self.children.len()); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); self.children.push(other_child_keytrie_node.clone()); } } } } } - + // IMPROVEMENT: cache sorting and update cache only when config is updated /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. @@ -87,7 +97,9 @@ impl KeyTrie { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length - unsafe { key_event_order.set_len(self.children.len()); } + unsafe { + key_event_order.set_len(self.children.len()); + } for (key_event, index) in &self.child_order { key_event_order[*index] = key_event.clone(); } @@ -99,7 +111,7 @@ impl KeyTrie { continue; } command.description() - }, + } KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, // FIX: default to a join of all command names // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. @@ -107,11 +119,12 @@ impl KeyTrie { KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; let key_event = key_event_order[index]; - match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { - Some(position) => body[position].0.push(key_event.to_string()), - None => { - body.push((vec![key_event.to_string()], documentation)) - }, + match body + .iter() + .position(|(_, existing_documentation)| &documentation == existing_documentation) + { + Some(position) => body[position].0.push(key_event.to_string()), + None => body.push((vec![key_event.to_string()], documentation)), } } @@ -120,20 +133,24 @@ impl KeyTrie { // Those events will always be placed after the one letter KeyEvent for (key_events, _) in body.iter_mut() { key_events.sort_unstable_by(|a, b| { - if a.len() == 1 { return Ordering::Less } + if a.len() == 1 { + return Ordering::Less; + } if b.len() > a.len() && b.starts_with("C-") { - return Ordering::Greater + return Ordering::Greater; } a.len().cmp(&b.len()) }); } - if sort_infobox { body = keyevent_sort_infobox(body); } + if sort_infobox { + body = keyevent_sort_infobox(body); + } let stringified_key_events_body: Vec<(String, &str)> = body - .iter().map(|(key_events, description)| { - (key_events.join(", "), *description) - }).collect(); + .iter() + .map(|(key_events, description)| (key_events.join(", "), *description)) + .collect(); Info::new(&self.documentation, &stringified_key_events_body) } @@ -165,7 +182,7 @@ impl<'de> Deserialize<'de> for KeyTrie { children.push(keytrie_node); } - Ok(Self { + Ok(Self { child_order, children, ..Default::default() @@ -177,14 +194,15 @@ impl<'de> Deserialize<'de> for KeyTrie { type InfoBoxRow<'a> = (Vec, &'a str); type InfoBoxBody<'a> = Vec>; /// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. -/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are +/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are /// placed together, and alphas are placed before the rest. fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { - use std::str::FromStr; - use std::collections::BTreeMap; use helix_view::keyboard::{KeyCode, KeyModifiers, MediaKeyCode}; + use std::collections::BTreeMap; + use std::str::FromStr; - let mut category_holder: BTreeMap>> = BTreeMap::new(); + let mut category_holder: BTreeMap>> = + BTreeMap::new(); let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); @@ -196,27 +214,20 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant // Hence the use of mock Variant values let keycode_category = match first_keyevent.code { - KeyCode::Char(_) => { - KeyCode::Char('a') - } - KeyCode::F(_) => { - KeyCode::F(0) - } - KeyCode::Media(_) => { - KeyCode::Media(MediaKeyCode::Play) - } - other_keycode => { - other_keycode - } + KeyCode::Char(_) => KeyCode::Char('a'), + KeyCode::F(_) => KeyCode::F(0), + KeyCode::Media(_) => KeyCode::Media(MediaKeyCode::Play), + other_keycode => other_keycode, }; - - let modifier_category = category_holder.get_mut(&first_keyevent.modifiers) + let modifier_category = category_holder + .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); if !modifier_category.contains_key(&keycode_category) { modifier_category.insert(keycode_category.clone(), Vec::new()); } - modifier_category.get_mut(&keycode_category) + modifier_category + .get_mut(&keycode_category) .expect("key existence should be checked") .push(infobox_row); } @@ -225,12 +236,14 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { for (keycode_category, mut infobox_rows) in keycode_categories { if infobox_rows.len() > 1 { match keycode_category { - KeyCode::Char(_) => { - infobox_rows.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + KeyCode::Char(_) => { + infobox_rows.sort_unstable_by(|a, b| { + a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase()) + }); // Consistently place lowercase before uppercase of the same letter. let mut x_index = 0; - let mut y_index = 1; + let mut y_index = 1; while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; @@ -238,9 +251,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { if x < y { infobox_rows.swap(x_index, y_index); } - } - x_index = y_index; - y_index += 1; + } + x_index = y_index; + y_index += 1; } // TEMP: until drain_filter becomes stable. Migth also be worth implementing @@ -254,19 +267,18 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { .is_some() { alphas.push(infobox_row); - } - else { + } else { misc.push(infobox_row); } } infobox_rows = Vec::with_capacity(alphas.len() + misc.len()); for alpha_row in alphas { - infobox_rows.push(alpha_row); + infobox_rows.push(alpha_row); } for misc_row in misc { - infobox_rows.push(misc_row); + infobox_rows.push(misc_row); } - }, + } _ => { infobox_rows.sort_unstable(); } diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 66a642ac902d..5511c49dd7c6 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,8 +1,8 @@ use super::keytrie::KeyTrie; use crate::commands::MappableCommand; use helix_view::input::KeyEvent; +use serde::{de::Visitor, Deserialize}; use std::collections::HashMap; -use serde::{Deserialize, de::Visitor}; /// Each variant includes a documentaion property. /// For the MappableCommand and CommandSequence variants, the property is self explanatory. @@ -29,14 +29,14 @@ impl PartialEq for KeyTrieNode { match (self, other) { (KeyTrieNode::MappableCommand(_self), KeyTrieNode::MappableCommand(_other)) => { _self == _other - }, + } (KeyTrieNode::CommandSequence(_self), KeyTrieNode::CommandSequence(_other)) => { - _self == _other - }, + _self == _other + } (KeyTrieNode::KeyTrie(_self), KeyTrieNode::KeyTrie(_other)) => { _self.get_children() == _other.get_children() - }, - _ => false + } + _ => false, } } } @@ -85,6 +85,10 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { child_order.insert(key_event, children.len()); children.push(key_trie_node); } - Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", child_order, children))) + Ok(KeyTrieNode::KeyTrie(KeyTrie::new( + "", + child_order, + children, + ))) } -} \ No newline at end of file +} diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index b834c43e56aa..c8f52128e3e4 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -116,5 +116,5 @@ macro_rules! keytrie { pub use alt; pub use ctrl; pub use key; -pub use shift; pub use keytrie; +pub use shift; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 4ab431c6e31d..bc5e061892bb 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,9 +1,9 @@ #[macro_use] #[cfg(test)] mod tests { + use crate::keymap::{macros::*, *}; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::keymap::{*, macros::*}; use std::collections::HashMap; #[test] @@ -25,8 +25,12 @@ mod tests { fn aliased_modes_are_same_in_default_keymap() { let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); assert_eq!( - normal_mode_keytrie_root.traverse(&[key!(' '), key!('w')]).unwrap(), - normal_mode_keytrie_root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + normal_mode_keytrie_root + .traverse(&[key!(' '), key!('w')]) + .unwrap(), + normal_mode_keytrie_root + .traverse(&["C-w".parse::().unwrap()]) + .unwrap(), "Mismatch for window mode on `Space-w` and `Ctrl-w`." ); assert_eq!( @@ -47,7 +51,9 @@ mod tests { "j" | "k" => move_line_down, }); - let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( + hashmap!(Mode::Normal => normal_mode), + )))); let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests @@ -60,10 +66,7 @@ mod tests { assert_eq!( command_list, HashMap::from([ - ( - "insert_mode".to_string(), - vec![key!('i').to_string()] - ), + ("insert_mode".to_string(), vec![key!('i').to_string()]), ( "goto_file_start".to_string(), vec![format!("{}→{}", key!('g'), key!('g'))] @@ -80,4 +83,4 @@ mod tests { "Mismatch" ) } -} \ No newline at end of file +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 94ead3f0f0e6..f4cfd51cf429 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::{KeymapResult, Keymap}, + keymap::{Keymap, KeymapResult}, ui::{Completion, ProgressSpinners}, }; @@ -931,7 +931,10 @@ impl EditorView { self.pseudo_pending.extend(self.keymap.pending()); let key_result = self.keymap.get(mode, event); let sort_infobox = cxt.editor.config.load().sorted_infobox; - cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox(sort_infobox)); + cxt.editor.autoinfo = self + .keymap + .sticky_keytrie() + .map(|node| node.infobox(sort_infobox)); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); From 44ad646d1568ac47d4668857eff2911ddecbe4ac Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 17:46:17 +0100 Subject: [PATCH 077/195] Use description conistenly --- helix-term/src/keymap/keytrie.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index d40002d09245..dc08695678d5 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -6,7 +6,7 @@ use std::{cmp::Ordering, collections::HashMap}; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] pub struct KeyTrie { - documentation: String, + description: String, /// Used for pre-defined order in infoboxes, values represent the index of the key tries children. child_order: HashMap, children: Vec, @@ -15,12 +15,12 @@ pub struct KeyTrie { impl KeyTrie { pub fn new( - documentation: &str, + description: &str, child_order: HashMap, children: Vec, ) -> Self { Self { - documentation: documentation.to_string(), + description: description.to_string(), child_order, children, is_sticky: false, @@ -105,26 +105,26 @@ impl KeyTrie { } for (index, key_trie) in self.children.iter().enumerate() { - let documentation: &str = match key_trie { + let description: &str = match key_trie { KeyTrieNode::MappableCommand(ref command) => { if command.name() == "no_op" { continue; } command.description() } - KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, + KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.description, // FIX: default to a join of all command names - // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. + // NOTE: Giving same description for all sequences will place all sequence keyvents together. // Regardless if the command sequence is different. KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; let key_event = key_event_order[index]; match body .iter() - .position(|(_, existing_documentation)| &documentation == existing_documentation) + .position(|(_, existing_description)| &description == existing_description) { Some(position) => body[position].0.push(key_event.to_string()), - None => body.push((vec![key_event.to_string()], documentation)), + None => body.push((vec![key_event.to_string()], description)), } } @@ -152,7 +152,7 @@ impl KeyTrie { .map(|(key_events, description)| (key_events.join(", "), *description)) .collect(); - Info::new(&self.documentation, &stringified_key_events_body) + Info::new(&self.description, &stringified_key_events_body) } } From eaea71237665f8ccc44a94ccbb564ff8255d485d Mon Sep 17 00:00:00 2001 From: Nachum Barcohen <38861757+nabaco@users.noreply.github.com> Date: Fri, 20 Jan 2023 04:30:05 +0200 Subject: [PATCH 078/195] Accept +num flag for opening at line number --- helix-term/src/args.rs | 15 +++++++++++++++ helix-term/src/main.rs | 1 + 2 files changed, 16 insertions(+) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index dd787f1fd18c..083aa322da87 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -17,6 +17,7 @@ pub struct Args { pub log_file: Option, pub config_file: Option, pub files: Vec<(PathBuf, Position)>, + pub line_number: usize, } impl Args { @@ -73,6 +74,16 @@ impl Args { } } } + arg if arg.starts_with('+') => { + let arg = arg.get(1..).unwrap(); + args.line_number = match arg.parse() { + Ok(n) => n, + _ => anyhow::bail!("bad line number after +"), + }; + if args.line_number > 0 { + args.line_number -= 1; + } + } arg => args.files.push(parse_file(arg)), } } @@ -82,6 +93,10 @@ impl Args { args.files.push(parse_file(&arg)); } + if let Some(file) = args.files.first_mut() { + file.1.row = args.line_number; + } + Ok(args) } } diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c5379f37..22e1e8ce5256 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -73,6 +73,7 @@ FLAGS: -V, --version Prints version information --vsplit Splits all given files vertically into different windows --hsplit Splits all given files horizontally into different windows + +N Goto line number N ", env!("CARGO_PKG_NAME"), VERSION_AND_GIT_HASH, From abf2f283ddce847754a91b22884d8945cd566415 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 23:03:56 +0100 Subject: [PATCH 079/195] Fix undeterministic test failure --- helix-term/src/config.rs | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 296526aeb71e..63c6b65d1a90 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -64,17 +64,18 @@ mod tests { use crate::{ commands::MappableCommand, config::Config, - keymap::{default, keytrienode::KeyTrieNode, macros::*}, + keymap::{default, keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::*}, }; use helix_core::hashmap; - use helix_view::document::Mode; + use helix_view::{document::Mode, input::KeyEvent}; + use std::collections::BTreeMap; #[test] fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] - S-C-a = "delete_selection" y = "move_line_down" + S-C-a = "delete_selection" [keys.normal] A-F12 = "move_next_word_end" @@ -83,8 +84,8 @@ mod tests { let config = Config { keys: hashmap! { Mode::Insert => keytrie!({ "Insert mode" - "S-C-a" => delete_selection, "y" => move_line_down, + "S-C-a" => delete_selection, }), Mode::Normal => keytrie!({ "Normal mode" "A-F12" => move_next_word_end, @@ -92,17 +93,30 @@ mod tests { }, ..Default::default() }; + for mode in config.keys.keys() { + // toml keymap config is placed into a hashmap, so order can not be presumed to be conserved + // hence the insertion into a BTreeMap assert_eq!( - config.keys.get(mode).unwrap().get_children(), - toml::from_str::(sample_keymaps) - .unwrap() - .keys - .get(mode) - .unwrap() - .get_children() + ordered_mapping(config.keys.get(mode).unwrap()), + ordered_mapping( + toml::from_str::(sample_keymaps) + .unwrap() + .keys + .get(mode) + .unwrap() + ) ); } + + fn ordered_mapping<'a>(keytrie: &'a KeyTrie) -> BTreeMap<&'a KeyEvent, KeyTrieNode> { + let children = keytrie.get_children(); + let mut ordered_keymap = BTreeMap::new(); + for (key_event, order) in keytrie.get_child_order() { + ordered_keymap.insert(key_event, children[*order].clone()); + } + ordered_keymap + } } #[test] From eccda6ec94b24e6f7dd5819725165a1b3bc0c2f9 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 00:06:24 +0100 Subject: [PATCH 080/195] Implement clippy tips --- helix-term/src/keymap.rs | 16 ++++++++-------- helix-term/src/keymap/keytrie.rs | 22 ++++++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c6b3f75b56c8..96fa7cc6a589 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -74,7 +74,7 @@ impl Keymap { // Check if sticky keytrie is to be used. let starting_keytrie = match self.sticky_keytrie { - None => &active_keymap, + None => active_keymap, Some(ref active_sticky_keytrie) => active_sticky_keytrie, }; @@ -84,10 +84,10 @@ impl Keymap { let pending_keytrie: KeyTrie = match starting_keytrie.traverse(&[*first_key]) { Some(KeyTrieNode::KeyTrie(sub_keytrie)) => sub_keytrie, Some(KeyTrieNode::MappableCommand(cmd)) => { - return KeymapResult::Matched(cmd.clone()); + return KeymapResult::Matched(cmd); } Some(KeyTrieNode::CommandSequence(cmds)) => { - return KeymapResult::MatchedCommandSequence(cmds.clone()); + return KeymapResult::MatchedCommandSequence(cmds); } None => return KeymapResult::NotFound, }; @@ -99,15 +99,15 @@ impl Keymap { self.pending_keys.clear(); self.sticky_keytrie = Some(map.clone()); } - KeymapResult::Pending(map.clone()) + KeymapResult::Pending(map) } Some(KeyTrieNode::MappableCommand(cmd)) => { self.pending_keys.clear(); - KeymapResult::Matched(cmd.clone()) + KeymapResult::Matched(cmd) } Some(KeyTrieNode::CommandSequence(cmds)) => { self.pending_keys.clear(); - KeymapResult::MatchedCommandSequence(cmds.clone()) + KeymapResult::MatchedCommandSequence(cmds) } None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), } @@ -138,8 +138,8 @@ impl Keymap { KeyTrieNode::KeyTrie(trie_node) => { for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { - temp_prefix.push_str("→"); + if !&temp_prefix.is_empty() { + temp_prefix.push('→'); } temp_prefix.push_str(&key_event.to_string()); _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index dc08695678d5..6bf4ebf86c45 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -55,7 +55,7 @@ impl KeyTrie { _found_child => return Some(_found_child.clone()), } } - return None; + None } } @@ -64,7 +64,7 @@ impl KeyTrie { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; match other_child_keytrie_node { KeyTrieNode::KeyTrie(ref other_child_keytrie) => { - if let Some(self_index) = self.child_order.get(&other_key_event) { + if let Some(self_index) = self.child_order.get(other_key_event) { if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { @@ -97,11 +97,12 @@ impl KeyTrie { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length + #[allow(clippy::uninit_vec)] unsafe { key_event_order.set_len(self.children.len()); } for (key_event, index) in &self.child_order { - key_event_order[*index] = key_event.clone(); + key_event_order[*index] = key_event; } for (index, key_trie) in self.children.iter().enumerate() { @@ -206,9 +207,7 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); - if !category_holder.contains_key(&first_keyevent.modifiers) { - category_holder.insert(first_keyevent.modifiers.clone(), BTreeMap::new()); - } + category_holder.entry(first_keyevent.modifiers).or_insert_with(BTreeMap::new); // HACK: inserting by variant not by variant value. // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant @@ -223,9 +222,7 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let modifier_category = category_holder .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); - if !modifier_category.contains_key(&keycode_category) { - modifier_category.insert(keycode_category.clone(), Vec::new()); - } + modifier_category.entry(keycode_category).or_insert_with(Vec::new); modifier_category .get_mut(&keycode_category) .expect("key existence should be checked") @@ -247,10 +244,8 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() { - if x < y { + if x.to_lowercase() == y.to_lowercase() && x < y{ infobox_rows.swap(x_index, y_index); - } } x_index = y_index; y_index += 1; @@ -263,8 +258,7 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { for infobox_row in infobox_rows { if ('a'..='z') .map(|char| char.to_string()) - .find(|alpha_char| *alpha_char == infobox_row.0[0].to_lowercase()) - .is_some() + .any(|alpha_char| *alpha_char == infobox_row.0[0].to_lowercase()) { alphas.push(infobox_row); } else { From bcf3cb0199c8f7ed157c2871091963c32e20d0d5 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 00:09:12 +0100 Subject: [PATCH 081/195] Implement clippy tips --- helix-loader/src/lib.rs | 2 +- helix-term/src/health.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 9ed456f55072..c034ff171d2c 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -78,7 +78,7 @@ pub fn cache_dir() -> PathBuf { pub fn runtime_dir() -> PathBuf { if let Some(runtime_dir) = RUNTIME_DIR.get() { - return runtime_dir.to_path_buf(); + runtime_dir.to_path_buf() } else { RUNTIME_DIR.set(_runtime_dir()).unwrap(); runtime_dir() diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index bd5539b937d7..7cf915ef16f9 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -132,7 +132,7 @@ fn display_language(lang_str: String) -> std::io::Result<()> { )?; let result = match which::which(&server_cmd) { Ok(path) => path.display().to_string().green(), - Err(_) => format!("Not found in $PATH").red(), + Err(_) => "Not found in $PATH".to_string().red(), }; writeln!(stdout, "Binary for {server_cmd}: {result}")? } From 2afcfcfc731172b04f482524f105d712d923cf0f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 00:14:25 +0100 Subject: [PATCH 082/195] Make cargo fmt happy --- helix-term/src/keymap/keytrie.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 6bf4ebf86c45..25ed9e8e2537 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -207,7 +207,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); - category_holder.entry(first_keyevent.modifiers).or_insert_with(BTreeMap::new); + category_holder + .entry(first_keyevent.modifiers) + .or_insert_with(BTreeMap::new); // HACK: inserting by variant not by variant value. // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant @@ -222,7 +224,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let modifier_category = category_holder .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); - modifier_category.entry(keycode_category).or_insert_with(Vec::new); + modifier_category + .entry(keycode_category) + .or_insert_with(Vec::new); modifier_category .get_mut(&keycode_category) .expect("key existence should be checked") @@ -244,8 +248,8 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() && x < y{ - infobox_rows.swap(x_index, y_index); + if x.to_lowercase() == y.to_lowercase() && x < y { + infobox_rows.swap(x_index, y_index); } x_index = y_index; y_index += 1; From c617fd15ae545d1e830ddb0fb29dcef5fc97e4c2 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 11:32:12 +0100 Subject: [PATCH 083/195] Make updated cargo clippy happy --- helix-term/src/config.rs | 18 +--- helix-term/src/keymap.rs | 1 + helix-term/src/keymap/tests.rs | 154 ++++++++++++++++----------------- 3 files changed, 80 insertions(+), 93 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 63c6b65d1a90..01b7e3f419d1 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -109,7 +109,7 @@ mod tests { ); } - fn ordered_mapping<'a>(keytrie: &'a KeyTrie) -> BTreeMap<&'a KeyEvent, KeyTrieNode> { + fn ordered_mapping(keytrie: &KeyTrie) -> BTreeMap<&KeyEvent, KeyTrieNode> { let children = keytrie.get_children(); let mut ordered_keymap = BTreeMap::new(); for (key_event, order) in keytrie.get_child_order() { @@ -194,22 +194,10 @@ mod tests { // Huh? assert!( - merged_config - .keys - .get(&Mode::Normal) - .unwrap() - .get_children() - .len() - > 1 + merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1 ); assert!( - merged_config - .keys - .get(&Mode::Insert) - .unwrap() - .get_children() - .len() - > 0 + !merged_config.keys.get(&Mode::Insert).unwrap().get_children().is_empty() ); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 96fa7cc6a589..2a77d0ca66cc 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -3,6 +3,7 @@ pub mod macros; // NOTE: Only pub becuase of their use in macros pub mod keytrie; pub mod keytrienode; +#[cfg(test)] mod tests; use self::{keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::key}; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index bc5e061892bb..32dbc305770f 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,86 +1,84 @@ -#[macro_use] -#[cfg(test)] -mod tests { - use crate::keymap::{macros::*, *}; - use helix_core::hashmap; - use helix_view::{document::Mode, input::KeyEvent}; - use std::collections::HashMap; +use std::{sync::Arc, collections::HashMap}; +use arc_swap::ArcSwap; +use helix_core::hashmap; +use helix_view::{document::Mode, input::KeyEvent}; +use crate::key; +use super::{Keymap, macros::keytrie}; - #[test] - #[should_panic] - fn duplicate_keys_should_panic() { - keytrie!({ "Normal mode" - "i" => normal_mode, - "i" => goto_definition, - }); - } - - #[test] - fn check_duplicate_keys_in_default_keymap() { - // will panic on duplicate keys, assumes that `Keymap` uses keymap! macro - Keymap::default(); - } +#[test] +#[should_panic] +fn duplicate_keys_should_panic() { + keytrie!({ "Normal mode" + "i" => normal_mode, + "i" => goto_definition, + }); +} - #[test] - fn aliased_modes_are_same_in_default_keymap() { - let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); - assert_eq!( - normal_mode_keytrie_root - .traverse(&[key!(' '), key!('w')]) - .unwrap(), - normal_mode_keytrie_root - .traverse(&["C-w".parse::().unwrap()]) - .unwrap(), - "Mismatch for window mode on `Space-w` and `Ctrl-w`." - ); - assert_eq!( - normal_mode_keytrie_root.traverse(&[key!('z')]).unwrap(), - normal_mode_keytrie_root.traverse(&[key!('Z')]).unwrap(), - "Mismatch for view mode on `z` and `Z`." - ); - } +#[test] +fn check_duplicate_keys_in_default_keymap() { + // will panic on duplicate keys, assumes that `Keymap` uses keymap! macro + Keymap::default(); +} - #[test] - fn command_list() { - let normal_mode = keytrie!({ "Normal mode" - "i" => insert_mode, - "g" => { "Goto" - "g" => goto_file_start, - "e" => goto_file_end, - }, - "j" | "k" => move_line_down, - }); +#[test] +fn aliased_modes_are_same_in_default_keymap() { + let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); + assert_eq!( + normal_mode_keytrie_root + .traverse(&[key!(' '), key!('w')]) + .unwrap(), + normal_mode_keytrie_root + .traverse(&["C-w".parse::().unwrap()]) + .unwrap(), + "Mismatch for window mode on `Space-w` and `Ctrl-w`." + ); + assert_eq!( + normal_mode_keytrie_root.traverse(&[key!('z')]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!('Z')]).unwrap(), + "Mismatch for view mode on `z` and `Z`." + ); +} - let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( - hashmap!(Mode::Normal => normal_mode), - )))); - let mut command_list = keymap.command_list(&Mode::Normal); +#[test] +fn command_list() { + let normal_mode = keytrie!({ "Normal mode" + "i" => insert_mode, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_file_end, + }, + "j" | "k" => move_line_down, + }); - // sort keybindings in order to have consistent tests - // HashMaps can be compared but we can still get different ordering of bindings - // for commands that have multiple bindings assigned - for v in command_list.values_mut() { - v.sort() - } + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( + hashmap!(Mode::Normal => normal_mode), + )))); + let mut command_list = keymap.command_list(&Mode::Normal); - assert_eq!( - command_list, - HashMap::from([ - ("insert_mode".to_string(), vec![key!('i').to_string()]), - ( - "goto_file_start".to_string(), - vec![format!("{}→{}", key!('g'), key!('g'))] - ), - ( - "goto_file_end".to_string(), - vec![format!("{}→{}", key!('g'), key!('e'))] - ), - ( - "move_line_down".to_string(), - vec![key!('j').to_string(), key!('k').to_string()] - ) - ]), - "Mismatch" - ) + // sort keybindings in order to have consistent tests + // HashMaps can be compared but we can still get different ordering of bindings + // for commands that have multiple bindings assigned + for v in command_list.values_mut() { + v.sort() } + + assert_eq!( + command_list, + HashMap::from([ + ("insert_mode".to_string(), vec![key!('i').to_string()]), + ( + "goto_file_start".to_string(), + vec![format!("{}→{}", key!('g'), key!('g'))] + ), + ( + "goto_file_end".to_string(), + vec![format!("{}→{}", key!('g'), key!('e'))] + ), + ( + "move_line_down".to_string(), + vec![key!('j').to_string(), key!('k').to_string()] + ) + ]), + "Mismatch" + ) } From 4d02459c3058b52627dc45bd9934293bfd929e8e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 12:09:33 +0100 Subject: [PATCH 084/195] Unblock #5656 --- helix-core/src/syntax.rs | 2 +- helix-core/tests/indent.rs | 4 ++-- helix-loader/src/lib.rs | 20 ++++++++++++-------- helix-term/src/config.rs | 4 ++-- helix-term/tests/test/helpers.rs | 2 +- helix-view/src/theme.rs | 8 ++++---- xtask/src/themelint.rs | 4 ++-- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 98dfec899e25..81d03dae93d9 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -77,7 +77,7 @@ impl LanguageConfigurations { impl Default for LanguageConfigurations { fn default() -> Self { - toml::from_slice(&std::fs::read(helix_loader::repo_paths::default_lang_configs()).unwrap()) + toml::from_str(&std::fs::read_to_string(helix_loader::repo_paths::default_lang_configs()).unwrap()) .expect("Failed to deserialize built-in languages.toml") } } diff --git a/helix-core/tests/indent.rs b/helix-core/tests/indent.rs index e1114f4a9e25..f74b576ac996 100644 --- a/helix-core/tests/indent.rs +++ b/helix-core/tests/indent.rs @@ -28,8 +28,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) { let mut config_file = test_dir; config_file.push("languages.toml"); - let config = std::fs::read(config_file).unwrap(); - let config = toml::from_slice(&config).unwrap(); + let config = std::fs::read_to_string(config_file).unwrap(); + let config = toml::from_str(&config).unwrap(); let loader = Loader::new(config); // set runtime path so we can find the queries diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index c034ff171d2c..d19d97d6b4f2 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -152,8 +152,8 @@ fn merge_toml_by_config_paths(config_paths: Vec) -> Result = Vec::with_capacity(config_paths.len()); for config_path in config_paths { if config_path.exists() { - let config_string = std::fs::read(&config_path)?; - let config = toml::from_slice(&config_string)?; + let config_string = std::fs::read_to_string(&config_path)?; + let config = toml::from_str(&config_string)?; configs.push(config); } } @@ -263,11 +263,13 @@ mod merge_toml_tests { indent = { tab-width = 4, unit = " ", test = "aaa" } "#; - let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) - .expect("Couldn't parse built-in languages config"); + // NOTE: Exact duplicate of helix_core::LanguageConfigurations::default() + let default: Value = + toml::from_str(&std::fs::read_to_string(crate::repo_paths::default_lang_configs()).unwrap()) + .expect("Failed to deserialize built-in languages.toml"); let user: Value = toml::from_str(USER).unwrap(); - let merged = merge_toml_values(base, user, 3); + let merged = merge_toml_values(default, user, 3); let languages = merged.get("language").unwrap().as_array().unwrap(); let nix = languages .iter() @@ -296,11 +298,13 @@ mod merge_toml_tests { language-server = { command = "deno", args = ["lsp"] } "#; - let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) - .expect("Couldn't parse built-in languages config"); + // NOTE: Exact duplicate of helix_core::LanguageConfigurations::default() + let default: Value = + toml::from_str(&std::fs::read_to_string(crate::repo_paths::default_lang_configs()).unwrap()) + .expect("Failed to deserialize built-in languages.toml"); let user: Value = toml::from_str(USER).unwrap(); - let merged = merge_toml_values(base, user, 3); + let merged = merge_toml_values(default, user, 3); let languages = merged.get("language").unwrap().as_array().unwrap(); let ts = languages .iter() diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index ee87c2ea9a5b..bb1289cf8058 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -17,8 +17,8 @@ pub struct Config { impl Config { /// Merge user config with system keymap pub fn merged() -> Result { - let config_string = std::fs::read(helix_loader::config_file())?; - toml::from_slice(&config_string) + let config_string = std::fs::read_to_string(helix_loader::config_file())?; + toml::from_str(&config_string) .map(|config: Config| config.merge_in_default_keymap()) .map_err(|error| anyhow!("{}", error)) } diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index ee2b41517a63..e40792a8ba40 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -151,7 +151,7 @@ pub async fn test_key_sequence_with_input_text>( /// added in `overrides`. pub fn test_syntax_conf(overrides: Option) -> LanguageConfigurations { let mut lang: toml::Value = - toml::from_slice(&std::fs::read(helix_loader::repo_paths::default_lang_configs()).unwrap()) + toml::from_str(&std::fs::read_to_string(helix_loader::repo_paths::default_lang_configs()).unwrap()) .unwrap(); for lang_config in lang diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 47bc67eade5e..71db26b78f65 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -15,12 +15,12 @@ use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME_DATA: Lazy = Lazy::new(|| { - toml::from_slice(&std::fs::read(repo_paths::default_theme()).unwrap()) + toml::from_str(&std::fs::read_to_string(repo_paths::default_theme()).unwrap()) .expect("Failed to parse default theme") }); pub static BASE16_DEFAULT_THEME_DATA: Lazy = Lazy::new(|| { - toml::from_slice(&std::fs::read(repo_paths::default_base16_theme()).unwrap()) + toml::from_str(&std::fs::read_to_string(repo_paths::default_base16_theme()).unwrap()) .expect("Failed to parse base 16 default theme") }); @@ -149,8 +149,8 @@ impl Loader { // Loads the theme data as `toml::Value` first from the user_dir then in default_dir fn load_toml(&self, path: PathBuf) -> Result { - let data = std::fs::read(&path)?; - let value = toml::from_slice(data.as_slice())?; + let data = std::fs::read_to_string(&path)?; + let value = toml::from_str(&data)?; Ok(value) } diff --git a/xtask/src/themelint.rs b/xtask/src/themelint.rs index 3f9821ee30b7..bd7eab1349d7 100644 --- a/xtask/src/themelint.rs +++ b/xtask/src/themelint.rs @@ -156,8 +156,8 @@ pub fn lint(file: String) -> Result<(), DynError> { return Ok(()); } let path = repo_paths::themes().join(file.clone() + ".toml"); - let theme = std::fs::read(&path).unwrap(); - let theme: Theme = toml::from_slice(&theme).expect("Failed to parse theme"); + let theme = std::fs::read_to_string(&path).unwrap(); + let theme: Theme = toml::from_str(&theme).expect("Failed to parse theme"); let mut messages: Vec = vec![]; get_rules().iter().for_each(|lint| match lint { From b4b82b43c0e10be861ef7492e40c2e0102269935 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 12:15:32 +0100 Subject: [PATCH 085/195] Fight with cargo fmt round 3 --- helix-term/src/config.rs | 17 +++++++++++++---- helix-term/src/keymap/tests.rs | 6 +++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 01b7e3f419d1..4c8bbe144d5a 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -194,10 +194,19 @@ mod tests { // Huh? assert!( - merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1 - ); - assert!( - !merged_config.keys.get(&Mode::Insert).unwrap().get_children().is_empty() + merged_config + .keys + .get(&Mode::Normal) + .unwrap() + .get_children() + .len() + > 1 ); + assert!(!merged_config + .keys + .get(&Mode::Insert) + .unwrap() + .get_children() + .is_empty()); } } diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 32dbc305770f..a0c52de6f77c 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,9 +1,9 @@ -use std::{sync::Arc, collections::HashMap}; +use super::{macros::keytrie, Keymap}; +use crate::key; use arc_swap::ArcSwap; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; -use crate::key; -use super::{Keymap, macros::keytrie}; +use std::{collections::HashMap, sync::Arc}; #[test] #[should_panic] From 3f760f127cf9f810243cd0f71182114927cb8b79 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 12:27:50 +0100 Subject: [PATCH 086/195] Run cargo fmt --- helix-core/src/syntax.rs | 6 ++++-- helix-loader/src/lib.rs | 14 ++++++++------ helix-term/tests/test/helpers.rs | 7 ++++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 81d03dae93d9..b522f1408bf8 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -77,8 +77,10 @@ impl LanguageConfigurations { impl Default for LanguageConfigurations { fn default() -> Self { - toml::from_str(&std::fs::read_to_string(helix_loader::repo_paths::default_lang_configs()).unwrap()) - .expect("Failed to deserialize built-in languages.toml") + toml::from_str( + &std::fs::read_to_string(helix_loader::repo_paths::default_lang_configs()).unwrap(), + ) + .expect("Failed to deserialize built-in languages.toml") } } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index d19d97d6b4f2..5deea0157076 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -264,9 +264,10 @@ mod merge_toml_tests { "#; // NOTE: Exact duplicate of helix_core::LanguageConfigurations::default() - let default: Value = - toml::from_str(&std::fs::read_to_string(crate::repo_paths::default_lang_configs()).unwrap()) - .expect("Failed to deserialize built-in languages.toml"); + let default: Value = toml::from_str( + &std::fs::read_to_string(crate::repo_paths::default_lang_configs()).unwrap(), + ) + .expect("Failed to deserialize built-in languages.toml"); let user: Value = toml::from_str(USER).unwrap(); let merged = merge_toml_values(default, user, 3); @@ -299,9 +300,10 @@ mod merge_toml_tests { "#; // NOTE: Exact duplicate of helix_core::LanguageConfigurations::default() - let default: Value = - toml::from_str(&std::fs::read_to_string(crate::repo_paths::default_lang_configs()).unwrap()) - .expect("Failed to deserialize built-in languages.toml"); + let default: Value = toml::from_str( + &std::fs::read_to_string(crate::repo_paths::default_lang_configs()).unwrap(), + ) + .expect("Failed to deserialize built-in languages.toml"); let user: Value = toml::from_str(USER).unwrap(); let merged = merge_toml_values(default, user, 3); diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index e40792a8ba40..48c2b520aeba 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -150,9 +150,10 @@ pub async fn test_key_sequence_with_input_text>( /// document. If a language-server is necessary for a test, it must be explicitly /// added in `overrides`. pub fn test_syntax_conf(overrides: Option) -> LanguageConfigurations { - let mut lang: toml::Value = - toml::from_str(&std::fs::read_to_string(helix_loader::repo_paths::default_lang_configs()).unwrap()) - .unwrap(); + let mut lang: toml::Value = toml::from_str( + &std::fs::read_to_string(helix_loader::repo_paths::default_lang_configs()).unwrap(), + ) + .unwrap(); for lang_config in lang .as_table_mut() From 2d463f1c538ba8a5285ffd3f9bc64e3822f74e4a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 16:42:18 +0100 Subject: [PATCH 087/195] make fmt and clippy happy --- helix-loader/src/grammar.rs | 3 ++- helix-loader/src/lib.rs | 45 ++++++++++++++++++++----------------- helix-term/src/health.rs | 4 +++- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index dfd7e957c995..50e8db3089ae 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -519,6 +519,7 @@ fn mtime(path: &Path) -> Result { /// Gives the contents of a file from a language's `runtime/queries/` /// directory pub fn load_runtime_file(language: &str, filename: &str) -> Result { - let path = crate::get_runtime_file(&PathBuf::new().join("queries").join(language).join(filename)); + let path = + crate::get_runtime_file(&PathBuf::new().join("queries").join(language).join(filename)); std::fs::read_to_string(&path) } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 36723cb801cd..074693c7b4cb 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -82,7 +82,7 @@ pub fn cache_dir() -> PathBuf { /// 4. Under path to helix executable, always included. However, it might not exist. pub fn get_runtime_dirs() -> &'static [PathBuf] { if let Some(runtime_dirs) = RUNTIME_DIRS.get() { - &runtime_dirs + runtime_dirs } else { RUNTIME_DIRS.set(_runtime_dirs()).unwrap(); get_runtime_dirs() @@ -108,33 +108,38 @@ fn _runtime_dirs() -> Vec { } // canonicalize the path in case the executable is symlinked - runtime_dirs.push(std::env::current_exe() - .ok() - .and_then(|path| std::fs::canonicalize(path).ok()) - .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RUNTIME_DIR_NAME))) - .unwrap() + runtime_dirs.push( + std::env::current_exe() + .ok() + .and_then(|path| std::fs::canonicalize(path).ok()) + .and_then(|path| { + path.parent() + .map(|path| path.to_path_buf().join(RUNTIME_DIR_NAME)) + }) + .unwrap(), ); runtime_dirs } - /// Search for a file in the runtime directories. /// Returns a non-existent path relative to the local executable if none are found. pub fn get_runtime_file(relative_path: &Path) -> PathBuf { - get_runtime_dirs().iter().find_map(|runtime_dir| { - let path = runtime_dir.join(relative_path); - match path.exists() { - true => Some(path), - false => None - } - }) - .unwrap_or_else(|| { - get_runtime_dirs() - .last() - .expect("Path to local executable.") - .join(relative_path) - }) + get_runtime_dirs() + .iter() + .find_map(|runtime_dir| { + let path = runtime_dir.join(relative_path); + match path.exists() { + true => Some(path), + false => None, + } + }) + .unwrap_or_else(|| { + get_runtime_dirs() + .last() + .expect("Path to local executable.") + .join(relative_path) + }) } pub fn merged_config() -> Result { diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 8ae80d9ab88c..bbce86b7327e 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -40,7 +40,9 @@ fn display_paths() -> std::io::Result<()> { writeln!(stdout, "Log file: {}", helix_loader::log_file().display())?; let rt_dirs = helix_loader::get_runtime_dirs(); - writeln!(stdout,"Runtime directories: {}", + writeln!( + stdout, + "Runtime directories: {}", rt_dirs .iter() .map(|rt_dir| rt_dir.to_string_lossy()) From f2afd7268c72252fa985dde91fb8bff3a321f6ed Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 16:49:37 +0100 Subject: [PATCH 088/195] Use PathBuf::from("rel_path") rather than pathbuf::new().join("rel_path") --- helix-loader/src/grammar.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 50e8db3089ae..18698faf5c9a 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -67,7 +67,7 @@ pub fn get_language(name: &str) -> Result { #[cfg(not(target_arch = "wasm32"))] pub fn get_language(name: &str) -> Result { use libloading::{Library, Symbol}; - let mut rel_library_path = PathBuf::new().join("grammars").join(name); + let mut rel_library_path = PathBuf::from("grammars").join(name); rel_library_path.set_extension(DYLIB_EXTENSION); let library_path = crate::get_runtime_file(&rel_library_path); @@ -519,7 +519,6 @@ fn mtime(path: &Path) -> Result { /// Gives the contents of a file from a language's `runtime/queries/` /// directory pub fn load_runtime_file(language: &str, filename: &str) -> Result { - let path = - crate::get_runtime_file(&PathBuf::new().join("queries").join(language).join(filename)); + let path = crate::get_runtime_file(&PathBuf::from("queries").join(language).join(filename)); std::fs::read_to_string(&path) } From 1f966d7b269e4b29312b6c2435c23b39ab9a71e3 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 16:59:46 +0100 Subject: [PATCH 089/195] Inline load_toml() introduced in #5411 --- helix-view/src/theme.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 3a1a9d6ee55c..3ea794561a11 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -79,7 +79,8 @@ impl Loader { let path = self .path(name, visited_paths) .ok_or_else(|| anyhow!("Theme: not found or recursively inheriting: {}", name))?; - let theme_toml = self.load_toml(path)?; + let theme_string = std::fs::read_to_string(&path)?; + let theme_toml: toml::Value = toml::from_str(&theme_string)?; let inherits = theme_toml.get("inherits"); @@ -147,14 +148,6 @@ impl Loader { merge_toml_values(theme, palette.into(), 1) } - // Loads the theme data as `toml::Value` - fn load_toml(&self, path: PathBuf) -> Result { - let data = std::fs::read_to_string(&path)?; - let value = toml::from_str(&data)?; - - Ok(value) - } - /// Returns the path to the theme with the given name /// /// Ignores paths already visited and follows directory priority order. From 0b9c2e80f49bcaa74f2f3ea9563e0c46dba175aa Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 17:33:06 +0100 Subject: [PATCH 090/195] Theme.rs * Clarify method signature of newly intrudused path -> find_remaining_path * Remove unused self.names() * Removed overly verbose comments. --- helix-view/src/theme.rs | 48 ++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 3ea794561a11..90678e7fa5b8 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -77,7 +77,7 @@ impl Loader { fn _load(&self, name: &str, visited_paths: &mut HashSet) -> Result { let path = self - .path(name, visited_paths) + .find_remaining_path(name, visited_paths) .ok_or_else(|| anyhow!("Theme: not found or recursively inheriting: {}", name))?; let theme_string = std::fs::read_to_string(&path)?; let theme_toml: toml::Value = toml::from_str(&theme_string)?; @@ -107,22 +107,6 @@ impl Loader { Ok(theme_toml) } - pub fn read_names(path: &Path) -> Vec { - std::fs::read_dir(path) - .map(|entries| { - entries - .filter_map(|entry| { - let entry = entry.ok()?; - let path = entry.path(); - (path.extension()? == "toml") - .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) - }) - .collect() - }) - .unwrap_or_default() - } - - // merge one theme into the parent theme fn merge_themes(&self, parent_theme_toml: Value, theme_toml: Value) -> Value { let parent_palette = parent_theme_toml.get("palette"); let palette = theme_toml.get("palette"); @@ -148,10 +132,23 @@ impl Loader { merge_toml_values(theme, palette.into(), 1) } - /// Returns the path to the theme with the given name - /// - /// Ignores paths already visited and follows directory priority order. - fn path(&self, name: &str, visited_paths: &mut HashSet) -> Option { + pub fn read_names(path: &Path) -> Vec { + std::fs::read_dir(path) + .map(|entries| { + entries + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "toml") + .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) + }) + .collect() + }) + .unwrap_or_default() + } + + + fn find_remaining_path(&self, name: &str, visited_paths: &mut HashSet) -> Option { let filename = format!("{}.toml", name); self.theme_dirs.iter().find_map(|dir| { @@ -165,15 +162,6 @@ impl Loader { }) } - /// Lists all theme names available in all directories - pub fn names(&self) -> Vec { - let mut names = Vec::new(); - for dir in &self.theme_dirs { - names.extend(Self::read_names(dir)); - } - names - } - pub fn default_theme(&self, true_color: bool) -> Theme { if true_color { self.default() From e7aee35b004a2525e61d3d300b53f430e56da200 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 17:53:55 +0100 Subject: [PATCH 091/195] Create helix_loader::get_first_runtime_dir Replaces the get_runtime_dirs().first().expect("...") in helix_loader:::grammar --- helix-loader/src/grammar.rs | 8 ++------ helix-loader/src/lib.rs | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 18698faf5c9a..a7bcfb417951 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -253,9 +253,7 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result { remote, revision, .. } = grammar.source { - let grammar_dir = crate::get_runtime_dirs() - .first() - .expect("should return at least one directory") + let grammar_dir = crate::get_first_runtime_dir() .join("grammars") .join("sources") .join(&grammar.grammar_id); @@ -353,9 +351,7 @@ fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result< let grammar_dir = if let GrammarSource::Local { path } = &grammar.source { PathBuf::from(&path) } else { - crate::get_runtime_dirs() - .first() - .expect("No runtime directories provided") // guaranteed by post-condition + crate::get_first_runtime_dir() .join("grammars") .join("sources") .join(&grammar.grammar_id) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 074693c7b4cb..f6adbbcff3c9 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -122,6 +122,12 @@ fn _runtime_dirs() -> Vec { runtime_dirs } +pub fn get_first_runtime_dir() -> &'static PathBuf { + get_runtime_dirs() + .first() + .expect("should return at least one directory") +} + /// Search for a file in the runtime directories. /// Returns a non-existent path relative to the local executable if none are found. pub fn get_runtime_file(relative_path: &Path) -> PathBuf { From c8dd563043c0309ba64c428ecdde1f8d4a3c7b4f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 15:33:55 +0100 Subject: [PATCH 092/195] remove one-liner solely used for a test --- helix-term/src/keymap.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4a131f0a5217..60b9ff1c31c8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -112,10 +112,6 @@ impl KeyTrieNode { } Info::from_keymap(self.name(), body) } - /// Get a reference to the key trie node's order. - pub fn order(&self) -> &[KeyEvent] { - self.order.as_slice() - } } impl Default for KeyTrieNode { @@ -541,7 +537,7 @@ mod tests { ); // Make sure an order was set during merge let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order().is_empty()) + assert!(!node.node().unwrap().order.as_slice().is_empty()) } #[test] From df49f601ca4876c98728ef2d78fcea864f867f04 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 19:55:30 +0100 Subject: [PATCH 093/195] Inline helper fuction of singular reference --- helix-term/src/config.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f838..f7cac6a36d10 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -28,7 +28,7 @@ impl Default for Config { } #[derive(Debug)] -pub enum ConfigLoadError { +pub enum ConfigLoadError { BadConfig(TomlError), Error(IOError), } @@ -43,18 +43,15 @@ impl Display for ConfigLoadError { } impl Config { - pub fn load(config_path: PathBuf) -> Result { - match std::fs::read_to_string(config_path) { + // REFACTOR? code similar to config assignment in main.rs, + pub fn load_default() -> Result { + match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) .map(merge_keys) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } } - - pub fn load_default() -> Result { - Config::load(helix_loader::config_file()) - } } #[cfg(test)] From 69ed13532be09b1686e91f17e7bf6820d877ed57 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 21:55:05 +0100 Subject: [PATCH 094/195] Remove order property of KeyTrie struct: Was not implemented correctly as the order was in the most cases assigned from the values of an HashMap, which does not guarantee order. This applied to when a keymap was both deserialized and merged. Info box body was then being sorted as a function of the fallacious `order` property, and by a method that yielded at worst a time complexity of at least n^2. Furthermore, the body contents were inerted at first by the order of the hash map keys, in which `order` itself was based on. A more reliable predifined sorting order is to be implemented. --- helix-term/src/keymap.rs | 29 ++++------------------------- helix-term/src/keymap/macros.rs | 4 +--- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 60b9ff1c31c8..03ac5ce18269 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -24,7 +24,6 @@ pub struct KeyTrieNode { /// A label for keys coming under this node, like "Goto mode" name: String, map: HashMap, - order: Vec, pub is_sticky: bool, } @@ -33,22 +32,18 @@ impl<'de> Deserialize<'de> for KeyTrieNode { where D: serde::Deserializer<'de>, { - let map = HashMap::::deserialize(deserializer)?; - let order = map.keys().copied().collect::>(); // NOTE: map.keys() has arbitrary order Ok(Self { - map, - order, + map: HashMap::::deserialize(deserializer)?, ..Default::default() }) } } impl KeyTrieNode { - pub fn new(name: &str, map: HashMap, order: Vec) -> Self { + pub fn new(name: &str, map: HashMap) -> Self { Self { name: name.to_string(), map, - order, is_sticky: false, } } @@ -70,11 +65,6 @@ impl KeyTrieNode { } self.map.insert(key, trie); } - for &key in self.map.keys() { - if !self.order.contains(&key) { - self.order.push(key); - } - } } pub fn infobox(&self) -> Info { @@ -97,12 +87,6 @@ impl KeyTrieNode { None => body.push((desc, BTreeSet::from([key]))), } } - body.sort_unstable_by_key(|(_, keys)| { - self.order - .iter() - .position(|&k| k == *keys.iter().next().unwrap()) - .unwrap() - }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { body = body @@ -116,7 +100,7 @@ impl KeyTrieNode { impl Default for KeyTrieNode { fn default() -> Self { - Self::new("", HashMap::new(), Vec::new()) + Self::new("", HashMap::new()) } } @@ -195,12 +179,10 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { M: serde::de::MapAccess<'de>, { let mut mapping = HashMap::new(); - let mut order = Vec::new(); while let Some((key, value)) = map.next_entry::()? { mapping.insert(key, value); - order.push(key); } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order))) + Ok(KeyTrie::Node(KeyTrieNode::new("", mapping))) } } @@ -535,9 +517,6 @@ mod tests { &KeyTrie::Leaf(MappableCommand::vsplit), "Leaf should be present in merged subnode" ); - // Make sure an order was set during merge - let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order.as_slice().is_empty()) } #[test] diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index c4a1bfbb3064..1cf4151c9a38 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -101,7 +101,6 @@ macro_rules! keymap { { let _cap = hashmap!(@count $($($key),+),*); let mut _map = ::std::collections::HashMap::with_capacity(_cap); - let mut _order = ::std::vec::Vec::with_capacity(_cap); $( $( let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); @@ -110,10 +109,9 @@ macro_rules! keymap { keymap!(@trie $value) ); assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); - _order.push(_key); )+ )* - let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); + let mut _node = $crate::keymap::KeyTrieNode::new($label, _map); $( _node.is_sticky = $sticky; )? $crate::keymap::KeyTrie::Node(_node) } From 10151a9193a064d30389cade60c3e66936842230 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 22:57:28 +0100 Subject: [PATCH 095/195] Infobox: Remove superflous command descripition pruning: Exist under the wrong (possibly just outdated) assumption that command descriptions are written with their KeyTrie name prefixed (Space, View, Goto etc.). For examle: The command `file_picker` is assumed to have the description "Space Open file picker", which is not the case , nor for any other command description. --- helix-term/src/keymap.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 03ac5ce18269..938e781210f9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -87,13 +87,7 @@ impl KeyTrieNode { None => body.push((desc, BTreeSet::from([key]))), } } - let prefix = format!("{} ", self.name()); - if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { - body = body - .into_iter() - .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) - .collect(); - } + Info::from_keymap(self.name(), body) } } From 465c0ef6aca196606eaccfb693e9c5407def932c Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 23:42:10 +0100 Subject: [PATCH 096/195] Keymap infobox: Use Vec in place of BTree: BTree was being used to store a list of keyevents for a given command. This list was only iterated over twice to in the end be converted to a Vec. Better to just use a Vec from start given the use- case. Temporalily reverts #952. --- helix-term/src/keymap.rs | 8 ++++---- helix-view/src/info.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 938e781210f9..b575ee97e9ff 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -11,7 +11,7 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ borrow::Cow, - collections::{BTreeSet, HashMap}, + collections::HashMap, ops::{Deref, DerefMut}, sync::Arc, }; @@ -68,7 +68,7 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); + let mut body: Vec<(&str, Vec)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => { @@ -82,9 +82,9 @@ impl KeyTrieNode { }; match body.iter().position(|(d, _)| d == &desc) { Some(pos) => { - body[pos].1.insert(key); + body[pos].1.push(key); } - None => body.push((desc, BTreeSet::from([key]))), + None => body.push((desc, Vec::from([key]))), } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 3080cf8e1b2f..26dc844d75b1 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,6 +1,6 @@ use crate::input::KeyEvent; use helix_core::{register::Registers, unicode::width::UnicodeWidthStr}; -use std::{collections::BTreeSet, fmt::Write}; +use std::fmt::Write; #[derive(Debug)] /// Info box used in editor. Rendering logic will be in other crate. @@ -55,7 +55,7 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet)>) -> Self { + pub fn from_keymap(title: &str, body: Vec<(&str, Vec)>) -> Self { let body: Vec<_> = body .into_iter() .map(|(desc, events)| { From 8768bf92f40518703ffcf4742b1cf907178bd28e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 17 Dec 2022 23:53:07 +0100 Subject: [PATCH 097/195] Keymap infobox: Place in correct order from start: Infobox body was being filled with description then KeyEvent list to only later be iterated over for the purpose of flippingin this order. --- helix-term/src/keymap.rs | 8 ++++---- helix-view/src/info.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b575ee97e9ff..1a91d997e862 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -68,7 +68,7 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, Vec)> = Vec::with_capacity(self.len()); + let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { let desc = match trie { KeyTrie::Leaf(cmd) => { @@ -80,11 +80,11 @@ impl KeyTrieNode { KeyTrie::Node(n) => n.name(), KeyTrie::Sequence(_) => "[Multiple commands]", }; - match body.iter().position(|(d, _)| d == &desc) { + match body.iter().position(|(_, d)| d == &desc) { Some(pos) => { - body[pos].1.push(key); + body[pos].0.push(key); } - None => body.push((desc, Vec::from([key]))), + None => body.push((Vec::from([key]), desc)), } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 26dc844d75b1..fea8f535cd59 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -55,10 +55,10 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(&str, Vec)>) -> Self { + pub fn from_keymap(title: &str, body: Vec<(Vec, &str)>) -> Self { let body: Vec<_> = body .into_iter() - .map(|(desc, events)| { + .map(|(events, desc)| { let events = events.iter().map(ToString::to_string).collect::>(); (events.join(", "), desc) }) From b50050b0c91998394d744425d7b00c0a020ba84a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 18 Dec 2022 00:56:15 +0100 Subject: [PATCH 098/195] Keymap infobox: Format body from start Vec was being created as an intermediary into Info.rs for it to be converted into a comma separated string. This commit removes this intermediate step but also the single purpose from_keymap public function in Info.rs, as it is no longer needed. --- helix-term/src/keymap.rs | 22 ++++++++++------------ helix-view/src/info.rs | 13 ------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 1a91d997e862..7ff4f83764f9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -68,27 +68,25 @@ impl KeyTrieNode { } pub fn infobox(&self) -> Info { - let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); + let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); for (&key, trie) in self.iter() { - let desc = match trie { - KeyTrie::Leaf(cmd) => { - if cmd.name() == "no_op" { + let description: &str = match trie { + KeyTrie::Leaf(command) => { + if command.name() == "no_op" { continue; } - cmd.doc() + command.doc() } - KeyTrie::Node(n) => n.name(), + KeyTrie::Node(key_trie) => key_trie.name(), KeyTrie::Sequence(_) => "[Multiple commands]", }; - match body.iter().position(|(_, d)| d == &desc) { - Some(pos) => { - body[pos].0.push(key); - } - None => body.push((Vec::from([key]), desc)), + match body.iter().position(|(_, existing_description)| &description == existing_description) { + Some(pos) => body[pos].0 += &format!(", {}", &key.to_string()), + None => body.push((key.to_string(), description)), } } - Info::from_keymap(self.name(), body) + Info::new(self.name(), &body) } } diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index fea8f535cd59..1503e855e362 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -1,4 +1,3 @@ -use crate::input::KeyEvent; use helix_core::{register::Registers, unicode::width::UnicodeWidthStr}; use std::fmt::Write; @@ -55,18 +54,6 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(Vec, &str)>) -> Self { - let body: Vec<_> = body - .into_iter() - .map(|(events, desc)| { - let events = events.iter().map(ToString::to_string).collect::>(); - (events.join(", "), desc) - }) - .collect(); - - Self::new(title, &body) - } - pub fn from_registers(registers: &Registers) -> Self { let body: Vec<_> = registers .inner() From eb50de1b9390a1b3d988a72bd9550e85b5ff9888 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 26 Dec 2022 17:19:33 +0100 Subject: [PATCH 099/195] keymap.rs file splitting, cleanup and descriptive naming: * Descriptive naming and correct use of data structure terminology example translations: keymap.reverse_map -> keymap.command_list keytrienode -> keytrie (struct) search->traverse merge->merge_keytrie keytrie -> keytrienode (enum describing node variants) Leaf -> MappableCommand Sequence -> CommandSequence Node -> KeyTrie (A leaf is a node, and the sequence was also a node. So old naming made absolutely no sense whatsoever.) * Splitting parts of the keymap.rs file into {keytrienode, keytrie, keymaps, tests}.rs. (Should be self-explanatory by looking at the old keymap.rs.) * Removed KeytrieNode enum functions node(&self) -> Option<&KeyTrie> and node_mut(&mut self) -> Option<&mut KeyTrie> from KeyTrieNode as they served no purpose that could not be used from elsewhere. Was also a bit strange to return a "parent struct" from an enum "building block". * Removed getters that could be achieved by making fields public for now. (E.g .root(), .map(), .name()) * Removed keymap.merge() and keytrienode.merge_nodes() All merging could be handled by keytrie.merge() and keymaps.merge_with_default(), unnecessary to have a second unused system that does about the same thing. We also don't want functions that can cause panics as merge_nodes() could. * Initial simplification and formatting of command palette. Formatting is done during the creation of the command lists, removes the need for the creation and and traversal of an intermediary Vec>. Keyevent formatting changed from "(w)" to ["space>w>C-q"]. Clarifying how commands are triggered by moving through submenus/keytries. --- helix-term/src/application.rs | 2 +- helix-term/src/commands.rs | 36 +- helix-term/src/config.rs | 4 +- helix-term/src/keymap.rs | 591 +++------------------------ helix-term/src/keymap.rs:41:29 | 0 helix-term/src/keymap/default.rs | 12 +- helix-term/src/keymap/keymaps.rs | 121 ++++++ helix-term/src/keymap/keytrie.rs | 129 ++++++ helix-term/src/keymap/keytrienode.rs | 72 ++++ helix-term/src/keymap/macros.rs | 44 +- helix-term/src/keymap/tests.rs | 181 ++++++++ helix-term/src/lib.rs | 1 + helix-term/src/ui/editor.rs | 6 +- 13 files changed, 602 insertions(+), 597 deletions(-) create mode 100644 helix-term/src/keymap.rs:41:29 create mode 100644 helix-term/src/keymap/keymaps.rs create mode 100644 helix-term/src/keymap/keytrie.rs create mode 100644 helix-term/src/keymap/keytrienode.rs create mode 100644 helix-term/src/keymap/tests.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c0cbc2451483..42daaa64d44a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -24,7 +24,7 @@ use crate::{ compositor::{Compositor, Event}, config::Config, job::Jobs, - keymap::Keymaps, + keymap::keymaps::Keymaps, ui::{self, overlay::overlayed}, }; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e70914016d60..ef166737a38e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -46,7 +46,7 @@ use crate::{ args, compositor::{self, Component, Compositor}, job::Callback, - keymap::ReverseKeymap, + keymap::CommandList, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; @@ -2454,30 +2454,21 @@ fn jumplist_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } -impl ui::menu::Item for MappableCommand { - type Data = ReverseKeymap; - fn format(&self, keymap: &Self::Data) -> Row { - let fmt_binding = |bindings: &Vec>| -> String { - bindings.iter().fold(String::new(), |mut acc, bind| { - if !acc.is_empty() { - acc.push(' '); - } - for key in bind { - acc.push_str(&key.key_sequence_format()); - } - acc - }) - }; + +impl ui::menu::Item for MappableCommand { + type Data = CommandList; + + fn label(&self, keymap: &Self::Data) -> Spans { match self { MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { - Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [:{}]", doc, name).into(), + Some(key_events) => format!("{} {:?} ':{}'", doc, key_events, name).into(), + None => format!("{} ':{}'", doc, name).into(), }, MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [{}]", doc, name).into(), + Some(key_events) => format!("{} {:?} '{}'", doc, key_events, name).into(), + None => format!("{} '{}'", doc, name).into(), }, } } @@ -2486,9 +2477,9 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap = compositor.find::().unwrap().keymaps.map() + let keymap_command_lists = compositor.find::().unwrap().keymaps.load_keymaps() [&cx.editor.mode] - .reverse_map(); + .command_list(); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2499,7 +2490,7 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap, move |cx, command, _action| { + let picker = Picker::new(commands, keymap_command_lists, move |cx, command, _action| { let mut ctx = Context { register: None, count: std::num::NonZeroUsize::new(1), @@ -2528,6 +2519,7 @@ pub fn command_palette(cx: &mut Context) { compositor.push(Box::new(overlayed(picker))); }, )); + } fn last_picker(cx: &mut Context) { diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index f7cac6a36d10..471b8c168a79 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,4 +1,4 @@ -use crate::keymap::{default::default, merge_keys, Keymap}; +use crate::keymap::{default::default, Keymap, keymaps::Keymaps}; use helix_view::document::Mode; use serde::Deserialize; use std::collections::HashMap; @@ -47,7 +47,7 @@ impl Config { pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(merge_keys) + .map(Keymaps::merge_with_default) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 7ff4f83764f9..edfa41df2705 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,570 +1,83 @@ +pub mod keymaps; +pub mod keytrienode; pub mod default; pub mod macros; +pub mod keytrie; +pub mod tests; -pub use crate::commands::MappableCommand; -use crate::config::Config; -use arc_swap::{ - access::{DynAccess, DynGuard}, - ArcSwap, -}; -use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; -use std::{ - borrow::Cow, - collections::HashMap, - ops::{Deref, DerefMut}, - sync::Arc, -}; - -use default::default; -use macros::key; - -#[derive(Debug, Clone)] -pub struct KeyTrieNode { - /// A label for keys coming under this node, like "Goto mode" - name: String, - map: HashMap, - pub is_sticky: bool, -} - -impl<'de> Deserialize<'de> for KeyTrieNode { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - Ok(Self { - map: HashMap::::deserialize(deserializer)?, - ..Default::default() - }) - } -} - -impl KeyTrieNode { - pub fn new(name: &str, map: HashMap) -> Self { - Self { - name: name.to_string(), - map, - is_sticky: false, - } - } - - pub fn name(&self) -> &str { - &self.name - } - - /// Merge another Node in. Leaves and subnodes from the other node replace - /// corresponding keyevent in self, except when both other and self have - /// subnodes for same key. In that case the merge is recursive. - pub fn merge(&mut self, mut other: Self) { - for (key, trie) in std::mem::take(&mut other.map) { - if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) { - if let KeyTrie::Node(other_node) = trie { - node.merge(other_node); - continue; - } - } - self.map.insert(key, trie); - } - } - - pub fn infobox(&self) -> Info { - let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); - for (&key, trie) in self.iter() { - let description: &str = match trie { - KeyTrie::Leaf(command) => { - if command.name() == "no_op" { - continue; - } - command.doc() - } - KeyTrie::Node(key_trie) => key_trie.name(), - KeyTrie::Sequence(_) => "[Multiple commands]", - }; - match body.iter().position(|(_, existing_description)| &description == existing_description) { - Some(pos) => body[pos].0 += &format!(", {}", &key.to_string()), - None => body.push((key.to_string(), description)), - } - } - - Info::new(self.name(), &body) - } -} - -impl Default for KeyTrieNode { - fn default() -> Self { - Self::new("", HashMap::new()) - } -} - -impl PartialEq for KeyTrieNode { - fn eq(&self, other: &Self) -> bool { - self.map == other.map - } -} - -impl Deref for KeyTrieNode { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.map - } -} - -impl DerefMut for KeyTrieNode { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum KeyTrie { - Leaf(MappableCommand), - Sequence(Vec), - Node(KeyTrieNode), -} - -impl<'de> Deserialize<'de> for KeyTrie { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(KeyTrieVisitor) - } -} - -struct KeyTrieVisitor; - -impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor { - type Value = KeyTrie; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a command, list of commands, or sub-keymap") - } - - fn visit_str(self, command: &str) -> Result - where - E: serde::de::Error, - { - command - .parse::() - .map(KeyTrie::Leaf) - .map_err(E::custom) - } - - fn visit_seq(self, mut seq: S) -> Result - where - S: serde::de::SeqAccess<'de>, - { - let mut commands = Vec::new(); - while let Some(command) = seq.next_element::<&str>()? { - commands.push( - command - .parse::() - .map_err(serde::de::Error::custom)?, - ) - } - Ok(KeyTrie::Sequence(commands)) - } - - fn visit_map(self, mut map: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - let mut mapping = HashMap::new(); - while let Some((key, value)) = map.next_entry::()? { - mapping.insert(key, value); - } - Ok(KeyTrie::Node(KeyTrieNode::new("", mapping))) - } -} - -impl KeyTrie { - pub fn node(&self) -> Option<&KeyTrieNode> { - match *self { - KeyTrie::Node(ref node) => Some(node), - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - } - } - - pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { - match *self { - KeyTrie::Node(ref mut node) => Some(node), - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - } - } - - /// Merge another KeyTrie in, assuming that this KeyTrie and the other - /// are both Nodes. Panics otherwise. - pub fn merge_nodes(&mut self, mut other: Self) { - let node = std::mem::take(other.node_mut().unwrap()); - self.node_mut().unwrap().merge(node); - } - - pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> { - let mut trie = self; - for key in keys { - trie = match trie { - KeyTrie::Node(map) => map.get(key), - // leaf encountered while keys left to process - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - }? - } - Some(trie) +use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use crate::{ + commands::MappableCommand, + keymap::{ + keytrie::KeyTrie, + keytrienode::KeyTrieNode } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum KeymapResult { - /// Needs more keys to execute a command. Contains valid keys for next keystroke. - Pending(KeyTrieNode), - Matched(MappableCommand), - /// Matched a sequence of commands to execute. - MatchedSequence(Vec), - /// Key was not found in the root keymap - NotFound, - /// Key is invalid in combination with previous keys. Contains keys leading upto - /// and including current (invalid) key. - Cancelled(Vec), -} +}; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] +/// KeyTrie starting point. pub struct Keymap { - /// Always a Node - root: KeyTrie, + pub root_node: KeyTrie } -/// A map of command names to keybinds that will execute the command. -pub type ReverseKeymap = HashMap>>; - +pub type CommandList = HashMap>; impl Keymap { - pub fn new(root: KeyTrie) -> Self { - Keymap { root } - } - - pub fn reverse_map(&self) -> ReverseKeymap { - // recursively visit all nodes in keymap - fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec) { + pub fn new(root_node: KeyTrie) -> Self { + Keymap { root_node } + } + + /// Returns a key-value list of all commands associated to a given Keymap. + /// Keys are the node names (see KeyTrieNode documentation) + /// Values are lists of stringified KeyEvents that triger the command. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". + /// Ancestor KeyEvents are in this case "space" and "w". + pub fn command_list(&self) -> CommandList { + let mut list = HashMap::new(); + _command_list(&mut list, &KeyTrieNode::KeyTrie(self.root_node.clone()), &mut String::new()); + return list; + + fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { match node { - KeyTrie::Leaf(cmd) => match cmd { - MappableCommand::Typable { name, .. } => { - cmd_map.entry(name.into()).or_default().push(keys.clone()) + KeyTrieNode::KeyTrie(trie_node) => { + for (key_event, subtrie_node) in trie_node.deref() { + let mut temp_prefix: String = prefix.to_string(); + if &temp_prefix != "" { + temp_prefix.push_str(">"); + } + temp_prefix.push_str(&key_event.to_string()); + _command_list(list, subtrie_node, &mut temp_prefix); } - MappableCommand::Static { name, .. } => cmd_map - .entry(name.to_string()) - .or_default() - .push(keys.clone()), }, - KeyTrie::Node(next) => { - for (key, trie) in &next.map { - keys.push(*key); - map_node(cmd_map, trie, keys); - keys.pop(); - } - } - KeyTrie::Sequence(_) => {} + KeyTrieNode::MappableCommand(mappable_command) => { + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + }, + KeyTrieNode::CommandSequence(_) => {} }; } - - let mut res = HashMap::new(); - map_node(&mut res, &self.root, &mut Vec::new()); - res - } - - pub fn root(&self) -> &KeyTrie { - &self.root - } - - pub fn merge(&mut self, other: Self) { - self.root.merge_nodes(other.root); - } -} - -impl Deref for Keymap { - type Target = KeyTrieNode; - - fn deref(&self) -> &Self::Target { - self.root.node().unwrap() } } impl Default for Keymap { fn default() -> Self { - Self::new(KeyTrie::Node(KeyTrieNode::default())) + Self::new(KeyTrie::default()) } } -pub struct Keymaps { - pub map: Box>>, - /// Stores pending keys waiting for the next key. This is relative to a - /// sticky node if one is in use. - state: Vec, - /// Stores the sticky node if one is activated. - pub sticky: Option, -} - -impl Keymaps { - pub fn new(map: Box>>) -> Self { - Self { - map, - state: Vec::new(), - sticky: None, - } - } - - pub fn map(&self) -> DynGuard> { - self.map.load() - } - - /// Returns list of keys waiting to be disambiguated in current mode. - pub fn pending(&self) -> &[KeyEvent] { - &self.state - } - - pub fn sticky(&self) -> Option<&KeyTrieNode> { - self.sticky.as_ref() - } - - /// Lookup `key` in the keymap to try and find a command to execute. Escape - /// key cancels pending keystrokes. If there are no pending keystrokes but a - /// sticky node is in use, it will be cleared. - pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { - // TODO: remove the sticky part and look up manually - let keymaps = &*self.map(); - let keymap = &keymaps[&mode]; - - if key!(Esc) == key { - if !self.state.is_empty() { - // Note that Esc is not included here - return KeymapResult::Cancelled(self.state.drain(..).collect()); - } - self.sticky = None; - } - - let first = self.state.get(0).unwrap_or(&key); - let trie_node = match self.sticky { - Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())), - None => Cow::Borrowed(&keymap.root), - }; - - let trie = match trie_node.search(&[*first]) { - Some(KeyTrie::Leaf(ref cmd)) => { - return KeymapResult::Matched(cmd.clone()); - } - Some(KeyTrie::Sequence(ref cmds)) => { - return KeymapResult::MatchedSequence(cmds.clone()); - } - None => return KeymapResult::NotFound, - Some(t) => t, - }; - - self.state.push(key); - match trie.search(&self.state[1..]) { - Some(KeyTrie::Node(map)) => { - if map.is_sticky { - self.state.clear(); - self.sticky = Some(map.clone()); - } - KeymapResult::Pending(map.clone()) - } - Some(KeyTrie::Leaf(cmd)) => { - self.state.clear(); - KeymapResult::Matched(cmd.clone()) - } - Some(KeyTrie::Sequence(cmds)) => { - self.state.clear(); - KeymapResult::MatchedSequence(cmds.clone()) - } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), - } - } -} - -impl Default for Keymaps { - fn default() -> Self { - Self::new(Box::new(ArcSwap::new(Arc::new(default())))) - } -} +/// Returns the Keymap root KeyTrie node. +impl Deref for Keymap { + type Target = KeyTrie; -/// 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 { - keys.merge(delta.remove(mode).unwrap_or_default()) + fn deref(&self) -> &Self::Target { + &self.root_node } - config } -#[cfg(test)] -mod tests { - use super::macros::keymap; - use super::*; - use arc_swap::access::Constant; - use helix_core::hashmap; - - #[test] - #[should_panic] - fn duplicate_keys_should_panic() { - keymap!({ "Normal mode" - "i" => normal_mode, - "i" => goto_definition, - }); - } - - #[test] - fn check_duplicate_keys_in_default_keymap() { - // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro - Keymaps::default(); - } - - #[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 mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); - assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "Leaf should replace leaf" - ); - assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New leaf should be present in merged keymap" - ); - // Assumes that z is a node in the default keymap - assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "Leaf should replace node" - ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_line_end), - "Leaf should be present in merged subnode" - ); - // Assumes that `gg` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::delete_char_forward), - "Leaf should replace old leaf in merged subnode" - ); - // Assumes that `ge` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_last_line), - "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); - } - - #[test] - fn order_should_be_set() { - let config = Config { - keys: 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(); - // Make sure mapping works - assert_eq!( - keymap - .root() - .search(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - &KeyTrie::Leaf(MappableCommand::vsplit), - "Leaf should be present in merged subnode" - ); - } - - #[test] - fn aliased_modes_are_same_in_default_keymap() { - let keymaps = Keymaps::default().map(); - let root = keymaps.get(&Mode::Normal).unwrap().root(); - assert_eq!( - root.search(&[key!(' '), key!('w')]).unwrap(), - root.search(&["C-w".parse::().unwrap()]).unwrap(), - "Mismatch for window mode on `Space-w` and `Ctrl-w`" - ); - assert_eq!( - root.search(&[key!('z')]).unwrap(), - root.search(&[key!('Z')]).unwrap(), - "Mismatch for view mode on `z` and `Z`" - ); - } - - #[test] - fn reverse_map() { - let normal_mode = keymap!({ "Normal mode" - "i" => insert_mode, - "g" => { "Goto" - "g" => goto_file_start, - "e" => goto_file_end, - }, - "j" | "k" => move_line_down, - }); - let keymap = Keymap::new(normal_mode); - let mut reverse_map = keymap.reverse_map(); - - // sort keybindings in order to have consistent tests - // HashMaps can be compared but we can still get different ordering of bindings - // for commands that have multiple bindings assigned - for v in reverse_map.values_mut() { - v.sort() - } - - assert_eq!( - reverse_map, - HashMap::from([ - ("insert_mode".to_string(), vec![vec![key!('i')]]), - ( - "goto_file_start".to_string(), - vec![vec![key!('g'), key!('g')]] - ), - ( - "goto_file_end".to_string(), - vec![vec![key!('g'), key!('e')]] - ), - ( - "move_line_down".to_string(), - vec![vec![key!('j')], vec![key!('k')]] - ), - ]), - "Mismatch" - ) +/// Returns the Keymap root KeyTrie node. +impl DerefMut for Keymap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.root_node } } diff --git a/helix-term/src/keymap.rs:41:29 b/helix-term/src/keymap.rs:41:29 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index ef93dee08a77..7ce2736021b4 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,11 +1,13 @@ use std::collections::HashMap; use super::macros::keymap; -use super::{Keymap, Mode}; +use super::Keymap; +use super::keytrie::KeyTrie; +use helix_view::document::Mode; use helix_core::hashmap; pub fn default() -> HashMap { - let normal = keymap!({ "Normal mode" + let normal: KeyTrie = keymap!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, "k" | "up" => move_line_up, @@ -317,8 +319,8 @@ pub fn default() -> HashMap { "C-a" => increment, "C-x" => decrement, }); - let mut select = normal.clone(); - select.merge_nodes(keymap!({ "Select mode" + let mut select: KeyTrie = normal.clone(); + select.merge_keytrie(keymap!({ "Select mode" "h" | "left" => extend_char_left, "j" | "down" => extend_line_down, "k" | "up" => extend_line_up, @@ -345,7 +347,7 @@ pub fn default() -> HashMap { "v" => normal_mode, })); - let insert = keymap!({ "Insert mode" + let insert: KeyTrie = keymap!({ "Insert mode" "esc" => normal_mode, "C-s" => commit_undo_checkpoint, diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs new file mode 100644 index 000000000000..fe03af6d48c6 --- /dev/null +++ b/helix-term/src/keymap/keymaps.rs @@ -0,0 +1,121 @@ +use std::{sync::Arc, collections::HashMap}; +use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use helix_view::{document::Mode, input::KeyEvent}; +use crate::commands::MappableCommand; +use crate::config::Config; +use super::{macros::key, keytrienode::KeyTrieNode, default}; +use super::keytrie::KeyTrie; +use super::Keymap; +use super::default::default; + +#[derive(Debug, Clone, PartialEq)] +pub enum KeymapResult { + Pending(KeyTrie), + Matched(MappableCommand), + MatchedCommandSequence(Vec), + NotFound, + /// Contains pressed KeyEvents leading up to the cancellation. + Cancelled(Vec), +} + +pub struct Keymaps { + pub keymaps: Box>>, + /// Relative to a sticky node if Some. + pending_keys: Vec, + pub sticky_keytrie: Option, +} + +impl Keymaps { + pub fn new(keymaps: Box>>) -> Self { + Self { + keymaps, + pending_keys: Vec::new(), + sticky_keytrie: None, + } + } + + pub fn load_keymaps(&self) -> DynGuard> { + self.keymaps.load() + } + + /// Returns list of keys waiting to be disambiguated in current mode. + pub fn pending(&self) -> &[KeyEvent] { + &self.pending_keys + } + + pub fn sticky_keytrie(&self) -> Option<&KeyTrie> { + self.sticky_keytrie.as_ref() + } + + pub fn merge_with_default(mut config: Config) -> Config { + let mut delta = std::mem::replace(&mut config.keys, default::default()); + for (mode, keys) in &mut config.keys { + keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) + } + config + } + + /// Lookup `key` in the keymap to try and find a command to execute. + /// Escape key represents cancellation. + /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. + pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + // TODO: remove the sticky part and look up manually + let keymaps = &*self.load_keymaps(); + let active_keymap = &keymaps[&mode]; + + if key == key!(Esc) { + if !self.pending_keys.is_empty() { + // NOTE: Esc is not included here + return KeymapResult::Cancelled(self.pending_keys.drain(..).collect()); + } + // TODO: Shouldn't we return here also? + self.sticky_keytrie = None; + } + + // Check if sticky keytrie is to be used. + let starting_keytrie = match self.sticky_keytrie { + None => &active_keymap.root_node, + Some(ref active_sticky_keytrie) => active_sticky_keytrie, + }; + + // TODO: why check either pending or regular key? + let first_key = self.pending_keys.get(0).unwrap_or(&key); + + let pending_keytrie: KeyTrie = match starting_keytrie.traverse(&[*first_key]) { + Some(KeyTrieNode::KeyTrie(sub_keytrie)) => sub_keytrie, + Some(KeyTrieNode::MappableCommand(cmd)) => { + return KeymapResult::Matched(cmd.clone()); + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + return KeymapResult::MatchedCommandSequence(cmds.clone()); + } + None => return KeymapResult::NotFound, + }; + + self.pending_keys.push(key); + match pending_keytrie.traverse(&self.pending_keys[1..]) { + Some(KeyTrieNode::KeyTrie(map)) => { + if map.is_sticky { + self.pending_keys.clear(); + self.sticky_keytrie = Some(map.clone()); + } + KeymapResult::Pending(map.clone()) + } + Some(KeyTrieNode::MappableCommand(cmd)) => { + self.pending_keys.clear(); + KeymapResult::Matched(cmd.clone()) + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + self.pending_keys.clear(); + KeymapResult::MatchedCommandSequence(cmds.clone()) + } + None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), + } + } +} + +impl Default for Keymaps { + fn default() -> Self { + Self::new(Box::new(ArcSwap::new(Arc::new(default())))) + } +} diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs new file mode 100644 index 000000000000..ae5ecb58f12d --- /dev/null +++ b/helix-term/src/keymap/keytrie.rs @@ -0,0 +1,129 @@ +use serde::Deserialize; +use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use helix_view::{info::Info, input::KeyEvent}; +use super::keytrienode::KeyTrieNode; + +/// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode +#[derive(Debug, Clone)] +pub struct KeyTrie { + documentation: String, + children: HashMap, + pub is_sticky: bool, +} + +impl KeyTrie { + pub fn new(documentation: &str, children: HashMap) -> Self { + Self { + documentation: documentation.to_string(), + children, + is_sticky: false, + } + } + + // None symbolizes NotFound + pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { + return _traverse(self, key_events, 0); + + fn _traverse(keytrie: &KeyTrie, key_events: &[KeyEvent], mut depth: usize) -> Option { + if depth == key_events.len() { + return Some(KeyTrieNode::KeyTrie(keytrie.clone())); + } + else if let Some(found_child) = keytrie.get(&key_events[depth]) { + match found_child { + KeyTrieNode::KeyTrie(sub_keytrie) => { + depth += 1; + return _traverse(sub_keytrie, key_events, depth) + }, + _ => return Some(found_child.clone()) + } + } + return None; + } + } + + pub fn merge_keytrie(&mut self, mut other_keytrie: Self) { + for (other_key_event, other_child_node) in std::mem::take(&mut other_keytrie.children) { + match other_child_node { + KeyTrieNode::KeyTrie(other_child_key_trie) => { + if let Some(KeyTrieNode::KeyTrie(self_clashing_child_key_trie)) = self.children.get_mut(&other_key_event) { + self_clashing_child_key_trie.merge_keytrie(other_child_key_trie); + } + else { + self.children.insert(other_key_event, KeyTrieNode::KeyTrie(other_child_key_trie)); + } + } + KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { + self.children.insert(other_key_event, other_child_node); + } + } + } + } + + /// Open an Info box for a given KeyTrie + /// Shows the children listed by possible KeyEvents + /// and thier associated documentation. + pub fn infobox(&self) -> Info { + let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); + for (&key_event, key_trie) in self.iter() { + let documentation: &str = match key_trie { + KeyTrieNode::MappableCommand(command) => { + if command.name() == "no_op" { + continue; + } + command.doc() + }, + KeyTrieNode::KeyTrie(key_trie) => &key_trie.documentation, + // FIX: default to a join of all command names + // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. + // Regardless if the command sequence is different. + KeyTrieNode::CommandSequence(_) => "[Multiple commands]", + }; + match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { + Some(position) => body[position].0 += &format!(", {}", &key_event.to_string()), + None => body.push((key_event.to_string(), documentation)), + } + } + + Info::new(&self.documentation, &body) + } +} + +impl Default for KeyTrie { + fn default() -> Self { + Self::new("", HashMap::new()) + } +} + +impl PartialEq for KeyTrie { + fn eq(&self, other: &Self) -> bool { + self.children == other.children + } +} + +/// Returns the children of the KeyTrie +impl Deref for KeyTrie { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.children + } +} + +/// Returns the children of the KeyTrie +impl DerefMut for KeyTrie { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.children + } +} + +impl<'de> Deserialize<'de> for KeyTrie { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self { + children: HashMap::::deserialize(deserializer)?, + ..Default::default() + }) + } +} diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs new file mode 100644 index 000000000000..a8a0ebba384d --- /dev/null +++ b/helix-term/src/keymap/keytrienode.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; +use super::MappableCommand; +use serde::Deserialize; +use serde::de::Visitor; +use super::keytrie::KeyTrie; +use helix_view::input::KeyEvent; + +/// Each variant includes a documentaion property. +/// For the MappableCommand and CommandSequence variants, the property is self explanatory. +/// For KeyTrie, the documentation is used for respective infobox titles, +/// or infobox KeyEvent descriptions that in themselves trigger the opening of another infobox. +#[derive(Debug, Clone, PartialEq)] +pub enum KeyTrieNode { + MappableCommand(MappableCommand), + CommandSequence(Vec), + KeyTrie(KeyTrie), +} + +impl<'de> Deserialize<'de> for KeyTrieNode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(KeyTrieNodeVisitor) + } +} + +struct KeyTrieNodeVisitor; + +impl<'de> Visitor<'de> for KeyTrieNodeVisitor { + type Value = KeyTrieNode; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a KeyTrieNode") + } + + fn visit_str(self, command: &str) -> Result + where + E: serde::de::Error, + { + command + .parse::() + .map(KeyTrieNode::MappableCommand) + .map_err(E::custom) + } + + fn visit_seq(self, mut seq: S) -> Result + where + S: serde::de::SeqAccess<'de>, + { + let mut commands = Vec::new(); + while let Some(command) = seq.next_element::<&str>()? { + commands.push( + command + .parse::() + .map_err(serde::de::Error::custom)?, + ) + } + Ok(KeyTrieNode::CommandSequence(commands)) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut sub_key_trie = HashMap::new(); + while let Some((key_event, key_trie_node)) = map.next_entry::()? { + sub_key_trie.insert(key_event, key_trie_node); + } + Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", sub_key_trie))) + } +} \ No newline at end of file diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 1cf4151c9a38..4d81d220ffb9 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -80,42 +80,36 @@ macro_rules! alt { /// ``` #[macro_export] macro_rules! keymap { - (@trie $cmd:ident) => { - $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd) - }; - - (@trie - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } - ) => { - keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) - }; - - (@trie [$($cmd:ident),* $(,)?]) => { - $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*]) - }; - - ( - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } - ) => { + ({ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { // modified from the hashmap! macro { let _cap = hashmap!(@count $($($key),+),*); - let mut _map = ::std::collections::HashMap::with_capacity(_cap); + let mut _map: ::std::collections::HashMap<::helix_view::input::KeyEvent, $crate::keymap::keytrienode::KeyTrieNode> = + ::std::collections::HashMap::with_capacity(_cap); $( $( let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); - let _duplicate = _map.insert( - _key, - keymap!(@trie $value) - ); - assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap()); + let _potential_duplicate = _map.insert(_key,keymap!(@trie $value)); + assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); )+ )* - let mut _node = $crate::keymap::KeyTrieNode::new($label, _map); + let mut _node = $crate::keymap::keytrie::KeyTrie::new($label, _map); $( _node.is_sticky = $sticky; )? - $crate::keymap::KeyTrie::Node(_node) + _node } }; + + (@trie {$label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { + $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) + }; + + (@trie $cmd:ident) => { + $crate::keymap::keytrienode::KeyTrieNode::MappableCommand($crate::commands::MappableCommand::$cmd) + }; + + (@trie [$($cmd:ident),* $(,)?]) => { + $crate::keymap::keytrienode::KeyTrieNode::CommandSequence(vec![$($crate::commands::Command::$cmd),*]) + }; } pub use alt; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs new file mode 100644 index 000000000000..55420d1f5f14 --- /dev/null +++ b/helix-term/src/keymap/tests.rs @@ -0,0 +1,181 @@ +#[macro_use] +#[cfg(test)] +mod tests { + use arc_swap::access::Constant; + use helix_core::hashmap; + use helix_view::{document::Mode, input::KeyEvent}; + use crate::config::Config; + use crate::commands::MappableCommand; + use crate::keymap::*; + use keymaps::{Keymaps, KeymapResult}; + use macros::*; + + #[test] + #[should_panic] + fn duplicate_keys_should_panic() { + keymap!({ "Normal mode" + "i" => normal_mode, + "i" => goto_definition, + }); + } + + #[test] + fn check_duplicate_keys_in_default_keymap() { + // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro + Keymaps::default(); + } + + #[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 mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + + let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + assert_eq!( + keymap.get(Mode::Normal, key!('i')), + KeymapResult::Matched(MappableCommand::normal_mode), + "New mappable command should ovveride default." + ); + assert_eq!( + keymap.get(Mode::Normal, key!('无')), + KeymapResult::Matched(MappableCommand::insert_mode), + "New mappable command should be present in merged keymap." + ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(Mode::Normal, key!('z')), + KeymapResult::Matched(MappableCommand::jump_backward), + "New Mappable command should replace default sub keytrie." + ); + + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), + "Mappable command should be present in merged keytrie." + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), + "Mappable command should replace default in merged keytrie." + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), + "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." + ); + + assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_config.keys.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, + }, + }, + }) + ) + }, + ..Default::default() + }; + let mut merged_config = merge_keys(config.clone()); + assert_ne!(config, merged_config); + let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Make sure mapping works + assert_eq!( + keymap_normal + .root_node + .traverse(&[key!(' '), key!('s'), key!('v')]) + .unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::vsplit), + "Mappable command should be present in merged keytrie." + ); + } + + #[test] + fn aliased_modes_are_same_in_default_keymap() { + let keymaps = Keymaps::default().keymaps; + let root = keymaps.load().get(&Mode::Normal).unwrap().root_node.clone(); + assert_eq!( + root.traverse(&[key!(' '), key!('w')]).unwrap(), + root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + "Mismatch for window mode on `Space-w` and `Ctrl-w`." + ); + assert_eq!( + root.traverse(&[key!('z')]).unwrap(), + root.traverse(&[key!('Z')]).unwrap(), + "Mismatch for view mode on `z` and `Z`." + ); + } + + #[test] + fn command_list() { + let normal_mode = keymap!({ "Normal mode" + "i" => insert_mode, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_file_end, + }, + "j" | "k" => move_line_down, + }); + let keymap = Keymap::new(normal_mode); + let mut command_list = keymap.command_list(); + + // sort keybindings in order to have consistent tests + // HashMaps can be compared but we can still get different ordering of bindings + // for commands that have multiple bindings assigned + for v in command_list.values_mut() { + v.sort() + } + + assert_eq!( + command_list, + HashMap::from([ + ( + "insert_mode".to_string(), + vec![key!('i').to_string()] + ), + ( + "goto_file_start".to_string(), + vec![format!("{}>{}", key!('g'), key!('g'))] + ), + ( + "goto_file_end".to_string(), + vec![format!("{}>{}", key!('g'), key!('e'))] + ), + ( + "move_line_down".to_string(), + vec![key!('j').to_string(), key!('k').to_string()] + ) + ]), + "Mismatch" + ) + } +} \ No newline at end of file diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index a945b20dedaf..58ae12c2e120 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -11,6 +11,7 @@ pub mod job; pub mod keymap; pub mod ui; pub use keymap::macros::*; +pub use keymap::tests; #[cfg(not(windows))] fn true_color() -> bool { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a0518964e2a7..bd5d99fbcbaa 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::{KeymapResult, Keymaps}, + keymap::keymaps::{KeymapResult, Keymaps}, ui::{Completion, ProgressSpinners}, }; @@ -936,7 +936,7 @@ impl EditorView { let mut last_mode = mode; self.pseudo_pending.extend(self.keymaps.pending()); let key_result = self.keymaps.get(mode, event); - cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); + cxt.editor.autoinfo = self.keymaps.sticky_keytrie().map(|node| node.infobox()); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); @@ -976,7 +976,7 @@ impl EditorView { execute_command(command); } KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), - KeymapResult::MatchedSequence(commands) => { + KeymapResult::MatchedCommandSequence(commands) => { for command in commands { execute_command(command); } From d74be02498f06e3d70cd73eeb337fb59653126fc Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 26 Dec 2022 19:08:52 +0100 Subject: [PATCH 100/195] Rename MappableCommand field doc to description: For it not to be confused with the upcoming ":help" feature. --- helix-term/src/commands.rs | 24 ++++++++++++------------ helix-term/src/keymap/keytrie.rs | 5 ++--- helix-term/src/main.rs | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ef166737a38e..7e2262b3702c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -133,23 +133,23 @@ pub enum MappableCommand { Typable { name: String, args: Vec, - doc: String, + description: String, }, Static { name: &'static str, fun: fn(cx: &mut Context), - doc: &'static str, + description: &'static str, }, } macro_rules! static_commands { - ( $($name:ident, $doc:literal,)* ) => { + ( $($name:ident, $description:literal,)* ) => { $( #[allow(non_upper_case_globals)] pub const $name: Self = Self::Static { name: stringify!($name), fun: $name, - doc: $doc + description: $description }; )* @@ -162,7 +162,7 @@ macro_rules! static_commands { impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { - Self::Typable { name, args, doc: _ } => { + Self::Typable { name, args, description: _ } => { let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { @@ -186,10 +186,10 @@ impl MappableCommand { } } - pub fn doc(&self) -> &str { + pub fn description(&self) -> &str { match &self { - Self::Typable { doc, .. } => doc, - Self::Static { doc, .. } => doc, + Self::Typable { description, .. } => description, + Self::Static { description, .. } => description, } } @@ -476,7 +476,7 @@ impl std::str::FromStr for MappableCommand { .get(name) .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), - doc: format!(":{} {:?}", cmd.name, args), + description: format!(":{} {:?}", cmd.name, args), args, }) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) @@ -2462,11 +2462,11 @@ impl ui::menu::Item for MappableCommand { fn label(&self, keymap: &Self::Data) -> Spans { match self { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { + MappableCommand::Typable { description: doc, name, .. } => match keymap.get(name as &String) { Some(key_events) => format!("{} {:?} ':{}'", doc, key_events, name).into(), None => format!("{} ':{}'", doc, name).into(), }, - MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { + MappableCommand::Static { description: doc, name, .. } => match keymap.get(*name) { Some(key_events) => format!("{} {:?} '{}'", doc, key_events, name).into(), None => format!("{} '{}'", doc, name).into(), }, @@ -2485,7 +2485,7 @@ pub fn command_palette(cx: &mut Context) { commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { MappableCommand::Typable { name: cmd.name.to_owned(), - doc: cmd.doc.to_owned(), + description: cmd.doc.to_owned(), args: Vec::new(), } })); diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index ae5ecb58f12d..25ab0b196afe 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -60,8 +60,7 @@ impl KeyTrie { } /// Open an Info box for a given KeyTrie - /// Shows the children listed by possible KeyEvents - /// and thier associated documentation. + /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self) -> Info { let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); for (&key_event, key_trie) in self.iter() { @@ -70,7 +69,7 @@ impl KeyTrie { if command.name() == "no_op" { continue; } - command.doc() + command.description() }, KeyTrieNode::KeyTrie(key_trie) => &key_trie.documentation, // FIX: default to a join of all command names diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c5379f37..bae827e30402 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -128,7 +128,7 @@ FLAGS: let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::merge_keys) + .map(helix_term::keymap::keymaps::Keymaps::merge_with_default) .unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); From 7192a4eddcc076266d09a1b7b58c39f93d2ef834 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 26 Dec 2022 22:44:50 +0100 Subject: [PATCH 101/195] Initial sorting of keymap info window --- helix-term/src/keymap/keytrie.rs | 39 ++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 25ab0b196afe..8020b6caafbf 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -62,7 +62,7 @@ impl KeyTrie { /// Open an Info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self) -> Info { - let mut body: Vec<(String, &str)> = Vec::with_capacity(self.len()); + let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); for (&key_event, key_trie) in self.iter() { let documentation: &str = match key_trie { KeyTrieNode::MappableCommand(command) => { @@ -78,12 +78,43 @@ impl KeyTrie { KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { - Some(position) => body[position].0 += &format!(", {}", &key_event.to_string()), - None => body.push((key_event.to_string(), documentation)), + Some(position) => body[position].0.push(key_event.to_string()), + None => { + let mut temp_vec: Vec = Vec::new(); + temp_vec.push(key_event.to_string()); + body.push((temp_vec, documentation)) + }, } } + // OPTIMIZATIONS? + // Change the children hashmap to an ordered datastructure? Like a regulap map maybe? + // Add sorted conditional? + // Add body as a keytrie field and reload it in keytrie merge? + + // Make the shortest keyevents appear first + let mut sorted_body = body + .iter() + .map(|(key_events, description)| { + let mut temp_key_events = key_events.clone(); + temp_key_events.sort_unstable_by(|a, b| a.len().cmp(&b.len())); + (temp_key_events, *description) + }) + .collect::, &str)>>(); + sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + + let stringified_key_events_body: Vec<(String, &str)> = sorted_body + .iter() + .map(|(key_events, description)| { + let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { + if acc.is_empty() { acc.push_str(key_event); } + else { acc.push_str(&format!(", {}", key_event)) } + acc + }); + (key_events_string, *description) + }) + .collect(); - Info::new(&self.documentation, &body) + Info::new(&self.documentation, &stringified_key_events_body) } } From 26adc32afd639b77aff56a810c7db08c611512c1 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 12:08:12 +0100 Subject: [PATCH 102/195] Infobox: Consistently place lowercase equivalents first This, along with previous commit reverts regression in #952 --- helix-term/src/keymap/keytrie.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 8020b6caafbf..e49b33d501b1 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -91,7 +91,7 @@ impl KeyTrie { // Add sorted conditional? // Add body as a keytrie field and reload it in keytrie merge? - // Make the shortest keyevents appear first + // Make the shortest keyevent appear first let mut sorted_body = body .iter() .map(|(key_events, description)| { @@ -102,6 +102,27 @@ impl KeyTrie { .collect::, &str)>>(); sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + // Consistently place lowercase before uppercase of the same letter. + if sorted_body.len() > 1 { + let mut x_index = 0; + let mut y_index = 1; + + while y_index < sorted_body.len() { + let x = &sorted_body[x_index].0[0]; + let y = &sorted_body[y_index].0[0]; + if x.to_lowercase() == y.to_lowercase() { + // Uppercase regarded as lower value. + if x < y { + let temp_holder = sorted_body[x_index].clone(); + sorted_body[x_index] = sorted_body[y_index].clone(); + sorted_body[y_index] = temp_holder; + } + } + x_index = y_index; + y_index += 1; + } + } + let stringified_key_events_body: Vec<(String, &str)> = sorted_body .iter() .map(|(key_events, description)| { From 4fb58f72ea1a946a788d556d0fbb107103e21542 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 13:15:46 +0100 Subject: [PATCH 103/195] Remove infobox optimization suggestion comment: Sort duration calculations averaged to about 0.04 ms when opening the larger infoboxes. --- helix-term/src/keymap/keytrie.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index e49b33d501b1..fa115770ed83 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -86,10 +86,6 @@ impl KeyTrie { }, } } - // OPTIMIZATIONS? - // Change the children hashmap to an ordered datastructure? Like a regulap map maybe? - // Add sorted conditional? - // Add body as a keytrie field and reload it in keytrie merge? // Make the shortest keyevent appear first let mut sorted_body = body @@ -101,7 +97,6 @@ impl KeyTrie { }) .collect::, &str)>>(); sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); - // Consistently place lowercase before uppercase of the same letter. if sorted_body.len() > 1 { let mut x_index = 0; From d33ff8bce5eb00001d819483f52ef6ae4dee8fb4 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 13:50:05 +0100 Subject: [PATCH 104/195] keymap testing touchups --- helix-term/src/keymap/keytrie.rs | 3 ++- helix-term/src/keymap/tests.rs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index fa115770ed83..89c441e2974a 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -87,7 +87,8 @@ impl KeyTrie { } } - // Make the shortest keyevent appear first + // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent + // Those events will always be placed after the one letter KeyEvent let mut sorted_body = body .iter() .map(|(key_events, description)| { diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 55420d1f5f14..557debb94cb1 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -89,7 +89,7 @@ mod tests { } #[test] - fn order_should_be_set() { + fn merges_with_default_keymap_config() { let config = Config { keys: hashmap! { Mode::Normal => Keymap::new( @@ -105,10 +105,9 @@ mod tests { }, ..Default::default() }; - let mut merged_config = merge_keys(config.clone()); + let mut merged_config = Keymaps::merge_with_default(config.clone()); assert_ne!(config, merged_config); let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Make sure mapping works assert_eq!( keymap_normal .root_node From 958e8bc3e17f9f6f36dad7ec88d14081b49da9c7 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 14:33:32 +0100 Subject: [PATCH 105/195] Exclude config no_op bindings in command palette. --- helix-term/src/keymap.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index edfa41df2705..58433629f09b 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -52,7 +52,8 @@ impl Keymap { } }, KeyTrieNode::MappableCommand(mappable_command) => { - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + if mappable_command.name() == "no_op" { return } + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); }, KeyTrieNode::CommandSequence(_) => {} }; From d0f93ec7c5861dcb5835e13ad9b0fc2f5aa16ff3 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 27 Dec 2022 20:22:59 +0100 Subject: [PATCH 106/195] Cleaner infobox join operation --- helix-term/src/keymap/keytrie.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 89c441e2974a..227a0a5b7d9c 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -123,8 +123,8 @@ impl KeyTrie { .iter() .map(|(key_events, description)| { let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { - if acc.is_empty() { acc.push_str(key_event); } - else { acc.push_str(&format!(", {}", key_event)) } + if !acc.is_empty() { acc.push_str(", "); } + acc.push_str(key_event); acc }); (key_events_string, *description) From 876885e8bdefa100dc1c3b5d1de1135adc275a94 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 28 Dec 2022 17:48:59 +0100 Subject: [PATCH 107/195] use statement cleanups --- helix-term/src/application.rs | 15 ++++++--------- helix-term/src/commands.rs | 28 +++++++++++----------------- helix-term/src/config.rs | 7 ++----- helix-term/src/keymap.rs | 9 +++++---- helix-term/src/keymap.rs:41:29 | 0 helix-term/src/keymap/default.rs | 7 ++----- helix-term/src/keymap/keymaps.rs | 12 ++++-------- helix-term/src/keymap/keytrie.rs | 6 +++--- helix-term/src/keymap/keytrienode.rs | 8 +++----- helix-term/src/keymap/tests.rs | 8 ++------ 10 files changed, 38 insertions(+), 62 deletions(-) delete mode 100644 helix-term/src/keymap.rs:41:29 diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 42daaa64d44a..b614f5395c12 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,5 +1,3 @@ -use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -15,9 +13,6 @@ use helix_view::{ tree::Layout, Align, Editor, }; -use serde_json::json; -use tui::backend::Backend; - use crate::{ args::Args, commands::apply_workspace_edit, @@ -27,16 +22,17 @@ use crate::{ keymap::keymaps::Keymaps, ui::{self, overlay::overlayed}, }; - -use log::{debug, error, warn}; use std::{ io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; - +use arc_swap::{access::Map, ArcSwap}; +use futures_util::Stream; +use log::{debug, error, warn}; use anyhow::{Context, Error}; - +use serde_json::json; +use tui::backend::Backend; use crossterm::{ event::{ DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, @@ -45,6 +41,7 @@ use crossterm::{ execute, terminal, tty::IsTty, }; + #[cfg(not(windows))] use { signal_hook::{consts::signal, low_level}, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7e2262b3702c..e21356545d7e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,11 +3,11 @@ pub(crate) mod lsp; pub(crate) mod typed; pub use dap::*; -use helix_vcs::Hunk; pub use lsp::*; use tui::widgets::Row; pub use typed::*; +use helix_vcs::Hunk; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, @@ -15,7 +15,7 @@ use helix_core::{ indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, - movement::{self, Direction}, + movement::{self, Direction, Movement}, object, pos_at_coords, pos_at_visual_coords, regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, @@ -36,33 +36,27 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, }; - -use anyhow::{anyhow, bail, ensure, Context as _}; -use fuzzy_matcher::FuzzyMatcher; -use insert::*; -use movement::Movement; - use crate::{ + commands::insert::*, args, compositor::{self, Component, Compositor}, - job::Callback, + job::{Callback, self, Jobs}, keymap::CommandList, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; - -use crate::job::{self, Jobs}; -use futures_util::StreamExt; -use std::{collections::HashMap, fmt, future::Future}; -use std::{collections::HashSet, num::NonZeroUsize}; - use std::{ + collections::{HashMap, HashSet}, + num::NonZeroUsize, + future::Future, borrow::Cow, path::{Path, PathBuf}, + fmt, }; - +use anyhow::{anyhow, bail, ensure, Context as _}; +use fuzzy_matcher::FuzzyMatcher; +use futures_util::StreamExt; use once_cell::sync::Lazy; use serde::de::{self, Deserialize, Deserializer}; - use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 471b8c168a79..d924bff79c90 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,11 +1,8 @@ use crate::keymap::{default::default, Keymap, keymaps::Keymaps}; use helix_view::document::Mode; -use serde::Deserialize; -use std::collections::HashMap; -use std::fmt::Display; -use std::io::Error as IOError; -use std::path::PathBuf; +use std::{fmt::Display, collections::HashMap, io::Error as IOError}; use toml::de::Error as TomlError; +use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 58433629f09b..f5cbcec212a5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,12 +1,11 @@ pub mod keymaps; -pub mod keytrienode; pub mod default; pub mod macros; -pub mod keytrie; pub mod tests; -use serde::Deserialize; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; +mod keytrie; +mod keytrienode; + use crate::{ commands::MappableCommand, keymap::{ @@ -14,6 +13,8 @@ use crate::{ keytrienode::KeyTrieNode } }; +use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] diff --git a/helix-term/src/keymap.rs:41:29 b/helix-term/src/keymap.rs:41:29 deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 7ce2736021b4..b74ae84c7b63 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,10 +1,7 @@ -use std::collections::HashMap; - -use super::macros::keymap; -use super::Keymap; -use super::keytrie::KeyTrie; +use super::{macros::keymap, Keymap, keytrie::KeyTrie}; use helix_view::document::Mode; use helix_core::hashmap; +use std::collections::HashMap; pub fn default() -> HashMap { let normal: KeyTrie = keymap!({ "Normal mode" diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index fe03af6d48c6..ec9ecbce8eb7 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -1,12 +1,8 @@ +use super::*; +use crate::{commands::MappableCommand, config::Config}; +use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; -use helix_view::{document::Mode, input::KeyEvent}; -use crate::commands::MappableCommand; -use crate::config::Config; -use super::{macros::key, keytrienode::KeyTrieNode, default}; -use super::keytrie::KeyTrie; -use super::Keymap; -use super::default::default; #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { @@ -116,6 +112,6 @@ impl Keymaps { impl Default for Keymaps { fn default() -> Self { - Self::new(Box::new(ArcSwap::new(Arc::new(default())))) + Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) } } diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 227a0a5b7d9c..0706182db228 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,7 +1,7 @@ -use serde::Deserialize; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; -use helix_view::{info::Info, input::KeyEvent}; use super::keytrienode::KeyTrieNode; +use helix_view::{info::Info, input::KeyEvent}; +use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; +use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index a8a0ebba384d..41b5a7cd3343 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,9 +1,7 @@ -use std::collections::HashMap; -use super::MappableCommand; -use serde::Deserialize; -use serde::de::Visitor; -use super::keytrie::KeyTrie; +use super::{MappableCommand, keytrie::KeyTrie}; use helix_view::input::KeyEvent; +use std::collections::HashMap; +use serde::{Deserialize, de::Visitor}; /// Each variant includes a documentaion property. /// For the MappableCommand and CommandSequence variants, the property is self explanatory. diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 557debb94cb1..cc06e9992b30 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,14 +1,10 @@ #[macro_use] #[cfg(test)] mod tests { - use arc_swap::access::Constant; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::config::Config; - use crate::commands::MappableCommand; - use crate::keymap::*; - use keymaps::{Keymaps, KeymapResult}; - use macros::*; + use crate::{config::Config, commands::MappableCommand, keymap::*}; + use arc_swap::access::Constant; #[test] #[should_panic] From 315fa89b52cdeda99a09ea771b7042ad80b1db90 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 28 Dec 2022 18:44:26 +0100 Subject: [PATCH 108/195] Move Config related tests from keymap tests Config tests --- helix-term/src/config.rs | 127 +++++++++++++++++++++++++++++---- helix-term/src/keymap.rs | 5 +- helix-term/src/keymap/tests.rs | 97 +------------------------ 3 files changed, 118 insertions(+), 111 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index d924bff79c90..21fbb60ea016 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,8 +1,8 @@ -use crate::keymap::{default::default, Keymap, keymaps::Keymaps}; +use crate::keymap::{default::default, keymaps::Keymaps, Keymap}; use helix_view::document::Mode; -use std::{fmt::Display, collections::HashMap, io::Error as IOError}; -use toml::de::Error as TomlError; use serde::Deserialize; +use std::{collections::HashMap, fmt::Display, io::Error as IOError}; +use toml::de::Error as TomlError; #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] @@ -25,7 +25,7 @@ impl Default for Config { } #[derive(Debug)] -pub enum ConfigLoadError { +pub enum ConfigLoadError { BadConfig(TomlError), Error(IOError), } @@ -40,7 +40,7 @@ impl Display for ConfigLoadError { } impl Config { - // REFACTOR? code similar to config assignment in main.rs, + // REFACTOR? code similar to config assignment in main.rs, pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) @@ -53,15 +53,23 @@ impl Config { #[cfg(test)] mod tests { - use super::*; + use crate::{ + commands::MappableCommand, + config::Config, + keymap::{ + default, + keymaps::{KeymapResult, Keymaps}, + keytrienode::KeyTrieNode, + macros::*, + Keymap, + }, + }; + use arc_swap::access::Constant; + use helix_core::hashmap; + use helix_view::document::Mode; #[test] fn parsing_keymaps_config_file() { - use crate::keymap; - use crate::keymap::Keymap; - use helix_core::hashmap; - use helix_view::document::Mode; - let sample_keymaps = r#" [keys.insert] y = "move_line_down" @@ -92,10 +100,103 @@ mod tests { fn keys_resolve_to_correct_defaults() { // From serde default let default_keys = toml::from_str::("").unwrap().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, default::default()); // From the Default trait let default_keys = Config::default().keys; - assert_eq!(default_keys, default()); + assert_eq!(default_keys, default::default()); + } + + #[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 mut merged_config = Keymaps::merge_with_default(config.clone()); + assert_ne!(config, merged_config); + + let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + assert_eq!( + keymap.get(Mode::Normal, key!('i')), + KeymapResult::Matched(MappableCommand::normal_mode), + "New mappable command should ovveride default." + ); + assert_eq!( + keymap.get(Mode::Normal, key!('无')), + KeymapResult::Matched(MappableCommand::insert_mode), + "New mappable command should be present in merged keymap." + ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(Mode::Normal, key!('z')), + KeymapResult::Matched(MappableCommand::jump_backward), + "New Mappable command should replace default sub keytrie." + ); + + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), + "Mappable command should be present in merged keytrie." + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), + "Mappable command should replace default in merged keytrie." + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), + "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." + ); + + assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); + assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + } + + #[test] + fn merges_with_default_keymap_config() { + let config = Config { + keys: hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "space" => { "" + "s" => { "" + "v" => vsplit, + "c" => hsplit, + }, + }, + }) + ) + }, + ..Default::default() + }; + let mut merged_config = Keymaps::merge_with_default(config.clone()); + assert_ne!(config, merged_config); + let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); + assert_eq!( + keymap_normal + .root_node + .traverse(&[key!(' '), key!('s'), key!('v')]) + .unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::vsplit), + "Mappable command should be present in merged keytrie." + ); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f5cbcec212a5..601046600b5e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -2,9 +2,8 @@ pub mod keymaps; pub mod default; pub mod macros; pub mod tests; - -mod keytrie; -mod keytrienode; +pub mod keytrienode; +pub mod keytrie; use crate::{ commands::MappableCommand, diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index cc06e9992b30..157230d816d3 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,8 +3,8 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::{config::Config, commands::MappableCommand, keymap::*}; - use arc_swap::access::Constant; + use crate::keymap::*; + use std::collections::HashMap; #[test] #[should_panic] @@ -21,99 +21,6 @@ mod tests { Keymaps::default(); } - #[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 mut merged_config = merge_keys(config.clone()); - assert_ne!(config, merged_config); - - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); - assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "New mappable command should ovveride default." - ); - assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New mappable command should be present in merged keymap." - ); - // Assumes that z is a node in the default keymap - assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "New Mappable command should replace default sub keytrie." - ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap - assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), - "Mappable command should be present in merged keytrie." - ); - // Assumes that `gg` is in default keymap - assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), - "Mappable command should replace default in merged keytrie." - ); - // Assumes that `ge` is in default keymap - assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), - "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." - ); - - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); - } - - #[test] - fn merges_with_default_keymap_config() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, - }, - }) - ) - }, - ..Default::default() - }; - let mut merged_config = Keymaps::merge_with_default(config.clone()); - assert_ne!(config, merged_config); - let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - assert_eq!( - keymap_normal - .root_node - .traverse(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::vsplit), - "Mappable command should be present in merged keytrie." - ); - } - #[test] fn aliased_modes_are_same_in_default_keymap() { let keymaps = Keymaps::default().keymaps; From fcf8e6b6692d629da585c4e4c1f141ec7053f056 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 28 Dec 2022 19:35:34 +0100 Subject: [PATCH 109/195] Config test cleanup * Elaborated on test descriptions * Removed duplicate unit test * Unified keytrie traversal in tests --- helix-term/src/config.rs | 84 ++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 21fbb60ea016..40d98378da10 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -58,18 +58,17 @@ mod tests { config::Config, keymap::{ default, - keymaps::{KeymapResult, Keymaps}, + keymaps::Keymaps, keytrienode::KeyTrieNode, macros::*, Keymap, }, }; - use arc_swap::access::Constant; use helix_core::hashmap; use helix_view::document::Mode; #[test] - fn parsing_keymaps_config_file() { + fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] y = "move_line_down" @@ -108,8 +107,8 @@ mod tests { } #[test] - fn merge_partial_keys() { - let config = Config { + fn user_config_merges_with_default() { + let user_config = Config { keys: hashmap! { Mode::Normal => Keymap::new( keymap!({ "Normal mode" @@ -125,78 +124,51 @@ mod tests { }, ..Default::default() }; - let mut merged_config = Keymaps::merge_with_default(config.clone()); - assert_ne!(config, merged_config); + let mut merged_config = Keymaps::merge_with_default(user_config.clone()); + assert_ne!( + user_config, + merged_config, + "Merged user keymap with default should differ from user keymap." + ); - let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone()))); + let keymap_normal_root_key_trie = &merged_config.keys.get_mut(&Mode::Normal).unwrap().root_node; assert_eq!( - keymap.get(Mode::Normal, key!('i')), - KeymapResult::Matched(MappableCommand::normal_mode), - "New mappable command should ovveride default." + keymap_normal_root_key_trie.traverse(&[key!('i')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::normal_mode), + "User supplied mappable command should ovveride default mappable command bound to the same key event." ); assert_eq!( - keymap.get(Mode::Normal, key!('无')), - KeymapResult::Matched(MappableCommand::insert_mode), - "New mappable command should be present in merged keymap." + keymap_normal_root_key_trie.traverse(&[key!('无')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::insert_mode), + "User supplied mappable command of new key event should be present in merged keymap." ); // Assumes that z is a node in the default keymap assert_eq!( - keymap.get(Mode::Normal, key!('z')), - KeymapResult::Matched(MappableCommand::jump_backward), - "New Mappable command should replace default sub keytrie." + keymap_normal_root_key_trie.traverse(&[key!('z')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::jump_backward), + "User supplied mappable command should replace a sub keytrie from default keymap bound to the same key event." ); - - let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - // Assumes that `g` is a node in default keymap + // Assumes that `g` is a sub key trie in default keymap assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('$')]).unwrap(), + keymap_normal_root_key_trie.traverse(&[key!('g'), key!('$')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), - "Mappable command should be present in merged keytrie." + "User supplied mappable command should be inserted under the correct sub keytrie." ); // Assumes that `gg` is in default keymap assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('g')]).unwrap(), + keymap_normal_root_key_trie.traverse(&[key!('g'), key!('g')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), - "Mappable command should replace default in merged keytrie." + "User supplied mappable command should replace default even in sub keytries." ); // Assumes that `ge` is in default keymap assert_eq!( - keymap.root_node.traverse(&[key!('g'), key!('e')]).unwrap(), + keymap_normal_root_key_trie.traverse(&[key!('g'), key!('e')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), - "Mappable commands from default keytrie should still be present in merged merged keytrie unless overridden." + "Default mappable commands that aren't ovveridden should exist in merged keymap." ); + // Huh? assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); } - - #[test] - fn merges_with_default_keymap_config() { - let config = Config { - keys: hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "space" => { "" - "s" => { "" - "v" => vsplit, - "c" => hsplit, - }, - }, - }) - ) - }, - ..Default::default() - }; - let mut merged_config = Keymaps::merge_with_default(config.clone()); - assert_ne!(config, merged_config); - let keymap_normal = merged_config.keys.get_mut(&Mode::Normal).unwrap(); - assert_eq!( - keymap_normal - .root_node - .traverse(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - KeyTrieNode::MappableCommand(MappableCommand::vsplit), - "Mappable command should be present in merged keytrie." - ); - } } From e233a1bc1b0685612c9c1bf64d109c0c882ed278 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 08:38:17 +0100 Subject: [PATCH 110/195] Load keymap config consistently * Moved config merging Keymaps to Config * Removed unused default EditorView function --- helix-term/src/config.rs | 18 +++++++++++++----- helix-term/src/keymap/keymaps.rs | 11 ++--------- helix-term/src/main.rs | 2 +- helix-term/src/ui/editor.rs | 6 ------ 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 40d98378da10..eae1da41fc94 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,4 +1,4 @@ -use crate::keymap::{default::default, keymaps::Keymaps, Keymap}; +use crate::keymap::{default, Keymap}; use helix_view::document::Mode; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, io::Error as IOError}; @@ -8,7 +8,7 @@ use toml::de::Error as TomlError; #[serde(deny_unknown_fields)] pub struct Config { pub theme: Option, - #[serde(default = "default")] + #[serde(default = "default::default")] pub keys: HashMap, #[serde(default)] pub editor: helix_view::editor::Config, @@ -18,7 +18,7 @@ impl Default for Config { fn default() -> Config { Config { theme: None, - keys: default(), + keys: default::default(), editor: helix_view::editor::Config::default(), } } @@ -44,11 +44,19 @@ impl Config { pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(Keymaps::merge_with_default) + .map(self::Config::merge_in_default_keymap) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } } + + pub fn merge_in_default_keymap(mut config: Config) -> Config { + let mut delta = std::mem::replace(&mut config.keys, default::default()); + for (mode, keys) in &mut config.keys { + keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) + } + config + } } #[cfg(test)] @@ -124,7 +132,7 @@ mod tests { }, ..Default::default() }; - let mut merged_config = Keymaps::merge_with_default(user_config.clone()); + let mut merged_config = Keymaps::merge_in_default_keymap(user_config.clone()); assert_ne!( user_config, merged_config, diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index ec9ecbce8eb7..5d899f49c7a2 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{commands::MappableCommand, config::Config}; +use crate::commands::MappableCommand; use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; @@ -43,14 +43,6 @@ impl Keymaps { self.sticky_keytrie.as_ref() } - pub fn merge_with_default(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default::default()); - for (mode, keys) in &mut config.keys { - keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) - } - config - } - /// Lookup `key` in the keymap to try and find a command to execute. /// Escape key represents cancellation. /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. @@ -110,6 +102,7 @@ impl Keymaps { } } +// NOTE: Only used for testing purposes impl Default for Keymaps { fn default() -> Self { Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index bae827e30402..f3cfd0800ed0 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -128,7 +128,7 @@ FLAGS: let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(helix_term::keymap::keymaps::Keymaps::merge_with_default) + .map(Config::merge_in_default_keymap) .unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index bd5d99fbcbaa..98da59c95097 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -47,12 +47,6 @@ pub enum InsertEvent { TriggerCompletion, } -impl Default for EditorView { - fn default() -> Self { - Self::new(Keymaps::default()) - } -} - impl EditorView { pub fn new(keymaps: Keymaps) -> Self { Self { From 4a188d2c03f6ea0ac2006ea422c25b123cb786cc Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 18:58:53 +0100 Subject: [PATCH 111/195] Make use of new self context in keymap config load --- helix-term/src/config.rs | 13 ++++++------- helix-term/src/main.rs | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index eae1da41fc94..57fe28a84045 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -44,18 +44,18 @@ impl Config { pub fn load_default() -> Result { match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(self::Config::merge_in_default_keymap) + .map(|config: Config| config.merge_in_default_keymap()) .map_err(ConfigLoadError::BadConfig), Err(err) => Err(ConfigLoadError::Error(err)), } } - pub fn merge_in_default_keymap(mut config: Config) -> Config { - let mut delta = std::mem::replace(&mut config.keys, default::default()); - for (mode, keys) in &mut config.keys { + pub fn merge_in_default_keymap(mut self) -> Config { + let mut delta = std::mem::replace(&mut self.keys, default::default()); + for (mode, keys) in &mut self.keys { keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) } - config + self } } @@ -66,7 +66,6 @@ mod tests { config::Config, keymap::{ default, - keymaps::Keymaps, keytrienode::KeyTrieNode, macros::*, Keymap, @@ -132,7 +131,7 @@ mod tests { }, ..Default::default() }; - let mut merged_config = Keymaps::merge_in_default_keymap(user_config.clone()); + let mut merged_config = user_config.clone().merge_in_default_keymap(); assert_ne!( user_config, merged_config, diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index f3cfd0800ed0..67e4f4a0cd55 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -128,7 +128,7 @@ FLAGS: let config = match std::fs::read_to_string(helix_loader::config_file()) { Ok(config) => toml::from_str(&config) - .map(Config::merge_in_default_keymap) + .map(|config: Config| config.merge_in_default_keymap()) .unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); From baf86401f8f6bb506a2fcc997120ce3870477149 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 21:27:37 +0100 Subject: [PATCH 112/195] Minor keymap module visibilyti cleanup --- helix-term/src/keymap.rs | 3 ++- helix-term/src/keymap/keymaps.rs | 1 - helix-term/src/lib.rs | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 601046600b5e..880b405f604e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,9 +1,10 @@ pub mod keymaps; pub mod default; pub mod macros; -pub mod tests; +// NOTE: Only pub becuase of their use in macros pub mod keytrienode; pub mod keytrie; +mod tests; use crate::{ commands::MappableCommand, diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index 5d899f49c7a2..f824401b1833 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -102,7 +102,6 @@ impl Keymaps { } } -// NOTE: Only used for testing purposes impl Default for Keymaps { fn default() -> Self { Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index 58ae12c2e120..fc8e934e1a7d 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -10,8 +10,6 @@ pub mod health; pub mod job; pub mod keymap; pub mod ui; -pub use keymap::macros::*; -pub use keymap::tests; #[cfg(not(windows))] fn true_color() -> bool { From 3466209d30d33c1e41fe96827925522d53daeaaa Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 21:54:41 +0100 Subject: [PATCH 113/195] Renamed the keymap! macro to keytrie!: It produces, after all, a Keytrie. It would be a bit like saying: let engine = car!() --- helix-term/src/config.rs | 6 +++--- helix-term/src/keymap/default.rs | 10 +++++----- helix-term/src/keymap/macros.rs | 18 +++++++++++++----- helix-term/src/keymap/tests.rs | 4 ++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 57fe28a84045..fc724d818ff0 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -89,11 +89,11 @@ mod tests { toml::from_str::(sample_keymaps).unwrap(), Config { keys: hashmap! { - Mode::Insert => Keymap::new(keymap!({ "Insert mode" + Mode::Insert => Keymap::new(keytrie!({ "Insert mode" "y" => move_line_down, "S-C-a" => delete_selection, })), - Mode::Normal => Keymap::new(keymap!({ "Normal mode" + Mode::Normal => Keymap::new(keytrie!({ "Normal mode" "A-F12" => move_next_word_end, })), }, @@ -118,7 +118,7 @@ mod tests { let user_config = Config { keys: hashmap! { Mode::Normal => Keymap::new( - keymap!({ "Normal mode" + keytrie!({ "Normal mode" "i" => normal_mode, "无" => insert_mode, "z" => jump_backward, diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index b74ae84c7b63..551cf1a79bed 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,10 +1,10 @@ -use super::{macros::keymap, Keymap, keytrie::KeyTrie}; +use super::{macros::keytrie, Keymap}; use helix_view::document::Mode; use helix_core::hashmap; use std::collections::HashMap; pub fn default() -> HashMap { - let normal: KeyTrie = keymap!({ "Normal mode" + let normal = keytrie!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, "k" | "up" => move_line_up, @@ -316,8 +316,8 @@ pub fn default() -> HashMap { "C-a" => increment, "C-x" => decrement, }); - let mut select: KeyTrie = normal.clone(); - select.merge_keytrie(keymap!({ "Select mode" + let mut select = normal.clone(); + select.merge_keytrie(keytrie!({ "Select mode" "h" | "left" => extend_char_left, "j" | "down" => extend_line_down, "k" | "up" => extend_line_up, @@ -344,7 +344,7 @@ pub fn default() -> HashMap { "v" => normal_mode, })); - let insert: KeyTrie = keymap!({ "Insert mode" + let insert = keytrie!({ "Insert mode" "esc" => normal_mode, "C-s" => commit_undo_checkpoint, diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 4d81d220ffb9..66fa78f7a34b 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -66,9 +66,8 @@ macro_rules! alt { /// /// ``` /// # use helix_core::hashmap; -/// # use helix_term::keymap; -/// # use helix_term::keymap::Keymap; -/// let normal_mode = keymap!({ "Normal mode" +/// # use helix_term::keymap::{Keymap, macros::keytrie}; +/// let normal_mode = keytrie!({ "Normal mode" /// "i" => insert_mode, /// "g" => { "Goto" /// "g" => goto_file_start, @@ -79,7 +78,7 @@ macro_rules! alt { /// let keymap = Keymap::new(normal_mode); /// ``` #[macro_export] -macro_rules! keymap { +macro_rules! keytrie { ({ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { // modified from the hashmap! macro { @@ -88,8 +87,13 @@ macro_rules! keymap { ::std::collections::HashMap::with_capacity(_cap); $( $( +<<<<<<< HEAD let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); let _potential_duplicate = _map.insert(_key,keymap!(@trie $value)); +======= + let _key = $key.parse::().unwrap(); + let _potential_duplicate = _map.insert(_key,keytrie!(@trie $value)); +>>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); )+ )* @@ -100,7 +104,7 @@ macro_rules! keymap { }; (@trie {$label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { - $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) + $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keytrie!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) }; (@trie $cmd:ident) => { @@ -113,7 +117,11 @@ macro_rules! keymap { } pub use alt; +<<<<<<< HEAD pub use ctrl; pub use key; pub use keymap; pub use shift; +======= +pub use keytrie; +>>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 157230d816d3..4570ac2e50eb 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -9,7 +9,7 @@ mod tests { #[test] #[should_panic] fn duplicate_keys_should_panic() { - keymap!({ "Normal mode" + keytrie!({ "Normal mode" "i" => normal_mode, "i" => goto_definition, }); @@ -39,7 +39,7 @@ mod tests { #[test] fn command_list() { - let normal_mode = keymap!({ "Normal mode" + let normal_mode = keytrie!({ "Normal mode" "i" => insert_mode, "g" => { "Goto" "g" => goto_file_start, From ff2538031b32bb72ffaca678b3fee87877a59944 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 19 Jan 2023 15:37:43 +0100 Subject: [PATCH 114/195] Fix failed cherry picks --- helix-term/src/commands.rs | 45 ++++++++++++++++++++++---------- helix-term/src/commands/typed.rs | 3 +-- helix-term/src/keymap.rs | 2 +- helix-term/src/keymap/keymaps.rs | 1 + helix-term/src/keymap/keytrie.rs | 2 +- helix-term/src/keymap/macros.rs | 9 ------- helix-term/src/keymap/tests.rs | 9 +++---- 7 files changed, 39 insertions(+), 32 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e21356545d7e..ce1a698d2b0f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,7 +4,7 @@ pub(crate) mod typed; pub use dap::*; pub use lsp::*; -use tui::widgets::Row; +use tui::widgets::{Row, Cell}; pub use typed::*; use helix_vcs::Hunk; @@ -34,7 +34,7 @@ use helix_view::{ keyboard::KeyCode, tree, view::View, - Document, DocumentId, Editor, ViewId, + Document, DocumentId, Editor, ViewId, apply_transaction, }; use crate::{ commands::insert::*, @@ -2448,22 +2448,40 @@ fn jumplist_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } - - - +// NOTE: does not present aliases impl ui::menu::Item for MappableCommand { type Data = CommandList; - fn label(&self, keymap: &Self::Data) -> Spans { + fn format(&self, command_list: &Self::Data) -> Row { match self { - MappableCommand::Typable { description: doc, name, .. } => match keymap.get(name as &String) { - Some(key_events) => format!("{} {:?} ':{}'", doc, key_events, name).into(), - None => format!("{} ':{}'", doc, name).into(), - }, - MappableCommand::Static { description: doc, name, .. } => match keymap.get(*name) { - Some(key_events) => format!("{} {:?} '{}'", doc, key_events, name).into(), - None => format!("{} '{}'", doc, name).into(), + MappableCommand::Typable { description: doc, name, .. } => { + let mut row: Vec = vec![Cell::from(&*name.as_str()), Cell::from(""), Cell::from(&*doc.as_str())]; + match command_list.get(name as &String) { + Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + None => {} + } + return Row::new(row); }, + MappableCommand::Static { description: doc, name, .. } => { + let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; + match command_list.get(*name) { + Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + None => {} + } + return Row::new(row) + } + } + + // TODO: Generalize into a Vec Display implemention? + fn format_key_events(key_events: &Vec) -> String { + let mut result_string: String = String::new(); + for key_event in key_events { + if !result_string.is_empty() { + result_string.push_str(", "); + } + result_string.push_str(key_event); + } + result_string } } } @@ -2513,7 +2531,6 @@ pub fn command_palette(cx: &mut Context) { compositor.push(Box::new(overlayed(picker))); }, )); - } fn last_picker(cx: &mut Context) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index bd91df5ae62a..deb5f7bc88a8 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,8 +1,7 @@ use std::ops::Deref; -use crate::job::Job; - use super::*; +use crate::job::*; use helix_view::editor::{Action, CloseError, ConfigEvent}; use ui::completers::{self, Completer}; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 880b405f604e..6b1b56a80f1a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -26,7 +26,7 @@ pub struct Keymap { pub type CommandList = HashMap>; impl Keymap { pub fn new(root_node: KeyTrie) -> Self { - Keymap { root_node } + Self { root_node } } /// Returns a key-value list of all commands associated to a given Keymap. diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index f824401b1833..773b6213003f 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -1,4 +1,5 @@ use super::*; +use crate::keymap::macros::*; use crate::commands::MappableCommand; use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 0706182db228..cb7798971246 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,6 +1,6 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; +use std::{collections::HashMap, ops::{Deref, DerefMut}}; use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 66fa78f7a34b..24fc7de212d8 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -87,13 +87,8 @@ macro_rules! keytrie { ::std::collections::HashMap::with_capacity(_cap); $( $( -<<<<<<< HEAD let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); - let _potential_duplicate = _map.insert(_key,keymap!(@trie $value)); -======= - let _key = $key.parse::().unwrap(); let _potential_duplicate = _map.insert(_key,keytrie!(@trie $value)); ->>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); )+ )* @@ -117,11 +112,7 @@ macro_rules! keytrie { } pub use alt; -<<<<<<< HEAD pub use ctrl; pub use key; -pub use keymap; pub use shift; -======= pub use keytrie; ->>>>>>> 8cb9a917 (Renamed the keymap! macro to keytrie!:) diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 4570ac2e50eb..a4cded6fb645 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,7 +3,7 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::keymap::*; + use crate::keymap::{*, macros::*}; use std::collections::HashMap; #[test] @@ -17,14 +17,13 @@ mod tests { #[test] fn check_duplicate_keys_in_default_keymap() { - // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro - Keymaps::default(); + // will panic on duplicate keys, assumes that `Keymap` uses keymap! macro + Keymap::default(); } #[test] fn aliased_modes_are_same_in_default_keymap() { - let keymaps = Keymaps::default().keymaps; - let root = keymaps.load().get(&Mode::Normal).unwrap().root_node.clone(); + let root = Keymap::default().get(&Mode::Normal).unwrap().root_node.clone(); assert_eq!( root.traverse(&[key!(' '), key!('w')]).unwrap(), root.traverse(&["C-w".parse::().unwrap()]).unwrap(), From 5c6c0ed93969c4889f4d8bddd33746dceabfa33d Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Thu, 29 Dec 2022 23:29:00 +0100 Subject: [PATCH 115/195] Removed keymap::Keymap: By the end of this refactroring it became clear that it was mostly acting as a useless wrapper for KeyTrie. --- helix-term/src/commands.rs | 21 ++++--- helix-term/src/config.rs | 22 ++++--- helix-term/src/keymap.rs | 85 ---------------------------- helix-term/src/keymap/default.rs | 10 ++-- helix-term/src/keymap/keymaps.rs | 48 ++++++++++++++-- helix-term/src/keymap/keytrienode.rs | 3 +- helix-term/src/keymap/macros.rs | 6 +- helix-term/src/keymap/tests.rs | 19 ++++++- 8 files changed, 91 insertions(+), 123 deletions(-) delete mode 100644 helix-term/src/keymap.rs diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ce1a698d2b0f..e03fa5bb230b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -7,6 +7,15 @@ pub use lsp::*; use tui::widgets::{Row, Cell}; pub use typed::*; +use crate::{ + commands::insert::*, + args, + keymap::keymaps::CommandList, + compositor::{self, Component, Compositor}, + job::{Callback, self, Jobs}, + ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, +}; + use helix_vcs::Hunk; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, @@ -36,14 +45,6 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, apply_transaction, }; -use crate::{ - commands::insert::*, - args, - compositor::{self, Component, Compositor}, - job::{Callback, self, Jobs}, - keymap::CommandList, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, -}; use std::{ collections::{HashMap, HashSet}, num::NonZeroUsize, @@ -2489,9 +2490,7 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymaps.load_keymaps() - [&cx.editor.mode] - .command_list(); + let keymap_command_lists = compositor.find::().unwrap().keymaps.command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index fc724d818ff0..8f7649578c70 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,4 +1,4 @@ -use crate::keymap::{default, Keymap}; +use crate::keymap::{default, keytrie::KeyTrie}; use helix_view::document::Mode; use serde::Deserialize; use std::{collections::HashMap, fmt::Display, io::Error as IOError}; @@ -9,7 +9,7 @@ use toml::de::Error as TomlError; pub struct Config { pub theme: Option, #[serde(default = "default::default")] - pub keys: HashMap, + pub keys: HashMap, #[serde(default)] pub editor: helix_view::editor::Config, } @@ -53,7 +53,7 @@ impl Config { pub fn merge_in_default_keymap(mut self) -> Config { let mut delta = std::mem::replace(&mut self.keys, default::default()); for (mode, keys) in &mut self.keys { - keys.merge_keytrie(delta.remove(mode).unwrap_or_default().root_node) + keys.merge_keytrie(delta.remove(mode).unwrap_or_default()) } self } @@ -68,7 +68,6 @@ mod tests { default, keytrienode::KeyTrieNode, macros::*, - Keymap, }, }; use helix_core::hashmap; @@ -89,13 +88,13 @@ mod tests { toml::from_str::(sample_keymaps).unwrap(), Config { keys: hashmap! { - Mode::Insert => Keymap::new(keytrie!({ "Insert mode" + Mode::Insert => keytrie!({ "Insert mode" "y" => move_line_down, "S-C-a" => delete_selection, - })), - Mode::Normal => Keymap::new(keytrie!({ "Normal mode" + }), + Mode::Normal => keytrie!({ "Normal mode" "A-F12" => move_next_word_end, - })), + }), }, ..Default::default() } @@ -117,8 +116,7 @@ mod tests { fn user_config_merges_with_default() { let user_config = Config { keys: hashmap! { - Mode::Normal => Keymap::new( - keytrie!({ "Normal mode" + Mode::Normal => keytrie!({ "Normal mode" "i" => normal_mode, "无" => insert_mode, "z" => jump_backward, @@ -127,7 +125,7 @@ mod tests { "g" => delete_char_forward, }, }) - ) + }, ..Default::default() }; @@ -138,7 +136,7 @@ mod tests { "Merged user keymap with default should differ from user keymap." ); - let keymap_normal_root_key_trie = &merged_config.keys.get_mut(&Mode::Normal).unwrap().root_node; + let keymap_normal_root_key_trie = &merged_config.keys.get_mut(&Mode::Normal).unwrap(); assert_eq!( keymap_normal_root_key_trie.traverse(&[key!('i')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::normal_mode), diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs deleted file mode 100644 index 6b1b56a80f1a..000000000000 --- a/helix-term/src/keymap.rs +++ /dev/null @@ -1,85 +0,0 @@ -pub mod keymaps; -pub mod default; -pub mod macros; -// NOTE: Only pub becuase of their use in macros -pub mod keytrienode; -pub mod keytrie; -mod tests; - -use crate::{ - commands::MappableCommand, - keymap::{ - keytrie::KeyTrie, - keytrienode::KeyTrieNode - } -}; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; -use serde::Deserialize; - -#[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(transparent)] -/// KeyTrie starting point. -pub struct Keymap { - pub root_node: KeyTrie -} - -pub type CommandList = HashMap>; -impl Keymap { - pub fn new(root_node: KeyTrie) -> Self { - Self { root_node } - } - - /// Returns a key-value list of all commands associated to a given Keymap. - /// Keys are the node names (see KeyTrieNode documentation) - /// Values are lists of stringified KeyEvents that triger the command. - /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. - /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". - /// Ancestor KeyEvents are in this case "space" and "w". - pub fn command_list(&self) -> CommandList { - let mut list = HashMap::new(); - _command_list(&mut list, &KeyTrieNode::KeyTrie(self.root_node.clone()), &mut String::new()); - return list; - - fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { - match node { - KeyTrieNode::KeyTrie(trie_node) => { - for (key_event, subtrie_node) in trie_node.deref() { - let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { - temp_prefix.push_str(">"); - } - temp_prefix.push_str(&key_event.to_string()); - _command_list(list, subtrie_node, &mut temp_prefix); - } - }, - KeyTrieNode::MappableCommand(mappable_command) => { - if mappable_command.name() == "no_op" { return } - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); - }, - KeyTrieNode::CommandSequence(_) => {} - }; - } - } -} - -impl Default for Keymap { - fn default() -> Self { - Self::new(KeyTrie::default()) - } -} - -/// Returns the Keymap root KeyTrie node. -impl Deref for Keymap { - type Target = KeyTrie; - - fn deref(&self) -> &Self::Target { - &self.root_node - } -} - -/// Returns the Keymap root KeyTrie node. -impl DerefMut for Keymap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.root_node - } -} diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 551cf1a79bed..5ee0706622e1 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,9 +1,9 @@ -use super::{macros::keytrie, Keymap}; +use super::{macros::keytrie, keytrie::KeyTrie}; use helix_view::document::Mode; use helix_core::hashmap; use std::collections::HashMap; -pub fn default() -> HashMap { +pub fn default() -> HashMap { let normal = keytrie!({ "Normal mode" "h" | "left" => move_char_left, "j" | "down" => move_line_down, @@ -370,8 +370,8 @@ pub fn default() -> HashMap { "end" => goto_line_end_newline, }); hashmap!( - Mode::Normal => Keymap::new(normal), - Mode::Select => Keymap::new(select), - Mode::Insert => Keymap::new(insert), + Mode::Normal => normal, + Mode::Select => select, + Mode::Insert => insert, ) } diff --git a/helix-term/src/keymap/keymaps.rs b/helix-term/src/keymap/keymaps.rs index 773b6213003f..f99cfe9bdebe 100644 --- a/helix-term/src/keymap/keymaps.rs +++ b/helix-term/src/keymap/keymaps.rs @@ -5,6 +5,8 @@ use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use std::ops::Deref; + #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { Pending(KeyTrie), @@ -16,14 +18,15 @@ pub enum KeymapResult { } pub struct Keymaps { - pub keymaps: Box>>, + pub keymaps: Box>>, /// Relative to a sticky node if Some. pending_keys: Vec, pub sticky_keytrie: Option, } +pub type CommandList = HashMap>; impl Keymaps { - pub fn new(keymaps: Box>>) -> Self { + pub fn new(keymaps: Box>>) -> Self { Self { keymaps, pending_keys: Vec::new(), @@ -31,7 +34,7 @@ impl Keymaps { } } - pub fn load_keymaps(&self) -> DynGuard> { + pub fn load_keymaps(&self) -> DynGuard> { self.keymaps.load() } @@ -63,7 +66,7 @@ impl Keymaps { // Check if sticky keytrie is to be used. let starting_keytrie = match self.sticky_keytrie { - None => &active_keymap.root_node, + None => &active_keymap, Some(ref active_sticky_keytrie) => active_sticky_keytrie, }; @@ -101,6 +104,43 @@ impl Keymaps { None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), } } + + fn get_keytrie(&self, mode: &Mode) -> KeyTrie { + // HELP: Unsure how I should handle this Option + self.keymaps.load().get(mode).unwrap().clone() + } + + /// Returns a key-value list of all commands associated to a given Keymap. + /// Keys are the node names (see KeyTrieNode documentation) + /// Values are lists of stringified KeyEvents that triger the command. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". + /// Ancestor KeyEvents are in this case "space" and "w". + pub fn command_list(&self, mode: &Mode) -> CommandList { + let mut list = HashMap::new(); + _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + return list; + + fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { + match node { + KeyTrieNode::KeyTrie(trie_node) => { + for (key_event, subtrie_node) in trie_node.deref() { + let mut temp_prefix: String = prefix.to_string(); + if &temp_prefix != "" { + temp_prefix.push_str(">"); + } + temp_prefix.push_str(&key_event.to_string()); + _command_list(list, subtrie_node, &mut temp_prefix); + } + }, + KeyTrieNode::MappableCommand(mappable_command) => { + if mappable_command.name() == "no_op" { return } + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + }, + KeyTrieNode::CommandSequence(_) => {} + }; + } + } } impl Default for Keymaps { diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 41b5a7cd3343..433372377787 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,4 +1,5 @@ -use super::{MappableCommand, keytrie::KeyTrie}; +use super::keytrie::KeyTrie; +use crate::commands::MappableCommand; use helix_view::input::KeyEvent; use std::collections::HashMap; use serde::{Deserialize, de::Visitor}; diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index 24fc7de212d8..e17bad91d48d 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -62,11 +62,11 @@ macro_rules! alt { }; } -/// Macro for defining the root of a `Keymap` object. Example: +/// Macro for defining the root of a `KeyTrie` object. Example: /// /// ``` /// # use helix_core::hashmap; -/// # use helix_term::keymap::{Keymap, macros::keytrie}; +/// # use helix_term::keymap::{keytrie::KeyTrie, macros::keytrie}; /// let normal_mode = keytrie!({ "Normal mode" /// "i" => insert_mode, /// "g" => { "Goto" @@ -75,7 +75,7 @@ macro_rules! alt { /// }, /// "j" | "down" => move_line_down, /// }); -/// let keymap = Keymap::new(normal_mode); +/// let keymap = normal_mode; /// ``` #[macro_export] macro_rules! keytrie { diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index a4cded6fb645..6d566c91f7ad 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,8 +3,17 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; +<<<<<<< HEAD use crate::keymap::{*, macros::*}; use std::collections::HashMap; +======= + use crate::{ + keymap::macros::*, + keymap::keymaps::Keymaps, + }; + use std::{sync::Arc, collections::HashMap}; + use arc_swap::ArcSwap; +>>>>>>> adb17e18 (Removed keymap::Keymap:) #[test] #[should_panic] @@ -23,7 +32,12 @@ mod tests { #[test] fn aliased_modes_are_same_in_default_keymap() { +<<<<<<< HEAD let root = Keymap::default().get(&Mode::Normal).unwrap().root_node.clone(); +======= + let keymaps = Keymaps::default().keymaps; + let root = keymaps.load().get(&Mode::Normal).unwrap().clone(); +>>>>>>> adb17e18 (Removed keymap::Keymap:) assert_eq!( root.traverse(&[key!(' '), key!('w')]).unwrap(), root.traverse(&["C-w".parse::().unwrap()]).unwrap(), @@ -46,8 +60,9 @@ mod tests { }, "j" | "k" => move_line_down, }); - let keymap = Keymap::new(normal_mode); - let mut command_list = keymap.command_list(); + + let keymap = Keymaps::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests // HashMaps can be compared but we can still get different ordering of bindings From ff6b53f55b73bd95bbb58869188e54a229627891 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Fri, 30 Dec 2022 10:59:05 +0100 Subject: [PATCH 116/195] Switched keymap::keymaps::Keymaps to keymap::Keymap Root keymap module was empty as old Keymap could be removed. And there was no point anymore in differentiating Keymaps and Keymap. --- helix-term/src/application.rs | 4 +- helix-term/src/commands.rs | 5 +- helix-term/src/keymap.rs | 161 +++++++++++++++++++++++++++++++++ helix-term/src/keymap/tests.rs | 26 ++---- helix-term/src/ui/editor.rs | 22 ++--- 5 files changed, 182 insertions(+), 36 deletions(-) create mode 100644 helix-term/src/keymap.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b614f5395c12..81fa20951122 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -18,8 +18,8 @@ use crate::{ commands::apply_workspace_edit, compositor::{Compositor, Event}, config::Config, + keymap::Keymap, job::Jobs, - keymap::keymaps::Keymaps, ui::{self, overlay::overlayed}, }; use std::{ @@ -177,7 +177,7 @@ impl Application { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + let editor_view = Box::new(ui::EditorView::new(Keymap::new(keys))); compositor.push(editor_view); if args.load_tutor { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e03fa5bb230b..72dd5bdc5b1a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4,13 +4,12 @@ pub(crate) mod typed; pub use dap::*; pub use lsp::*; -use tui::widgets::{Row, Cell}; pub use typed::*; use crate::{ commands::insert::*, args, - keymap::keymaps::CommandList, + keymap::CommandList, compositor::{self, Component, Compositor}, job::{Callback, self, Jobs}, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, @@ -2490,7 +2489,7 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymaps.command_list(&cx.editor.mode); + let keymap_command_lists = compositor.find::().unwrap().keymap.command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs new file mode 100644 index 000000000000..780b7e6531aa --- /dev/null +++ b/helix-term/src/keymap.rs @@ -0,0 +1,161 @@ +pub mod default; +pub mod macros; +// NOTE: Only pub becuase of their use in macros +pub mod keytrienode; +pub mod keytrie; +mod tests; + +use self::{ + keytrienode::KeyTrieNode, + keytrie::KeyTrie, + macros::key, +}; + +use crate::commands::MappableCommand; +use helix_view::{document::Mode, input::KeyEvent}; +use std::{sync::Arc, collections::HashMap}; +use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; + +use std::ops::Deref; + +#[derive(Debug, Clone, PartialEq)] +pub enum KeymapResult { + Pending(KeyTrie), + Matched(MappableCommand), + MatchedCommandSequence(Vec), + NotFound, + /// Contains pressed KeyEvents leading up to the cancellation. + Cancelled(Vec), +} + +pub struct Keymap { + pub keytries: Box>>, + /// Relative to a sticky node if Some. + pending_keys: Vec, + pub sticky_keytrie: Option, +} + +pub type CommandList = HashMap>; +impl Keymap { + pub fn new(keymaps: Box>>) -> Self { + Self { + keytries: keymaps, + pending_keys: Vec::new(), + sticky_keytrie: None, + } + } + + pub fn load_keymaps(&self) -> DynGuard> { + self.keytries.load() + } + + /// Returns list of keys waiting to be disambiguated in current mode. + pub fn pending(&self) -> &[KeyEvent] { + &self.pending_keys + } + + pub fn sticky_keytrie(&self) -> Option<&KeyTrie> { + self.sticky_keytrie.as_ref() + } + + /// Lookup `key` in the keymap to try and find a command to execute. + /// Escape key represents cancellation. + /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. + pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + // TODO: remove the sticky part and look up manually + let keymaps = &*self.load_keymaps(); + let active_keymap = &keymaps[&mode]; + + if key == key!(Esc) { + if !self.pending_keys.is_empty() { + // NOTE: Esc is not included here + return KeymapResult::Cancelled(self.pending_keys.drain(..).collect()); + } + // TODO: Shouldn't we return here also? + self.sticky_keytrie = None; + } + + // Check if sticky keytrie is to be used. + let starting_keytrie = match self.sticky_keytrie { + None => &active_keymap, + Some(ref active_sticky_keytrie) => active_sticky_keytrie, + }; + + // TODO: why check either pending or regular key? + let first_key = self.pending_keys.get(0).unwrap_or(&key); + + let pending_keytrie: KeyTrie = match starting_keytrie.traverse(&[*first_key]) { + Some(KeyTrieNode::KeyTrie(sub_keytrie)) => sub_keytrie, + Some(KeyTrieNode::MappableCommand(cmd)) => { + return KeymapResult::Matched(cmd.clone()); + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + return KeymapResult::MatchedCommandSequence(cmds.clone()); + } + None => return KeymapResult::NotFound, + }; + + self.pending_keys.push(key); + match pending_keytrie.traverse(&self.pending_keys[1..]) { + Some(KeyTrieNode::KeyTrie(map)) => { + if map.is_sticky { + self.pending_keys.clear(); + self.sticky_keytrie = Some(map.clone()); + } + KeymapResult::Pending(map.clone()) + } + Some(KeyTrieNode::MappableCommand(cmd)) => { + self.pending_keys.clear(); + KeymapResult::Matched(cmd.clone()) + } + Some(KeyTrieNode::CommandSequence(cmds)) => { + self.pending_keys.clear(); + KeymapResult::MatchedCommandSequence(cmds.clone()) + } + None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), + } + } + + fn get_keytrie(&self, mode: &Mode) -> KeyTrie { + // HELP: Unsure how I should handle this Option + self.keytries.load().get(mode).unwrap().clone() + } + + /// Returns a key-value list of all commands associated to a given Keymap. + /// Keys are the node names (see KeyTrieNode documentation) + /// Values are lists of stringified KeyEvents that triger the command. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". + /// Ancestor KeyEvents are in this case "space" and "w". + pub fn command_list(&self, mode: &Mode) -> CommandList { + let mut list = HashMap::new(); + _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + return list; + + fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { + match node { + KeyTrieNode::KeyTrie(trie_node) => { + for (key_event, subtrie_node) in trie_node.deref() { + let mut temp_prefix: String = prefix.to_string(); + if &temp_prefix != "" { + temp_prefix.push_str(">"); + } + temp_prefix.push_str(&key_event.to_string()); + _command_list(list, subtrie_node, &mut temp_prefix); + } + }, + KeyTrieNode::MappableCommand(mappable_command) => { + if mappable_command.name() == "no_op" { return } + list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); + }, + KeyTrieNode::CommandSequence(_) => {} + }; + } + } +} + +impl Default for Keymap { + fn default() -> Self { + Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) + } +} diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 6d566c91f7ad..07145bdf310a 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -3,17 +3,8 @@ mod tests { use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; -<<<<<<< HEAD use crate::keymap::{*, macros::*}; use std::collections::HashMap; -======= - use crate::{ - keymap::macros::*, - keymap::keymaps::Keymaps, - }; - use std::{sync::Arc, collections::HashMap}; - use arc_swap::ArcSwap; ->>>>>>> adb17e18 (Removed keymap::Keymap:) #[test] #[should_panic] @@ -32,20 +23,15 @@ mod tests { #[test] fn aliased_modes_are_same_in_default_keymap() { -<<<<<<< HEAD - let root = Keymap::default().get(&Mode::Normal).unwrap().root_node.clone(); -======= - let keymaps = Keymaps::default().keymaps; - let root = keymaps.load().get(&Mode::Normal).unwrap().clone(); ->>>>>>> adb17e18 (Removed keymap::Keymap:) + let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); assert_eq!( - root.traverse(&[key!(' '), key!('w')]).unwrap(), - root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!(' '), key!('w')]).unwrap(), + normal_mode_keytrie_root.traverse(&["C-w".parse::().unwrap()]).unwrap(), "Mismatch for window mode on `Space-w` and `Ctrl-w`." ); assert_eq!( - root.traverse(&[key!('z')]).unwrap(), - root.traverse(&[key!('Z')]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!('z')]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!('Z')]).unwrap(), "Mismatch for view mode on `z` and `Z`." ); } @@ -61,7 +47,7 @@ mod tests { "j" | "k" => move_line_down, }); - let keymap = Keymaps::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 98da59c95097..a4f3f182d9c2 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::keymaps::{KeymapResult, Keymaps}, + keymap::{KeymapResult, Keymap}, ui::{Completion, ProgressSpinners}, }; @@ -32,7 +32,7 @@ use super::lsp::SignatureHelp; use super::statusline; pub struct EditorView { - pub keymaps: Keymaps, + pub keymap: Keymap, on_next_key: Option>, pseudo_pending: Vec, last_insert: (commands::MappableCommand, Vec), @@ -48,9 +48,9 @@ pub enum InsertEvent { } impl EditorView { - pub fn new(keymaps: Keymaps) -> Self { + pub fn new(keymap: Keymap) -> Self { Self { - keymaps, + keymap, on_next_key: None, pseudo_pending: Vec::new(), last_insert: (commands::MappableCommand::normal_mode, Vec::new()), @@ -928,9 +928,9 @@ impl EditorView { event: KeyEvent, ) -> Option { let mut last_mode = mode; - self.pseudo_pending.extend(self.keymaps.pending()); - let key_result = self.keymaps.get(mode, event); - cxt.editor.autoinfo = self.keymaps.sticky_keytrie().map(|node| node.infobox()); + self.pseudo_pending.extend(self.keymap.pending()); + let key_result = self.keymap.get(mode, event); + cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox()); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); @@ -994,7 +994,7 @@ impl EditorView { Some(ch) => commands::insert::insert_char(cx, ch), None => { if let KeymapResult::Matched(command) = - self.keymaps.get(Mode::Insert, ev) + self.keymap.get(Mode::Insert, ev) { command.execute(cx); } @@ -1016,7 +1016,7 @@ impl EditorView { std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); } // special handling for repeat operator - (key!('.'), _) if self.keymaps.pending().is_empty() => { + (key!('.'), _) if self.keymap.pending().is_empty() => { for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) { // first execute whatever put us into insert mode self.last_insert.0.execute(cxt); @@ -1063,7 +1063,7 @@ impl EditorView { cxt.register = cxt.editor.selected_register.take(); self.handle_keymap_event(mode, cxt, event); - if self.keymaps.pending().is_empty() { + if self.keymap.pending().is_empty() { cxt.editor.count = None } } @@ -1521,7 +1521,7 @@ impl Component for EditorView { if let Some(count) = cx.editor.count { disp.push_str(&count.to_string()) } - for key in self.keymaps.pending() { + for key in self.keymap.pending() { disp.push_str(&key.key_sequence_format()); } for key in &self.pseudo_pending { From aed2e90c8783363d94b95e901f24a6f927511264 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Fri, 20 Jan 2023 17:34:59 +0100 Subject: [PATCH 117/195] Re-implemented pre-defined orders in keytrie * Fixed aliased_modes_are_same_in_default_keymap to check for order too. It failed to detect a bug in which the swap_view_* were ordered differently under space-w and c-w. --- helix-term/src/config.rs | 11 +- helix-term/src/keymap.rs | 9 +- helix-term/src/keymap/default.rs | 4 +- helix-term/src/keymap/keytrie.rs | 162 ++++++++++++++++----------- helix-term/src/keymap/keytrienode.rs | 27 ++++- helix-term/src/keymap/macros.rs | 38 ++++--- helix-term/src/keymap/tests.rs | 4 +- 7 files changed, 150 insertions(+), 105 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 8f7649578c70..3ed2fa52323b 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -103,11 +103,8 @@ 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::default()); - - // From the Default trait + let serde_default = toml::from_str::("").unwrap().keys; + assert_eq!(serde_default, default::default()); let default_keys = Config::default().keys; assert_eq!(default_keys, default::default()); } @@ -173,7 +170,7 @@ mod tests { ); // Huh? - assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0); + assert!(merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1); + assert!(merged_config.keys.get(&Mode::Insert).unwrap().get_children().len() > 0); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 780b7e6531aa..9bd04222e39f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -16,8 +16,6 @@ use helix_view::{document::Mode, input::KeyEvent}; use std::{sync::Arc, collections::HashMap}; use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; -use std::ops::Deref; - #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { Pending(KeyTrie), @@ -135,13 +133,14 @@ impl Keymap { fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { match node { KeyTrieNode::KeyTrie(trie_node) => { - for (key_event, subtrie_node) in trie_node.deref() { + for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); if &temp_prefix != "" { - temp_prefix.push_str(">"); + temp_prefix.push_str("→"); } temp_prefix.push_str(&key_event.to_string()); - _command_list(list, subtrie_node, &mut temp_prefix); + _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); + } }, KeyTrieNode::MappableCommand(mappable_command) => { diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5ee0706622e1..3fffaa08c517 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -187,10 +187,10 @@ pub fn default() -> HashMap { "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, "C-l" | "l" | "right" => jump_view_right, - "L" => swap_view_right, - "K" => swap_view_up, "H" => swap_view_left, "J" => swap_view_down, + "K" => swap_view_up, + "L" => swap_view_right, "n" => { "New split scratch buffer" "C-s" | "s" => hsplit_new, "C-v" | "v" => vsplit_new, diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index cb7798971246..8d97c7a8ca5b 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,25 +1,36 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, ops::{Deref, DerefMut}}; +use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] pub struct KeyTrie { documentation: String, - children: HashMap, + /// Used for pre-defined order in infoboxes, values represent the index of the key tries children. + child_order: HashMap, + children: Vec, pub is_sticky: bool, } impl KeyTrie { - pub fn new(documentation: &str, children: HashMap) -> Self { + pub fn new(documentation: &str, child_order: HashMap, children: Vec) -> Self { Self { documentation: documentation.to_string(), + child_order, children, is_sticky: false, } } + pub fn get_child_order(&self) -> &HashMap { + &self.child_order + } + + pub fn get_children(&self) -> &Vec { + &self.children + } + // None symbolizes NotFound pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { return _traverse(self, key_events, 0); @@ -28,13 +39,13 @@ impl KeyTrie { if depth == key_events.len() { return Some(KeyTrieNode::KeyTrie(keytrie.clone())); } - else if let Some(found_child) = keytrie.get(&key_events[depth]) { - match found_child { + else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { + match &keytrie.children[*found_index] { KeyTrieNode::KeyTrie(sub_keytrie) => { depth += 1; return _traverse(sub_keytrie, key_events, depth) }, - _ => return Some(found_child.clone()) + _found_child => return Some(_found_child.clone()) } } return None; @@ -42,84 +53,81 @@ impl KeyTrie { } pub fn merge_keytrie(&mut self, mut other_keytrie: Self) { - for (other_key_event, other_child_node) in std::mem::take(&mut other_keytrie.children) { - match other_child_node { - KeyTrieNode::KeyTrie(other_child_key_trie) => { - if let Some(KeyTrieNode::KeyTrie(self_clashing_child_key_trie)) = self.children.get_mut(&other_key_event) { - self_clashing_child_key_trie.merge_keytrie(other_child_key_trie); + for (other_key_event, other_index) in other_keytrie.get_child_order() { + let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; + match other_child_keytrie_node { + KeyTrieNode::KeyTrie(ref other_child_keytrie) => { + if let Some(self_index) = self.child_order.get(&other_key_event) { + if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { + self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); + } } else { - self.children.insert(other_key_event, KeyTrieNode::KeyTrie(other_child_key_trie)); + self.child_order.insert(*other_key_event, self.children.len()); + self.children.push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); } } KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { - self.children.insert(other_key_event, other_child_node); + if let Some(existing_index) = self.child_order.get(other_key_event) { + self.children[*existing_index] = other_child_keytrie_node.clone(); + } + else { + self.child_order.insert(*other_key_event, self.children.len()); + self.children.push(other_child_keytrie_node.clone()); + } } } } } - + /// Open an Info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self) -> Info { - let mut body: Vec<(Vec, &str)> = Vec::with_capacity(self.len()); - for (&key_event, key_trie) in self.iter() { + let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); + let mut key_event_order = Vec::with_capacity(self.children.len()); + // child_order and children is of same length + unsafe { key_event_order.set_len(self.children.len()); } + for (key_event, index) in &self.child_order { + key_event_order[*index] = key_event.clone(); + } + + for (index, key_trie) in self.children.iter().enumerate() { let documentation: &str = match key_trie { - KeyTrieNode::MappableCommand(command) => { + KeyTrieNode::MappableCommand(ref command) => { if command.name() == "no_op" { continue; } command.description() }, - KeyTrieNode::KeyTrie(key_trie) => &key_trie.documentation, + KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, // FIX: default to a join of all command names // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. // Regardless if the command sequence is different. KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; + let key_event = key_event_order[index]; match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { Some(position) => body[position].0.push(key_event.to_string()), None => { - let mut temp_vec: Vec = Vec::new(); - temp_vec.push(key_event.to_string()); - body.push((temp_vec, documentation)) + body.push((vec![key_event.to_string()], documentation)) }, } } // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent // Those events will always be placed after the one letter KeyEvent - let mut sorted_body = body - .iter() - .map(|(key_events, description)| { - let mut temp_key_events = key_events.clone(); - temp_key_events.sort_unstable_by(|a, b| a.len().cmp(&b.len())); - (temp_key_events, *description) - }) - .collect::, &str)>>(); - sorted_body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); - // Consistently place lowercase before uppercase of the same letter. - if sorted_body.len() > 1 { - let mut x_index = 0; - let mut y_index = 1; - - while y_index < sorted_body.len() { - let x = &sorted_body[x_index].0[0]; - let y = &sorted_body[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() { - // Uppercase regarded as lower value. - if x < y { - let temp_holder = sorted_body[x_index].clone(); - sorted_body[x_index] = sorted_body[y_index].clone(); - sorted_body[y_index] = temp_holder; - } + for (key_events, _) in body.iter_mut() { + key_events.sort_unstable_by(|a, b| { + if a.len() == 1 { return Ordering::Less } + if b.len() > a.len() && b.starts_with("C-") { + return Ordering::Greater } - x_index = y_index; - y_index += 1; - } + a.len().cmp(&b.len()) + }); } - let stringified_key_events_body: Vec<(String, &str)> = sorted_body + // TODO: conditional sort added here by calling infobox_sort(body) + let stringified_key_events_body: Vec<(String, &str)> = body .iter() .map(|(key_events, description)| { let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { @@ -137,7 +145,7 @@ impl KeyTrie { impl Default for KeyTrie { fn default() -> Self { - Self::new("", HashMap::new()) + Self::new("", HashMap::new(), Vec::new()) } } @@ -147,30 +155,50 @@ impl PartialEq for KeyTrie { } } -/// Returns the children of the KeyTrie -impl Deref for KeyTrie { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.children - } -} - -/// Returns the children of the KeyTrie -impl DerefMut for KeyTrie { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.children - } -} - impl<'de> Deserialize<'de> for KeyTrie { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { + // NOTE: no assumption of pre-defined order in config + let child_collection = HashMap::::deserialize(deserializer)?; + let mut child_order = HashMap::::new(); + let mut children = Vec::new(); + for (key_event, keytrie_node) in child_collection { + child_order.insert(key_event, children.len()); + children.push(keytrie_node); + } + Ok(Self { - children: HashMap::::deserialize(deserializer)?, + child_order, + children, ..Default::default() }) } } + +type InfoBoxBody<'a> = Vec<(Vec, &'a str)>; +fn infobox_sort(mut body: InfoBoxBody) -> InfoBoxBody { + body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + // Consistently place lowercase before uppercase of the same letter. + if body.len() > 1 { + let mut x_index = 0; + let mut y_index = 1; + + while y_index < body.len() { + let x = &body[x_index].0[0]; + let y = &body[y_index].0[0]; + if x.to_lowercase() == y.to_lowercase() { + // Uppercase regarded as lower value. + if x < y { + let temp_holder = body[x_index].clone(); + body[x_index] = body[y_index].clone(); + body[y_index] = temp_holder; + } + } + x_index = y_index; + y_index += 1; + } + } + body +} diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 433372377787..66a642ac902d 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, de::Visitor}; /// For the MappableCommand and CommandSequence variants, the property is self explanatory. /// For KeyTrie, the documentation is used for respective infobox titles, /// or infobox KeyEvent descriptions that in themselves trigger the opening of another infobox. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub enum KeyTrieNode { MappableCommand(MappableCommand), CommandSequence(Vec), @@ -24,6 +24,23 @@ impl<'de> Deserialize<'de> for KeyTrieNode { } } +impl PartialEq for KeyTrieNode { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (KeyTrieNode::MappableCommand(_self), KeyTrieNode::MappableCommand(_other)) => { + _self == _other + }, + (KeyTrieNode::CommandSequence(_self), KeyTrieNode::CommandSequence(_other)) => { + _self == _other + }, + (KeyTrieNode::KeyTrie(_self), KeyTrieNode::KeyTrie(_other)) => { + _self.get_children() == _other.get_children() + }, + _ => false + } + } +} + struct KeyTrieNodeVisitor; impl<'de> Visitor<'de> for KeyTrieNodeVisitor { @@ -62,10 +79,12 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { where M: serde::de::MapAccess<'de>, { - let mut sub_key_trie = HashMap::new(); + let mut children = Vec::new(); + let mut child_order = HashMap::new(); while let Some((key_event, key_trie_node)) = map.next_entry::()? { - sub_key_trie.insert(key_event, key_trie_node); + child_order.insert(key_event, children.len()); + children.push(key_trie_node); } - Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", sub_key_trie))) + Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", child_order, children))) } } \ No newline at end of file diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index e17bad91d48d..b834c43e56aa 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -1,8 +1,8 @@ #[macro_export] macro_rules! key { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; @@ -16,9 +16,9 @@ macro_rules! key { #[macro_export] macro_rules! shift { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, } }; @@ -32,9 +32,9 @@ macro_rules! shift { #[macro_export] macro_rules! ctrl { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, } }; @@ -48,9 +48,9 @@ macro_rules! ctrl { #[macro_export] macro_rules! alt { - ($key:ident) => { + ($key_event:ident) => { ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, + code: ::helix_view::keyboard::KeyCode::$key_event, modifiers: ::helix_view::keyboard::KeyModifiers::ALT, } }; @@ -79,27 +79,29 @@ macro_rules! alt { /// ``` #[macro_export] macro_rules! keytrie { - ({ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { - // modified from the hashmap! macro + // Sub key_trie + ({ $label:literal $(sticky=$sticky:literal)? $($($key_event:literal)|+ => $value:tt,)+ }) => { { - let _cap = hashmap!(@count $($($key),+),*); - let mut _map: ::std::collections::HashMap<::helix_view::input::KeyEvent, $crate::keymap::keytrienode::KeyTrieNode> = - ::std::collections::HashMap::with_capacity(_cap); + let _cap = hashmap!(@count $($($key_event),+),*); + let mut _children: Vec<$crate::keymap::keytrienode::KeyTrieNode> = ::std::vec::Vec::new(); + let mut _child_order: ::std::collections::HashMap<::helix_view::input::KeyEvent, usize> = ::std::collections::HashMap::with_capacity(_cap); $( $( - let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); - let _potential_duplicate = _map.insert(_key,keytrie!(@trie $value)); + let _key_event = $key_event.parse::<::helix_view::input::KeyEvent>().unwrap(); + let _potential_duplicate = _child_order.insert(_key_event, _children.len()); assert!(_potential_duplicate.is_none(), "Duplicate key found: {:?}", _potential_duplicate.unwrap()); + _children.push(keytrie!(@trie $value)); )+ )* - let mut _node = $crate::keymap::keytrie::KeyTrie::new($label, _map); + + let mut _node = $crate::keymap::keytrie::KeyTrie::new($label, _child_order, _children); $( _node.is_sticky = $sticky; )? _node } }; - (@trie {$label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }) => { - $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keytrie!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })) + (@trie {$label:literal $(sticky=$sticky:literal)? $($($key_event:literal)|+ => $value:tt,)+ }) => { + $crate::keymap::keytrienode::KeyTrieNode::KeyTrie(keytrie!({ $label $(sticky=$sticky)? $($($key_event)|+ => $value,)+ })) }; (@trie $cmd:ident) => { diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 07145bdf310a..4ab431c6e31d 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -66,11 +66,11 @@ mod tests { ), ( "goto_file_start".to_string(), - vec![format!("{}>{}", key!('g'), key!('g'))] + vec![format!("{}→{}", key!('g'), key!('g'))] ), ( "goto_file_end".to_string(), - vec![format!("{}>{}", key!('g'), key!('e'))] + vec![format!("{}→{}", key!('g'), key!('e'))] ), ( "move_line_down".to_string(), From 5edce2e269c80e8931d09a8a74b755d405bacf40 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sat, 21 Jan 2023 16:20:05 +0100 Subject: [PATCH 118/195] Add sort_infobox editor config option --- book/src/configuration.md | 1 + helix-term/src/keymap/keytrie.rs | 7 ++++--- helix-term/src/ui/editor.rs | 5 +++-- helix-view/src/editor.rs | 3 +++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index ab229f772a1c..9e9c07451c31 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -53,6 +53,7 @@ on unix operating systems. | `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | | `auto-info` | Whether to display infoboxes | `true` | +| `sorted-infobox` | Sort infoboxes by key event category rather than by predefined command categories | `false` | | `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | | `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` | diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 8d97c7a8ca5b..d2b9734f1cf2 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -82,7 +82,7 @@ impl KeyTrie { /// Open an Info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. - pub fn infobox(&self) -> Info { + pub fn infobox(&self, alphabetical_sort: bool) -> Info { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length @@ -126,7 +126,8 @@ impl KeyTrie { }); } - // TODO: conditional sort added here by calling infobox_sort(body) + if alphabetical_sort { body = alphabetially_sort_infobox(body); } + let stringified_key_events_body: Vec<(String, &str)> = body .iter() .map(|(key_events, description)| { @@ -178,7 +179,7 @@ impl<'de> Deserialize<'de> for KeyTrie { } type InfoBoxBody<'a> = Vec<(Vec, &'a str)>; -fn infobox_sort(mut body: InfoBoxBody) -> InfoBoxBody { +fn alphabetially_sort_infobox(mut body: InfoBoxBody) -> InfoBoxBody { body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); // Consistently place lowercase before uppercase of the same letter. if body.len() > 1 { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a4f3f182d9c2..94ead3f0f0e6 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -930,7 +930,8 @@ impl EditorView { let mut last_mode = mode; self.pseudo_pending.extend(self.keymap.pending()); let key_result = self.keymap.get(mode, event); - cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox()); + let sort_infobox = cxt.editor.config.load().sorted_infobox; + cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox(sort_infobox)); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); @@ -969,7 +970,7 @@ impl EditorView { KeymapResult::Matched(command) => { execute_command(command); } - KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), + KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox(sort_infobox)), KeymapResult::MatchedCommandSequence(commands) => { for command in commands { execute_command(command); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index eef4a3f99e91..3e487d9b6189 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -246,6 +246,8 @@ pub struct Config { pub completion_trigger_len: u8, /// Whether to display infoboxes. Defaults to true. pub auto_info: bool, + /// Sort infoboxes alphabetically rather than by predefined categories. Defaults to `false`. + pub sorted_infobox: bool, pub file_picker: FilePickerConfig, /// Configuration of the statusline elements pub statusline: StatusLineConfig, @@ -717,6 +719,7 @@ impl Default for Config { bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), color_modes: false, + sorted_infobox: false, } } } From 0b08e38a6021cce68b08286b4b0a345b4c55aaa5 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 16:45:50 +0100 Subject: [PATCH 119/195] Refine sorting behavior Sorts by modifier, then by KeyCode category, then by each KeyEvent. Single character alphas are placed before the rest (ex. "space") in KeyCode::Char. --- helix-term/Cargo.toml | 1 - helix-term/src/keymap/keytrie.rs | 126 +++++++++++++++++++++++++------ helix-view/src/keyboard.rs | 59 +++++---------- 3 files changed, 120 insertions(+), 66 deletions(-) diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index d4eebefb19e2..4efd8911cb0b 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -36,7 +36,6 @@ helix-loader = { version = "0.6", path = "../helix-loader" } anyhow = "1" once_cell = "1.17" - which = "4.4" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index d2b9734f1cf2..7fe5df9a8610 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -80,9 +80,9 @@ impl KeyTrie { } } - /// Open an Info box for a given KeyTrie + /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. - pub fn infobox(&self, alphabetical_sort: bool) -> Info { + pub fn infobox(&self, sort_infobox: bool) -> Info { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length @@ -114,6 +114,7 @@ impl KeyTrie { } } + // TODO: Add "A-" aknowledgement? // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent // Those events will always be placed after the one letter KeyEvent for (key_events, _) in body.iter_mut() { @@ -126,7 +127,7 @@ impl KeyTrie { }); } - if alphabetical_sort { body = alphabetially_sort_infobox(body); } + if sort_infobox { body = keyevent_sort_infobox(body); } let stringified_key_events_body: Vec<(String, &str)> = body .iter() @@ -178,28 +179,107 @@ impl<'de> Deserialize<'de> for KeyTrie { } } -type InfoBoxBody<'a> = Vec<(Vec, &'a str)>; -fn alphabetially_sort_infobox(mut body: InfoBoxBody) -> InfoBoxBody { - body.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); - // Consistently place lowercase before uppercase of the same letter. - if body.len() > 1 { - let mut x_index = 0; - let mut y_index = 1; - - while y_index < body.len() { - let x = &body[x_index].0[0]; - let y = &body[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() { - // Uppercase regarded as lower value. - if x < y { - let temp_holder = body[x_index].clone(); - body[x_index] = body[y_index].clone(); - body[y_index] = temp_holder; +// (KeyEvents, Description) +type InfoBoxRow<'a> = (Vec, &'a str); +type InfoBoxBody<'a> = Vec>; +/// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. +/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are +/// placed together, and alphas are placed before the rest. +fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { + use std::str::FromStr; + use std::collections::BTreeMap; + use helix_view::keyboard::{KeyCode, KeyModifiers, MediaKeyCode}; + + let mut category_holder: BTreeMap>> = BTreeMap::new(); + let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); + for infobox_row in body { + let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); + if !category_holder.contains_key(&first_keyevent.modifiers) { + category_holder.insert(first_keyevent.modifiers.clone(), BTreeMap::new()); + } + + // HACK: inserting by variant not by variant value. + // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant + // Hence the use of mock Variant values + let keycode_category = match first_keyevent.code { + KeyCode::Char(_) => { + KeyCode::Char('a') + } + KeyCode::F(_) => { + KeyCode::F(0) + } + KeyCode::Media(_) => { + KeyCode::Media(MediaKeyCode::Play) + } + other_keycode => { + other_keycode + } + }; + + + let modifier_category = category_holder.get_mut(&first_keyevent.modifiers) + .expect("keycode category existence should be checked."); + if !modifier_category.contains_key(&keycode_category) { + modifier_category.insert(keycode_category.clone(), Vec::new()); + } + modifier_category.get_mut(&keycode_category) + .expect("key existence should be checked") + .push(infobox_row); + } + + for (_, keycode_categories) in category_holder { + for (keycode_category, mut infobox_rows) in keycode_categories { + if infobox_rows.len() > 1 { + match keycode_category { + KeyCode::Char(_) => { + infobox_rows.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + + // Consistently place lowercase before uppercase of the same letter. + let mut x_index = 0; + let mut y_index = 1; + while y_index < infobox_rows.len() { + let x = &infobox_rows[x_index].0[0]; + let y = &infobox_rows[y_index].0[0]; + if x.to_lowercase() == y.to_lowercase() { + if x < y { + infobox_rows.swap(x_index, y_index); + } + } + x_index = y_index; + y_index += 1; + } + + // TEMP: until drain_filter becomes stable. Migth also be worth implementing + // FromIterator on InfoboxBody by then, last two for loops could then also be replaced by Iter::chain + let mut alphas = Vec::new(); + let mut misc = Vec::new(); + for infobox_row in infobox_rows { + if ('a'..='z') + .map(|char| char.to_string()) + .find(|alpha_char| *alpha_char == infobox_row.0[0].to_lowercase()) + .is_some() + { + alphas.push(infobox_row); + } + else { + misc.push(infobox_row); + } + } + infobox_rows = Vec::with_capacity(alphas.len() + misc.len()); + for alpha_row in alphas { + infobox_rows.push(alpha_row); + } + for misc_row in misc { + infobox_rows.push(misc_row); + } + }, + _ => { + infobox_rows.sort_unstable(); + } } } - x_index = y_index; - y_index += 1; + sorted_body.append(infobox_rows.as_mut()); } } - body + sorted_body } diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 04a9922a99bb..20e592d36c28 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -211,63 +211,38 @@ impl From for ModifierKeyCode { } /// Represents a key. +/// Variant order determines order in keymap infobox if sorted_infobox is set to true. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum KeyCode { - /// Backspace key. - Backspace, - /// Enter key. - Enter, - /// Left arrow key. - Left, - /// Right arrow key. - Right, - /// Up arrow key. + /// Character key. + /// Ex: `KeyCode::Char('c')` represents the `c` character. + Char(char), + /// Function key. + /// Ex: `KeyCode::F(1)` represents the F1 key. + F(u8), Up, - /// Down arrow key. Down, - /// Home key. + Left, + Right, + Enter, + Esc, + Tab, + Backspace, + Insert, + Delete, Home, - /// End key. End, - /// Page up key. PageUp, - /// Page down key. PageDown, - /// Tab key. - Tab, - /// Delete key. - Delete, - /// Insert key. - Insert, - /// F key. - /// - /// `KeyCode::F(1)` represents F1 key, etc. - F(u8), - /// A character. - /// - /// `KeyCode::Char('c')` represents `c` character, etc. - Char(char), - /// Null. Null, - /// Escape key. - Esc, - /// CapsLock key. CapsLock, - /// ScrollLock key. ScrollLock, - /// NumLock key. NumLock, - /// PrintScreen key. - PrintScreen, - /// Pause key. - Pause, - /// Menu key. Menu, - /// KeypadBegin key. + Pause, + PrintScreen, KeypadBegin, - /// A media key. Media(MediaKeyCode), - /// A modifier key. Modifier(ModifierKeyCode), } From 5049e7c85fc6b492e1de8aba65d5851c6092717e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 16:53:05 +0100 Subject: [PATCH 120/195] Removed warnings --- helix-term/src/keymap/keytrie.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 7fe5df9a8610..04a2424dcec2 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,6 +1,6 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, ops::{Deref, DerefMut}, cmp::Ordering}; +use std::{collections::HashMap, cmp::Ordering}; use serde::Deserialize; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode @@ -52,7 +52,7 @@ impl KeyTrie { } } - pub fn merge_keytrie(&mut self, mut other_keytrie: Self) { + pub fn merge_keytrie(&mut self, other_keytrie: Self) { for (other_key_event, other_index) in other_keytrie.get_child_order() { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; match other_child_keytrie_node { From 4f758ee240d5c2cd373458aeb4032ae68f202c1e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 17:01:47 +0100 Subject: [PATCH 121/195] Use .join(", ") rather that own implementation --- helix-term/src/keymap/keytrie.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 04a2424dcec2..01c03ec9d1b1 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -80,6 +80,7 @@ impl KeyTrie { } } + // IMPROVEMENT: cache sorting and update cache only when config is updated /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. pub fn infobox(&self, sort_infobox: bool) -> Info { @@ -130,16 +131,9 @@ impl KeyTrie { if sort_infobox { body = keyevent_sort_infobox(body); } let stringified_key_events_body: Vec<(String, &str)> = body - .iter() - .map(|(key_events, description)| { - let key_events_string: String = key_events.iter().fold(String::new(), |mut acc, key_event| { - if !acc.is_empty() { acc.push_str(", "); } - acc.push_str(key_event); - acc - }); - (key_events_string, *description) - }) - .collect(); + .iter().map(|(key_events, description)| { + (key_events.join(", "), *description) + }).collect(); Info::new(&self.documentation, &stringified_key_events_body) } From c224bf339437b732bb40992f9004a6be2ef0cb1a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 18:05:24 +0100 Subject: [PATCH 122/195] Fix apply_trasaction rebase fail --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 72dd5bdc5b1a..d12b5b98110e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -42,7 +42,7 @@ use helix_view::{ keyboard::KeyCode, tree, view::View, - Document, DocumentId, Editor, ViewId, apply_transaction, + Document, DocumentId, Editor, ViewId, }; use std::{ collections::{HashMap, HashSet}, From 95a2b8784a77f6ca955987a3fa1508fb290e72ec Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Sun, 22 Jan 2023 18:43:26 +0100 Subject: [PATCH 123/195] Fix failing test --- helix-term/src/config.rs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 3ed2fa52323b..4838e2d79c26 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -77,28 +77,31 @@ mod tests { fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] - y = "move_line_down" S-C-a = "delete_selection" + y = "move_line_down" [keys.normal] A-F12 = "move_next_word_end" "#; - assert_eq!( - toml::from_str::(sample_keymaps).unwrap(), - Config { - keys: hashmap! { - Mode::Insert => keytrie!({ "Insert mode" - "y" => move_line_down, - "S-C-a" => delete_selection, - }), - Mode::Normal => keytrie!({ "Normal mode" - "A-F12" => move_next_word_end, - }), - }, - ..Default::default() - } - ); + let config = Config { + keys: hashmap! { + Mode::Insert => keytrie!({ "Insert mode" + "S-C-a" => delete_selection, + "y" => move_line_down, + }), + Mode::Normal => keytrie!({ "Normal mode" + "A-F12" => move_next_word_end, + }), + }, + ..Default::default() + }; + for mode in config.keys.keys() { + assert_eq!( + config.keys.get(mode).unwrap().get_children(), + toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + ); + } } #[test] From 941624bdec8ba672c0cd9179679e0c2cc39af224 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 17:28:18 +0100 Subject: [PATCH 124/195] Run cargo fmt --- helix-term/src/application.rs | 44 ++++----- helix-term/src/commands.rs | 127 +++++++++++++++---------- helix-term/src/config.rs | 50 +++++++--- helix-term/src/keymap.rs | 40 ++++---- helix-term/src/keymap/default.rs | 4 +- helix-term/src/keymap/keytrie.rs | 134 +++++++++++++++------------ helix-term/src/keymap/keytrienode.rs | 20 ++-- helix-term/src/keymap/macros.rs | 2 +- helix-term/src/keymap/tests.rs | 21 +++-- helix-term/src/ui/editor.rs | 7 +- 10 files changed, 266 insertions(+), 183 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 81fa20951122..adaed7530bcf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,3 +1,23 @@ +use crate::{ + args::Args, + commands::apply_workspace_edit, + compositor::{Compositor, Event}, + config::Config, + job::Jobs, + keymap::Keymap, + ui::{self, overlay::overlayed}, +}; +use anyhow::{Context, Error}; +use arc_swap::{access::Map, ArcSwap}; +use crossterm::{ + event::{ + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, + }, + execute, terminal, + tty::IsTty, +}; +use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, @@ -13,34 +33,14 @@ use helix_view::{ tree::Layout, Align, Editor, }; -use crate::{ - args::Args, - commands::apply_workspace_edit, - compositor::{Compositor, Event}, - config::Config, - keymap::Keymap, - job::Jobs, - ui::{self, overlay::overlayed}, -}; +use log::{debug, error, warn}; +use serde_json::json; use std::{ io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; -use arc_swap::{access::Map, ArcSwap}; -use futures_util::Stream; -use log::{debug, error, warn}; -use anyhow::{Context, Error}; -use serde_json::json; use tui::backend::Backend; -use crossterm::{ - event::{ - DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, - }, - execute, terminal, - tty::IsTty, -}; #[cfg(not(windows))] use { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d12b5b98110e..cb907479d04b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -7,15 +7,24 @@ pub use lsp::*; pub use typed::*; use crate::{ - commands::insert::*, args, - keymap::CommandList, + commands::insert::*, compositor::{self, Component, Compositor}, - job::{Callback, self, Jobs}, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, menu::{Cell, Row}}, + job::{self, Callback, Jobs}, + keymap::CommandList, + ui::{ + self, + menu::{Cell, Row}, + overlay::overlayed, + FilePicker, Picker, Popup, Prompt, PromptEvent, + }, }; -use helix_vcs::Hunk; +use anyhow::{anyhow, bail, ensure, Context as _}; +use futures_util::StreamExt; +use fuzzy_matcher::FuzzyMatcher; +use grep_regex::RegexMatcherBuilder; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, @@ -33,6 +42,7 @@ use helix_core::{ visual_coords_at_pos, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; +use helix_vcs::Hunk; use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, @@ -44,22 +54,17 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, }; +use ignore::{DirEntry, WalkBuilder, WalkState}; +use once_cell::sync::Lazy; +use serde::de::{self, Deserialize, Deserializer}; use std::{ + borrow::Cow, collections::{HashMap, HashSet}, - num::NonZeroUsize, + fmt, future::Future, - borrow::Cow, + num::NonZeroUsize, path::{Path, PathBuf}, - fmt, }; -use anyhow::{anyhow, bail, ensure, Context as _}; -use fuzzy_matcher::FuzzyMatcher; -use futures_util::StreamExt; -use once_cell::sync::Lazy; -use serde::de::{self, Deserialize, Deserializer}; -use grep_regex::RegexMatcherBuilder; -use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; -use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; pub struct Context<'a> { @@ -156,7 +161,11 @@ macro_rules! static_commands { impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { - Self::Typable { name, args, description: _ } => { + Self::Typable { + name, + args, + description: _, + } => { let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { @@ -2454,22 +2463,38 @@ impl ui::menu::Item for MappableCommand { fn format(&self, command_list: &Self::Data) -> Row { match self { - MappableCommand::Typable { description: doc, name, .. } => { - let mut row: Vec = vec![Cell::from(&*name.as_str()), Cell::from(""), Cell::from(&*doc.as_str())]; + MappableCommand::Typable { + description: doc, + name, + .. + } => { + let mut row: Vec = vec![ + Cell::from(&*name.as_str()), + Cell::from(""), + Cell::from(&*doc.as_str()), + ]; match command_list.get(name as &String) { - Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + Some(key_events) => { + row[1] = Cell::from(format_key_events(key_events)); + } None => {} } return Row::new(row); - }, - MappableCommand::Static { description: doc, name, .. } => { + } + MappableCommand::Static { + description: doc, + name, + .. + } => { let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; match command_list.get(*name) { - Some(key_events) => { row[1] = Cell::from(format_key_events(key_events)); }, + Some(key_events) => { + row[1] = Cell::from(format_key_events(key_events)); + } None => {} } - return Row::new(row) - } + return Row::new(row); + } } // TODO: Generalize into a Vec Display implemention? @@ -2489,7 +2514,11 @@ impl ui::menu::Item for MappableCommand { pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { - let keymap_command_lists = compositor.find::().unwrap().keymap.command_list(&cx.editor.mode); + let keymap_command_lists = compositor + .find::() + .unwrap() + .keymap + .command_list(&cx.editor.mode); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2500,32 +2529,36 @@ pub fn command_palette(cx: &mut Context) { } })); - let picker = Picker::new(commands, keymap_command_lists, move |cx, command, _action| { - let mut ctx = Context { - register: None, - count: std::num::NonZeroUsize::new(1), - editor: cx.editor, - callback: None, - on_next_key_callback: None, - jobs: cx.jobs, - }; - let focus = view!(ctx.editor).id; + let picker = Picker::new( + commands, + keymap_command_lists, + move |cx, command, _action| { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: None, + on_next_key_callback: None, + jobs: cx.jobs, + }; + let focus = view!(ctx.editor).id; - command.execute(&mut ctx); + command.execute(&mut ctx); - if ctx.editor.tree.contains(focus) { - let config = ctx.editor.config(); - let mode = ctx.editor.mode(); - let view = view_mut!(ctx.editor, focus); - let doc = doc_mut!(ctx.editor, &view.doc); + if ctx.editor.tree.contains(focus) { + let config = ctx.editor.config(); + let mode = ctx.editor.mode(); + let view = view_mut!(ctx.editor, focus); + let doc = doc_mut!(ctx.editor, &view.doc); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff); - if mode != Mode::Insert { - doc.append_changes_to_history(view); + if mode != Mode::Insert { + doc.append_changes_to_history(view); + } } - } - }); + }, + ); compositor.push(Box::new(overlayed(picker))); }, )); diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4838e2d79c26..296526aeb71e 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -64,11 +64,7 @@ mod tests { use crate::{ commands::MappableCommand, config::Config, - keymap::{ - default, - keytrienode::KeyTrieNode, - macros::*, - }, + keymap::{default, keytrienode::KeyTrieNode, macros::*}, }; use helix_core::hashmap; use helix_view::document::Mode; @@ -99,7 +95,12 @@ mod tests { for mode in config.keys.keys() { assert_eq!( config.keys.get(mode).unwrap().get_children(), - toml::from_str::(sample_keymaps).unwrap().keys.get(mode).unwrap().get_children() + toml::from_str::(sample_keymaps) + .unwrap() + .keys + .get(mode) + .unwrap() + .get_children() ); } } @@ -125,14 +126,13 @@ mod tests { "g" => delete_char_forward, }, }) - + }, ..Default::default() }; let mut merged_config = user_config.clone().merge_in_default_keymap(); assert_ne!( - user_config, - merged_config, + user_config, merged_config, "Merged user keymap with default should differ from user keymap." ); @@ -155,25 +155,47 @@ mod tests { ); // Assumes that `g` is a sub key trie in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('$')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('$')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_line_end), "User supplied mappable command should be inserted under the correct sub keytrie." ); // Assumes that `gg` is in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('g')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('g')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::delete_char_forward), "User supplied mappable command should replace default even in sub keytries." ); // Assumes that `ge` is in default keymap assert_eq!( - keymap_normal_root_key_trie.traverse(&[key!('g'), key!('e')]).unwrap(), + keymap_normal_root_key_trie + .traverse(&[key!('g'), key!('e')]) + .unwrap(), KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), "Default mappable commands that aren't ovveridden should exist in merged keymap." ); // Huh? - assert!(merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1); - assert!(merged_config.keys.get(&Mode::Insert).unwrap().get_children().len() > 0); + assert!( + merged_config + .keys + .get(&Mode::Normal) + .unwrap() + .get_children() + .len() + > 1 + ); + assert!( + merged_config + .keys + .get(&Mode::Insert) + .unwrap() + .get_children() + .len() + > 0 + ); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 9bd04222e39f..c6b3f75b56c8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,20 +1,19 @@ pub mod default; pub mod macros; // NOTE: Only pub becuase of their use in macros -pub mod keytrienode; pub mod keytrie; +pub mod keytrienode; mod tests; -use self::{ - keytrienode::KeyTrieNode, - keytrie::KeyTrie, - macros::key, -}; +use self::{keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::key}; use crate::commands::MappableCommand; +use arc_swap::{ + access::{DynAccess, DynGuard}, + ArcSwap, +}; use helix_view::{document::Mode, input::KeyEvent}; -use std::{sync::Arc, collections::HashMap}; -use arc_swap::{access::{DynAccess, DynGuard}, ArcSwap}; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { @@ -57,7 +56,7 @@ impl Keymap { } /// Lookup `key` in the keymap to try and find a command to execute. - /// Escape key represents cancellation. + /// Escape key represents cancellation. /// This means clearing pending keystrokes, or the sticky_keytrie if none were present. pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { // TODO: remove the sticky part and look up manually @@ -122,12 +121,16 @@ impl Keymap { /// Returns a key-value list of all commands associated to a given Keymap. /// Keys are the node names (see KeyTrieNode documentation) /// Values are lists of stringified KeyEvents that triger the command. - /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. + /// Each element in the KeyEvent list is prefixed with prefixed the ancestor KeyEvents. /// For example: Stringified KeyEvent element for the 'goto_next_window' command could be "space>w>w". /// Ancestor KeyEvents are in this case "space" and "w". pub fn command_list(&self, mode: &Mode) -> CommandList { let mut list = HashMap::new(); - _command_list(&mut list, &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), &mut String::new()); + _command_list( + &mut list, + &KeyTrieNode::KeyTrie(self.get_keytrie(mode)), + &mut String::new(), + ); return list; fn _command_list(list: &mut CommandList, node: &KeyTrieNode, prefix: &mut String) { @@ -135,18 +138,21 @@ impl Keymap { KeyTrieNode::KeyTrie(trie_node) => { for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { + if &temp_prefix != "" { temp_prefix.push_str("→"); } temp_prefix.push_str(&key_event.to_string()); _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); - } - }, + } KeyTrieNode::MappableCommand(mappable_command) => { - if mappable_command.name() == "no_op" { return } - list.entry(mappable_command.name().to_string()).or_default().push(prefix.to_string()); - }, + if mappable_command.name() == "no_op" { + return; + } + list.entry(mappable_command.name().to_string()) + .or_default() + .push(prefix.to_string()); + } KeyTrieNode::CommandSequence(_) => {} }; } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 3fffaa08c517..24d53b8565d0 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -1,6 +1,6 @@ -use super::{macros::keytrie, keytrie::KeyTrie}; -use helix_view::document::Mode; +use super::{keytrie::KeyTrie, macros::keytrie}; use helix_core::hashmap; +use helix_view::document::Mode; use std::collections::HashMap; pub fn default() -> HashMap { diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 01c03ec9d1b1..d40002d09245 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -1,7 +1,7 @@ use super::keytrienode::KeyTrieNode; use helix_view::{info::Info, input::KeyEvent}; -use std::{collections::HashMap, cmp::Ordering}; use serde::Deserialize; +use std::{cmp::Ordering, collections::HashMap}; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] @@ -14,7 +14,11 @@ pub struct KeyTrie { } impl KeyTrie { - pub fn new(documentation: &str, child_order: HashMap, children: Vec) -> Self { + pub fn new( + documentation: &str, + child_order: HashMap, + children: Vec, + ) -> Self { Self { documentation: documentation.to_string(), child_order, @@ -35,17 +39,20 @@ impl KeyTrie { pub fn traverse(&self, key_events: &[KeyEvent]) -> Option { return _traverse(self, key_events, 0); - fn _traverse(keytrie: &KeyTrie, key_events: &[KeyEvent], mut depth: usize) -> Option { + fn _traverse( + keytrie: &KeyTrie, + key_events: &[KeyEvent], + mut depth: usize, + ) -> Option { if depth == key_events.len() { return Some(KeyTrieNode::KeyTrie(keytrie.clone())); - } - else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { + } else if let Some(found_index) = keytrie.child_order.get(&key_events[depth]) { match &keytrie.children[*found_index] { KeyTrieNode::KeyTrie(sub_keytrie) => { depth += 1; - return _traverse(sub_keytrie, key_events, depth) - }, - _found_child => return Some(_found_child.clone()) + return _traverse(sub_keytrie, key_events, depth); + } + _found_child => return Some(_found_child.clone()), } } return None; @@ -58,28 +65,31 @@ impl KeyTrie { match other_child_keytrie_node { KeyTrieNode::KeyTrie(ref other_child_keytrie) => { if let Some(self_index) = self.child_order.get(&other_key_event) { - if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { + if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = + self.children[*self_index] + { self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); } - } - else { - self.child_order.insert(*other_key_event, self.children.len()); - self.children.push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); + self.children + .push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); } } KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { if let Some(existing_index) = self.child_order.get(other_key_event) { self.children[*existing_index] = other_child_keytrie_node.clone(); - } - else { - self.child_order.insert(*other_key_event, self.children.len()); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); self.children.push(other_child_keytrie_node.clone()); } } } } } - + // IMPROVEMENT: cache sorting and update cache only when config is updated /// Open an info box for a given KeyTrie /// Shows the children as possible KeyEvents and thier associated description. @@ -87,7 +97,9 @@ impl KeyTrie { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length - unsafe { key_event_order.set_len(self.children.len()); } + unsafe { + key_event_order.set_len(self.children.len()); + } for (key_event, index) in &self.child_order { key_event_order[*index] = key_event.clone(); } @@ -99,7 +111,7 @@ impl KeyTrie { continue; } command.description() - }, + } KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, // FIX: default to a join of all command names // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. @@ -107,11 +119,12 @@ impl KeyTrie { KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; let key_event = key_event_order[index]; - match body.iter().position(|(_, existing_documentation)| &documentation == existing_documentation) { - Some(position) => body[position].0.push(key_event.to_string()), - None => { - body.push((vec![key_event.to_string()], documentation)) - }, + match body + .iter() + .position(|(_, existing_documentation)| &documentation == existing_documentation) + { + Some(position) => body[position].0.push(key_event.to_string()), + None => body.push((vec![key_event.to_string()], documentation)), } } @@ -120,20 +133,24 @@ impl KeyTrie { // Those events will always be placed after the one letter KeyEvent for (key_events, _) in body.iter_mut() { key_events.sort_unstable_by(|a, b| { - if a.len() == 1 { return Ordering::Less } + if a.len() == 1 { + return Ordering::Less; + } if b.len() > a.len() && b.starts_with("C-") { - return Ordering::Greater + return Ordering::Greater; } a.len().cmp(&b.len()) }); } - if sort_infobox { body = keyevent_sort_infobox(body); } + if sort_infobox { + body = keyevent_sort_infobox(body); + } let stringified_key_events_body: Vec<(String, &str)> = body - .iter().map(|(key_events, description)| { - (key_events.join(", "), *description) - }).collect(); + .iter() + .map(|(key_events, description)| (key_events.join(", "), *description)) + .collect(); Info::new(&self.documentation, &stringified_key_events_body) } @@ -165,7 +182,7 @@ impl<'de> Deserialize<'de> for KeyTrie { children.push(keytrie_node); } - Ok(Self { + Ok(Self { child_order, children, ..Default::default() @@ -177,14 +194,15 @@ impl<'de> Deserialize<'de> for KeyTrie { type InfoBoxRow<'a> = (Vec, &'a str); type InfoBoxBody<'a> = Vec>; /// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. -/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are +/// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are /// placed together, and alphas are placed before the rest. fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { - use std::str::FromStr; - use std::collections::BTreeMap; use helix_view::keyboard::{KeyCode, KeyModifiers, MediaKeyCode}; + use std::collections::BTreeMap; + use std::str::FromStr; - let mut category_holder: BTreeMap>> = BTreeMap::new(); + let mut category_holder: BTreeMap>> = + BTreeMap::new(); let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); @@ -196,27 +214,20 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant // Hence the use of mock Variant values let keycode_category = match first_keyevent.code { - KeyCode::Char(_) => { - KeyCode::Char('a') - } - KeyCode::F(_) => { - KeyCode::F(0) - } - KeyCode::Media(_) => { - KeyCode::Media(MediaKeyCode::Play) - } - other_keycode => { - other_keycode - } + KeyCode::Char(_) => KeyCode::Char('a'), + KeyCode::F(_) => KeyCode::F(0), + KeyCode::Media(_) => KeyCode::Media(MediaKeyCode::Play), + other_keycode => other_keycode, }; - - let modifier_category = category_holder.get_mut(&first_keyevent.modifiers) + let modifier_category = category_holder + .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); if !modifier_category.contains_key(&keycode_category) { modifier_category.insert(keycode_category.clone(), Vec::new()); } - modifier_category.get_mut(&keycode_category) + modifier_category + .get_mut(&keycode_category) .expect("key existence should be checked") .push(infobox_row); } @@ -225,12 +236,14 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { for (keycode_category, mut infobox_rows) in keycode_categories { if infobox_rows.len() > 1 { match keycode_category { - KeyCode::Char(_) => { - infobox_rows.sort_unstable_by(|a, b| a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase())); + KeyCode::Char(_) => { + infobox_rows.sort_unstable_by(|a, b| { + a.0[0].to_lowercase().cmp(&b.0[0].to_lowercase()) + }); // Consistently place lowercase before uppercase of the same letter. let mut x_index = 0; - let mut y_index = 1; + let mut y_index = 1; while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; @@ -238,9 +251,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { if x < y { infobox_rows.swap(x_index, y_index); } - } - x_index = y_index; - y_index += 1; + } + x_index = y_index; + y_index += 1; } // TEMP: until drain_filter becomes stable. Migth also be worth implementing @@ -254,19 +267,18 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { .is_some() { alphas.push(infobox_row); - } - else { + } else { misc.push(infobox_row); } } infobox_rows = Vec::with_capacity(alphas.len() + misc.len()); for alpha_row in alphas { - infobox_rows.push(alpha_row); + infobox_rows.push(alpha_row); } for misc_row in misc { - infobox_rows.push(misc_row); + infobox_rows.push(misc_row); } - }, + } _ => { infobox_rows.sort_unstable(); } diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 66a642ac902d..5511c49dd7c6 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -1,8 +1,8 @@ use super::keytrie::KeyTrie; use crate::commands::MappableCommand; use helix_view::input::KeyEvent; +use serde::{de::Visitor, Deserialize}; use std::collections::HashMap; -use serde::{Deserialize, de::Visitor}; /// Each variant includes a documentaion property. /// For the MappableCommand and CommandSequence variants, the property is self explanatory. @@ -29,14 +29,14 @@ impl PartialEq for KeyTrieNode { match (self, other) { (KeyTrieNode::MappableCommand(_self), KeyTrieNode::MappableCommand(_other)) => { _self == _other - }, + } (KeyTrieNode::CommandSequence(_self), KeyTrieNode::CommandSequence(_other)) => { - _self == _other - }, + _self == _other + } (KeyTrieNode::KeyTrie(_self), KeyTrieNode::KeyTrie(_other)) => { _self.get_children() == _other.get_children() - }, - _ => false + } + _ => false, } } } @@ -85,6 +85,10 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { child_order.insert(key_event, children.len()); children.push(key_trie_node); } - Ok(KeyTrieNode::KeyTrie(KeyTrie::new("", child_order, children))) + Ok(KeyTrieNode::KeyTrie(KeyTrie::new( + "", + child_order, + children, + ))) } -} \ No newline at end of file +} diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs index b834c43e56aa..c8f52128e3e4 100644 --- a/helix-term/src/keymap/macros.rs +++ b/helix-term/src/keymap/macros.rs @@ -116,5 +116,5 @@ macro_rules! keytrie { pub use alt; pub use ctrl; pub use key; -pub use shift; pub use keytrie; +pub use shift; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 4ab431c6e31d..bc5e061892bb 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,9 +1,9 @@ #[macro_use] #[cfg(test)] mod tests { + use crate::keymap::{macros::*, *}; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; - use crate::keymap::{*, macros::*}; use std::collections::HashMap; #[test] @@ -25,8 +25,12 @@ mod tests { fn aliased_modes_are_same_in_default_keymap() { let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); assert_eq!( - normal_mode_keytrie_root.traverse(&[key!(' '), key!('w')]).unwrap(), - normal_mode_keytrie_root.traverse(&["C-w".parse::().unwrap()]).unwrap(), + normal_mode_keytrie_root + .traverse(&[key!(' '), key!('w')]) + .unwrap(), + normal_mode_keytrie_root + .traverse(&["C-w".parse::().unwrap()]) + .unwrap(), "Mismatch for window mode on `Space-w` and `Ctrl-w`." ); assert_eq!( @@ -47,7 +51,9 @@ mod tests { "j" | "k" => move_line_down, }); - let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new(hashmap!(Mode::Normal => normal_mode))))); + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( + hashmap!(Mode::Normal => normal_mode), + )))); let mut command_list = keymap.command_list(&Mode::Normal); // sort keybindings in order to have consistent tests @@ -60,10 +66,7 @@ mod tests { assert_eq!( command_list, HashMap::from([ - ( - "insert_mode".to_string(), - vec![key!('i').to_string()] - ), + ("insert_mode".to_string(), vec![key!('i').to_string()]), ( "goto_file_start".to_string(), vec![format!("{}→{}", key!('g'), key!('g'))] @@ -80,4 +83,4 @@ mod tests { "Mismatch" ) } -} \ No newline at end of file +} diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 94ead3f0f0e6..f4cfd51cf429 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::{KeymapResult, Keymap}, + keymap::{Keymap, KeymapResult}, ui::{Completion, ProgressSpinners}, }; @@ -931,7 +931,10 @@ impl EditorView { self.pseudo_pending.extend(self.keymap.pending()); let key_result = self.keymap.get(mode, event); let sort_infobox = cxt.editor.config.load().sorted_infobox; - cxt.editor.autoinfo = self.keymap.sticky_keytrie().map(|node| node.infobox(sort_infobox)); + cxt.editor.autoinfo = self + .keymap + .sticky_keytrie() + .map(|node| node.infobox(sort_infobox)); let mut execute_command = |command: &commands::MappableCommand| { command.execute(cxt); From 7735b348081e2e379e6e0617e9440af9955e2327 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 17:46:17 +0100 Subject: [PATCH 125/195] Use description conistenly --- helix-term/src/keymap/keytrie.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index d40002d09245..dc08695678d5 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -6,7 +6,7 @@ use std::{cmp::Ordering, collections::HashMap}; /// Edges of the trie are KeyEvents and the nodes are descrbibed by KeyTrieNode #[derive(Debug, Clone)] pub struct KeyTrie { - documentation: String, + description: String, /// Used for pre-defined order in infoboxes, values represent the index of the key tries children. child_order: HashMap, children: Vec, @@ -15,12 +15,12 @@ pub struct KeyTrie { impl KeyTrie { pub fn new( - documentation: &str, + description: &str, child_order: HashMap, children: Vec, ) -> Self { Self { - documentation: documentation.to_string(), + description: description.to_string(), child_order, children, is_sticky: false, @@ -105,26 +105,26 @@ impl KeyTrie { } for (index, key_trie) in self.children.iter().enumerate() { - let documentation: &str = match key_trie { + let description: &str = match key_trie { KeyTrieNode::MappableCommand(ref command) => { if command.name() == "no_op" { continue; } command.description() } - KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.documentation, + KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.description, // FIX: default to a join of all command names - // NOTE: Giving same documentation for all sequences will place all sequence keyvents together. + // NOTE: Giving same description for all sequences will place all sequence keyvents together. // Regardless if the command sequence is different. KeyTrieNode::CommandSequence(_) => "[Multiple commands]", }; let key_event = key_event_order[index]; match body .iter() - .position(|(_, existing_documentation)| &documentation == existing_documentation) + .position(|(_, existing_description)| &description == existing_description) { Some(position) => body[position].0.push(key_event.to_string()), - None => body.push((vec![key_event.to_string()], documentation)), + None => body.push((vec![key_event.to_string()], description)), } } @@ -152,7 +152,7 @@ impl KeyTrie { .map(|(key_events, description)| (key_events.join(", "), *description)) .collect(); - Info::new(&self.documentation, &stringified_key_events_body) + Info::new(&self.description, &stringified_key_events_body) } } From 8e1a3dad60f20fd2d6f1ef2ec8cae4dd521aa05e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Mon, 23 Jan 2023 23:03:56 +0100 Subject: [PATCH 126/195] Fix undeterministic test failure --- helix-term/src/config.rs | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 296526aeb71e..63c6b65d1a90 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -64,17 +64,18 @@ mod tests { use crate::{ commands::MappableCommand, config::Config, - keymap::{default, keytrienode::KeyTrieNode, macros::*}, + keymap::{default, keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::*}, }; use helix_core::hashmap; - use helix_view::document::Mode; + use helix_view::{document::Mode, input::KeyEvent}; + use std::collections::BTreeMap; #[test] fn parses_keymap_from_toml() { let sample_keymaps = r#" [keys.insert] - S-C-a = "delete_selection" y = "move_line_down" + S-C-a = "delete_selection" [keys.normal] A-F12 = "move_next_word_end" @@ -83,8 +84,8 @@ mod tests { let config = Config { keys: hashmap! { Mode::Insert => keytrie!({ "Insert mode" - "S-C-a" => delete_selection, "y" => move_line_down, + "S-C-a" => delete_selection, }), Mode::Normal => keytrie!({ "Normal mode" "A-F12" => move_next_word_end, @@ -92,17 +93,30 @@ mod tests { }, ..Default::default() }; + for mode in config.keys.keys() { + // toml keymap config is placed into a hashmap, so order can not be presumed to be conserved + // hence the insertion into a BTreeMap assert_eq!( - config.keys.get(mode).unwrap().get_children(), - toml::from_str::(sample_keymaps) - .unwrap() - .keys - .get(mode) - .unwrap() - .get_children() + ordered_mapping(config.keys.get(mode).unwrap()), + ordered_mapping( + toml::from_str::(sample_keymaps) + .unwrap() + .keys + .get(mode) + .unwrap() + ) ); } + + fn ordered_mapping<'a>(keytrie: &'a KeyTrie) -> BTreeMap<&'a KeyEvent, KeyTrieNode> { + let children = keytrie.get_children(); + let mut ordered_keymap = BTreeMap::new(); + for (key_event, order) in keytrie.get_child_order() { + ordered_keymap.insert(key_event, children[*order].clone()); + } + ordered_keymap + } } #[test] From 16e2d74bbd25175f5cd707cde4d01f2250e27a08 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 00:06:24 +0100 Subject: [PATCH 127/195] Implement clippy tips --- helix-term/src/keymap.rs | 16 ++++++++-------- helix-term/src/keymap/keytrie.rs | 22 ++++++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index c6b3f75b56c8..96fa7cc6a589 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -74,7 +74,7 @@ impl Keymap { // Check if sticky keytrie is to be used. let starting_keytrie = match self.sticky_keytrie { - None => &active_keymap, + None => active_keymap, Some(ref active_sticky_keytrie) => active_sticky_keytrie, }; @@ -84,10 +84,10 @@ impl Keymap { let pending_keytrie: KeyTrie = match starting_keytrie.traverse(&[*first_key]) { Some(KeyTrieNode::KeyTrie(sub_keytrie)) => sub_keytrie, Some(KeyTrieNode::MappableCommand(cmd)) => { - return KeymapResult::Matched(cmd.clone()); + return KeymapResult::Matched(cmd); } Some(KeyTrieNode::CommandSequence(cmds)) => { - return KeymapResult::MatchedCommandSequence(cmds.clone()); + return KeymapResult::MatchedCommandSequence(cmds); } None => return KeymapResult::NotFound, }; @@ -99,15 +99,15 @@ impl Keymap { self.pending_keys.clear(); self.sticky_keytrie = Some(map.clone()); } - KeymapResult::Pending(map.clone()) + KeymapResult::Pending(map) } Some(KeyTrieNode::MappableCommand(cmd)) => { self.pending_keys.clear(); - KeymapResult::Matched(cmd.clone()) + KeymapResult::Matched(cmd) } Some(KeyTrieNode::CommandSequence(cmds)) => { self.pending_keys.clear(); - KeymapResult::MatchedCommandSequence(cmds.clone()) + KeymapResult::MatchedCommandSequence(cmds) } None => KeymapResult::Cancelled(self.pending_keys.drain(..).collect()), } @@ -138,8 +138,8 @@ impl Keymap { KeyTrieNode::KeyTrie(trie_node) => { for (key_event, index) in trie_node.get_child_order() { let mut temp_prefix: String = prefix.to_string(); - if &temp_prefix != "" { - temp_prefix.push_str("→"); + if !&temp_prefix.is_empty() { + temp_prefix.push('→'); } temp_prefix.push_str(&key_event.to_string()); _command_list(list, &trie_node.get_children()[*index], &mut temp_prefix); diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index dc08695678d5..6bf4ebf86c45 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -55,7 +55,7 @@ impl KeyTrie { _found_child => return Some(_found_child.clone()), } } - return None; + None } } @@ -64,7 +64,7 @@ impl KeyTrie { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; match other_child_keytrie_node { KeyTrieNode::KeyTrie(ref other_child_keytrie) => { - if let Some(self_index) = self.child_order.get(&other_key_event) { + if let Some(self_index) = self.child_order.get(other_key_event) { if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] { @@ -97,11 +97,12 @@ impl KeyTrie { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); // child_order and children is of same length + #[allow(clippy::uninit_vec)] unsafe { key_event_order.set_len(self.children.len()); } for (key_event, index) in &self.child_order { - key_event_order[*index] = key_event.clone(); + key_event_order[*index] = key_event; } for (index, key_trie) in self.children.iter().enumerate() { @@ -206,9 +207,7 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); - if !category_holder.contains_key(&first_keyevent.modifiers) { - category_holder.insert(first_keyevent.modifiers.clone(), BTreeMap::new()); - } + category_holder.entry(first_keyevent.modifiers).or_insert_with(BTreeMap::new); // HACK: inserting by variant not by variant value. // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant @@ -223,9 +222,7 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let modifier_category = category_holder .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); - if !modifier_category.contains_key(&keycode_category) { - modifier_category.insert(keycode_category.clone(), Vec::new()); - } + modifier_category.entry(keycode_category).or_insert_with(Vec::new); modifier_category .get_mut(&keycode_category) .expect("key existence should be checked") @@ -247,10 +244,8 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() { - if x < y { + if x.to_lowercase() == y.to_lowercase() && x < y{ infobox_rows.swap(x_index, y_index); - } } x_index = y_index; y_index += 1; @@ -263,8 +258,7 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { for infobox_row in infobox_rows { if ('a'..='z') .map(|char| char.to_string()) - .find(|alpha_char| *alpha_char == infobox_row.0[0].to_lowercase()) - .is_some() + .any(|alpha_char| *alpha_char == infobox_row.0[0].to_lowercase()) { alphas.push(infobox_row); } else { From dc8e463be2c42c33863592f6caf4d7a436f759c6 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 00:14:25 +0100 Subject: [PATCH 128/195] Make cargo fmt happy --- helix-term/src/keymap/keytrie.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 6bf4ebf86c45..25ed9e8e2537 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -207,7 +207,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let mut sorted_body: InfoBoxBody = Vec::with_capacity(body.len()); for infobox_row in body { let first_keyevent = KeyEvent::from_str(infobox_row.0[0].as_str()).unwrap(); - category_holder.entry(first_keyevent.modifiers).or_insert_with(BTreeMap::new); + category_holder + .entry(first_keyevent.modifiers) + .or_insert_with(BTreeMap::new); // HACK: inserting by variant not by variant value. // KeyCode:: Char, F, and MediaKeys can have muiltiple values for the given variant @@ -222,7 +224,9 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { let modifier_category = category_holder .get_mut(&first_keyevent.modifiers) .expect("keycode category existence should be checked."); - modifier_category.entry(keycode_category).or_insert_with(Vec::new); + modifier_category + .entry(keycode_category) + .or_insert_with(Vec::new); modifier_category .get_mut(&keycode_category) .expect("key existence should be checked") @@ -244,8 +248,8 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { while y_index < infobox_rows.len() { let x = &infobox_rows[x_index].0[0]; let y = &infobox_rows[y_index].0[0]; - if x.to_lowercase() == y.to_lowercase() && x < y{ - infobox_rows.swap(x_index, y_index); + if x.to_lowercase() == y.to_lowercase() && x < y { + infobox_rows.swap(x_index, y_index); } x_index = y_index; y_index += 1; From 4bfa470722859c5fbe3dcd3943a05855b6101907 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 11:32:12 +0100 Subject: [PATCH 129/195] Make updated cargo clippy happy --- helix-term/src/config.rs | 18 +--- helix-term/src/keymap.rs | 1 + helix-term/src/keymap/tests.rs | 154 ++++++++++++++++----------------- 3 files changed, 80 insertions(+), 93 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 63c6b65d1a90..01b7e3f419d1 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -109,7 +109,7 @@ mod tests { ); } - fn ordered_mapping<'a>(keytrie: &'a KeyTrie) -> BTreeMap<&'a KeyEvent, KeyTrieNode> { + fn ordered_mapping(keytrie: &KeyTrie) -> BTreeMap<&KeyEvent, KeyTrieNode> { let children = keytrie.get_children(); let mut ordered_keymap = BTreeMap::new(); for (key_event, order) in keytrie.get_child_order() { @@ -194,22 +194,10 @@ mod tests { // Huh? assert!( - merged_config - .keys - .get(&Mode::Normal) - .unwrap() - .get_children() - .len() - > 1 + merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1 ); assert!( - merged_config - .keys - .get(&Mode::Insert) - .unwrap() - .get_children() - .len() - > 0 + !merged_config.keys.get(&Mode::Insert).unwrap().get_children().is_empty() ); } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 96fa7cc6a589..2a77d0ca66cc 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -3,6 +3,7 @@ pub mod macros; // NOTE: Only pub becuase of their use in macros pub mod keytrie; pub mod keytrienode; +#[cfg(test)] mod tests; use self::{keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::key}; diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index bc5e061892bb..32dbc305770f 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,86 +1,84 @@ -#[macro_use] -#[cfg(test)] -mod tests { - use crate::keymap::{macros::*, *}; - use helix_core::hashmap; - use helix_view::{document::Mode, input::KeyEvent}; - use std::collections::HashMap; +use std::{sync::Arc, collections::HashMap}; +use arc_swap::ArcSwap; +use helix_core::hashmap; +use helix_view::{document::Mode, input::KeyEvent}; +use crate::key; +use super::{Keymap, macros::keytrie}; - #[test] - #[should_panic] - fn duplicate_keys_should_panic() { - keytrie!({ "Normal mode" - "i" => normal_mode, - "i" => goto_definition, - }); - } - - #[test] - fn check_duplicate_keys_in_default_keymap() { - // will panic on duplicate keys, assumes that `Keymap` uses keymap! macro - Keymap::default(); - } +#[test] +#[should_panic] +fn duplicate_keys_should_panic() { + keytrie!({ "Normal mode" + "i" => normal_mode, + "i" => goto_definition, + }); +} - #[test] - fn aliased_modes_are_same_in_default_keymap() { - let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); - assert_eq!( - normal_mode_keytrie_root - .traverse(&[key!(' '), key!('w')]) - .unwrap(), - normal_mode_keytrie_root - .traverse(&["C-w".parse::().unwrap()]) - .unwrap(), - "Mismatch for window mode on `Space-w` and `Ctrl-w`." - ); - assert_eq!( - normal_mode_keytrie_root.traverse(&[key!('z')]).unwrap(), - normal_mode_keytrie_root.traverse(&[key!('Z')]).unwrap(), - "Mismatch for view mode on `z` and `Z`." - ); - } +#[test] +fn check_duplicate_keys_in_default_keymap() { + // will panic on duplicate keys, assumes that `Keymap` uses keymap! macro + Keymap::default(); +} - #[test] - fn command_list() { - let normal_mode = keytrie!({ "Normal mode" - "i" => insert_mode, - "g" => { "Goto" - "g" => goto_file_start, - "e" => goto_file_end, - }, - "j" | "k" => move_line_down, - }); +#[test] +fn aliased_modes_are_same_in_default_keymap() { + let normal_mode_keytrie_root = Keymap::default().get_keytrie(&Mode::Normal); + assert_eq!( + normal_mode_keytrie_root + .traverse(&[key!(' '), key!('w')]) + .unwrap(), + normal_mode_keytrie_root + .traverse(&["C-w".parse::().unwrap()]) + .unwrap(), + "Mismatch for window mode on `Space-w` and `Ctrl-w`." + ); + assert_eq!( + normal_mode_keytrie_root.traverse(&[key!('z')]).unwrap(), + normal_mode_keytrie_root.traverse(&[key!('Z')]).unwrap(), + "Mismatch for view mode on `z` and `Z`." + ); +} - let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( - hashmap!(Mode::Normal => normal_mode), - )))); - let mut command_list = keymap.command_list(&Mode::Normal); +#[test] +fn command_list() { + let normal_mode = keytrie!({ "Normal mode" + "i" => insert_mode, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_file_end, + }, + "j" | "k" => move_line_down, + }); - // sort keybindings in order to have consistent tests - // HashMaps can be compared but we can still get different ordering of bindings - // for commands that have multiple bindings assigned - for v in command_list.values_mut() { - v.sort() - } + let keymap = Keymap::new(Box::new(ArcSwap::new(Arc::new( + hashmap!(Mode::Normal => normal_mode), + )))); + let mut command_list = keymap.command_list(&Mode::Normal); - assert_eq!( - command_list, - HashMap::from([ - ("insert_mode".to_string(), vec![key!('i').to_string()]), - ( - "goto_file_start".to_string(), - vec![format!("{}→{}", key!('g'), key!('g'))] - ), - ( - "goto_file_end".to_string(), - vec![format!("{}→{}", key!('g'), key!('e'))] - ), - ( - "move_line_down".to_string(), - vec![key!('j').to_string(), key!('k').to_string()] - ) - ]), - "Mismatch" - ) + // sort keybindings in order to have consistent tests + // HashMaps can be compared but we can still get different ordering of bindings + // for commands that have multiple bindings assigned + for v in command_list.values_mut() { + v.sort() } + + assert_eq!( + command_list, + HashMap::from([ + ("insert_mode".to_string(), vec![key!('i').to_string()]), + ( + "goto_file_start".to_string(), + vec![format!("{}→{}", key!('g'), key!('g'))] + ), + ( + "goto_file_end".to_string(), + vec![format!("{}→{}", key!('g'), key!('e'))] + ), + ( + "move_line_down".to_string(), + vec![key!('j').to_string(), key!('k').to_string()] + ) + ]), + "Mismatch" + ) } From 03088b2b67a518ef708dc5878b001c7e6cf2d782 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 24 Jan 2023 12:15:32 +0100 Subject: [PATCH 130/195] Fight with cargo fmt round 3 --- helix-term/src/config.rs | 17 +++++++++++++---- helix-term/src/keymap/tests.rs | 6 +++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 01b7e3f419d1..4c8bbe144d5a 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -194,10 +194,19 @@ mod tests { // Huh? assert!( - merged_config.keys.get(&Mode::Normal).unwrap().get_children().len() > 1 - ); - assert!( - !merged_config.keys.get(&Mode::Insert).unwrap().get_children().is_empty() + merged_config + .keys + .get(&Mode::Normal) + .unwrap() + .get_children() + .len() + > 1 ); + assert!(!merged_config + .keys + .get(&Mode::Insert) + .unwrap() + .get_children() + .is_empty()); } } diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index 32dbc305770f..a0c52de6f77c 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,9 +1,9 @@ -use std::{sync::Arc, collections::HashMap}; +use super::{macros::keytrie, Keymap}; +use crate::key; use arc_swap::ArcSwap; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; -use crate::key; -use super::{Keymap, macros::keytrie}; +use std::{collections::HashMap, sync::Arc}; #[test] #[should_panic] From 4bf9f824f73eb9af37c53b5f1f2653f48e61619e Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 15:30:41 +0100 Subject: [PATCH 131/195] Attempt race condition fix for RUNTIME dirs. --- helix-loader/src/lib.rs | 119 +++++++++++++++++++--------------------- helix-view/src/theme.rs | 7 ++- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index f6adbbcff3c9..11ac43962887 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -8,9 +8,28 @@ use std::path::{Path, PathBuf}; pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); -static RUNTIME_DIRS: once_cell::sync::OnceCell> = once_cell::sync::OnceCell::new(); -static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); static LOG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); +static RUNTIME_DIRS: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(set_runtime_dirs); + +pub fn log_file() -> PathBuf { + match LOG_FILE.get() { + Some(log_path) => log_path.to_path_buf(), + None => { + setup_log_file(None); + log_file() + } + } +} + +// TODO: allow env var override +pub fn cache_dir() -> PathBuf { + choose_base_strategy() + .expect("Unable to determine system base directory specification!") + .cache_dir() + .join("helix") +} pub fn config_file() -> PathBuf { match CONFIG_FILE.get() { @@ -22,14 +41,41 @@ pub fn config_file() -> PathBuf { } } -pub fn log_file() -> PathBuf { - match LOG_FILE.get() { - Some(log_path) => log_path.to_path_buf(), - None => { - setup_log_file(None); - log_file() - } - } +// TODO: allow env var override +pub fn user_config_dir() -> PathBuf { + choose_base_strategy() + .expect("Unable to determine system base directory specification!") + .config_dir() + .join("helix") +} + +/// Returns a non-existent path relative to the local executable if none are found. +pub fn get_runtime_file(relative_path: &Path) -> PathBuf { + get_runtime_dirs() + .iter() + .find_map(|runtime_dir| { + let path = runtime_dir.join(relative_path); + match path.exists() { + true => Some(path), + false => None, + } + }) + .unwrap_or_else(|| { + get_runtime_dirs() + .last() + .expect("Path to local executable.") + .join(relative_path) + }) +} + +pub fn get_first_runtime_dir() -> &'static PathBuf { + get_runtime_dirs() + .first() + .expect("should return at least one directory") +} + +pub fn get_runtime_dirs() -> &'static [PathBuf] { + &RUNTIME_DIRS } pub fn user_lang_config_file() -> PathBuf { @@ -58,38 +104,13 @@ pub fn setup_log_file(specified_file: Option) { LOG_FILE.set(log_file).ok(); } -// TODO: allow env var override -pub fn user_config_dir() -> PathBuf { - choose_base_strategy() - .expect("Unable to determine system base directory specification!") - .config_dir() - .join("helix") -} - -// TODO: allow env var override -pub fn cache_dir() -> PathBuf { - choose_base_strategy() - .expect("Unable to determine system base directory specification!") - .cache_dir() - .join("helix") -} - /// Runtime directory location priority: /// 1. Sibling directory to `CARGO_MANIFEST_DIR`, given that environment variable is set. (Often done by cargo) // TODO: XDG_RUNTIME_DIR /// 2. Under user config directory, given that it exists. /// 3. `HELIX_RUNTIME`, given that the environment variable is set. /// 4. Under path to helix executable, always included. However, it might not exist. -pub fn get_runtime_dirs() -> &'static [PathBuf] { - if let Some(runtime_dirs) = RUNTIME_DIRS.get() { - runtime_dirs - } else { - RUNTIME_DIRS.set(_runtime_dirs()).unwrap(); - get_runtime_dirs() - } -} - -fn _runtime_dirs() -> Vec { +fn set_runtime_dirs() -> Vec { let mut runtime_dirs = Vec::new(); const RUNTIME_DIR_NAME: &str = "runtime"; if std::env::var("CARGO_MANIFEST_DIR").is_ok() { @@ -122,32 +143,6 @@ fn _runtime_dirs() -> Vec { runtime_dirs } -pub fn get_first_runtime_dir() -> &'static PathBuf { - get_runtime_dirs() - .first() - .expect("should return at least one directory") -} - -/// Search for a file in the runtime directories. -/// Returns a non-existent path relative to the local executable if none are found. -pub fn get_runtime_file(relative_path: &Path) -> PathBuf { - get_runtime_dirs() - .iter() - .find_map(|runtime_dir| { - let path = runtime_dir.join(relative_path); - match path.exists() { - true => Some(path), - false => None, - } - }) - .unwrap_or_else(|| { - get_runtime_dirs() - .last() - .expect("Path to local executable.") - .join(relative_path) - }) -} - pub fn merged_config() -> Result { let config_paths: Vec = local_config_dirs() .into_iter() diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 90678e7fa5b8..480d2a9253db 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -147,8 +147,11 @@ impl Loader { .unwrap_or_default() } - - fn find_remaining_path(&self, name: &str, visited_paths: &mut HashSet) -> Option { + fn find_remaining_path( + &self, + name: &str, + visited_paths: &mut HashSet, + ) -> Option { let filename = format!("{}.toml", name); self.theme_dirs.iter().find_map(|dir| { From 816a9dd06dcb5e2dc976d4ec7cdb4df6824a19cf Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 19:12:26 +0100 Subject: [PATCH 132/195] Grammar fetch/build into OS data directory --- helix-loader/src/lib.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index dd71aa431805..bbfe4e1231e0 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -25,10 +25,7 @@ pub fn log_file() -> PathBuf { // TODO: allow env var override pub fn cache_dir() -> PathBuf { - choose_base_strategy() - .expect("Unable to determine system base directory specification!") - .cache_dir() - .join("helix") + get_base_stategy().cache_dir().join("helix") } pub fn config_file() -> PathBuf { @@ -43,10 +40,7 @@ pub fn config_file() -> PathBuf { // TODO: allow env var override pub fn user_config_dir() -> PathBuf { - choose_base_strategy() - .expect("Unable to determine system base directory specification!") - .config_dir() - .join("helix") + get_base_stategy().config_dir().join("helix") } /// Returns a non-existent path relative to the local executable if none are found. @@ -105,11 +99,11 @@ pub fn setup_log_file(specified_file: Option) { } /// Runtime directory location priority: -/// 1. Sibling directory to `CARGO_MANIFEST_DIR`, given that environment variable is set. (Often done by cargo) -// TODO: XDG_RUNTIME_DIR -/// 2. Under user config directory, given that it exists. -/// 3. `HELIX_RUNTIME`, given that the environment variable is set. -/// 4. Under path to helix executable, always included. However, it might not exist. +/// 1. Sibling directory to `$CARGO_MANIFEST_DIR`, if set. (Often done by cargo) +/// 2. User data directory `$XDG_RUNTIME_DIR`/`%AppData%`, if it exists. +/// 3. Under user config directory, given that it exists. +/// 4. `$HELIX_RUNTIME`, if set. +/// 5. Under path to helix executable, always included. However, it might not exist. fn set_runtime_dirs() -> Vec { let mut runtime_dirs = Vec::new(); const RUNTIME_DIR_NAME: &str = "runtime"; @@ -119,6 +113,11 @@ fn set_runtime_dirs() -> Vec { runtime_dirs.push(path); } + let data_dir = get_base_stategy().data_dir(); + if data_dir.exists() { + runtime_dirs.push(data_dir.join("helix").join(RUNTIME_DIR_NAME)); + } + let conf_dir = user_config_dir().join(RUNTIME_DIR_NAME); if conf_dir.exists() { runtime_dirs.push(conf_dir); @@ -143,6 +142,10 @@ fn set_runtime_dirs() -> Vec { runtime_dirs } +fn get_base_stategy() -> impl BaseStrategy { + choose_base_strategy().expect("Unable to determine system base directory specification!") +} + pub fn merged_config() -> Result { let config_paths: Vec = local_config_dirs() .into_iter() From eb31bd7e43252471f3ac736d78913bae10c8abc1 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 19:40:28 +0100 Subject: [PATCH 133/195] Proper runtime directory health diagnostics Instead of blurting out, say "does not exist", without refererring to which directory the diagnostic relates to. --- helix-term/src/health.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index bbce86b7327e..4a005e997da2 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -42,25 +42,23 @@ fn display_paths() -> std::io::Result<()> { let rt_dirs = helix_loader::get_runtime_dirs(); writeln!( stdout, - "Runtime directories: {}", - rt_dirs - .iter() - .map(|rt_dir| rt_dir.to_string_lossy()) - .collect::>() - .join(";") + "Runtime directories by order of priority:", )?; for rt_dir in rt_dirs { + write!(stdout, "- {};", rt_dir.display())?; + if let Ok(path) = std::fs::read_link(&rt_dir) { - let msg = format!("Runtime directory is symlinked to {}", path.display()); - writeln!(stdout, "{}", msg.yellow())?; - } - if !rt_dir.exists() { - writeln!(stdout, "{}", "Runtime directory does not exist.".red())?; + let msg = format!(" (symlinked to {})", path.display()); + write!(stdout, "{}", msg.yellow())?; } if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { - writeln!(stdout, "{}", "Runtime directory is empty.".red())?; + write!(stdout, "{}", " is empty.".yellow())?; } + if !rt_dir.exists() { + write!(stdout, "{}", " does not exist.".red())?; + } + writeln!(stdout)?; } Ok(()) From 13a9f3edaef2d58b96ef90b418a1e0c8ef967e69 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 20:28:12 +0100 Subject: [PATCH 134/195] merge escaped keymap test --- helix-term/src/keymap.rs | 40 ---------------------------- helix-term/src/keymap/keytrienode.rs | 2 +- helix-term/src/keymap/tests.rs | 35 ++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 187cbb656b73..7f6fb234693c 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -17,7 +17,6 @@ use arc_swap::{ use helix_view::{document::Mode, input::KeyEvent}; use std::{collections::HashMap, sync::Arc}; - #[derive(Debug, Clone, PartialEq)] pub enum KeymapResult { Pending(KeyTrie), @@ -166,43 +165,4 @@ impl Default for Keymap { fn default() -> Self { Self::new(Box::new(ArcSwap::new(Arc::new(default::default())))) } - - #[test] - fn escaped_keymap() { - use crate::commands::MappableCommand; - use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; - - let keys = r#" -"+" = [ - "select_all", - ":pipe sed -E 's/\\s+$//g'", -] - "#; - - let key = KeyEvent { - code: KeyCode::Char('+'), - modifiers: KeyModifiers::NONE, - }; - - let expectation = Keymap::new(KeyTrie::Node(KeyTrieNode::new( - "", - hashmap! { - key => KeyTrie::Sequence(vec!{ - MappableCommand::select_all, - MappableCommand::Typable { - name: "pipe".to_string(), - args: vec!{ - "sed".to_string(), - "-E".to_string(), - "'s/\\s+$//g'".to_string() - }, - doc: "".to_string(), - }, - }) - }, - vec![key], - ))); - - assert_eq!(toml::from_str(keys), Ok(expectation)); - } } diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 5511c49dd7c6..18696b1eccb8 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -65,7 +65,7 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { S: serde::de::SeqAccess<'de>, { let mut commands = Vec::new(); - while let Some(command) = seq.next_element::<&str>()? { + while let Some(command) = seq.next_element::()? { commands.push( command .parse::() diff --git a/helix-term/src/keymap/tests.rs b/helix-term/src/keymap/tests.rs index a0c52de6f77c..18a008b28be1 100644 --- a/helix-term/src/keymap/tests.rs +++ b/helix-term/src/keymap/tests.rs @@ -1,5 +1,8 @@ -use super::{macros::keytrie, Keymap}; -use crate::key; +use crate::{ + commands::MappableCommand, + key, + keymap::{keytrie::KeyTrie, keytrienode::KeyTrieNode, macros::keytrie, Keymap}, +}; use arc_swap::ArcSwap; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; @@ -82,3 +85,31 @@ fn command_list() { "Mismatch" ) } + +#[test] +fn escaped_keymap() { + let parsed_keytrie: KeyTrie = toml::from_str( + r#" +"+" = [ + "select_all", + ":pipe sed -E 's/\\s+$//g'", +] + "#, + ) + .unwrap(); + + let command_sequence = KeyTrieNode::CommandSequence(vec![ + MappableCommand::select_all, + MappableCommand::Typable { + name: "pipe".to_string(), + args: vec![ + "sed".to_string(), + "-E".to_string(), + "'s/\\s+$//g'".to_string(), + ], + description: "".to_string(), + }, + ]); + + assert_eq!(parsed_keytrie.get_children()[0], command_sequence); +} From c5e505e6c1376f56908f8623a007f6decd461785 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 20:36:01 +0100 Subject: [PATCH 135/195] cargo fmt --- helix-term/src/health.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 4a005e997da2..fc92494334f1 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -40,10 +40,7 @@ fn display_paths() -> std::io::Result<()> { writeln!(stdout, "Log file: {}", helix_loader::log_file().display())?; let rt_dirs = helix_loader::get_runtime_dirs(); - writeln!( - stdout, - "Runtime directories by order of priority:", - )?; + writeln!(stdout, "Runtime directories by order of priority:",)?; for rt_dir in rt_dirs { write!(stdout, "- {};", rt_dir.display())?; From 8dd2169d81ef74bad187e1a8eead89a1d86ca4bb Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 22:52:16 +0100 Subject: [PATCH 136/195] Initial alternative solution to #5203 --- helix-term/src/keymap/keytrie.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 25ed9e8e2537..cbdd241d1b9d 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -106,18 +106,23 @@ impl KeyTrie { } for (index, key_trie) in self.children.iter().enumerate() { - let description: &str = match key_trie { + let description: String = match key_trie { KeyTrieNode::MappableCommand(ref command) => { if command.name() == "no_op" { continue; } - command.description() + command.description().to_string() } - KeyTrieNode::KeyTrie(ref key_trie) => &key_trie.description, // FIX: default to a join of all command names // NOTE: Giving same description for all sequences will place all sequence keyvents together. // Regardless if the command sequence is different. - KeyTrieNode::CommandSequence(_) => "[Multiple commands]", + KeyTrieNode::CommandSequence(ref command_sequence) => command_sequence + .iter() + .map(|command| command.name().to_string()) + .collect::>() + .join(" → ") + .clone(), + KeyTrieNode::KeyTrie(key_trie) => key_trie.description.clone(), }; let key_event = key_event_order[index]; match body @@ -148,10 +153,11 @@ impl KeyTrie { body = keyevent_sort_infobox(body); } - let stringified_key_events_body: Vec<(String, &str)> = body - .iter() - .map(|(key_events, description)| (key_events.join(", "), *description)) - .collect(); + // TODO: create InfoboxBody collect + let mut stringified_key_events_body = Vec::with_capacity(body.len()); + for (key_events, description) in body { + stringified_key_events_body.push((key_events.join(", "), description)); + } Info::new(&self.description, &stringified_key_events_body) } @@ -192,8 +198,8 @@ impl<'de> Deserialize<'de> for KeyTrie { } // (KeyEvents, Description) -type InfoBoxRow<'a> = (Vec, &'a str); -type InfoBoxBody<'a> = Vec>; +type InfoBoxRow = (Vec, String); +type InfoBoxBody = Vec; /// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. /// KeyCode::Char sorting is special in that lower-case and upper-case equivalents are /// placed together, and alphas are placed before the rest. From b8e1ee46b1709765172070875ddce0a1385214ec Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 23:06:38 +0100 Subject: [PATCH 137/195] Scrapped FromIterator idea Removes the ability to create the Vec using with_capacity if I'm not mistaken. --- helix-term/src/keymap/keytrie.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index cbdd241d1b9d..9c7f484b1986 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -90,9 +90,9 @@ impl KeyTrie { } } - // IMPROVEMENT: cache sorting and update cache only when config is updated + // IMPROVEMENT: cache contents and update cache only when config is updated /// Open an info box for a given KeyTrie - /// Shows the children as possible KeyEvents and thier associated description. + /// Shows the children as possible KeyEvents with thier associated description. pub fn infobox(&self, sort_infobox: bool) -> Info { let mut body: InfoBoxBody = Vec::with_capacity(self.children.len()); let mut key_event_order = Vec::with_capacity(self.children.len()); @@ -113,9 +113,6 @@ impl KeyTrie { } command.description().to_string() } - // FIX: default to a join of all command names - // NOTE: Giving same description for all sequences will place all sequence keyvents together. - // Regardless if the command sequence is different. KeyTrieNode::CommandSequence(ref command_sequence) => command_sequence .iter() .map(|command| command.name().to_string()) @@ -153,7 +150,6 @@ impl KeyTrie { body = keyevent_sort_infobox(body); } - // TODO: create InfoboxBody collect let mut stringified_key_events_body = Vec::with_capacity(body.len()); for (key_events, description) in body { stringified_key_events_body.push((key_events.join(", "), description)); @@ -197,7 +193,7 @@ impl<'de> Deserialize<'de> for KeyTrie { } } -// (KeyEvents, Description) +/// (KeyEvents as strings, Description) type InfoBoxRow = (Vec, String); type InfoBoxBody = Vec; /// Sorts by `ModifierKeyCode`, then by each `KeyCode` category, then by each `KeyEvent`. @@ -261,8 +257,6 @@ fn keyevent_sort_infobox(body: InfoBoxBody) -> InfoBoxBody { y_index += 1; } - // TEMP: until drain_filter becomes stable. Migth also be worth implementing - // FromIterator on InfoboxBody by then, last two for loops could then also be replaced by Iter::chain let mut alphas = Vec::new(); let mut misc = Vec::new(); for infobox_row in infobox_rows { From 375238fa50d742fdffd2a28dec6652cfcf629293 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 25 Jan 2023 23:53:03 +0100 Subject: [PATCH 138/195] Infobox: Typaple commands w/ arguments formatting --- helix-term/src/commands.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e6e9e778630e..1d3541f1f477 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -482,11 +482,12 @@ impl std::str::FromStr for MappableCommand { let args = typable_command .map(|s| s.to_owned()) .collect::>(); + typed::TYPABLE_COMMAND_MAP .get(name) .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), - description: format!(":{} {:?}", cmd.name, args), + description: format!(":{} {}", cmd.name, args.join(" ")), args, }) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) @@ -2475,14 +2476,12 @@ impl ui::menu::Item for MappableCommand { fn format(&self, command_list: &Self::Data) -> Row { match self { MappableCommand::Typable { - description: doc, - name, - .. + description, name, .. } => { let mut row: Vec = vec![ - Cell::from(&*name.as_str()), + Cell::from(name.as_str()), Cell::from(""), - Cell::from(&*doc.as_str()), + Cell::from(description.as_str()), ]; match command_list.get(name as &String) { Some(key_events) => { From dc06713eec57b878ac4d0a48360106a845b1d802 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 26 Jan 2023 14:28:56 -0500 Subject: [PATCH 139/195] health: add formatter binary This adds a new output to --health that displays the formatter binary. --- helix-term/src/health.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 6558fe19fb4c..818bc6505eab 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -262,6 +262,12 @@ pub fn language(lang_str: String) -> std::io::Result<()> { lang.debugger.as_ref().map(|dap| dap.command.to_string()), )?; + probe_commands( + lang.formatter + .as_ref() + .map(|fmtcfg| fmtcfg.command.to_string()), + )?; + for ts_feat in TsFeature::all() { probe_treesitter_feature(&lang_str, *ts_feat)? } @@ -269,6 +275,23 @@ pub fn language(lang_str: String) -> std::io::Result<()> { Ok(()) } +/// Display any additional binaries that are configured as commands for the +/// language. +fn probe_commands(formatter_cmd: Option) -> std::io::Result<()> { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + + if let Some(cmd) = formatter_cmd { + let path = match which::which(&cmd) { + Ok(path) => path.display().to_string().green(), + Err(_) => format!("'{}' not found in $PATH", cmd).red(), + }; + writeln!(stdout, "Binary for formatter: {}", path)?; + } + + Ok(()) +} + /// Display diagnostics about LSP and DAP. fn probe_protocol(protocol_name: &str, server_cmd: Option) -> std::io::Result<()> { let stdout = std::io::stdout(); From 1e6c605cf6dee877a1a09e7f12c8b4c21d69c231 Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Sun, 12 Feb 2023 11:24:49 +0100 Subject: [PATCH 140/195] Add prefix position to file args A cursor position can now be specified using common prefix notation used by vim, nvim, kak, emacs and nano. eg. `hx +10 Cargo.toml` This prefix notation also allows the user to set the cursor at the end of the file. eg. `hx +: Cargo.toml` This allows `hx` to play nicely with other tools that want to open a file at a given position using prefix notation. The existing postfix notation can still be used (`hx Cargo.toml:10`) but has been extended to support jumping to the end of a buffer. eg. `hx Cargo.toml:` --- Cargo.lock | 7 + helix-term/Cargo.toml | 1 + helix-term/src/application.rs | 6 +- helix-term/src/args.rs | 423 +++++++++++++++++++++++++------ helix-term/src/commands.rs | 2 +- helix-term/src/commands/typed.rs | 7 +- helix-term/src/main.rs | 6 +- 7 files changed, 360 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a76beed265b..9c96cf4203d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "atoi" version = "2.0.0" @@ -1198,6 +1204,7 @@ version = "0.6.0" dependencies = [ "anyhow", "arc-swap", + "assert_matches", "chrono", "content_inspector", "crossterm", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 603f37d39ab8..4b7612fc8fec 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -77,6 +77,7 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] +assert_matches = "1.5.0" smallvec = "1.10" indoc = "2.0.0" tempfile = "3.3.0" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a1685fcfa956..fbcb1d2d4efe 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -3,7 +3,7 @@ use futures_util::Stream; use helix_core::{ diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, - pos_at_coords, syntax, Selection, + syntax, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ @@ -223,8 +223,8 @@ impl Application { // opened last is focused on. let view_id = editor.tree.focus; let doc = doc_mut!(editor, &doc_id); - let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); - doc.set_selection(view_id, pos); + let selection = pos.selection_for_doc(doc); + doc.set_selection(view_id, selection); } } editor.set_status(format!( diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index dd787f1fd18c..66461c2bc3c2 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -1,9 +1,58 @@ +use std::{borrow::Cow, iter::Peekable, path::PathBuf}; + use anyhow::Result; -use helix_core::Position; -use helix_view::tree::Layout; -use std::path::{Path, PathBuf}; +use helix_core::{pos_at_coords, Position, Selection}; +use helix_view::{tree::Layout, Document}; + +/// As files are parse from the CLI they can either contain an explicit postion or a request to +/// jump to the end of the file. An explicit postion can be declared in either prefix or postfix +/// notation. +/// +/// Prefix notation to open a file at line 10 `hx +10 Cargo.toml`. +/// Postfix notation to open a file at line 10 `hx Cargo.toml:10`. +/// +/// Both notation can also be used to place ther cursor at the last line / Eof. +/// +/// `hx +: Cargo.toml` +/// `hx Cargo.toml:` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PositionRequest { + /// Set the file cursor to the given postion, + Explicit(Position), + /// Set the file cursor to the last line when opened + Eof, +} + +impl Default for PositionRequest { + fn default() -> Self { + PositionRequest::Explicit(Position::default()) + } +} -#[derive(Default)] +impl PositionRequest { + /// Return a Selection based on this PositionRequest. + pub(crate) fn selection_for_doc(self, doc: &Document) -> Selection { + let text = doc.text().slice(..); + match self { + PositionRequest::Explicit(pos) => { + let pos = pos_at_coords(text, pos, true); + Selection::point(pos) + } + PositionRequest::Eof => { + let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 { + // If the last line is blank, don't jump to it. + text.len_lines().saturating_sub(2) + } else { + text.len_lines() - 1 + }; + let pos = text.line_to_char(line_idx); + Selection::point(pos) + } + } + } +} + +#[derive(Debug, Default)] pub struct Args { pub display_help: bool, pub display_version: bool, @@ -16,106 +65,312 @@ pub struct Args { pub verbosity: u64, pub log_file: Option, pub config_file: Option, - pub files: Vec<(PathBuf, Position)>, + pub files: Vec<(PathBuf, PositionRequest)>, } impl Args { pub fn parse_args() -> Result { - let mut args = Args::default(); let mut argv = std::env::args().peekable(); + parse_args(&mut argv) + } +} - argv.next(); // skip the program, we don't care about that - - while let Some(arg) = argv.next() { - match arg.as_str() { - "--" => break, // stop parsing at this point treat the remaining as files - "--version" => args.display_version = true, - "--help" => args.display_help = true, - "--tutor" => args.load_tutor = true, - "--vsplit" => match args.split { - Some(_) => anyhow::bail!("can only set a split once of a specific type"), - None => args.split = Some(Layout::Vertical), - }, - "--hsplit" => match args.split { - Some(_) => anyhow::bail!("can only set a split once of a specific type"), - None => args.split = Some(Layout::Horizontal), - }, - "--health" => { - args.health = true; - args.health_arg = argv.next_if(|opt| !opt.starts_with('-')); - } - "-g" | "--grammar" => match argv.next().as_deref() { - Some("fetch") => args.fetch_grammars = true, - Some("build") => args.build_grammars = true, - _ => { - anyhow::bail!("--grammar must be followed by either 'fetch' or 'build'") - } - }, - "-c" | "--config" => match argv.next().as_deref() { - Some(path) => args.config_file = Some(path.into()), - None => anyhow::bail!("--config must specify a path to read"), - }, - "--log" => match argv.next().as_deref() { - Some(path) => args.log_file = Some(path.into()), - None => anyhow::bail!("--log must specify a path to write"), - }, - arg if arg.starts_with("--") => { - anyhow::bail!("unexpected double dash argument: {}", arg) +fn parse_args(argv: &mut Peekable>) -> Result { + let mut args = Args::default(); + argv.next(); // skip the program, we don't care about that + + while let Some(arg) = argv.next() { + match arg.as_str() { + "--" => break, // stop parsing at this point treat the remaining as files + "--version" => args.display_version = true, + "--help" => args.display_help = true, + "--tutor" => args.load_tutor = true, + "--vsplit" => match args.split { + Some(_) => anyhow::bail!("can only set a split once of a specific type"), + None => args.split = Some(Layout::Vertical), + }, + "--hsplit" => match args.split { + Some(_) => anyhow::bail!("can only set a split once of a specific type"), + None => args.split = Some(Layout::Horizontal), + }, + "--health" => { + args.health = true; + args.health_arg = argv.next_if(|opt| !opt.starts_with('-')); + } + "-g" | "--grammar" => match argv.next().as_deref() { + Some("fetch") => args.fetch_grammars = true, + Some("build") => args.build_grammars = true, + _ => { + anyhow::bail!("--grammar must be followed by either 'fetch' or 'build'") } - arg if arg.starts_with('-') => { - let arg = arg.get(1..).unwrap().chars(); - for chr in arg { - match chr { - 'v' => args.verbosity += 1, - 'V' => args.display_version = true, - 'h' => args.display_help = true, - _ => anyhow::bail!("unexpected short arg {}", chr), - } + }, + "-c" | "--config" => match argv.next().as_deref() { + Some(path) => args.config_file = Some(path.into()), + None => anyhow::bail!("--config must specify a path to read"), + }, + "--log" => match argv.next().as_deref() { + Some(path) => args.log_file = Some(path.into()), + None => anyhow::bail!("--log must specify a path to write"), + }, + arg if arg.starts_with("--") => { + anyhow::bail!("unexpected double dash argument: {}", arg) + } + arg if arg.starts_with('-') => { + let arg = arg.get(1..).unwrap().chars(); + for chr in arg { + match chr { + 'v' => args.verbosity += 1, + 'V' => args.display_version = true, + 'h' => args.display_help = true, + _ => anyhow::bail!("unexpected short arg {}", chr), } } - arg => args.files.push(parse_file(arg)), + } + _ => { + let file = parse_positional_arg(arg, argv)?; + args.files.push(file); } } + } + + // push the remaining args, if any to the files + while let Some(arg) = argv.next() { + let file = parse_positional_arg(arg, argv)?; + args.files.push(file); + } - // push the remaining args, if any to the files - for arg in argv { - args.files.push(parse_file(&arg)); + Ok(args) +} + +/// Parse a positional arg. All of which are expected to be file related. +/// If an arg is a prefixed file position, then the next arg is expected to be a file. +/// File paths are not validated, that's left to the consumer. +pub(crate) fn parse_positional_arg( + arg: String, + argv: &mut impl Iterator, +) -> Result<(PathBuf, PositionRequest)> { + let file = if arg.starts_with('+') { + let prefix_pos = parse_file_position(&arg[1..]); + let (path, postfix_pos) = match argv.next() { + Some(file) => parse_file(file), + None => anyhow::bail!("expected a file after a position"), + }; + + if postfix_pos.is_some() { + anyhow::bail!("unexpected postfix position after prefix position"); } - Ok(args) - } + (path, prefix_pos.unwrap_or_default()) + } else { + let (path, pos) = parse_file(arg); + (path, pos.unwrap_or_default()) + }; + + Ok(file) } -/// Parse arg into [`PathBuf`] and position. -pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) { - let def = || (PathBuf::from(s), Position::default()); - if Path::new(s).exists() { - return def(); +pub(crate) fn parse_file_position(s: &str) -> Option { + if s.chars().all(|c| c == ':') { + return Some(PositionRequest::Eof); } - split_path_row_col(s) - .or_else(|| split_path_row(s)) - .unwrap_or_else(def) -} -/// Split file.rs:10:2 into [`PathBuf`], row and col. -/// -/// Does not validate if file.rs is a file or directory. -fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> { - let mut s = s.rsplitn(3, ':'); - let col: usize = s.next()?.parse().ok()?; + let mut s = s.splitn(2, ':'); let row: usize = s.next()?.parse().ok()?; - let path = s.next()?.into(); + let col: usize = s.next().and_then(|x| x.parse().ok()).unwrap_or_default(); + let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); - Some((path, pos)) + Some(PositionRequest::Explicit(pos)) } -/// Split file.rs:10 into [`PathBuf`] and row. -/// -/// Does not validate if file.rs is a file or directory. -fn split_path_row(s: &str) -> Option<(PathBuf, Position)> { - let (path, row) = s.rsplit_once(':')?; - let row: usize = row.parse().ok()?; - let path = path.into(); - let pos = Position::new(row.saturating_sub(1), 0); - Some((path, pos)) +/// Parse arg into [`PathBuf`] and position. +pub(crate) fn parse_file<'a>(s: impl Into>) -> (PathBuf, Option) { + let s = s.into(); + match s.split_once(':') { + Some((s, rest)) => (s.into(), parse_file_position(rest)), + None => (s.into_owned().into(), None), + } +} + +#[cfg(test)] +mod tests { + use std::{iter::Peekable, path::PathBuf, str::FromStr}; + + use assert_matches::assert_matches; + + use helix_core::Position; + + use super::{parse_args, parse_file, parse_file_position, PositionRequest}; + + #[test] + fn should_parse_file() { + assert_matches!( + parse_file(""), + (path, None) if path == PathBuf::from_str("").unwrap() + ); + + assert_matches!( + parse_file(":"), + (path, Some(PositionRequest::Eof)) if path == PathBuf::from_str("").unwrap() + ); + + assert_matches!( + parse_file("file"), + (path, None) if path == PathBuf::from_str("file").unwrap() + ); + + assert_matches!( + parse_file("file:"), + (path, Some(PositionRequest::Eof)) if path == PathBuf::from_str("file").unwrap() + ); + + assert_matches!( + parse_file("file:10"), + (path, Some(PositionRequest::Explicit(Position { row: 9, col: 0 }))) + if path == PathBuf::from_str("file").unwrap() + ); + + assert_matches!( + parse_file("file:10:"), + (path, Some(PositionRequest::Explicit(Position { row: 9, col: 0 }))) + if path == PathBuf::from_str("file").unwrap() + ); + + assert_matches!( + parse_file("file:10:20"), + (path, Some(PositionRequest::Explicit(Position { row: 9, col: 19 }))) + if path == PathBuf::from_str("file").unwrap() + ); + } + + #[test] + fn should_parse_file_position() { + assert_matches!(parse_file_position(":"), Some(PositionRequest::Eof)); + assert_matches!(parse_file_position("::"), Some(PositionRequest::Eof)); + assert_matches!( + parse_file_position("10"), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ); + assert_matches!( + parse_file_position("10:"), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ); + assert_matches!( + parse_file_position("10:20"), + Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + ); + assert_matches!(parse_file_position("x"), None); + assert_matches!(parse_file_position("x:y"), None); + } + + #[test] + fn should_parse_positional_args() { + let args = parse_args(&mut str_to_argv("hx Cargo.toml")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position::default()) + ) + ); + + let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ) + ); + + let args = parse_args(&mut str_to_argv("hx +: Cargo.toml")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Eof + ) + ); + + let args = parse_args(&mut str_to_argv("hx Cargo.toml:")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Eof + ) + ); + + parse_args(&mut str_to_argv("hx +10")).unwrap_err(); + parse_args(&mut str_to_argv("hx +10 Cargo.toml +20")).unwrap_err(); + parse_args(&mut str_to_argv("hx +10 Cargo.toml:20")).unwrap_err(); + parse_args(&mut str_to_argv("hx +10 Cargo.toml:")).unwrap_err(); + + let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml +20 README")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ) + ); + assert_eq!( + args.files[1], + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ); + + let args = parse_args(&mut str_to_argv("hx -- Cargo.toml")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position::default()) + ) + ); + + let args = + parse_args(&mut str_to_argv("hx --vsplit -- +10 Cargo.toml +20 README")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ) + ); + assert_eq!( + args.files[1], + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ); + } + + #[test] + fn should_parse_config() { + let args = parse_args(&mut str_to_argv("hx --config other/config.toml")).unwrap(); + assert_eq!( + args.config_file, + Some(PathBuf::from_str("other/config.toml").unwrap()) + ); + } + + #[test] + fn should_parse_layout() { + let args = parse_args(&mut str_to_argv("hx --vsplit Cargo.toml")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + + let args = parse_args(&mut str_to_argv("hx --hsplit Cargo.toml")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Horizontal)); + + parse_args(&mut str_to_argv("hx --hsplit -vsplit Cargo.toml")).unwrap_err(); + } + + /// Return a peekable Iterator of arguments naively split on whitespace + fn str_to_argv(s: &'static str) -> Peekable> { + s.split_whitespace().map(ToOwned::to_owned).peekable() + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f8a96074df8b..ea55ca47fe40 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -18,7 +18,7 @@ use helix_core::{ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, move_vertically_visual, Direction}, - object, pos_at_coords, + object, regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, selection, shellwords, surround, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 0cc1b7432978..d8f013dbdf51 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -63,7 +63,8 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> ensure!(!args.is_empty(), "wrong argument count"); for arg in args { - let (path, pos) = args::parse_file(arg); + let (path, pos) = args::parse_file(arg.to_owned()); + let pos = pos.unwrap_or_default(); let path = helix_core::path::expand_tilde(&path); // If the path is a directory, open a file picker on that directory and update the status // message @@ -82,8 +83,8 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> // Otherwise, just open the file let _ = cx.editor.open(&path, Action::Replace)?; let (view, doc) = current!(cx.editor); - let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); - doc.set_selection(view.id, pos); + let selection = pos.selection_for_doc(doc); + doc.set_selection(view.id, selection); // does not affect opening a buffer without pos align_view(doc, view, Align::Center); } diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index aac5c5379f37..d48903763288 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -57,7 +57,10 @@ USAGE: hx [FLAGS] [files]... ARGS: - ... Sets the input file to use, position can also be specified via file[:row[:col]] + ... Open each file in a buffer. The cursor position can be specified via prefix + or postfix postion. `[+[:col]|+:] file_name` or `file_name[:row[:col]]|:`. + Prefixing the file name with a (`+:`) or postfixing with only a (`:`) will + position the cursor at the end of the file's buffer. FLAGS: -h, --help Prints help information @@ -73,6 +76,7 @@ FLAGS: -V, --version Prints version information --vsplit Splits all given files vertically into different windows --hsplit Splits all given files horizontally into different windows + ", env!("CARGO_PKG_NAME"), VERSION_AND_GIT_HASH, From 0a75108e6071d79d4ed45ea461ebef9589aad5bb Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:36:16 +0100 Subject: [PATCH 141/195] Fix clippies --- helix-term/src/args.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 66461c2bc3c2..0ffe139956ae 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -149,8 +149,8 @@ pub(crate) fn parse_positional_arg( arg: String, argv: &mut impl Iterator, ) -> Result<(PathBuf, PositionRequest)> { - let file = if arg.starts_with('+') { - let prefix_pos = parse_file_position(&arg[1..]); + let file = if let Some(s) = arg.strip_prefix('+') { + let prefix_pos = parse_file_position(s); let (path, postfix_pos) = match argv.next() { Some(file) => parse_file(file), None => anyhow::bail!("expected a file after a position"), From 461e31f2ca8059f19a387bb3ae5df21075c99dce Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:39:51 +0100 Subject: [PATCH 142/195] Fix typos --- helix-term/src/args.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 0ffe139956ae..cd042a0f2e71 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -4,20 +4,20 @@ use anyhow::Result; use helix_core::{pos_at_coords, Position, Selection}; use helix_view::{tree::Layout, Document}; -/// As files are parse from the CLI they can either contain an explicit postion or a request to -/// jump to the end of the file. An explicit postion can be declared in either prefix or postfix +/// As files are parsed from the CLI they can either contain an explicit position or a request to +/// jump to the end of the file. An explicit position can be declared in either prefix or postfix /// notation. /// /// Prefix notation to open a file at line 10 `hx +10 Cargo.toml`. /// Postfix notation to open a file at line 10 `hx Cargo.toml:10`. /// -/// Both notation can also be used to place ther cursor at the last line / Eof. +/// Both notation can also be used to place there cursor at the last line / Eof. /// /// `hx +: Cargo.toml` /// `hx Cargo.toml:` #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PositionRequest { - /// Set the file cursor to the given postion, + /// Set the file cursor to the given position, Explicit(Position), /// Set the file cursor to the last line when opened Eof, From 22e491e2bc73b510f72880d4c699c8f126376780 Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:44:56 +0100 Subject: [PATCH 143/195] Fix typos --- helix-term/src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index cd042a0f2e71..75d9726dd562 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -11,7 +11,7 @@ use helix_view::{tree::Layout, Document}; /// Prefix notation to open a file at line 10 `hx +10 Cargo.toml`. /// Postfix notation to open a file at line 10 `hx Cargo.toml:10`. /// -/// Both notation can also be used to place there cursor at the last line / Eof. +/// Both notation can also be used to place the cursor at the last line / Eof. /// /// `hx +: Cargo.toml` /// `hx Cargo.toml:` From 872f01ff05d52a0e8090636608f110ac696ccbd8 Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:44:06 +0100 Subject: [PATCH 144/195] Fix integration tests and split parse tests into fns --- helix-term/src/args.rs | 285 ++++++++++++++++++++------- helix-term/tests/integration.rs | 7 +- helix-term/tests/test/auto_indent.rs | 2 +- helix-term/tests/test/helpers.rs | 4 +- helix-term/tests/test/movement.rs | 8 +- 5 files changed, 228 insertions(+), 78 deletions(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 75d9726dd562..ded0f5520b98 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -23,6 +23,12 @@ pub enum PositionRequest { Eof, } +impl From for PositionRequest { + fn from(p: Position) -> Self { + Self::Explicit(p) + } +} + impl Default for PositionRequest { fn default() -> Self { PositionRequest::Explicit(Position::default()) @@ -170,16 +176,18 @@ pub(crate) fn parse_positional_arg( } pub(crate) fn parse_file_position(s: &str) -> Option { - if s.chars().all(|c| c == ':') { + let s = s.trim_matches(':'); + + if s.is_empty() { return Some(PositionRequest::Eof); } - let mut s = s.splitn(2, ':'); - let row: usize = s.next()?.parse().ok()?; - let col: usize = s.next().and_then(|x| x.parse().ok()).unwrap_or_default(); - + let (row, col) = s.split_once(':').unwrap_or((s, "1")); + let row: usize = row.parse().ok()?; + let col: usize = col.parse().ok()?; let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); - Some(PositionRequest::Explicit(pos)) + + Some(pos.into()) } /// Parse arg into [`PathBuf`] and position. @@ -202,39 +210,108 @@ mod tests { use super::{parse_args, parse_file, parse_file_position, PositionRequest}; #[test] - fn should_parse_file() { + fn should_parse_binary_only() { + parse_args(&mut str_to_argv("hx")).unwrap(); + } + + #[test] + fn should_parse_file_position_eof() { + assert_matches!(parse_file_position(":"), Some(PositionRequest::Eof)); + assert_matches!(parse_file_position("::"), Some(PositionRequest::Eof)); + } + + #[test] + fn should_parse_file_position_line_only() { + assert_matches!( + parse_file_position("10"), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ); + } + + #[test] + fn should_parse_file_position_line_only_with_trailing_delimiter() { + assert_matches!( + parse_file_position("10:"), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ); + } + + #[test] + fn should_parse_file_position_line_col() { + assert_matches!( + parse_file_position("10:20"), + Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + ); + } + + #[test] + fn should_parse_file_position_line_col_with_trailing_delimiter() { + assert_matches!( + parse_file_position("10:20:"), + Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + ); + } + + #[test] + fn should_given_none_if_any_pos_arg_invalid() { + assert_matches!(parse_file_position("x"), None); + assert_matches!(parse_file_position("x:y"), None); + assert_matches!(parse_file_position("10:y"), None); + assert_matches!(parse_file_position("x:20"), None); + } + + #[test] + fn should_parse_empty_file() { assert_matches!( parse_file(""), (path, None) if path == PathBuf::from_str("").unwrap() ); + } + #[test] + fn should_parse_empty_file_with_eof_pos() { assert_matches!( parse_file(":"), (path, Some(PositionRequest::Eof)) if path == PathBuf::from_str("").unwrap() ); + } + #[test] + fn should_parse_file_with_name_only() { assert_matches!( parse_file("file"), (path, None) if path == PathBuf::from_str("file").unwrap() ); + } + #[test] + fn should_parse_file_with_eof_pos() { assert_matches!( parse_file("file:"), (path, Some(PositionRequest::Eof)) if path == PathBuf::from_str("file").unwrap() ); + } + #[test] + fn should_parse_file_with_line_pos() { assert_matches!( parse_file("file:10"), (path, Some(PositionRequest::Explicit(Position { row: 9, col: 0 }))) if path == PathBuf::from_str("file").unwrap() ); + } + #[test] + fn should_parse_file_with_line_pos_and_trailing_delimiter() { assert_matches!( parse_file("file:10:"), (path, Some(PositionRequest::Explicit(Position { row: 9, col: 0 }))) if path == PathBuf::from_str("file").unwrap() ); + } + #[test] + fn should_parse_file_with_line_and_col_pos() { assert_matches!( parse_file("file:10:20"), (path, Some(PositionRequest::Explicit(Position { row: 9, col: 19 }))) @@ -243,109 +320,177 @@ mod tests { } #[test] - fn should_parse_file_position() { - assert_matches!(parse_file_position(":"), Some(PositionRequest::Eof)); - assert_matches!(parse_file_position("::"), Some(PositionRequest::Eof)); - assert_matches!( - parse_file_position("10"), - Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) - ); - assert_matches!( - parse_file_position("10:"), - Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + fn should_parse_bare_files_args() { + let args = parse_args(&mut str_to_argv("hx Cargo.toml")).unwrap(); + assert_eq!( + args.files[0], + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::default() + ) ); - assert_matches!( - parse_file_position("10:20"), - Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + + let args = parse_args(&mut str_to_argv("hx Cargo.toml README")).unwrap(); + assert_eq!( + args.files, + [ + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::default() + ), + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::default() + ) + ] ); - assert_matches!(parse_file_position("x"), None); - assert_matches!(parse_file_position("x:y"), None); - } - #[test] - fn should_parse_positional_args() { - let args = parse_args(&mut str_to_argv("hx Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_argv("hx -- Cargo.toml")).unwrap(); assert_eq!( args.files[0], ( PathBuf::from_str("Cargo.toml").unwrap(), - PositionRequest::Explicit(Position::default()) + PositionRequest::default() ) ); + } + #[test] + fn should_parse_prefix_pos_files() { let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml")).unwrap(); assert_eq!( - args.files[0], - ( + args.files, + [( PathBuf::from_str("Cargo.toml").unwrap(), PositionRequest::Explicit(Position { row: 9, col: 0 }) - ) + )] ); let args = parse_args(&mut str_to_argv("hx +: Cargo.toml")).unwrap(); assert_eq!( - args.files[0], - ( + args.files, + [( PathBuf::from_str("Cargo.toml").unwrap(), PositionRequest::Eof - ) + )] ); - let args = parse_args(&mut str_to_argv("hx Cargo.toml:")).unwrap(); + let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml +20 README")).unwrap(); assert_eq!( - args.files[0], - ( - PathBuf::from_str("Cargo.toml").unwrap(), - PositionRequest::Eof - ) + args.files, + [ + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] ); - parse_args(&mut str_to_argv("hx +10")).unwrap_err(); - parse_args(&mut str_to_argv("hx +10 Cargo.toml +20")).unwrap_err(); + let args = + parse_args(&mut str_to_argv("hx --vsplit -- +10 Cargo.toml +20 README")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + assert_eq!( + args.files, + [ + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + } + + #[test] + fn should_parse_intermixed_file_pos_notation() { + let args = parse_args(&mut str_to_argv("hx CHANGELOG +10 Cargo.toml README:20")).unwrap(); + assert_eq!( + args.files, + [ + ( + PathBuf::from_str("CHANGELOG").unwrap(), + PositionRequest::default(), + ), + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + } + + #[test] + fn should_fail_on_file_with_prefix_and_postfix_pos() { parse_args(&mut str_to_argv("hx +10 Cargo.toml:20")).unwrap_err(); parse_args(&mut str_to_argv("hx +10 Cargo.toml:")).unwrap_err(); + } - let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml +20 README")).unwrap(); + #[test] + fn should_fail_on_orphan_prefix_pos() { + parse_args(&mut str_to_argv("hx +10")).unwrap_err(); + parse_args(&mut str_to_argv("hx +10 Cargo.toml +20")).unwrap_err(); + } + + #[test] + fn should_parse_postfix_pos_files() { + let args = parse_args(&mut str_to_argv("hx Cargo.toml:10")).unwrap(); assert_eq!( - args.files[0], - ( + args.files, + [( PathBuf::from_str("Cargo.toml").unwrap(), PositionRequest::Explicit(Position { row: 9, col: 0 }) - ) - ); - assert_eq!( - args.files[1], - ( - PathBuf::from_str("README").unwrap(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) + )] ); - let args = parse_args(&mut str_to_argv("hx -- Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_argv("hx Cargo.toml:")).unwrap(); assert_eq!( - args.files[0], - ( + args.files, + [( PathBuf::from_str("Cargo.toml").unwrap(), - PositionRequest::Explicit(Position::default()) - ) + PositionRequest::Eof + )] ); - let args = - parse_args(&mut str_to_argv("hx --vsplit -- +10 Cargo.toml +20 README")).unwrap(); - assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + let args = parse_args(&mut str_to_argv("hx Cargo.toml:10 README:20")).unwrap(); assert_eq!( - args.files[0], - ( - PathBuf::from_str("Cargo.toml").unwrap(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - ) + args.files, + [ + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] ); + + let args = parse_args(&mut str_to_argv("hx --vsplit -- Cargo.toml:10 README:20")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); assert_eq!( - args.files[1], - ( - PathBuf::from_str("README").unwrap(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) + args.files, + [ + ( + PathBuf::from_str("Cargo.toml").unwrap(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + PathBuf::from_str("README").unwrap(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] ); } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index a378af7a9b25..4780e832b1e0 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -4,8 +4,11 @@ mod test { use std::path::PathBuf; - use helix_core::{syntax::AutoPairConfig, Position, Selection}; - use helix_term::{args::Args, config::Config}; + use helix_core::{syntax::AutoPairConfig, Selection}; + use helix_term::{ + args::{Args, PositionRequest}, + config::Config, + }; use indoc::indoc; diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs index 2d9082853dcf..1a2c1788633a 100644 --- a/helix-term/tests/test/auto_indent.rs +++ b/helix-term/tests/test/auto_indent.rs @@ -4,7 +4,7 @@ use super::*; async fn auto_indent_c() -> anyhow::Result<()> { test_with_config( Args { - files: vec![(PathBuf::from("foo.c"), Position::default())], + files: vec![(PathBuf::from("foo.c"), PositionRequest::default())], ..Default::default() }, helpers::test_config(), diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index fb12ef12cff2..3eab21e94057 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -279,7 +279,9 @@ impl AppBuilder { path: P, pos: Option, ) -> Self { - self.args.files.push((path.into(), pos.unwrap_or_default())); + self.args + .files + .push((path.into(), pos.unwrap_or_default().into())); self } diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index e6ea3f951592..649af197066c 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -414,7 +414,7 @@ async fn cursor_position_append_eof() -> anyhow::Result<()> { async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::Result<()> { test_with_config( Args { - files: vec![(PathBuf::from("foo.rs"), Position::default())], + files: vec![(PathBuf::from("foo.rs"), PositionRequest::default())], ..Default::default() }, Config::default(), @@ -446,7 +446,7 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow:: async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Result<()> { test_with_config( Args { - files: vec![(PathBuf::from("foo.rs"), Position::default())], + files: vec![(PathBuf::from("foo.rs"), PositionRequest::default())], ..Default::default() }, Config::default(), @@ -479,7 +479,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any // Note: the anchor stays put and the head moves back. test_with_config( Args { - files: vec![(PathBuf::from("foo.rs"), Position::default())], + files: vec![(PathBuf::from("foo.rs"), PositionRequest::default())], ..Default::default() }, Config::default(), @@ -510,7 +510,7 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any test_with_config( Args { - files: vec![(PathBuf::from("foo.rs"), Position::default())], + files: vec![(PathBuf::from("foo.rs"), PositionRequest::default())], ..Default::default() }, Config::default(), From 0622cdc320090c183c3634bd90d0d6b7ebe694ea Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:57:26 +0100 Subject: [PATCH 145/195] Rm assert_matches dep --- Cargo.lock | 7 ----- helix-term/Cargo.toml | 1 - helix-term/src/args.rs | 70 ++++++++++++++++++++++-------------------- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c96cf4203d9..1a76beed265b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,12 +61,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" -[[package]] -name = "assert_matches" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" - [[package]] name = "atoi" version = "2.0.0" @@ -1204,7 +1198,6 @@ version = "0.6.0" dependencies = [ "anyhow", "arc-swap", - "assert_matches", "chrono", "content_inspector", "crossterm", diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 4b7612fc8fec..603f37d39ab8 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -77,7 +77,6 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] -assert_matches = "1.5.0" smallvec = "1.10" indoc = "2.0.0" tempfile = "3.3.0" diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index ded0f5520b98..02ef8929d2c2 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -11,7 +11,7 @@ use helix_view::{tree::Layout, Document}; /// Prefix notation to open a file at line 10 `hx +10 Cargo.toml`. /// Postfix notation to open a file at line 10 `hx Cargo.toml:10`. /// -/// Both notation can also be used to place the cursor at the last line / Eof. +/// Both notations can also be used to place the cursor at the last line / Eof. /// /// `hx +: Cargo.toml` /// `hx Cargo.toml:` @@ -203,8 +203,6 @@ pub(crate) fn parse_file<'a>(s: impl Into>) -> (PathBuf, Option Date: Mon, 13 Feb 2023 11:02:47 +0100 Subject: [PATCH 146/195] Rm file indexing in tests --- helix-term/src/args.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 02ef8929d2c2..85ed16a67277 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -327,11 +327,11 @@ mod tests { fn should_parse_bare_files_args() { let args = parse_args(&mut str_to_argv("hx Cargo.toml")).unwrap(); assert_eq!( - args.files[0], - ( + args.files, + [( PathBuf::from_str("Cargo.toml").unwrap(), PositionRequest::default() - ) + )] ); let args = parse_args(&mut str_to_argv("hx Cargo.toml README")).unwrap(); @@ -351,11 +351,11 @@ mod tests { let args = parse_args(&mut str_to_argv("hx -- Cargo.toml")).unwrap(); assert_eq!( - args.files[0], - ( + args.files, + [( PathBuf::from_str("Cargo.toml").unwrap(), PositionRequest::default() - ) + )] ); } From e480a16ad3b48e09c222636752a81d3a8d5b3e49 Mon Sep 17 00:00:00 2001 From: Brian Heylin <3947+bheylin@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:07:50 +0100 Subject: [PATCH 147/195] Rephrase PathBug instances in tests --- helix-term/src/args.rs | 83 ++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 55 deletions(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 85ed16a67277..e00e0d3ae053 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -201,7 +201,7 @@ pub(crate) fn parse_file<'a>(s: impl Into>) -> (PathBuf, Option Date: Tue, 14 Feb 2023 00:37:11 +0100 Subject: [PATCH 148/195] fix clippy warnigs --- helix-term/src/config.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 2ebd097f60b1..65ac4e037895 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -136,11 +136,9 @@ mod tests { let parsed_description = parsed_node.get_description().unwrap(); assert_eq!(parsed_description, "Edit Config"); - if let KeyTrieNode::MappableCommand(command) = parsed_node { - if let MappableCommand::Typable { name, .. } = command { - assert_eq!(name, "open".to_string()); - return; - } + if let KeyTrieNode::MappableCommand(MappableCommand::Typable { name, .. }) = parsed_node { + assert_eq!(name, "open".to_string()); + return; } panic!("KeyTrieNode::MappableCommand::Typable expected.") } @@ -190,7 +188,6 @@ mod tests { assert_eq!(parsed_description, "Buffer menu"); if let KeyTrieNode::KeyTrie(_) = parsed_node { - return; } else { panic!("KeyTrieNode::KeyTrie expected.") } From 56189c924d2e6ba591b8b4dad3d9674b93c30ed9 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 14 Feb 2023 12:28:29 +0100 Subject: [PATCH 149/195] fix cargo doc warnings --- helix-loader/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index bbfe4e1231e0..4fbe63d61b2a 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -216,16 +216,16 @@ fn merge_toml_by_config_paths(config_paths: Vec) -> Result toml::Value { From f44a34875c1d7aabf9169a976497d2847c3ee24f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 14 Feb 2023 13:01:34 +0100 Subject: [PATCH 150/195] Sorted infobox: place A- before C- bindings --- helix-term/src/keymap/keytrie.rs | 3 +-- helix-view/src/keyboard.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index e6529fc9be50..7916caccefa9 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -143,7 +143,7 @@ impl KeyTrie { } // TODO: Add "A-" acknowledgement? - // Shortest keyevent (as string) appears first, unless is a "C-" KeyEvent + // Shortest keyevent (as string) appears first, unless it's a "C-" KeyEvent // Those events will always be placed after the one letter KeyEvent for (key_events, _) in body.iter_mut() { key_events.sort_unstable_by(|a, b| { @@ -189,7 +189,6 @@ impl<'de> Deserialize<'de> for KeyTrie { { // NOTE: no assumption of pre-defined order in config let child_collection = HashMap::::deserialize(deserializer)?; - // TODO: common pattern, generalize (found in keytrinode deserialize too) let mut child_order = HashMap::::new(); let mut children = Vec::new(); for (key_event, keytrie_node) in child_collection { diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 20e592d36c28..858d7ecd1f51 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -4,8 +4,8 @@ bitflags! { /// Represents key modifiers (shift, control, alt). pub struct KeyModifiers: u8 { const SHIFT = 0b0000_0001; - const CONTROL = 0b0000_0010; - const ALT = 0b0000_0100; + const ALT = 0b0000_0010; + const CONTROL = 0b0000_0100; const NONE = 0b0000_0000; } } From b62d9a434928010a329b98b5ed77fc54b3ce5cc2 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 14 Feb 2023 13:06:39 +0100 Subject: [PATCH 151/195] keyboard.rs: remove reduntant comments --- helix-view/src/keyboard.rs | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 858d7ecd1f51..14d4c6c8b1fa 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -1,7 +1,6 @@ use bitflags::bitflags; bitflags! { - /// Represents key modifiers (shift, control, alt). pub struct KeyModifiers: u8 { const SHIFT = 0b0000_0001; const ALT = 0b0000_0010; @@ -55,31 +54,18 @@ impl From for KeyModifiers { /// Represents a media key (as part of [`KeyCode::Media`]). #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum MediaKeyCode { - /// Play media key. Play, - /// Pause media key. Pause, - /// Play/Pause media key. PlayPause, - /// Reverse media key. Reverse, - /// Stop media key. Stop, - /// Fast-forward media key. FastForward, - /// Rewind media key. Rewind, - /// Next-track media key. TrackNext, - /// Previous-track media key. TrackPrevious, - /// Record media key. Record, - /// Lower-volume media key. LowerVolume, - /// Raise-volume media key. RaiseVolume, - /// Mute media key. MuteVolume, } @@ -132,33 +118,19 @@ impl From for MediaKeyCode { /// Represents a media key (as part of [`KeyCode::Modifier`]). #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum ModifierKeyCode { - /// Left Shift key. LeftShift, - /// Left Control key. LeftControl, - /// Left Alt key. LeftAlt, - /// Left Super key. LeftSuper, - /// Left Hyper key. LeftHyper, - /// Left Meta key. LeftMeta, - /// Right Shift key. RightShift, - /// Right Control key. RightControl, - /// Right Alt key. RightAlt, - /// Right Super key. RightSuper, - /// Right Hyper key. RightHyper, - /// Right Meta key. RightMeta, - /// Iso Level3 Shift key. IsoLevel3Shift, - /// Iso Level5 Shift key. IsoLevel5Shift, } @@ -210,15 +182,10 @@ impl From for ModifierKeyCode { } } -/// Represents a key. /// Variant order determines order in keymap infobox if sorted_infobox is set to true. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum KeyCode { - /// Character key. - /// Ex: `KeyCode::Char('c')` represents the `c` character. Char(char), - /// Function key. - /// Ex: `KeyCode::F(1)` represents the F1 key. F(u8), Up, Down, From b69a7d4893276ccdac6cf34bc483bd86138bcfa7 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 14 Feb 2023 16:07:05 +0100 Subject: [PATCH 152/195] args.rs: remove reduntant comments --- helix-term/src/args.rs | 74 ++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 521a73caebd9..0784f8b308c1 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -4,22 +4,9 @@ use anyhow::Result; use helix_core::{pos_at_coords, Position, Selection}; use helix_view::{tree::Layout, Document}; -/// As files are parsed from the CLI they can either contain an explicit position or a request to -/// jump to the end of the file. An explicit position can be declared in either prefix or postfix -/// notation. -/// -/// Prefix notation to open a file at line 10 `hx +10 Cargo.toml`. -/// Postfix notation to open a file at line 10 `hx Cargo.toml:10`. -/// -/// Both notations can also be used to place the cursor at the last line / Eof. -/// -/// `hx +: Cargo.toml` -/// `hx Cargo.toml:` #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PositionRequest { - /// Set the file cursor to the given position, Explicit(Position), - /// Set the file cursor to the last line when opened Eof, } @@ -36,7 +23,6 @@ impl Default for PositionRequest { } impl PositionRequest { - /// Return a Selection based on this PositionRequest. pub(crate) fn selection_for_doc(self, doc: &Document) -> Selection { let text = doc.text().slice(..); match self { @@ -131,7 +117,7 @@ fn parse_args(argv: &mut Peekable>) -> Result _ => anyhow::bail!("unexpected short arg {}", chr), } } - } + } _ => { let file = parse_positional_arg(arg, argv)?; args.files.push(file); @@ -139,7 +125,6 @@ fn parse_args(argv: &mut Peekable>) -> Result } } - // push the remaining args, if any to the files while let Some(arg) = argv.next() { let file = parse_positional_arg(arg, argv)?; args.files.push(file); @@ -148,7 +133,6 @@ fn parse_args(argv: &mut Peekable>) -> Result Ok(args) } -/// Parse a positional arg. All of which are expected to be file related. /// If an arg is a prefixed file position, then the next arg is expected to be a file. /// File paths are not validated, that's left to the consumer. pub(crate) fn parse_positional_arg( @@ -190,7 +174,6 @@ pub(crate) fn parse_file_position(s: &str) -> Option { Some(pos.into()) } -/// Parse arg into [`PathBuf`] and position. pub(crate) fn parse_file<'a>(s: impl Into>) -> (PathBuf, Option) { let s = s.into(); match s.split_once(':') { @@ -209,7 +192,7 @@ mod tests { #[test] fn should_parse_binary_only() { - parse_args(&mut str_to_argv("hx")).unwrap(); + parse_args(&mut str_to_arg_peekable("hx")).unwrap(); } #[test] @@ -319,13 +302,13 @@ mod tests { #[test] fn should_parse_bare_files_args() { - let args = parse_args(&mut str_to_argv("hx Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml")).unwrap(); assert_eq!( args.files, [("Cargo.toml".to_owned().into(), PositionRequest::default())] ); - let args = parse_args(&mut str_to_argv("hx Cargo.toml README")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml README")).unwrap(); assert_eq!( args.files, [ @@ -334,7 +317,7 @@ mod tests { ] ); - let args = parse_args(&mut str_to_argv("hx -- Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx -- Cargo.toml")).unwrap(); assert_eq!( args.files, [("Cargo.toml".to_owned().into(), PositionRequest::default())] @@ -343,7 +326,7 @@ mod tests { #[test] fn should_parse_prefix_pos_files() { - let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml")).unwrap(); assert_eq!( args.files, [( @@ -352,13 +335,13 @@ mod tests { )] ); - let args = parse_args(&mut str_to_argv("hx +: Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx +: Cargo.toml")).unwrap(); assert_eq!( args.files, [("Cargo.toml".to_owned().into(), PositionRequest::Eof)] ); - let args = parse_args(&mut str_to_argv("hx +10 Cargo.toml +20 README")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml +20 README")).unwrap(); assert_eq!( args.files, [ @@ -373,8 +356,10 @@ mod tests { ] ); - let args = - parse_args(&mut str_to_argv("hx --vsplit -- +10 Cargo.toml +20 README")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable( + "hx --vsplit -- +10 Cargo.toml +20 README", + )) + .unwrap(); assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); assert_eq!( args.files, @@ -393,7 +378,10 @@ mod tests { #[test] fn should_parse_intermixed_file_pos_notation() { - let args = parse_args(&mut str_to_argv("hx CHANGELOG +10 Cargo.toml README:20")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable( + "hx CHANGELOG +10 Cargo.toml README:20", + )) + .unwrap(); assert_eq!( args.files, [ @@ -412,19 +400,19 @@ mod tests { #[test] fn should_fail_on_file_with_prefix_and_postfix_pos() { - parse_args(&mut str_to_argv("hx +10 Cargo.toml:20")).unwrap_err(); - parse_args(&mut str_to_argv("hx +10 Cargo.toml:")).unwrap_err(); + parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml:20")).unwrap_err(); + parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml:")).unwrap_err(); } #[test] fn should_fail_on_orphan_prefix_pos() { - parse_args(&mut str_to_argv("hx +10")).unwrap_err(); - parse_args(&mut str_to_argv("hx +10 Cargo.toml +20")).unwrap_err(); + parse_args(&mut str_to_arg_peekable("hx +10")).unwrap_err(); + parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml +20")).unwrap_err(); } #[test] fn should_parse_postfix_pos_files() { - let args = parse_args(&mut str_to_argv("hx Cargo.toml:10")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:10")).unwrap(); assert_eq!( args.files, [( @@ -433,13 +421,13 @@ mod tests { )] ); - let args = parse_args(&mut str_to_argv("hx Cargo.toml:")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:")).unwrap(); assert_eq!( args.files, [("Cargo.toml".to_owned().into(), PositionRequest::Eof)] ); - let args = parse_args(&mut str_to_argv("hx Cargo.toml:10 README:20")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:10 README:20")).unwrap(); assert_eq!( args.files, [ @@ -454,7 +442,10 @@ mod tests { ] ); - let args = parse_args(&mut str_to_argv("hx --vsplit -- Cargo.toml:10 README:20")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable( + "hx --vsplit -- Cargo.toml:10 README:20", + )) + .unwrap(); assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); assert_eq!( args.files, @@ -473,7 +464,7 @@ mod tests { #[test] fn should_parse_config() { - let args = parse_args(&mut str_to_argv("hx --config other/config.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx --config other/config.toml")).unwrap(); assert_eq!( args.config_file, Some("other/config.toml".to_owned().into()) @@ -482,17 +473,16 @@ mod tests { #[test] fn should_parse_layout() { - let args = parse_args(&mut str_to_argv("hx --vsplit Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx --vsplit Cargo.toml")).unwrap(); assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); - let args = parse_args(&mut str_to_argv("hx --hsplit Cargo.toml")).unwrap(); + let args = parse_args(&mut str_to_arg_peekable("hx --hsplit Cargo.toml")).unwrap(); assert_eq!(args.split, Some(helix_view::tree::Layout::Horizontal)); - parse_args(&mut str_to_argv("hx --hsplit -vsplit Cargo.toml")).unwrap_err(); + parse_args(&mut str_to_arg_peekable("hx --hsplit -vsplit Cargo.toml")).unwrap_err(); } - /// Return a peekable Iterator of arguments naively split on whitespace - fn str_to_argv(s: &'static str) -> Peekable> { + fn str_to_arg_peekable(s: &'static str) -> Peekable> { s.split_whitespace().map(ToOwned::to_owned).peekable() } } From 2665ae89474f78e4f30d07634ee9e2bc111174ec Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 14 Feb 2023 17:06:45 +0100 Subject: [PATCH 153/195] args.rs: move PositionRequest for file readability * Moved some helper functions to become associated functins of PositionRequest. * Increased usage of Self. * Moved PositionRequest logic and tests into it's one file. --- helix-term/src/application.rs | 4 +- helix-term/src/args.rs | 407 +----------------------- helix-term/src/args/position_request.rs | 406 +++++++++++++++++++++++ helix-term/src/commands/typed.rs | 6 +- 4 files changed, 418 insertions(+), 405 deletions(-) create mode 100644 helix-term/src/args/position_request.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1aa64af520d5..7ed84b10b014 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -193,7 +193,7 @@ impl Application { compositor.push(Box::new(overlayed(picker))); } else { let nr_of_files = args.files.len(); - for (i, (file, pos)) in args.files.into_iter().enumerate() { + for (i, (file, position_request)) in args.files.into_iter().enumerate() { if file.is_dir() { return Err(anyhow::anyhow!( "expected a path to file, found a directory. (to open a directory pass it as first argument)" @@ -219,7 +219,7 @@ impl Application { // opened last is focused on. let view_id = editor.tree.focus; let doc = doc_mut!(editor, &doc_id); - let selection = pos.selection_for_doc(doc); + let selection = position_request.selection_for_doc(doc); doc.set_selection(view_id, selection); } } diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 0784f8b308c1..abf3cd750e20 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -1,48 +1,9 @@ -use std::{borrow::Cow, iter::Peekable, path::PathBuf}; +mod position_request; use anyhow::Result; -use helix_core::{pos_at_coords, Position, Selection}; -use helix_view::{tree::Layout, Document}; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum PositionRequest { - Explicit(Position), - Eof, -} - -impl From for PositionRequest { - fn from(p: Position) -> Self { - Self::Explicit(p) - } -} - -impl Default for PositionRequest { - fn default() -> Self { - PositionRequest::Explicit(Position::default()) - } -} - -impl PositionRequest { - pub(crate) fn selection_for_doc(self, doc: &Document) -> Selection { - let text = doc.text().slice(..); - match self { - PositionRequest::Explicit(pos) => { - let pos = pos_at_coords(text, pos, true); - Selection::point(pos) - } - PositionRequest::Eof => { - let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 { - // If the last line is blank, don't jump to it. - text.len_lines().saturating_sub(2) - } else { - text.len_lines() - 1 - }; - let pos = text.line_to_char(line_idx); - Selection::point(pos) - } - } - } -} +use helix_view::tree::Layout; +pub use position_request::PositionRequest; +use std::{iter::Peekable, path::PathBuf}; #[derive(Debug, Default)] pub struct Args { @@ -67,7 +28,7 @@ impl Args { } } -fn parse_args(argv: &mut Peekable>) -> Result { +pub fn parse_args(argv: &mut Peekable>) -> Result { let mut args = Args::default(); argv.next(); // skip the program, we don't care about that @@ -119,370 +80,16 @@ fn parse_args(argv: &mut Peekable>) -> Result } } _ => { - let file = parse_positional_arg(arg, argv)?; + let file = PositionRequest::parse_positional_arg(arg, argv)?; args.files.push(file); } } } while let Some(arg) = argv.next() { - let file = parse_positional_arg(arg, argv)?; + let file = PositionRequest::parse_positional_arg(arg, argv)?; args.files.push(file); } Ok(args) } - -/// If an arg is a prefixed file position, then the next arg is expected to be a file. -/// File paths are not validated, that's left to the consumer. -pub(crate) fn parse_positional_arg( - arg: String, - argv: &mut impl Iterator, -) -> Result<(PathBuf, PositionRequest)> { - let file = if let Some(s) = arg.strip_prefix('+') { - let prefix_pos = parse_file_position(s); - let (path, postfix_pos) = match argv.next() { - Some(file) => parse_file(file), - None => anyhow::bail!("expected a file after a position"), - }; - - if postfix_pos.is_some() { - anyhow::bail!("unexpected postfix position after prefix position"); - } - - (path, prefix_pos.unwrap_or_default()) - } else { - let (path, pos) = parse_file(arg); - (path, pos.unwrap_or_default()) - }; - - Ok(file) -} - -pub(crate) fn parse_file_position(s: &str) -> Option { - let s = s.trim_matches(':'); - - if s.is_empty() { - return Some(PositionRequest::Eof); - } - - let (row, col) = s.split_once(':').unwrap_or((s, "1")); - let row: usize = row.parse().ok()?; - let col: usize = col.parse().ok()?; - let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); - - Some(pos.into()) -} - -pub(crate) fn parse_file<'a>(s: impl Into>) -> (PathBuf, Option) { - let s = s.into(); - match s.split_once(':') { - Some((s, rest)) => (s.into(), parse_file_position(rest)), - None => (s.into_owned().into(), None), - } -} - -#[cfg(test)] -mod tests { - use std::iter::Peekable; - - use helix_core::Position; - - use super::{parse_args, parse_file, parse_file_position, PositionRequest}; - - #[test] - fn should_parse_binary_only() { - parse_args(&mut str_to_arg_peekable("hx")).unwrap(); - } - - #[test] - fn should_parse_file_position_eof() { - assert_eq!(parse_file_position(":"), Some(PositionRequest::Eof)); - assert_eq!(parse_file_position("::"), Some(PositionRequest::Eof)); - } - - #[test] - fn should_parse_file_position_line_only() { - assert_eq!( - parse_file_position("10"), - Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) - ); - } - - #[test] - fn should_parse_file_position_line_only_with_trailing_delimiter() { - assert_eq!( - parse_file_position("10:"), - Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) - ); - } - - #[test] - fn should_parse_file_position_line_col() { - assert_eq!( - parse_file_position("10:20"), - Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) - ); - } - - #[test] - fn should_parse_file_position_line_col_with_trailing_delimiter() { - assert_eq!( - parse_file_position("10:20:"), - Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) - ); - } - - #[test] - fn should_give_none_if_any_pos_arg_invalid() { - assert_eq!(parse_file_position("x"), None); - assert_eq!(parse_file_position("x:y"), None); - assert_eq!(parse_file_position("10:y"), None); - assert_eq!(parse_file_position("x:20"), None); - } - - #[test] - fn should_parse_empty_file() { - assert_eq!(parse_file(""), ("".to_owned().into(), None)); - } - - #[test] - fn should_parse_empty_file_with_eof_pos() { - assert_eq!( - parse_file(":"), - ("".to_owned().into(), Some(PositionRequest::Eof)) - ); - } - - #[test] - fn should_parse_file_with_name_only() { - assert_eq!(parse_file("file"), ("file".to_owned().into(), None)); - } - - #[test] - fn should_parse_file_with_eof_pos() { - assert_eq!( - parse_file("file:"), - ("file".to_owned().into(), Some(PositionRequest::Eof)) - ); - } - - #[test] - fn should_parse_file_with_line_pos() { - assert_eq!( - parse_file("file:10"), - ( - "file".to_owned().into(), - Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) - ) - ); - } - - #[test] - fn should_parse_file_with_line_pos_and_trailing_delimiter() { - assert_eq!( - parse_file("file:10:"), - ( - "file".to_owned().into(), - Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) - ) - ); - } - - #[test] - fn should_parse_file_with_line_and_col_pos() { - assert_eq!( - parse_file("file:10:20"), - ( - "file".to_owned().into(), - Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) - ) - ); - } - - #[test] - fn should_parse_bare_files_args() { - let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml")).unwrap(); - assert_eq!( - args.files, - [("Cargo.toml".to_owned().into(), PositionRequest::default())] - ); - - let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml README")).unwrap(); - assert_eq!( - args.files, - [ - ("Cargo.toml".to_owned().into(), PositionRequest::default()), - ("README".to_owned().into(), PositionRequest::default()) - ] - ); - - let args = parse_args(&mut str_to_arg_peekable("hx -- Cargo.toml")).unwrap(); - assert_eq!( - args.files, - [("Cargo.toml".to_owned().into(), PositionRequest::default())] - ); - } - - #[test] - fn should_parse_prefix_pos_files() { - let args = parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml")).unwrap(); - assert_eq!( - args.files, - [( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - )] - ); - - let args = parse_args(&mut str_to_arg_peekable("hx +: Cargo.toml")).unwrap(); - assert_eq!( - args.files, - [("Cargo.toml".to_owned().into(), PositionRequest::Eof)] - ); - - let args = parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml +20 README")).unwrap(); - assert_eq!( - args.files, - [ - ( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - ), - ( - "README".to_owned().into(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) - ] - ); - - let args = parse_args(&mut str_to_arg_peekable( - "hx --vsplit -- +10 Cargo.toml +20 README", - )) - .unwrap(); - assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); - assert_eq!( - args.files, - [ - ( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - ), - ( - "README".to_owned().into(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) - ] - ); - } - - #[test] - fn should_parse_intermixed_file_pos_notation() { - let args = parse_args(&mut str_to_arg_peekable( - "hx CHANGELOG +10 Cargo.toml README:20", - )) - .unwrap(); - assert_eq!( - args.files, - [ - ("CHANGELOG".to_owned().into(), PositionRequest::default(),), - ( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - ), - ( - "README".to_owned().into(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) - ] - ); - } - - #[test] - fn should_fail_on_file_with_prefix_and_postfix_pos() { - parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml:20")).unwrap_err(); - parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml:")).unwrap_err(); - } - - #[test] - fn should_fail_on_orphan_prefix_pos() { - parse_args(&mut str_to_arg_peekable("hx +10")).unwrap_err(); - parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml +20")).unwrap_err(); - } - - #[test] - fn should_parse_postfix_pos_files() { - let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:10")).unwrap(); - assert_eq!( - args.files, - [( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - )] - ); - - let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:")).unwrap(); - assert_eq!( - args.files, - [("Cargo.toml".to_owned().into(), PositionRequest::Eof)] - ); - - let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:10 README:20")).unwrap(); - assert_eq!( - args.files, - [ - ( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - ), - ( - "README".to_owned().into(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) - ] - ); - - let args = parse_args(&mut str_to_arg_peekable( - "hx --vsplit -- Cargo.toml:10 README:20", - )) - .unwrap(); - assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); - assert_eq!( - args.files, - [ - ( - "Cargo.toml".to_owned().into(), - PositionRequest::Explicit(Position { row: 9, col: 0 }) - ), - ( - "README".to_owned().into(), - PositionRequest::Explicit(Position { row: 19, col: 0 }) - ) - ] - ); - } - - #[test] - fn should_parse_config() { - let args = parse_args(&mut str_to_arg_peekable("hx --config other/config.toml")).unwrap(); - assert_eq!( - args.config_file, - Some("other/config.toml".to_owned().into()) - ); - } - - #[test] - fn should_parse_layout() { - let args = parse_args(&mut str_to_arg_peekable("hx --vsplit Cargo.toml")).unwrap(); - assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); - - let args = parse_args(&mut str_to_arg_peekable("hx --hsplit Cargo.toml")).unwrap(); - assert_eq!(args.split, Some(helix_view::tree::Layout::Horizontal)); - - parse_args(&mut str_to_arg_peekable("hx --hsplit -vsplit Cargo.toml")).unwrap_err(); - } - - fn str_to_arg_peekable(s: &'static str) -> Peekable> { - s.split_whitespace().map(ToOwned::to_owned).peekable() - } -} diff --git a/helix-term/src/args/position_request.rs b/helix-term/src/args/position_request.rs new file mode 100644 index 000000000000..c5ab8b866111 --- /dev/null +++ b/helix-term/src/args/position_request.rs @@ -0,0 +1,406 @@ +use std::{borrow::Cow, path::PathBuf}; + +use anyhow::Result; +use helix_core::{Position, Selection}; +use helix_view::Document; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PositionRequest { + Explicit(Position), + Eof, +} + +impl From for PositionRequest { + fn from(p: Position) -> Self { + Self::Explicit(p) + } +} + +impl Default for PositionRequest { + fn default() -> Self { + PositionRequest::Explicit(Position::default()) + } +} + +impl PositionRequest { + pub fn selection_for_doc(self, doc: &Document) -> Selection { + let text = doc.text().slice(..); + match self { + Self::Explicit(pos) => { + let pos = helix_core::pos_at_coords(text, pos, true); + Selection::point(pos) + } + Self::Eof => { + let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 { + // If the last line is blank, don't jump to it. + text.len_lines().saturating_sub(2) + } else { + text.len_lines() - 1 + }; + let pos = text.line_to_char(line_idx); + Selection::point(pos) + } + } + } + + pub fn parse_file<'a>(s: impl Into>) -> (PathBuf, Option) { + let s = s.into(); + match s.split_once(':') { + Some((s, rest)) => (s.into(), Self::parse_file_position(rest)), + None => (s.into_owned().into(), None), + } + } + + /// If an arg is a prefixed file position, then the next arg is expected to be a file. + /// File paths are not validated, that's left to the consumer. + pub fn parse_positional_arg( + arg: String, + argv: &mut impl Iterator, + ) -> Result<(PathBuf, Self)> { + let file = if let Some(s) = arg.strip_prefix('+') { + let prefix_pos = Self::parse_file_position(s); + let (path, postfix_pos) = match argv.next() { + Some(file) => Self::parse_file(file), + None => anyhow::bail!("expected a file after a position"), + }; + + if postfix_pos.is_some() { + anyhow::bail!("unexpected postfix position after prefix position"); + } + + (path, prefix_pos.unwrap_or_default()) + } else { + let (path, pos) = Self::parse_file(arg); + (path, pos.unwrap_or_default()) + }; + + Ok(file) + } + + pub fn parse_file_position(s: &str) -> Option { + let s = s.trim_matches(':'); + + if s.is_empty() { + return Some(PositionRequest::Eof); + } + + let (row, col) = s.split_once(':').unwrap_or((s, "1")); + let row: usize = row.parse().ok()?; + let col: usize = col.parse().ok()?; + let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1)); + + Some(pos.into()) + } +} + +#[cfg(test)] +mod tests { + use crate::args::{parse_args, PositionRequest}; + use helix_core::Position; + use std::iter::Peekable; + + #[test] + fn should_parse_binary_only() { + parse_args(&mut str_to_arg_peekable("hx")).unwrap(); + } + + #[test] + fn should_parse_file_position_eof() { + assert_eq!( + PositionRequest::parse_file_position(":"), + Some(PositionRequest::Eof) + ); + assert_eq!( + PositionRequest::parse_file_position("::"), + Some(PositionRequest::Eof) + ); + } + + #[test] + fn should_parse_file_position_line_only() { + assert_eq!( + PositionRequest::parse_file_position("10"), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ); + } + + #[test] + fn should_parse_file_position_line_only_with_trailing_delimiter() { + assert_eq!( + PositionRequest::parse_file_position("10:"), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ); + } + + #[test] + fn should_parse_file_position_line_col() { + assert_eq!( + PositionRequest::parse_file_position("10:20"), + Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + ); + } + + #[test] + fn should_parse_file_position_line_col_with_trailing_delimiter() { + assert_eq!( + PositionRequest::parse_file_position("10:20:"), + Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + ); + } + + #[test] + fn should_give_none_if_any_pos_arg_invalid() { + assert_eq!(PositionRequest::parse_file_position("x"), None); + assert_eq!(PositionRequest::parse_file_position("x:y"), None); + assert_eq!(PositionRequest::parse_file_position("10:y"), None); + assert_eq!(PositionRequest::parse_file_position("x:20"), None); + } + + #[test] + fn should_parse_empty_file() { + assert_eq!( + PositionRequest::parse_file(""), + ("".to_owned().into(), None) + ); + } + + #[test] + fn should_parse_empty_file_with_eof_pos() { + assert_eq!( + PositionRequest::parse_file(":"), + ("".to_owned().into(), Some(PositionRequest::Eof)) + ); + } + + #[test] + fn should_parse_file_with_name_only() { + assert_eq!( + PositionRequest::parse_file("file"), + ("file".to_owned().into(), None) + ); + } + + #[test] + fn should_parse_file_with_eof_pos() { + assert_eq!( + PositionRequest::parse_file("file:"), + ("file".to_owned().into(), Some(PositionRequest::Eof)) + ); + } + + #[test] + fn should_parse_file_with_line_pos() { + assert_eq!( + PositionRequest::parse_file("file:10"), + ( + "file".to_owned().into(), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ) + ); + } + + #[test] + fn should_parse_file_with_line_pos_and_trailing_delimiter() { + assert_eq!( + PositionRequest::parse_file("file:10:"), + ( + "file".to_owned().into(), + Some(PositionRequest::Explicit(Position { row: 9, col: 0 })) + ) + ); + } + + #[test] + fn should_parse_file_with_line_and_col_pos() { + assert_eq!( + PositionRequest::parse_file("file:10:20"), + ( + "file".to_owned().into(), + Some(PositionRequest::Explicit(Position { row: 9, col: 19 })) + ) + ); + } + + #[test] + fn should_parse_bare_files_args() { + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml")).unwrap(); + assert_eq!( + args.files, + [("Cargo.toml".to_owned().into(), PositionRequest::default())] + ); + + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml README")).unwrap(); + assert_eq!( + args.files, + [ + ("Cargo.toml".to_owned().into(), PositionRequest::default()), + ("README".to_owned().into(), PositionRequest::default()) + ] + ); + + let args = parse_args(&mut str_to_arg_peekable("hx -- Cargo.toml")).unwrap(); + assert_eq!( + args.files, + [("Cargo.toml".to_owned().into(), PositionRequest::default())] + ); + } + + #[test] + fn should_parse_prefix_pos_files() { + let args = parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml")).unwrap(); + assert_eq!( + args.files, + [( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + )] + ); + + let args = parse_args(&mut str_to_arg_peekable("hx +: Cargo.toml")).unwrap(); + assert_eq!( + args.files, + [("Cargo.toml".to_owned().into(), PositionRequest::Eof)] + ); + + let args = parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml +20 README")).unwrap(); + assert_eq!( + args.files, + [ + ( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + "README".to_owned().into(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + + let args = parse_args(&mut str_to_arg_peekable( + "hx --vsplit -- +10 Cargo.toml +20 README", + )) + .unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + assert_eq!( + args.files, + [ + ( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + "README".to_owned().into(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + } + + #[test] + fn should_parse_intermixed_file_pos_notation() { + let args = parse_args(&mut str_to_arg_peekable( + "hx CHANGELOG +10 Cargo.toml README:20", + )) + .unwrap(); + assert_eq!( + args.files, + [ + ("CHANGELOG".to_owned().into(), PositionRequest::default(),), + ( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + "README".to_owned().into(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + } + + #[test] + fn should_fail_on_file_with_prefix_and_postfix_pos() { + parse_args(&mut str_to_arg_peekable("hx +10 Cargo.toml:20")).unwrap_err(); + } + + #[test] + fn should_fail_on_orphan_prefix_pos() { + parse_args(&mut str_to_arg_peekable("hx +10")).unwrap_err(); + } + + #[test] + fn should_parse_postfix_pos_files() { + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:10")).unwrap(); + assert_eq!( + args.files, + [( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + )] + ); + + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:")).unwrap(); + assert_eq!( + args.files, + [("Cargo.toml".to_owned().into(), PositionRequest::Eof)] + ); + + let args = parse_args(&mut str_to_arg_peekable("hx Cargo.toml:10 README:20")).unwrap(); + assert_eq!( + args.files, + [ + ( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + "README".to_owned().into(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + + let args = parse_args(&mut str_to_arg_peekable( + "hx --vsplit -- Cargo.toml:10 README:20", + )) + .unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + assert_eq!( + args.files, + [ + ( + "Cargo.toml".to_owned().into(), + PositionRequest::Explicit(Position { row: 9, col: 0 }) + ), + ( + "README".to_owned().into(), + PositionRequest::Explicit(Position { row: 19, col: 0 }) + ) + ] + ); + } + + #[test] + fn should_parse_config() { + let args = parse_args(&mut str_to_arg_peekable("hx --config other/config.toml")).unwrap(); + assert_eq!( + args.config_file, + Some("other/config.toml".to_owned().into()) + ); + } + + #[test] + fn should_parse_layout() { + let args = parse_args(&mut str_to_arg_peekable("hx --vsplit Cargo.toml")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Vertical)); + + let args = parse_args(&mut str_to_arg_peekable("hx --hsplit Cargo.toml")).unwrap(); + assert_eq!(args.split, Some(helix_view::tree::Layout::Horizontal)); + parse_args(&mut str_to_arg_peekable("hx --hsplit -vsplit Cargo.toml")).unwrap_err(); + } + + fn str_to_arg_peekable(s: &'static str) -> Peekable> { + s.split_whitespace().map(ToOwned::to_owned).peekable() + } +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 06beea33b9ec..734cf366c7d7 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -63,8 +63,8 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> ensure!(!args.is_empty(), "wrong argument count"); for arg in args { - let (path, pos) = args::parse_file(arg.to_owned()); - let pos = pos.unwrap_or_default(); + let (path, position_request) = args::PositionRequest::parse_file(arg.to_owned()); + let position_request = position_request.unwrap_or_default(); let path = helix_core::path::expand_tilde(&path); // If the path is a directory, open a file picker on that directory and update the status // message @@ -83,7 +83,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> // Otherwise, just open the file let _ = cx.editor.open(&path, Action::Replace)?; let (view, doc) = current!(cx.editor); - let selection = pos.selection_for_doc(doc); + let selection = position_request.selection_for_doc(doc); doc.set_selection(view.id, selection); // does not affect opening a buffer without pos align_view(doc, view, Align::Center); From 7b5b268656464e80ce5fb7b4fbd68388e89de839 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Tue, 14 Feb 2023 17:36:08 +0100 Subject: [PATCH 154/195] load-local-config set to false by default Otherwise it becomes impossible to run `hx -c /dev/null` --- helix-term/src/main.rs | 1 + helix-view/src/editor.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 965ea2fba486..30f5b0ae9568 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -47,6 +47,7 @@ async fn main_impl() -> Result { } helix_loader::setup_config_file(args.config_file.clone()); + let mut config = Config::merged().unwrap_or_else(|err| { eprintln!("Bad config: {}", err); eprintln!("Press to continue with default config"); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 10f160a8eb87..898360726301 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -745,7 +745,7 @@ impl Default for Config { auto_completion: true, auto_format: true, auto_save: false, - load_local_config: true, + load_local_config: false, idle_timeout: Duration::from_millis(400), completion_trigger_len: 2, auto_info: true, From 72e1581c82d397cfd6f50bf36e58c7c1b5f3ae48 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 10:26:13 +0100 Subject: [PATCH 155/195] Sticky can now overwrite pre-defined sticky. --- helix-term/src/keymap/keytrie.rs | 10 +++++++++- helix-term/src/keymap/keytrienode.rs | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 7916caccefa9..15fad2e59546 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -11,6 +11,8 @@ pub struct KeyTrie { child_order: HashMap, children: Vec, pub is_sticky: bool, + /// Used to respect pre-defined stickyness. + pub explicitly_set_sticky: bool, } impl KeyTrie { @@ -24,6 +26,7 @@ impl KeyTrie { child_order, children, is_sticky: false, + explicitly_set_sticky: false } } @@ -63,11 +66,16 @@ impl KeyTrie { } } + /// Other takes precedent. pub fn merge_keytrie(&mut self, other_keytrie: Self) { + if other_keytrie.explicitly_set_sticky { + self.is_sticky = other_keytrie.is_sticky; + } + for (other_key_event, other_index) in other_keytrie.get_child_order() { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; match other_child_keytrie_node { - KeyTrieNode::KeyTrie(ref other_child_keytrie) => { + KeyTrieNode::KeyTrie(other_child_keytrie) => { if let Some(self_index) = self.child_order.get(other_key_event) { if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = self.children[*self_index] diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index cb5b533d4e42..9bfe00a8be8e 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -121,11 +121,13 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { let mut children = Vec::new(); let mut child_order = HashMap::new(); let mut keytrie_is_sticky = false; + let mut user_explicit_sticky = false; let mut next_key = Some(peeked_key); while let Some(ref peeked_key) = next_key { if peeked_key == "sticky" { keytrie_is_sticky = map.next_value::()?; + user_explicit_sticky = true; } else { let key_event = peeked_key @@ -144,6 +146,7 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { children, ); keytrie.is_sticky = keytrie_is_sticky; + keytrie.explicitly_set_sticky = user_explicit_sticky; Ok(KeyTrieNode::KeyTrie(keytrie)) }; From ad9bc2a9228f7c345efe7dfc8bd28296be0219e8 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 10:28:09 +0100 Subject: [PATCH 156/195] cargo fmt --- helix-term/src/keymap/keytrie.rs | 4 ++-- helix-term/src/keymap/keytrienode.rs | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 15fad2e59546..9398e1edfe95 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -26,7 +26,7 @@ impl KeyTrie { child_order, children, is_sticky: false, - explicitly_set_sticky: false + explicitly_set_sticky: false, } } @@ -71,7 +71,7 @@ impl KeyTrie { if other_keytrie.explicitly_set_sticky { self.is_sticky = other_keytrie.is_sticky; } - + for (other_key_event, other_index) in other_keytrie.get_child_order() { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; match other_child_keytrie_node { diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 9bfe00a8be8e..2a911b935652 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -128,8 +128,7 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { if peeked_key == "sticky" { keytrie_is_sticky = map.next_value::()?; user_explicit_sticky = true; - } - else { + } else { let key_event = peeked_key .parse::() .map_err(serde::de::Error::custom)?; @@ -140,11 +139,7 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { next_key = map.next_key::()?; } - let mut keytrie = KeyTrie::new( - description, - child_order, - children, - ); + let mut keytrie = KeyTrie::new(description, child_order, children); keytrie.is_sticky = keytrie_is_sticky; keytrie.explicitly_set_sticky = user_explicit_sticky; Ok(KeyTrieNode::KeyTrie(keytrie)) From 34db3413f9faef6c49261149ed95d185de0a4933 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 10:34:30 +0100 Subject: [PATCH 157/195] Clarify sticky in remapping.md --- book/src/remapping.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/book/src/remapping.md b/book/src/remapping.md index ad62928d8fd7..0d58fd719610 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -101,7 +101,8 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes Keys can be disabled by binding them to the `no_op` command. -Making a mode "sticky" can be achieved by adding `sticky = true` to the mapping. +Making a mode "sticky" can be achieved by adding `sticky = true` to the mapping. +(Predefined sticky keytries can like wise be made unsticky with `sticky = false`.) Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands. > Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`. From 2d9cea80957f307f296e8e311bac327a96fe0766 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 11:16:56 +0100 Subject: [PATCH 158/195] Add tests for user defined sticky --- helix-term/src/config.rs | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 65ac4e037895..f62bc1964556 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -119,6 +119,71 @@ mod tests { } } + #[test] + fn false_to_true_sticky_override() { + let sample_keymap = r#" + [keys.normal.space] + sticky = true + "#; + let space_keytrie: KeyTrieNode = toml::from_str::(sample_keymap) + .unwrap() + .merge_in_default_keymap() + .keys + .get(&Mode::Normal) + .unwrap() + .traverse(&[KeyEvent::from_str("space").unwrap()]) + .unwrap(); + if let KeyTrieNode::KeyTrie(_space_keytrie) = space_keytrie { + assert!(_space_keytrie.is_sticky) + } else { + panic!("KeyTrieNode::KeyTrie expected.") + } + } + + #[test] + fn true_to_undefined_remains_sticky() { + // NOTE: assumes Z binding is predefined as sticky. + let sample_keymap = r#" + [keys.normal.Z] + c = "no_op" + "#; + let sticky_keytrie: KeyTrieNode = toml::from_str::(sample_keymap) + .unwrap() + .merge_in_default_keymap() + .keys + .get(&Mode::Normal) + .unwrap() + .traverse(&[KeyEvent::from_str("Z").unwrap()]) + .unwrap(); + if let KeyTrieNode::KeyTrie(_sticky_keytrie) = sticky_keytrie { + assert!(_sticky_keytrie.is_sticky) + } else { + panic!("KeyTrieNode::KeyTrie expected.") + } + } + + #[test] + fn true_to_false_sticky_override() { + // NOTE: assumes Z binding is predefined as sticky. + let sample_keymap = r#" + [keys.normal.Z] + sticky = false + "#; + let sticky_keytrie: KeyTrieNode = toml::from_str::(sample_keymap) + .unwrap() + .merge_in_default_keymap() + .keys + .get(&Mode::Normal) + .unwrap() + .traverse(&[KeyEvent::from_str("Z").unwrap()]) + .unwrap(); + if let KeyTrieNode::KeyTrie(_sticky_keytrie) = sticky_keytrie { + assert!(!_sticky_keytrie.is_sticky) + } else { + panic!("KeyTrieNode::KeyTrie expected.") + } + } + #[test] fn parses_custom_typable_command_label_from_toml() { let sample_keymap = r#" From 68c66ace491ccec0226e020a72af8b76a4e58472 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 12:13:50 +0100 Subject: [PATCH 159/195] config.rs: clean up tests --- helix-term/src/config.rs | 115 +++++++++++++-------------------------- 1 file changed, 37 insertions(+), 78 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index f62bc1964556..da4281668373 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -125,41 +125,17 @@ mod tests { [keys.normal.space] sticky = true "#; - let space_keytrie: KeyTrieNode = toml::from_str::(sample_keymap) - .unwrap() - .merge_in_default_keymap() - .keys - .get(&Mode::Normal) - .unwrap() - .traverse(&[KeyEvent::from_str("space").unwrap()]) - .unwrap(); - if let KeyTrieNode::KeyTrie(_space_keytrie) = space_keytrie { - assert!(_space_keytrie.is_sticky) - } else { - panic!("KeyTrieNode::KeyTrie expected.") - } + assert!(_normal_mode_keytrie("space", sample_keymap).is_sticky) } #[test] fn true_to_undefined_remains_sticky() { // NOTE: assumes Z binding is predefined as sticky. let sample_keymap = r#" - [keys.normal.Z] - c = "no_op" - "#; - let sticky_keytrie: KeyTrieNode = toml::from_str::(sample_keymap) - .unwrap() - .merge_in_default_keymap() - .keys - .get(&Mode::Normal) - .unwrap() - .traverse(&[KeyEvent::from_str("Z").unwrap()]) - .unwrap(); - if let KeyTrieNode::KeyTrie(_sticky_keytrie) = sticky_keytrie { - assert!(_sticky_keytrie.is_sticky) - } else { - panic!("KeyTrieNode::KeyTrie expected.") - } + [keys.normal.Z] + c = "no_op" + "#; + assert!(_normal_mode_keytrie("Z", sample_keymap).is_sticky) } #[test] @@ -168,20 +144,8 @@ mod tests { let sample_keymap = r#" [keys.normal.Z] sticky = false - "#; - let sticky_keytrie: KeyTrieNode = toml::from_str::(sample_keymap) - .unwrap() - .merge_in_default_keymap() - .keys - .get(&Mode::Normal) - .unwrap() - .traverse(&[KeyEvent::from_str("Z").unwrap()]) - .unwrap(); - if let KeyTrieNode::KeyTrie(_sticky_keytrie) = sticky_keytrie { - assert!(!_sticky_keytrie.is_sticky) - } else { - panic!("KeyTrieNode::KeyTrie expected.") - } + "#; + assert!(!_normal_mode_keytrie("Z", sample_keymap).is_sticky) } #[test] @@ -190,14 +154,7 @@ mod tests { [keys.normal] A-k = { description = "Edit Config", exec = ":open ~/.config/helix/config.toml" } "#; - let parsed_node: KeyTrieNode = toml::from_str::(sample_keymap) - .unwrap() - .keys - .get(&Mode::Normal) - .unwrap() - .traverse(&[KeyEvent::from_str("A-k").unwrap()]) - .unwrap(); - + let parsed_node: KeyTrieNode = _normal_mode_keytrie_node("A-k", sample_keymap); let parsed_description = parsed_node.get_description().unwrap(); assert_eq!(parsed_description, "Edit Config"); @@ -215,14 +172,7 @@ mod tests { "C-r" = { "description" = "Sort selection", "exec" = ["split_selection_on_newline", ":sort", "collapse_selection", "keep_primary_selection"] } "#; - let parsed_node: KeyTrieNode = toml::from_str::(sample_keymap) - .unwrap() - .keys - .get(&Mode::Normal) - .unwrap() - .traverse(&[KeyEvent::from_str("C-r").unwrap()]) - .unwrap(); - + let parsed_node: KeyTrieNode = _normal_mode_keytrie_node("C-r", sample_keymap); let parsed_description = parsed_node.get_description().unwrap(); assert_eq!(parsed_description, "Sort selection"); @@ -237,25 +187,13 @@ mod tests { #[test] fn parses_custom_infobox_label_from_toml() { let sample_keymap = r#" - [keys.normal] - b = { description = "Buffer menu", b = "buffer_picker", n = "goto_next_buffer" } + [keys.normal.A-b] + description = "Buffer menu" + b = "buffer_picker" + n = "goto_next_buffer" "#; - - let parsed_node: KeyTrieNode = toml::from_str::(sample_keymap) - .unwrap() - .keys - .get(&Mode::Normal) - .unwrap() - .traverse(&[KeyEvent::from_str("b").unwrap()]) - .unwrap(); - - let parsed_description = parsed_node.get_description().unwrap(); - assert_eq!(parsed_description, "Buffer menu"); - - if let KeyTrieNode::KeyTrie(_) = parsed_node { - } else { - panic!("KeyTrieNode::KeyTrie expected.") - } + let parsed_node: KeyTrieNode = _normal_mode_keytrie_node("A-b", sample_keymap); + assert_eq!(parsed_node.get_description().unwrap(), "Buffer menu"); } #[test] @@ -293,7 +231,7 @@ mod tests { assert_eq!( keymap_normal_root_key_trie.traverse(&[key!('i')]).unwrap(), KeyTrieNode::MappableCommand(MappableCommand::normal_mode), - "User supplied mappable command should ovveride default mappable command bound to the same key event." + "User supplied mappable command should override default mappable command bound to the same key event." ); assert_eq!( keymap_normal_root_key_trie.traverse(&[key!('无')]).unwrap(), @@ -348,4 +286,25 @@ mod tests { .get_children() .is_empty()); } + + fn _normal_mode_keytrie(key_event_str: &str, sample_keymap: &str) -> KeyTrie { + if let KeyTrieNode::KeyTrie(_parsed_keytrie) = + _normal_mode_keytrie_node(key_event_str, sample_keymap) + { + _parsed_keytrie + } else { + panic!("KeyTrieNode::KeyTrie expected.") + } + } + + fn _normal_mode_keytrie_node(key_event_str: &str, sample_keymap: &str) -> KeyTrieNode { + toml::from_str::(sample_keymap) + .unwrap() + .merge_in_default_keymap() + .keys + .get(&Mode::Normal) + .unwrap() + .traverse(&[KeyEvent::from_str(key_event_str).unwrap()]) + .unwrap() + } } From 6a812153a49450493d99fc409f7b76cb6c48bb4c Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 12:35:49 +0100 Subject: [PATCH 160/195] fix keytrie on default mappable command override bug I had introduced a bug in which it wasn't possible to override mappable commands with keytries. Test has been added to catch this miss. --- helix-term/src/config.rs | 13 +++++++++++-- helix-term/src/keymap/keytrie.rs | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index da4281668373..2259c3485ef7 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -187,12 +187,12 @@ mod tests { #[test] fn parses_custom_infobox_label_from_toml() { let sample_keymap = r#" - [keys.normal.A-b] + [keys.normal.b] description = "Buffer menu" b = "buffer_picker" n = "goto_next_buffer" "#; - let parsed_node: KeyTrieNode = _normal_mode_keytrie_node("A-b", sample_keymap); + let parsed_node: KeyTrieNode = _normal_mode_keytrie_node("b", sample_keymap); assert_eq!(parsed_node.get_description().unwrap(), "Buffer menu"); } @@ -216,6 +216,9 @@ mod tests { "$" => goto_line_end, "g" => delete_char_forward, }, + "b" => { "Buffer menu" + "b" => buffer_picker, + }, }) }, @@ -268,6 +271,12 @@ mod tests { KeyTrieNode::MappableCommand(MappableCommand::goto_last_line), "Default mappable commands that aren't ovveridden should exist in merged keymap." ); + // Assumes that `b` is a MappableCommand in default keymap + assert_ne!( + keymap_normal_root_key_trie.traverse(&[key!('b')]).unwrap(), + KeyTrieNode::MappableCommand(MappableCommand::move_prev_word_start), + "Keytrie can override default mappable command." + ); // Huh? assert!( diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index 9398e1edfe95..fbca705cc40d 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -81,6 +81,8 @@ impl KeyTrie { self.children[*self_index] { self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); + } else { + self.children[*self_index] = other_child_keytrie_node.clone(); } } else { self.child_order From 91d8d92cdb6b922f7b0d89d546afd7b5a0ed5771 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 13:02:26 +0100 Subject: [PATCH 161/195] keytrie.rs: merge_keytrie() refactor --- helix-term/src/keymap/keytrie.rs | 36 +++++++++++--------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/helix-term/src/keymap/keytrie.rs b/helix-term/src/keymap/keytrie.rs index fbca705cc40d..0bd4f71185c4 100644 --- a/helix-term/src/keymap/keytrie.rs +++ b/helix-term/src/keymap/keytrie.rs @@ -74,32 +74,20 @@ impl KeyTrie { for (other_key_event, other_index) in other_keytrie.get_child_order() { let other_child_keytrie_node = &other_keytrie.get_children()[*other_index]; - match other_child_keytrie_node { - KeyTrieNode::KeyTrie(other_child_keytrie) => { - if let Some(self_index) = self.child_order.get(other_key_event) { - if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = - self.children[*self_index] - { - self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); - } else { - self.children[*self_index] = other_child_keytrie_node.clone(); - } - } else { - self.child_order - .insert(*other_key_event, self.children.len()); - self.children - .push(KeyTrieNode::KeyTrie(other_child_keytrie.clone())); - } - } - KeyTrieNode::MappableCommand(_) | KeyTrieNode::CommandSequence(_) => { - if let Some(existing_index) = self.child_order.get(other_key_event) { - self.children[*existing_index] = other_child_keytrie_node.clone(); - } else { - self.child_order - .insert(*other_key_event, self.children.len()); - self.children.push(other_child_keytrie_node.clone()); + if let Some(existing_index) = self.child_order.get(other_key_event) { + if let KeyTrieNode::KeyTrie(ref mut self_clashing_child_key_trie) = + self.children[*existing_index] + { + if let KeyTrieNode::KeyTrie(other_child_keytrie) = other_child_keytrie_node { + self_clashing_child_key_trie.merge_keytrie(other_child_keytrie.clone()); + continue; } } + self.children[*existing_index] = other_child_keytrie_node.clone(); + } else { + self.child_order + .insert(*other_key_event, self.children.len()); + self.children.push(other_child_keytrie_node.clone()); } } } From 159150b24a4b01c0a65479139c45fc115351d0ff Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 13:17:22 +0100 Subject: [PATCH 162/195] replace custom join with vec::join --- helix-term/src/commands.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1d98bdee9041..4d9091e3e958 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2605,7 +2605,7 @@ impl ui::menu::Item for MappableCommand { ]; match command_list.get(name as &String) { Some(key_events) => { - row[1] = Cell::from(format_key_events(key_events)); + row[1] = Cell::from(key_events.join(", ")); } None => {} } @@ -2619,25 +2619,13 @@ impl ui::menu::Item for MappableCommand { let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; match command_list.get(*name) { Some(key_events) => { - row[1] = Cell::from(format_key_events(key_events)); + row[1] = Cell::from(key_events.join(", ")); } None => {} } return Row::new(row); } } - - // TODO: Generalize into a Vec Display implemention? - fn format_key_events(key_events: &Vec) -> String { - let mut result_string: String = String::new(); - for key_event in key_events { - if !result_string.is_empty() { - result_string.push_str(", "); - } - result_string.push_str(key_event); - } - result_string - } } } From 683931a8cd6feff5e0b92c96aae2beade3c14a8a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 14:17:48 +0100 Subject: [PATCH 163/195] revert some cleanup of use statements ...in the hopes that it simplifies review. The changes should have probably be codified into `cargo fmt`. --- helix-term/Cargo.toml | 1 + helix-term/src/application.rs | 39 +++++++++++--------- helix-term/src/commands.rs | 63 +++++++++++++++++--------------- helix-term/src/commands/typed.rs | 3 +- 4 files changed, 58 insertions(+), 48 deletions(-) diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index d4df572fdb13..603f37d39ab8 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -36,6 +36,7 @@ helix-loader = { version = "0.6", path = "../helix-loader" } anyhow = "1" once_cell = "1.17" + which = "4.4" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2ede96bad019..74052c8b7d69 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,3 +1,13 @@ +use arc_swap::{access::Map, ArcSwap}; +use futures_util::Stream; +use helix_core::{ + diagnostic::{DiagnosticTag, NumberOrString}, + path::get_relative_path, + pos_at_coords, syntax, Selection, +}; +use serde_json::json; +use tui::backend::Backend; + use crate::{ args::Args, commands::apply_workspace_edit, @@ -7,22 +17,7 @@ use crate::{ keymap::Keymap, ui::{self, overlay::overlayed}, }; -use anyhow::{Context, Error}; -use arc_swap::{access::Map, ArcSwap}; -use crossterm::{ - event::{ - DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, - }, - execute, terminal, - tty::IsTty, -}; -use futures_util::Stream; -use helix_core::{ - diagnostic::{DiagnosticTag, NumberOrString}, - path::get_relative_path, - pos_at_coords, syntax, Selection, -}; + use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ align_view, @@ -34,14 +29,22 @@ use helix_view::{ Align, Editor, }; use log::{debug, error, warn}; -use serde_json::json; use std::{ io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; -use tui::backend::Backend; +use anyhow::{Context, Error}; + +use crossterm::{ + event::{ + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, + }, + execute, terminal, + tty::IsTty, +}; #[cfg(not(windows))] use { signal_hook::{consts::signal, low_level}, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4d9091e3e958..b5e876b21712 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,29 +3,10 @@ pub(crate) mod lsp; pub(crate) mod typed; pub use dap::*; +use helix_vcs::Hunk; pub use lsp::*; pub use typed::*; -use crate::{ - args, - commands::insert::*, - compositor::{self, Component, Compositor}, - filter_picker_entry, - job::{self, Callback, Jobs}, - keymap::CommandList, - ui::{ - self, - menu::{Cell, Row}, - overlay::overlayed, - FilePicker, Picker, Popup, Prompt, PromptEvent, - }, -}; - -use anyhow::{anyhow, bail, ensure, Context as _}; -use futures_util::StreamExt; -use fuzzy_matcher::FuzzyMatcher; -use grep_regex::RegexMatcherBuilder; -use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use helix_core::{ char_idx_at_visual_offset, comment, doc_formatter::TextFormat, @@ -35,7 +16,7 @@ use helix_core::{ indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, - movement::{self, move_vertically_visual, Direction, Movement}, + movement::{self, move_vertically_visual, Direction}, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, @@ -47,7 +28,6 @@ use helix_core::{ visual_offset_from_block, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, }; -use helix_vcs::Hunk; use helix_view::{ clipboard::ClipboardType, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, @@ -59,17 +39,42 @@ use helix_view::{ view::View, Document, DocumentId, Editor, ViewId, }; -use ignore::{DirEntry, WalkBuilder, WalkState}; -use once_cell::sync::Lazy; -use serde::de::{self, Deserialize, Deserializer}; + +use anyhow::{anyhow, bail, ensure, Context as _}; +use fuzzy_matcher::FuzzyMatcher; +use insert::*; +use movement::Movement; + +use crate::{ + args, + compositor::{self, Component, Compositor}, + filter_picker_entry, + job::Callback, + keymap::CommandList, + ui::{ + self, + menu::{Cell, Row}, + overlay::overlayed, + FilePicker, Picker, Popup, Prompt, PromptEvent, + }, +}; + +use crate::job::{self, Jobs}; +use futures_util::StreamExt; +use std::{collections::HashMap, fmt, future::Future}; +use std::{collections::HashSet, num::NonZeroUsize}; + use std::{ borrow::Cow, - collections::{HashMap, HashSet}, - fmt, - future::Future, - num::NonZeroUsize, path::{Path, PathBuf}, }; + +use once_cell::sync::Lazy; +use serde::de::{self, Deserialize, Deserializer}; + +use grep_regex::RegexMatcherBuilder; +use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; +use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; pub type OnKeyCallback = Box; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 241d281808ac..740d978db9cc 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,9 +1,10 @@ use std::fmt::Write; use std::ops::Deref; -use super::*; use crate::job::*; +use super::*; + use helix_core::encoding; use helix_view::editor::{Action, CloseError, ConfigEvent}; use serde_json::Value; From e98afd29abc1119381452077e1dcfbfab9ae2613 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 14:50:47 +0100 Subject: [PATCH 164/195] improve keymap deserialize error handlig Replace two `.expect()` with returning Err. Using the let else pattern for this. Only became stable in 1.65.0, hence the bump. --- helix-term/src/keymap/keytrienode.rs | 12 ++++++------ rust-toolchain.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/helix-term/src/keymap/keytrienode.rs b/helix-term/src/keymap/keytrienode.rs index 2a911b935652..3605dde07c87 100644 --- a/helix-term/src/keymap/keytrienode.rs +++ b/helix-term/src/keymap/keytrienode.rs @@ -145,18 +145,18 @@ impl<'de> Visitor<'de> for KeyTrieNodeVisitor { Ok(KeyTrieNode::KeyTrie(keytrie)) }; - let first_key = map - .next_key::()? - .expect("Maps without keys are undefined keymap remapping behaviour."); + let Some(first_key) = map.next_key::()? else { + return Err(serde::de::Error::custom("Maps without keys are undefined keymap remapping behaviour.")) + }; if first_key != "description" { return into_keytrie(first_key, map, ""); } let description = map.next_value::()?; - let second_key = map - .next_key::()? - .expect("Associated key when a 'description' key is provided"); + let Some(second_key) = map.next_key::()? else { + return Err(serde::de::Error::custom("Associated key when a 'description' key is provided")) + }; if &second_key != "exec" { return into_keytrie(second_key, map, &description); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ace4f5f96e55..2abc5665236b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.63.0" +channel = "1.65.0" components = ["rustfmt", "rust-src"] From 27c45bafc395375981da9d3648d80e0af6d3cd9a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 15:26:27 +0100 Subject: [PATCH 165/195] fix refresh theme bug: Probably introduced when merging with #3207. Refreshing config from failed state would not update to set theme from default theme. Reason being that theme was being loaded before config was being set. One would have to refresh the config twice to update the theme as well. This commit changes the refresh order such that the config is actually set before refreshing the theme. --- helix-term/src/application.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2badd52c1d3e..9ab0403d79eb 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -420,23 +420,26 @@ impl Application { let mut refresh_config = || -> Result<(), Error> { let merged_user_config = Config::merged() .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; + self.config.store(Arc::new(merged_user_config)); + self.refresh_language_config()?; if let Some(theme) = &self.config.load().theme { - let true_color = self.config.load().editor.true_color || crate::true_color(); let theme = self .theme_loader .load(theme) .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; - if true_color || theme.is_16_color() { + if self.config.load().editor.true_color + || crate::true_color() + || theme.is_16_color() + { self.editor.set_theme(theme); } else { anyhow::bail!("Theme requires truecolor support, which is not available!") } } - self.config.store(Arc::new(merged_user_config)); Ok(()) }; From f3d102ee5cdbf610e108e0f4c7c6b012e86390a6 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 15:30:22 +0100 Subject: [PATCH 166/195] clippy lints --- helix-term/src/commands.rs | 20 ++++++++------------ helix-term/src/commands/typed.rs | 2 +- helix-term/src/health.rs | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 37089bf8c6a1..68bb6733f441 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2608,13 +2608,11 @@ impl ui::menu::Item for MappableCommand { Cell::from(""), Cell::from(description.as_str()), ]; - match command_list.get(name as &String) { - Some(key_events) => { - row[1] = Cell::from(key_events.join(", ")); - } - None => {} + if let Some(key_events) = command_list.get(name as &String) { + row[1] = Cell::from(key_events.join(", ")); } - return Row::new(row); + + Row::new(row) } MappableCommand::Static { description: doc, @@ -2622,13 +2620,11 @@ impl ui::menu::Item for MappableCommand { .. } => { let mut row: Vec = vec![Cell::from(*name), Cell::from(""), Cell::from(*doc)]; - match command_list.get(*name) { - Some(key_events) => { - row[1] = Cell::from(key_events.join(", ")); - } - None => {} + if let Some(key_events) = command_list.get(*name) { + row[1] = Cell::from(key_events.join(", ")); } - return Row::new(row); + + Row::new(row) } } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 4f01ded07061..351e5f254308 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -64,7 +64,7 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> ensure!(!args.is_empty(), "wrong argument count"); for arg in args { - let (path, position_request) = args::PositionRequest::parse_file(arg.to_owned()); + let (path, position_request) = args::PositionRequest::parse_file(arg.clone()); let position_request = position_request.unwrap_or_default(); let path = helix_core::path::expand_tilde(&path); // If the path is a directory, open a file picker on that directory and update the status diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index c918bf220e93..10403b871776 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -45,7 +45,7 @@ fn display_paths() -> std::io::Result<()> { for rt_dir in rt_dirs { write!(stdout, "- {};", rt_dir.display())?; - if let Ok(path) = std::fs::read_link(&rt_dir) { + if let Ok(path) = std::fs::read_link(rt_dir) { let msg = format!(" (symlinked to {})", path.display()); write!(stdout, "{}", msg.yellow())?; } From fcd8b311d87833491a41f4818a6a573aaf041f3b Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 16:50:52 +0100 Subject: [PATCH 167/195] unify theme loading pt. 1 * theme_loader in editor only * consistent use of lang_configs_loader --- helix-term/src/application.rs | 5 +---- helix-view/src/document.rs | 4 ++-- helix-view/src/editor.rs | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 9ab0403d79eb..d5ae421f7a68 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -262,12 +262,8 @@ impl Application { compositor, terminal, editor, - config, - - theme_loader, lang_configs_loader, - signals, jobs: Jobs::new(), lsp_progress: LspProgressMap::new(), @@ -426,6 +422,7 @@ impl Application { if let Some(theme) = &self.config.load().theme { let theme = self + .editor .theme_loader .load(theme) .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 11a0dbf8b964..478231ea75d4 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -406,7 +406,7 @@ impl Document { pub fn open( path: &Path, encoding: Option<&'static encoding::Encoding>, - config_loader: Option>, + lang_configs_loader: Option>, config: Arc>, ) -> Result { // Open the file if it exists, otherwise assume it is a new file (and thus empty). @@ -423,7 +423,7 @@ impl Document { // set the path and try detecting the language doc.set_path(Some(path))?; - if let Some(loader) = config_loader { + if let Some(loader) = lang_configs_loader { doc.detect_language(loader); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 898360726301..d01067d226f8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -924,7 +924,7 @@ impl Editor { pub fn new( mut area: Rect, theme_loader: Arc, - syn_loader: Arc, + lang_configs_loader: Arc, config: Arc>, ) -> Self { let conf = config.load(); @@ -952,7 +952,7 @@ impl Editor { debugger: None, debugger_events: SelectAll::new(), breakpoints: HashMap::new(), - lang_configs_loader: syn_loader, + lang_configs_loader, theme_loader, last_theme: None, last_line_number: None, From 9bcd089620e66ef793303edbb055f7ae9a915a6f Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 16:56:48 +0100 Subject: [PATCH 168/195] unify theme loading pt. 2 Arcswap for theme_loader can now be removed as Editor is it's sole owner --- helix-term/src/application.rs | 15 ++++++--------- helix-view/src/editor.rs | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index d5ae421f7a68..9cf13545f7b7 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -76,8 +76,6 @@ pub struct Application { config: Arc>, - #[allow(dead_code)] - theme_loader: Arc, #[allow(dead_code)] lang_configs_loader: Arc, @@ -138,7 +136,7 @@ impl Application { let mut theme_parent_dirs = vec![helix_loader::user_config_dir()]; theme_parent_dirs.extend_from_slice(helix_loader::get_runtime_dirs()); - let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); + let theme_loader = theme::Loader::new(&theme_parent_dirs); let true_color = config.editor.true_color || crate::true_color(); let theme = config @@ -170,7 +168,7 @@ impl Application { let config = Arc::new(ArcSwap::from_pointee(config)); let mut editor = Editor::new( area, - theme_loader.clone(), + theme_loader, lang_configs_loader.clone(), Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor @@ -421,11 +419,10 @@ impl Application { self.refresh_language_config()?; if let Some(theme) = &self.config.load().theme { - let theme = self - .editor - .theme_loader - .load(theme) - .map_err(|err| anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err))?; + let theme = + self.editor.theme_loader.load(theme).map_err(|err| { + anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err) + })?; if self.config.load().editor.true_color || crate::true_color() diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index d01067d226f8..8201398b5d5b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -832,7 +832,7 @@ pub struct Editor { pub clipboard_provider: Box, pub lang_configs_loader: Arc, - pub theme_loader: Arc, + pub theme_loader: theme::Loader, /// last_theme is used for theme previews. We store the current theme here, /// and if previewing is cancelled, we can return to it. pub last_theme: Option, @@ -923,7 +923,7 @@ pub enum CloseError { impl Editor { pub fn new( mut area: Rect, - theme_loader: Arc, + theme_loader: theme::Loader, lang_configs_loader: Arc, config: Arc>, ) -> Self { From 9de789305c2691a4b028052d4eab276ea35e4e8a Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 17:29:40 +0100 Subject: [PATCH 169/195] unify theme loading pt. 3 * theme_dirs generation done in helix_loader and not application.rs * getting theme directories is done through Editor for now. Goal is to unify all set_theme() preperation calls, somewhere. As it is currenply duplicated in typed.rs, application.rs, and editor.rs Toghether these points remove the need to pass down themeloader from `Application` to `Editor`. --- helix-loader/src/lib.rs | 6 ++++++ helix-term/src/application.rs | 39 +++++++++++++++-------------------- helix-view/src/editor.rs | 3 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 4fbe63d61b2a..d205c5d8774b 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -72,6 +72,12 @@ pub fn get_runtime_dirs() -> &'static [PathBuf] { &RUNTIME_DIRS } +pub fn theme_dirs() -> Vec { + let mut theme_dirs = vec![user_config_dir()]; + theme_dirs.extend_from_slice(get_runtime_dirs()); + theme_dirs +} + pub fn user_lang_config_file() -> PathBuf { user_config_dir().join("languages.toml") } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 9cf13545f7b7..1ed44c53ed5b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -25,7 +25,6 @@ use helix_view::{ document::DocumentSavedEventResult, editor::{ConfigEvent, EditorEvent}, graphics::Rect, - theme, tree::Layout, Align, Editor, }; @@ -134,26 +133,6 @@ impl Application { #[cfg(feature = "integration")] setup_integration_logging(); - let mut theme_parent_dirs = vec![helix_loader::user_config_dir()]; - theme_parent_dirs.extend_from_slice(helix_loader::get_runtime_dirs()); - let theme_loader = theme::Loader::new(&theme_parent_dirs); - - let true_color = config.editor.true_color || crate::true_color(); - let theme = config - .theme - .as_ref() - .and_then(|theme| { - theme_loader - .load(theme) - .map_err(|e| { - log::warn!("failed to load theme `{}` - {}", theme, e); - e - }) - .ok() - .filter(|theme| (true_color || theme.is_16_color())) - }) - .unwrap_or_else(|| theme_loader.default_theme(true_color)); - let lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(langauge_configurations)); #[cfg(not(feature = "integration"))] @@ -168,7 +147,6 @@ impl Application { let config = Arc::new(ArcSwap::from_pointee(config)); let mut editor = Editor::new( area, - theme_loader, lang_configs_loader.clone(), Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor @@ -248,6 +226,23 @@ impl Application { .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } + let true_color = config.load().editor.true_color || crate::true_color(); + let theme = config + .load() + .theme + .as_ref() + .and_then(|theme| { + editor + .theme_loader + .load(theme) + .map_err(|e| { + log::warn!("failed to load theme `{}` - {}", theme, e); + e + }) + .ok() + .filter(|theme| (true_color || theme.is_16_color())) + }) + .unwrap_or_else(|| editor.theme_loader.default_theme(true_color)); editor.set_theme(theme); #[cfg(windows)] diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 8201398b5d5b..f676cae7af15 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -923,13 +923,12 @@ pub enum CloseError { impl Editor { pub fn new( mut area: Rect, - theme_loader: theme::Loader, lang_configs_loader: Arc, config: Arc>, ) -> Self { let conf = config.load(); let auto_pairs = (&conf.auto_pairs).into(); - + let theme_loader = theme::Loader::new(&helix_loader::theme_dirs()); // HAXX: offset the render area height by 1 to account for prompt/commandline area.height -= 1; From 737b85674e876cb93d04aec8aaf098cd364c21b8 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 17:58:10 +0100 Subject: [PATCH 170/195] unify theme loading pt. 4 Just moved some lines around for a better understanding of what goes together with what. --- helix-term/src/application.rs | 15 +++++++-------- helix-term/src/main.rs | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1ed44c53ed5b..aa14a0046142 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -23,7 +23,7 @@ use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{ align_view, document::DocumentSavedEventResult, - editor::{ConfigEvent, EditorEvent}, + editor::{Action, ConfigEvent, EditorEvent}, graphics::Rect, tree::Layout, Align, Editor, @@ -129,22 +129,20 @@ impl Application { config: Config, langauge_configurations: LanguageConfigurations, ) -> Result { - use helix_view::editor::Action; #[cfg(feature = "integration")] setup_integration_logging(); - let lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(langauge_configurations)); - #[cfg(not(feature = "integration"))] let backend = CrosstermBackend::new(stdout()); - #[cfg(feature = "integration")] let backend = TestBackend::new(120, 150); let terminal = Terminal::new(backend)?; let area = terminal.size().expect("couldn't get terminal size"); - let mut compositor = Compositor::new(area); + let config = Arc::new(ArcSwap::from_pointee(config)); + let lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(langauge_configurations)); + let mut editor = Editor::new( area, lang_configs_loader.clone(), @@ -156,8 +154,9 @@ impl Application { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymap::new(keys))); - compositor.push(editor_view); + + let mut compositor = Compositor::new(area); + compositor.push(Box::new(ui::EditorView::new(Keymap::new(keys)))); if args.load_tutor { let path = helix_loader::get_runtime_file(Path::new("tutor")); diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 30f5b0ae9568..3eb44d6c074c 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -74,8 +74,7 @@ async fn main_impl() -> Result { // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config, language_configurations) .context("unable to create new application")?; - let exit_code = app.run(&mut EventStream::new()).await?; - Ok(exit_code) + app.run(&mut EventStream::new()).await } fn setup_logging(logpath: Option, verbosity: u64) -> Result<()> { From ba7520b38a2b7101c2704184bbdf7f2ca1a37bfb Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 18:25:15 +0100 Subject: [PATCH 171/195] application.rs: Inline refresh_lang_config. --- helix-term/src/application.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index aa14a0046142..1e91b9dbbefd 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -390,27 +390,19 @@ impl Application { } } - /// refresh language config after config change - fn refresh_language_config(&mut self) -> Result<(), Error> { - let language_configs = LanguageConfigurations::merged() - .map_err(|err| anyhow::anyhow!("Failed to load merged language config: {}", err))?; - - self.lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(language_configs)); - self.editor.lang_configs_loader = self.lang_configs_loader.clone(); - for document in self.editor.documents.values_mut() { - document.detect_language(self.lang_configs_loader.clone()); - } - - Ok(()) - } - fn refresh_config(&mut self) { let mut refresh_config = || -> Result<(), Error> { let merged_user_config = Config::merged() .map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?; self.config.store(Arc::new(merged_user_config)); - self.refresh_language_config()?; + let language_configs = LanguageConfigurations::merged() + .map_err(|err| anyhow::anyhow!("Failed to load merged language config: {}", err))?; + self.lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(language_configs)); + self.editor.lang_configs_loader = self.lang_configs_loader.clone(); + for document in self.editor.documents.values_mut() { + document.detect_language(self.lang_configs_loader.clone()); + } if let Some(theme) = &self.config.load().theme { let theme = From 692d7448a0d0672e913809fbdbb61a18a289ba58 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 18:38:30 +0100 Subject: [PATCH 172/195] Remove lang_configs_loader field from Appliction Main purpose in application.rs was for it to then be passed down to Editor. --- helix-term/src/application.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 1e91b9dbbefd..f3013812be7e 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -72,12 +72,7 @@ pub struct Application { compositor: Compositor, terminal: Terminal, pub editor: Editor, - config: Arc>, - - #[allow(dead_code)] - lang_configs_loader: Arc, - signals: Signals, jobs: Jobs, lsp_progress: LspProgressMap, @@ -141,11 +136,10 @@ impl Application { let area = terminal.size().expect("couldn't get terminal size"); let config = Arc::new(ArcSwap::from_pointee(config)); - let lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(langauge_configurations)); let mut editor = Editor::new( area, - lang_configs_loader.clone(), + Arc::new(syntax::Loader::new(langauge_configurations)), Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor })), @@ -255,7 +249,6 @@ impl Application { terminal, editor, config, - lang_configs_loader, signals, jobs: Jobs::new(), lsp_progress: LspProgressMap::new(), @@ -398,10 +391,11 @@ impl Application { let language_configs = LanguageConfigurations::merged() .map_err(|err| anyhow::anyhow!("Failed to load merged language config: {}", err))?; - self.lang_configs_loader = std::sync::Arc::new(syntax::Loader::new(language_configs)); - self.editor.lang_configs_loader = self.lang_configs_loader.clone(); + + self.editor.lang_configs_loader = Arc::new(syntax::Loader::new(language_configs)); + self.editor.lang_configs_loader = self.editor.lang_configs_loader.clone(); for document in self.editor.documents.values_mut() { - document.detect_language(self.lang_configs_loader.clone()); + document.detect_language(self.editor.lang_configs_loader.clone()); } if let Some(theme) = &self.config.load().theme { From 4ba73b3ba868242210a935899f0bb4bae3399e1d Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 18:46:06 +0100 Subject: [PATCH 173/195] Unwrap in terminal.size() All calls to the function were unwrapping it immediatedly after. Where `.expect("couldn't get terminal size")` was duplicated thrice in application.rs. --- helix-term/src/application.rs | 6 +++--- helix-tui/src/terminal.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index f3013812be7e..b0d2eaa29aaf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -133,7 +133,7 @@ impl Application { let backend = TestBackend::new(120, 150); let terminal = Terminal::new(backend)?; - let area = terminal.size().expect("couldn't get terminal size"); + let area = terminal.size(); let config = Arc::new(ArcSwap::from_pointee(config)); @@ -447,7 +447,7 @@ impl Application { signal::SIGCONT => { self.claim_term().await.unwrap(); // redraw the terminal - let area = self.terminal.size().expect("couldn't get terminal size"); + let area = self.terminal.size(); self.compositor.resize(area); self.terminal.clear().expect("couldn't clear terminal"); @@ -594,7 +594,7 @@ impl Application { .resize(Rect::new(0, 0, width, height)) .expect("Unable to resize terminal"); - let area = self.terminal.size().expect("couldn't get terminal size"); + let area = self.terminal.size(); self.compositor.resize(area); diff --git a/helix-tui/src/terminal.rs b/helix-tui/src/terminal.rs index 22e9232f3f86..ef365d68c003 100644 --- a/helix-tui/src/terminal.rs +++ b/helix-tui/src/terminal.rs @@ -139,7 +139,7 @@ where /// Queries the backend for size and resizes if it doesn't match the previous size. pub fn autoresize(&mut self) -> io::Result { - let size = self.size()?; + let size = self.size(); if size != self.viewport.area { self.resize(size)?; }; @@ -219,7 +219,7 @@ where } /// Queries the real size of the backend. - pub fn size(&self) -> io::Result { - self.backend.size() + pub fn size(&self) -> Rect { + self.backend.size().expect("couldn't get terminal size") } } From cf9fd5145af905752f0de89d40ed53b9fec0cab2 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 19:29:08 +0100 Subject: [PATCH 174/195] Tweak usage of lang_configs_loader --- helix-term/src/application.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b0d2eaa29aaf..eb45f1d55744 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -393,7 +393,6 @@ impl Application { .map_err(|err| anyhow::anyhow!("Failed to load merged language config: {}", err))?; self.editor.lang_configs_loader = Arc::new(syntax::Loader::new(language_configs)); - self.editor.lang_configs_loader = self.editor.lang_configs_loader.clone(); for document in self.editor.documents.values_mut() { document.detect_language(self.editor.lang_configs_loader.clone()); } @@ -517,12 +516,10 @@ impl Application { return; } - let loader = self.editor.lang_configs_loader.clone(); - // borrowing the same doc again to get around the borrow checker let doc = doc_mut!(self.editor, &doc_save_event.doc_id); let id = doc.id(); - doc.detect_language(loader); + doc.detect_language(self.editor.lang_configs_loader.clone()); let _ = self.editor.refresh_language_server(id); } From ebe7ddf007e40de6375532a1719c9dd05aae1259 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 19:51:22 +0100 Subject: [PATCH 175/195] Unify true_color detection config.load().editor.true_color || crate::true_color() or the like is was found a handful of times. Moves the logic of crate::true_color to Editor::default() constructor. This allows it to still be overriden by user configs whilst also removing the repeated OR expression. --- helix-term/src/application.rs | 7 ++----- helix-term/src/commands/typed.rs | 2 +- helix-term/src/lib.rs | 11 ----------- helix-view/src/editor.rs | 8 +++++++- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index eb45f1d55744..80c39b4dd578 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -219,7 +219,7 @@ impl Application { .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } - let true_color = config.load().editor.true_color || crate::true_color(); + let true_color = config.load().editor.true_color; let theme = config .load() .theme @@ -403,10 +403,7 @@ impl Application { anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err) })?; - if self.config.load().editor.true_color - || crate::true_color() - || theme.is_16_color() - { + if self.config.load().editor.true_color || theme.is_16_color() { self.editor.set_theme(theme); } else { anyhow::bail!("Theme requires truecolor support, which is not available!") diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 351e5f254308..94283903cb4e 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -771,7 +771,7 @@ fn theme( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { - let true_color = cx.editor.config.load().true_color || crate::true_color(); + let true_color = cx.editor.config.load().true_color; match event { PromptEvent::Abort => { cx.editor.unset_theme_preview(); diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index 2f6ec12b13fd..8e2677e4ae01 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -15,17 +15,6 @@ use std::path::Path; use ignore::DirEntry; pub use keymap::macros::*; -#[cfg(not(windows))] -fn true_color() -> bool { - std::env::var("COLORTERM") - .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) - .unwrap_or(false) -} -#[cfg(windows)] -fn true_color() -> bool { - true -} - /// Function used for filtering dir entries in the various file pickers. fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool { // We always want to ignore the .git directory, otherwise if diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index f676cae7af15..310f8aaeedbf 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -752,7 +752,13 @@ impl Default for Config { file_picker: FilePickerConfig::default(), statusline: StatusLineConfig::default(), cursor_shape: CursorShapeConfig::default(), - true_color: false, + true_color: if cfg!(windows) { + true + } else { + std::env::var("COLORTERM") + .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) + .unwrap_or(false) + }, search: SearchConfig::default(), lsp: LspConfig::default(), terminal: get_terminal_provider(), From 05d04f37789082f80ce78b571654de5547f30e26 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 20:15:51 +0100 Subject: [PATCH 176/195] unify theme color support checking --- helix-term/src/application.rs | 7 ++----- helix-term/src/commands/typed.rs | 10 ++-------- helix-view/src/editor.rs | 7 +++++++ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 80c39b4dd578..0f7effdca41f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -403,11 +403,8 @@ impl Application { anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err) })?; - if self.config.load().editor.true_color || theme.is_16_color() { - self.editor.set_theme(theme); - } else { - anyhow::bail!("Theme requires truecolor support, which is not available!") - } + self.editor.check_theme_color_support(&theme)?; + self.editor.set_theme(theme); } Ok(()) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 94283903cb4e..f4460bbfe863 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -771,7 +771,6 @@ fn theme( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { - let true_color = cx.editor.config.load().true_color; match event { PromptEvent::Abort => { cx.editor.unset_theme_preview(); @@ -782,9 +781,7 @@ fn theme( 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 !(true_color || theme.is_16_color()) { - bail!("Unsupported theme: theme requires true color support"); - } + cx.editor.check_theme_color_support(&theme)?; cx.editor.set_theme_preview(theme); }; }; @@ -796,13 +793,10 @@ fn theme( .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"); - } + cx.editor.check_theme_color_support(&theme)?; cx.editor.set_theme(theme); } else { let name = cx.editor.theme.name().to_string(); - cx.editor.set_status(name); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 310f8aaeedbf..5ea5fb641217 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1042,6 +1042,13 @@ impl Editor { .unwrap_or(false) } + pub fn check_theme_color_support(&self, theme: &Theme) -> anyhow::Result<()> { + if !(self.config.load().true_color || theme.is_16_color()) { + anyhow::bail!("Unsupported theme: theme requires true color support"); + } + Ok(()) + } + pub fn unset_theme_preview(&mut self) { if let Some(last_theme) = self.last_theme.take() { self.set_theme(last_theme); From 1055cd2638bbd5c853a951df867200a097415255 Mon Sep 17 00:00:00 2001 From: gibbz00 Date: Wed, 15 Feb 2023 23:38:07 +0100 Subject: [PATCH 177/195] Unify theme loading * True color support is now looked for once in main.rs, and checked internally in theme upon each theme update. * Removes theme::Loader all together * Theme errors notified on start-up just like all the other config parse errors. --- helix-loader/src/lib.rs | 2 +- helix-term/src/application.rs | 36 +---- helix-term/src/commands/typed.rs | 12 +- helix-term/src/main.rs | 21 ++- helix-term/src/ui/mod.rs | 5 +- helix-term/tests/test/helpers.rs | 18 ++- helix-view/src/editor.rs | 37 ++--- helix-view/src/theme.rs | 261 +++++++++++++++---------------- xtask/src/themelint.rs | 5 +- 9 files changed, 187 insertions(+), 210 deletions(-) diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index d205c5d8774b..0181559b1327 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -75,7 +75,7 @@ pub fn get_runtime_dirs() -> &'static [PathBuf] { pub fn theme_dirs() -> Vec { let mut theme_dirs = vec![user_config_dir()]; theme_dirs.extend_from_slice(get_runtime_dirs()); - theme_dirs + theme_dirs.iter().map(|p| p.join("themes")).collect() } pub fn user_lang_config_file() -> PathBuf { diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 0f7effdca41f..e5539c8f9ae3 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -26,7 +26,7 @@ use helix_view::{ editor::{Action, ConfigEvent, EditorEvent}, graphics::Rect, tree::Layout, - Align, Editor, + Align, Editor, Theme, }; use log::{debug, error, warn}; use std::{ @@ -122,6 +122,7 @@ impl Application { pub fn new( args: Args, config: Config, + theme: Theme, langauge_configurations: LanguageConfigurations, ) -> Result { #[cfg(feature = "integration")] @@ -139,10 +140,11 @@ impl Application { let mut editor = Editor::new( area, - Arc::new(syntax::Loader::new(langauge_configurations)), Arc::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor })), + theme, + Arc::new(syntax::Loader::new(langauge_configurations)), ); let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { @@ -219,25 +221,6 @@ impl Application { .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } - let true_color = config.load().editor.true_color; - let theme = config - .load() - .theme - .as_ref() - .and_then(|theme| { - editor - .theme_loader - .load(theme) - .map_err(|e| { - log::warn!("failed to load theme `{}` - {}", theme, e); - e - }) - .ok() - .filter(|theme| (true_color || theme.is_16_color())) - }) - .unwrap_or_else(|| editor.theme_loader.default_theme(true_color)); - editor.set_theme(theme); - #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] @@ -397,14 +380,9 @@ impl Application { document.detect_language(self.editor.lang_configs_loader.clone()); } - if let Some(theme) = &self.config.load().theme { - let theme = - self.editor.theme_loader.load(theme).map_err(|err| { - anyhow::anyhow!("Failed to load theme `{}`: {}", theme, err) - })?; - - self.editor.check_theme_color_support(&theme)?; - self.editor.set_theme(theme); + if let Some(theme_name) = &self.config.load().theme { + self.editor + .set_theme(self.editor.theme.update(theme_name.clone())?); } Ok(()) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index f4460bbfe863..4f3af6d7a737 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -780,21 +780,15 @@ 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) { - cx.editor.check_theme_color_support(&theme)?; + if let Ok(theme) = cx.editor.theme.update(theme_name.to_string()) { 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))?; - cx.editor.check_theme_color_support(&theme)?; - cx.editor.set_theme(theme); + cx.editor + .set_theme(cx.editor.theme.update(theme_name.to_string())?); } else { let name = cx.editor.theme.name().to_string(); cx.editor.set_status(name); diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 3eb44d6c074c..f55a83b00b00 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -7,6 +7,7 @@ use helix_loader::VERSION_AND_GIT_HASH; use helix_term::application::Application; use helix_term::args::Args; use helix_term::config::Config; +use helix_view::Theme; use std::path::PathBuf; fn main() -> Result<()> { @@ -71,8 +72,26 @@ async fn main_impl() -> Result { LanguageConfigurations::default() }); + let true_color_support = { + config.editor.true_color || { + if cfg!(windows) { + true + } else { + std::env::var("COLORTERM") + .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) + .unwrap_or(false) + } + } + }; + let theme = Theme::new(config.theme.clone(), true_color_support).unwrap_or_else(|err| { + eprintln!("Bad theme config: {}", err); + eprintln!("Press to continue with default theme config"); + let _wait_for_enter = std::io::Read::read(&mut std::io::stdin(), &mut []); + Theme::new(None, true_color_support).expect("default themes must be correct") + }); + // TODO: use the thread local executor to spawn the application task separately from the work pool - let mut app = Application::new(args, config, language_configurations) + let mut app = Application::new(args, config, theme, language_configurations) .context("unable to create new application")?; app.run(&mut EventStream::new()).await } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 115bb68d4152..3ae36e1aa3ae 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -280,9 +280,10 @@ pub mod completers { } pub fn theme(_editor: &Editor, input: &str) -> Vec { - let mut names = theme::Loader::read_names(&helix_loader::user_config_dir().join("themes")); + // TODO: theme names vec should be created in Theme where it reads all themes in its themes_dirs + let mut names = theme::Theme::read_names(&helix_loader::user_config_dir().join("themes")); for rt_dir in helix_loader::get_runtime_dirs() { - names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); + names.extend(theme::Theme::read_names(&rt_dir.join("themes"))); } names.push("default".into()); names.push("base16_default".into()); diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index de83a00d4175..c3e2b3dc9f94 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -11,7 +11,7 @@ use helix_core::{ diagnostic::Severity, syntax::LanguageConfigurations, test, Selection, Transaction, }; use helix_term::{application::Application, args::Args, config::Config}; -use helix_view::{doc, input::parse_macro, Editor}; +use helix_view::{doc, input::parse_macro, Editor, Theme}; use tempfile::NamedTempFile; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -120,7 +120,12 @@ pub async fn test_key_sequence_with_input_text>( let test_case = test_case.into(); let mut app = match app { Some(app) => app, - None => Application::new(Args::default(), test_config(), test_syntax_conf(None))?, + None => Application::new( + Args::default(), + test_config(), + Theme::new(None, true)?, + test_syntax_conf(None), + )?, }; let (view, doc) = helix_view::current!(app.editor); @@ -173,7 +178,7 @@ pub async fn test_with_config>( test_case: T, ) -> anyhow::Result<()> { let test_case = test_case.into(); - let app = Application::new(args, config, syn_conf)?; + let app = Application::new(args, config, Theme::new(None, true)?, syn_conf)?; test_key_sequence_with_input_text( Some(app), @@ -304,7 +309,12 @@ impl AppBuilder { } pub fn build(self) -> anyhow::Result { - let mut app = Application::new(self.args, self.config, self.syn_conf)?; + let mut app = Application::new( + self.args, + self.config, + Theme::new(None, true)?, + self.syn_conf, + )?; if let Some((text, selection)) = self.input { let (view, doc) = helix_view::current!(app.editor); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5ea5fb641217..75c762b29997 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -5,7 +5,7 @@ use crate::{ graphics::{CursorKind, Rect}, info::Info, input::KeyEvent, - theme::{self, Theme}, + theme::Theme, tree::{self, Tree}, view::ViewPosition, Align, Document, DocumentId, View, ViewId, @@ -752,13 +752,7 @@ impl Default for Config { file_picker: FilePickerConfig::default(), statusline: StatusLineConfig::default(), cursor_shape: CursorShapeConfig::default(), - true_color: if cfg!(windows) { - true - } else { - std::env::var("COLORTERM") - .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) - .unwrap_or(false) - }, + true_color: false, search: SearchConfig::default(), lsp: LspConfig::default(), terminal: get_terminal_provider(), @@ -838,7 +832,6 @@ pub struct Editor { pub clipboard_provider: Box, pub lang_configs_loader: Arc, - pub theme_loader: theme::Loader, /// last_theme is used for theme previews. We store the current theme here, /// and if previewing is cancelled, we can return to it. pub last_theme: Option, @@ -929,16 +922,18 @@ pub enum CloseError { impl Editor { pub fn new( mut area: Rect, - lang_configs_loader: Arc, config: Arc>, + theme: Theme, + lang_configs_loader: Arc, ) -> Self { let conf = config.load(); + let auto_pairs = (&conf.auto_pairs).into(); - let theme_loader = theme::Loader::new(&helix_loader::theme_dirs()); // HAXX: offset the render area height by 1 to account for prompt/commandline area.height -= 1; - Self { + // TEMP: until its decided on what to do with set_theme + let mut editor_temp = Self { mode: Mode::Normal, tree: Tree::new(area), next_document_id: DocumentId::default(), @@ -950,7 +945,7 @@ impl Editor { selected_register: None, macro_recording: None, macro_replaying: Vec::new(), - theme: theme_loader.default(), + theme: theme.clone(), language_servers: helix_lsp::Registry::new(), diagnostics: BTreeMap::new(), diff_providers: DiffProviderRegistry::default(), @@ -958,7 +953,6 @@ impl Editor { debugger_events: SelectAll::new(), breakpoints: HashMap::new(), lang_configs_loader, - theme_loader, last_theme: None, last_line_number: None, registers: Registers::default(), @@ -975,7 +969,9 @@ impl Editor { redraw_handle: Default::default(), needs_redraw: false, cursor_cache: Cell::new(None), - } + }; + editor_temp.set_theme(theme); + editor_temp } /// Current editing mode for the [`Editor`]. @@ -1042,13 +1038,6 @@ impl Editor { .unwrap_or(false) } - pub fn check_theme_color_support(&self, theme: &Theme) -> anyhow::Result<()> { - if !(self.config.load().true_color || theme.is_16_color()) { - anyhow::bail!("Unsupported theme: theme requires true color support"); - } - Ok(()) - } - pub fn unset_theme_preview(&mut self) { if let Some(last_theme) = self.last_theme.take() { self.set_theme(last_theme); @@ -1071,9 +1060,7 @@ impl Editor { return; } - let scopes = theme.scopes(); - self.lang_configs_loader.set_scopes(scopes.to_vec()); - + self.lang_configs_loader.set_scopes(theme.scopes().to_vec()); match preview { ThemeAction::Preview => { let last_theme = std::mem::replace(&mut self.theme, theme); diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index f21f9e4a68b1..76c7b6a1f28a 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -35,19 +35,116 @@ pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| Theme { ..Theme::from(BASE16_DEFAULT_THEME_DATA.clone()) }); -#[derive(Clone, Debug)] -pub struct Loader { - /// Order implies search priority, highest first. - theme_dirs: Vec, +#[derive(Clone, Debug, Default)] +pub struct Theme { + name: String, + true_color_support: bool, + // UI styles are stored in a HashMap + styles: HashMap, + // tree-sitter highlight styles are stored in a Vec to optimize lookups + scopes: Vec, + highlights: Vec