From bee05dd32a685b58015514492525673b1b568b0d Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 25 Mar 2022 05:05:20 -0400 Subject: [PATCH] Add refresh-config and open-config command (#1803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add refresh-config and open-config command * clippy * Use dynamic dispatch for editor config * Refactor Result::Ok to Ok * Remove unused import * cargo fmt * Modify config error handling * cargo xtask docgen * impl display for ConfigLoadError * cargo fmt * Put keymaps behind dyn access, refactor config.load() * Update command names * Update helix-term/src/application.rs Co-authored-by: Blaž Hrastnik * Switch to unbounded_channel * Remove --edit-config command * Update configuration docs * Revert "Put keymaps behind dyn access", too hard This reverts commit 06bad8cf492b9331d0a2d1e9242f3ad4e2c1cf79. * Add refresh for keys * Refactor default_keymaps, fix config default, add test * swap -> store, remove unneeded clone * cargo fmt * Rename default_keymaps to default Co-authored-by: Blaž Hrastnik --- Cargo.lock | 2 + book/src/configuration.md | 2 +- book/src/generated/typable-cmd.md | 2 + helix-term/Cargo.toml | 1 + helix-term/src/application.rs | 84 ++++- helix-term/src/args.rs | 2 - helix-term/src/commands.rs | 35 +- helix-term/src/commands/typed.rs | 45 ++- helix-term/src/config.rs | 71 +++- helix-term/src/keymap.rs | 532 ++---------------------------- helix-term/src/keymap/default.rs | 359 ++++++++++++++++++++ helix-term/src/keymap/macros.rs | 127 +++++++ helix-term/src/lib.rs | 1 + helix-term/src/main.rs | 35 +- helix-term/src/ui/editor.rs | 20 +- helix-term/src/ui/mod.rs | 5 +- helix-view/Cargo.toml | 6 +- helix-view/src/editor.rs | 45 ++- helix-view/src/gutter.rs | 4 +- 19 files changed, 797 insertions(+), 581 deletions(-) create mode 100644 helix-term/src/keymap/default.rs create mode 100644 helix-term/src/keymap/macros.rs diff --git a/Cargo.lock b/Cargo.lock index e92d97e7..1770fc3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,6 +435,7 @@ name = "helix-term" version = "0.6.0" dependencies = [ "anyhow", + "arc-swap", "chrono", "content_inspector", "crossterm", @@ -483,6 +484,7 @@ name = "helix-view" version = "0.6.0" dependencies = [ "anyhow", + "arc-swap", "bitflags", "chardetng", "clipboard-win", diff --git a/book/src/configuration.md b/book/src/configuration.md index 2b29379e..f110edd7 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -5,7 +5,7 @@ To override global configuration parameters, create a `config.toml` file located * Linux and Mac: `~/.config/helix/config.toml` * Windows: `%AppData%\helix\config.toml` -> Note: You may use `hx --edit-config` to create and edit the `config.toml` file. +> Hint: You can easily open the config file by typing `:config-open` within Helix normal mode. Example config: diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 2d89c97f..1ee2fac4 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -55,3 +55,5 @@ | `:sort` | Sort ranges in selection. | | `:rsort` | Sort ranges in selection in reverse order. | | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | +| `:config-reload` | Refreshes helix's config. | +| `:config-open` | Open the helix config.toml file. | diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 4f869b62..2e0b774b 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -41,6 +41,7 @@ crossterm = { version = "0.23", features = ["event-stream"] } signal-hook = "0.3" tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } +arc-swap = { version = "1.5.0" } # Logging fern = "0.6" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index d8208eed..9b21c2a0 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,10 +1,14 @@ +use arc_swap::{access::Map, ArcSwap}; use helix_core::{ config::{default_syntax_loader, user_syntax_loader}, pos_at_coords, syntax, Selection, }; use helix_dap::{self as dap, Payload, Request}; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; -use helix_view::{editor::Breakpoint, theme, Editor}; +use helix_view::{ + editor::{Breakpoint, ConfigEvent}, + theme, Editor, +}; use serde_json::json; use crate::{ @@ -13,6 +17,7 @@ use crate::{ compositor::Compositor, config::Config, job::Jobs, + keymap::Keymaps, ui::{self, overlay::overlayed}, }; @@ -42,8 +47,7 @@ pub struct Application { compositor: Compositor, editor: Editor, - // TODO: share an ArcSwap with Editor? - config: Config, + config: Arc>, #[allow(dead_code)] theme_loader: Arc, @@ -56,7 +60,7 @@ pub struct Application { } impl Application { - pub fn new(args: Args, mut config: Config) -> Result { + pub fn new(args: Args, config: Config) -> Result { use helix_view::editor::Action; let mut compositor = Compositor::new()?; let size = compositor.size(); @@ -98,14 +102,20 @@ impl Application { }); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); + let config = Arc::new(ArcSwap::from_pointee(config)); let mut editor = Editor::new( size, theme_loader.clone(), syn_loader.clone(), - config.editor.clone(), + Box::new(Map::new(Arc::clone(&config), |config: &Config| { + &config.editor + })), ); - let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys))); + let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { + &config.keys + })); + let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); compositor.push(editor_view); if args.load_tutor { @@ -113,15 +123,12 @@ impl Application { editor.open(path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; - } else if args.edit_config { - let path = conf_dir.join("config.toml"); - editor.open(path, Action::VerticalSplit)?; } else if !args.files.is_empty() { let first = &args.files[0].0; // we know it's not empty if first.is_dir() { std::env::set_current_dir(&first)?; editor.new_file(Action::VerticalSplit); - let picker = ui::file_picker(".".into(), &config.editor); + let picker = ui::file_picker(".".into(), &config.load().editor); compositor.push(Box::new(overlayed(picker))); } else { let nr_of_files = args.files.len(); @@ -228,6 +235,10 @@ impl Application { Some(payload) = self.editor.debugger_events.next() => { self.handle_debugger_message(payload).await; } + Some(config_event) = self.editor.config_events.1.recv() => { + self.handle_config_events(config_event); + self.render(); + } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); @@ -245,6 +256,55 @@ impl Application { } } + pub fn handle_config_events(&mut self, config_event: ConfigEvent) { + match config_event { + ConfigEvent::Refresh => self.refresh_config(), + + // Since only the Application can make changes to Editor's config, + // the Editor must send up a new copy of a modified config so that + // the Application can apply it. + ConfigEvent::Update(editor_config) => { + let mut app_config = (*self.config.load().clone()).clone(); + app_config.editor = editor_config; + self.config.store(Arc::new(app_config)); + } + } + } + + fn refresh_config(&mut self) { + let config = Config::load(helix_loader::config_file()).unwrap_or_else(|err| { + self.editor.set_error(err.to_string()); + Config::default() + }); + + // Refresh theme + if let Some(theme) = config.theme.clone() { + let true_color = self.true_color(); + self.editor.set_theme( + self.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(|| { + if true_color { + self.theme_loader.default() + } else { + self.theme_loader.base16_default() + } + }), + ); + } + self.config.store(Arc::new(config)); + } + + 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: ()) {} @@ -700,7 +760,7 @@ impl Application { self.lsp_progress.update(server_id, token, work); } - if self.config.lsp.display_messages { + if self.config.load().lsp.display_messages { self.editor.set_status(status); } } @@ -809,7 +869,7 @@ impl Application { terminal::enable_raw_mode()?; let mut stdout = stdout(); execute!(stdout, terminal::EnterAlternateScreen)?; - if self.config.editor.mouse { + if self.config.load().editor.mouse { execute!(stdout, EnableMouseCapture)?; } Ok(()) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index e0f0af00..b99c7d1a 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -13,7 +13,6 @@ pub struct Args { pub build_grammars: bool, pub verbosity: u64, pub files: Vec<(PathBuf, Position)>, - pub edit_config: bool, } impl Args { @@ -29,7 +28,6 @@ impl Args { "--version" => args.display_version = true, "--help" => args.display_help = true, "--tutor" => args.load_tutor = true, - "--edit-config" => args.edit_config = true, "--health" => { args.health = true; args.health_arg = argv.next_if(|opt| !opt.starts_with('-')); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c7489810..0b624f25 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -842,6 +842,7 @@ fn align_selections(cx: &mut Context) { fn goto_window(cx: &mut Context, align: Align) { let count = cx.count() - 1; + let config = cx.editor.config(); let (view, doc) = current!(cx.editor); let height = view.inner_area().height as usize; @@ -850,7 +851,7 @@ fn goto_window(cx: &mut Context, align: Align) { // - 1 so we have at least one gap in the middle. // a height of 6 with padding of 3 on each side will keep shifting the view back and forth // as we type - let scrolloff = cx.editor.config.scrolloff.min(height.saturating_sub(1) / 2); + let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2); let last_line = view.last_line(doc); @@ -1274,6 +1275,7 @@ fn switch_to_lowercase(cx: &mut Context) { pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { use Direction::*; + let config = cx.editor.config(); let (view, doc) = current!(cx.editor); let range = doc.selection(view.id).primary(); @@ -1292,7 +1294,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { let height = view.inner_area().height; - let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2); + let scrolloff = config.scrolloff.min(height as usize / 2); view.offset.row = match direction { Forward => view.offset.row + offset, @@ -1585,8 +1587,9 @@ fn rsearch(cx: &mut Context) { fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); - let scrolloff = cx.editor.config.scrolloff; - let wrap_around = cx.editor.config.search.wrap_around; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let wrap_around = config.search.wrap_around; let doc = doc!(cx.editor); @@ -1629,13 +1632,14 @@ fn searcher(cx: &mut Context, direction: Direction) { } fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) { - let scrolloff = cx.editor.config.scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; let (view, doc) = current!(cx.editor); let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { let query = query.last().unwrap(); let contents = doc.text().slice(..).to_string(); - let search_config = &cx.editor.config.search; + let search_config = &config.search; let case_insensitive = if search_config.smart_case { !query.chars().any(char::is_uppercase) } else { @@ -1695,8 +1699,9 @@ fn search_selection(cx: &mut Context) { fn global_search(cx: &mut Context) { let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); - let smart_case = cx.editor.config.search.smart_case; - let file_picker_config = cx.editor.config.file_picker.clone(); + let config = cx.editor.config(); + let smart_case = config.search.smart_case; + let file_picker_config = config.file_picker.clone(); let completions = search_completions(cx, None); let prompt = ui::regex_prompt( @@ -2028,7 +2033,7 @@ fn append_mode(cx: &mut Context) { fn file_picker(cx: &mut Context) { // We don't specify language markers, root will be the root of the current git repo let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./")); - let picker = ui::file_picker(root, &cx.editor.config); + let picker = ui::file_picker(root, &cx.editor.config()); cx.push_layer(Box::new(overlayed(picker))); } @@ -2105,7 +2110,7 @@ pub fn command_palette(cx: &mut Context) { move |compositor: &mut Compositor, cx: &mut compositor::Context| { let doc = doc_mut!(cx.editor); let keymap = - compositor.find::().unwrap().keymaps.map[&doc.mode].reverse_map(); + compositor.find::().unwrap().keymaps.map()[&doc.mode].reverse_map(); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2571,6 +2576,7 @@ pub mod insert { // It trigger completion when idle timer reaches deadline // Only trigger completion if the word under cursor is longer than n characters pub fn idle_completion(cx: &mut Context) { + let config = cx.editor.config(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(text); @@ -2578,7 +2584,7 @@ pub mod insert { use helix_core::chars::char_is_word; let mut iter = text.chars_at(cursor); iter.reverse(); - for _ in 0..cx.editor.config.completion_trigger_len { + for _ in 0..config.completion_trigger_len { match iter.next() { Some(c) if char_is_word(c) => {} _ => return, @@ -4154,7 +4160,7 @@ fn shell_keep_pipe(cx: &mut Context) { Some('|'), ui::completers::none, move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let shell = &cx.editor.config.shell; + let shell = &cx.editor.config().shell; if event != PromptEvent::Validate { return; } @@ -4250,7 +4256,8 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { Some('|'), ui::completers::none, move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let shell = &cx.editor.config.shell; + let config = cx.editor.config(); + let shell = &config.shell; if event != PromptEvent::Validate { return; } @@ -4295,7 +4302,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { // after replace cursor may be out of bounds, do this to // make sure cursor is in view and update scroll as well - view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff); }, ); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index d35b7082..8b7f481b 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,6 +1,6 @@ use super::*; -use helix_view::editor::Action; +use helix_view::editor::{Action, ConfigEvent}; use ui::completers::{self, Completer}; #[derive(Clone)] @@ -540,7 +540,7 @@ fn theme( .theme_loader .load(theme) .with_context(|| format!("Failed setting theme {}", theme))?; - let true_color = cx.editor.config.true_color || crate::true_color(); + let true_color = cx.editor.config().true_color || crate::true_color(); if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } @@ -894,7 +894,7 @@ fn setting( let key_error = || anyhow::anyhow!("Unknown key `{key}`"); let field_error = |_| anyhow::anyhow!("Could not parse field `{arg}`"); - let mut config = serde_json::to_value(&cx.editor.config).unwrap(); + let mut config = serde_json::to_value(&cx.editor.config().clone()).unwrap(); let pointer = format!("/{}", key.replace('.', "/")); let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; @@ -904,8 +904,12 @@ fn setting( } else { arg.parse().map_err(field_error)? }; - cx.editor.config = serde_json::from_value(config).map_err(field_error)?; + let config = serde_json::from_value(config).map_err(field_error)?; + cx.editor + .config_events + .0 + .send(ConfigEvent::Update(config))?; Ok(()) } @@ -995,6 +999,25 @@ fn tree_sitter_subtree( Ok(()) } +fn open_config( + cx: &mut compositor::Context, + _args: &[Cow], + _event: PromptEvent, +) -> anyhow::Result<()> { + cx.editor + .open(helix_loader::config_file(), Action::Replace)?; + Ok(()) +} + +fn refresh_config( + cx: &mut compositor::Context, + _args: &[Cow], + _event: PromptEvent, +) -> anyhow::Result<()> { + cx.editor.config_events.0.send(ConfigEvent::Refresh)?; + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -1381,6 +1404,20 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: tree_sitter_subtree, completer: None, }, + TypableCommand { + name: "config-reload", + aliases: &[], + doc: "Refreshes helix's config.", + fun: refresh_config, + completer: None, + }, + TypableCommand { + name: "config-open", + aliases: &[], + doc: "Open the helix config.toml file.", + fun: open_config, + completer: None, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 1c6289ec..06e44ad9 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,25 +1,71 @@ +use crate::keymap::{default::default, 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 crate::keymap::Keymaps; - -#[derive(Debug, Default, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { pub theme: Option, #[serde(default)] pub lsp: LspConfig, - #[serde(default)] - pub keys: Keymaps, + #[serde(default = "default")] + pub keys: HashMap, #[serde(default)] pub editor: helix_view::editor::Config, } +impl Default for Config { + fn default() -> Config { + Config { + theme: None, + lsp: LspConfig::default(), + keys: 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), + } + } +} + #[derive(Debug, Default, Clone, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LspConfig { pub display_messages: bool, } +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::*; @@ -43,7 +89,7 @@ mod tests { assert_eq!( toml::from_str::(sample_keymaps).unwrap(), Config { - keys: Keymaps::new(hashmap! { + keys: hashmap! { Mode::Insert => Keymap::new(keymap!({ "Insert mode" "y" => move_line_down, "S-C-a" => delete_selection, @@ -51,9 +97,20 @@ mod tests { Mode::Normal => Keymap::new(keymap!({ "Normal mode" "A-F12" => move_next_word_end, })), - }), + }, ..Default::default() } ); } + + #[test] + fn keys_resolve_to_correct_defaults() { + // From serde default + let default_keys = toml::from_str::("").unwrap().keys; + assert_eq!(default_keys, default()); + + // From the Default trait + let default_keys = Config::default().keys; + assert_eq!(default_keys, default()); + } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 992a0cb8..37dbc5de 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,135 +1,23 @@ +pub mod default; +pub mod macros; + pub use crate::commands::MappableCommand; use crate::config::Config; -use helix_core::hashmap; +use arc_swap::{ + access::{DynAccess, DynGuard}, + ArcSwap, +}; use helix_view::{document::Mode, info::Info, input::KeyEvent}; use serde::Deserialize; use std::{ borrow::Cow, collections::{BTreeSet, HashMap}, ops::{Deref, DerefMut}, + sync::Arc, }; -#[macro_export] -macro_rules! key { - ($key:ident) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, - modifiers: ::helix_view::keyboard::KeyModifiers::NONE, - } - }; - ($($ch:tt)*) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: ::helix_view::keyboard::KeyModifiers::NONE, - } - }; -} - -#[macro_export] -macro_rules! shift { - ($key:ident) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, - modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, - } - }; - ($($ch:tt)*) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, - } - }; -} - -#[macro_export] -macro_rules! ctrl { - ($key:ident) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, - modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, - } - }; - ($($ch:tt)*) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, - } - }; -} - -#[macro_export] -macro_rules! alt { - ($key:ident) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::$key, - modifiers: ::helix_view::keyboard::KeyModifiers::ALT, - } - }; - ($($ch:tt)*) => { - ::helix_view::input::KeyEvent { - code: ::helix_view::keyboard::KeyCode::Char($($ch)*), - modifiers: ::helix_view::keyboard::KeyModifiers::ALT, - } - }; -} - -/// Macro for defining the root of a `Keymap` object. Example: -/// -/// ``` -/// # use helix_core::hashmap; -/// # use helix_term::keymap; -/// # use helix_term::keymap::Keymap; -/// let normal_mode = keymap!({ "Normal mode" -/// "i" => insert_mode, -/// "g" => { "Goto" -/// "g" => goto_file_start, -/// "e" => goto_file_end, -/// }, -/// "j" | "down" => move_line_down, -/// }); -/// let keymap = Keymap::new(normal_mode); -/// ``` -#[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,)+ } - ) => { - // modified from the hashmap! macro - { - 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(); - let _duplicate = _map.insert( - _key, - 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); - $( _node.is_sticky = $sticky; )? - $crate::keymap::KeyTrie::Node(_node) - } - }; -} +use default::default; +use macros::key; #[derive(Debug, Clone)] pub struct KeyTrieNode { @@ -381,23 +269,17 @@ impl Default for Keymap { } } -#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Keymaps { - #[serde(flatten)] - pub map: HashMap, - + pub map: Box>>, /// Stores pending keys waiting for the next key. This is relative to a /// sticky node if one is in use. - #[serde(skip)] state: Vec, - /// Stores the sticky node if one is activated. - #[serde(skip)] pub sticky: Option, } impl Keymaps { - pub fn new(map: HashMap) -> Self { + pub fn new(map: Box>>) -> Self { Self { map, state: Vec::new(), @@ -405,6 +287,10 @@ impl Keymaps { } } + 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 @@ -419,7 +305,8 @@ impl Keymaps { /// 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 keymap = &self.map[&mode]; + let keymaps = &*self.map(); + let keymap = &keymaps[&mode]; if key!(Esc) == key { if !self.state.is_empty() { @@ -470,372 +357,25 @@ impl Keymaps { impl Default for Keymaps { fn default() -> Self { - let normal = keymap!({ "Normal mode" - "h" | "left" => move_char_left, - "j" | "down" => move_line_down, - "k" | "up" => move_line_up, - "l" | "right" => move_char_right, - - "t" => find_till_char, - "f" => find_next_char, - "T" => till_prev_char, - "F" => find_prev_char, - "r" => replace, - "R" => replace_with_yanked, - "A-." => repeat_last_motion, - - "~" => switch_case, - "`" => switch_to_lowercase, - "A-`" => switch_to_uppercase, - - "home" => goto_line_start, - "end" => goto_line_end, - - "w" => move_next_word_start, - "b" => move_prev_word_start, - "e" => move_next_word_end, - - "W" => move_next_long_word_start, - "B" => move_prev_long_word_start, - "E" => move_next_long_word_end, - - "v" => select_mode, - "G" => goto_line, - "g" => { "Goto" - "g" => goto_file_start, - "e" => goto_last_line, - "f" => goto_file, - "h" => goto_line_start, - "l" => goto_line_end, - "s" => goto_first_nonwhitespace, - "d" => goto_definition, - "y" => goto_type_definition, - "r" => goto_reference, - "i" => goto_implementation, - "t" => goto_window_top, - "c" => goto_window_center, - "b" => goto_window_bottom, - "a" => goto_last_accessed_file, - "m" => goto_last_modified_file, - "n" => goto_next_buffer, - "p" => goto_previous_buffer, - "." => goto_last_modification, - }, - ":" => command_mode, - - "i" => insert_mode, - "I" => prepend_to_line, - "a" => append_mode, - "A" => append_to_line, - "o" => open_below, - "O" => open_above, - - "d" => delete_selection, - "A-d" => delete_selection_noyank, - "c" => change_selection, - "A-c" => change_selection_noyank, - - "C" => copy_selection_on_next_line, - "A-C" => copy_selection_on_prev_line, - - - "s" => select_regex, - "A-s" => split_selection_on_newline, - "S" => split_selection, - ";" => collapse_selection, - "A-;" => flip_selections, - "A-k" | "A-up" => expand_selection, - "A-j" | "A-down" => shrink_selection, - "A-h" | "A-left" => select_prev_sibling, - "A-l" | "A-right" => select_next_sibling, - - "%" => select_all, - "x" => extend_line, - "X" => extend_to_line_bounds, - // crop_to_whole_line - - "m" => { "Match" - "m" => match_brackets, - "s" => surround_add, - "r" => surround_replace, - "d" => surround_delete, - "a" => select_textobject_around, - "i" => select_textobject_inner, - }, - "[" => { "Left bracket" - "d" => goto_prev_diag, - "D" => goto_first_diag, - "f" => goto_prev_function, - "c" => goto_prev_class, - "a" => goto_prev_parameter, - "o" => goto_prev_comment, - "space" => add_newline_above, - }, - "]" => { "Right bracket" - "d" => goto_next_diag, - "D" => goto_last_diag, - "f" => goto_next_function, - "c" => goto_next_class, - "a" => goto_next_parameter, - "o" => goto_next_comment, - "space" => add_newline_below, - }, - - "/" => search, - "?" => rsearch, - "n" => search_next, - "N" => search_prev, - "*" => search_selection, - - "u" => undo, - "U" => redo, - "A-u" => earlier, - "A-U" => later, - - "y" => yank, - // yank_all - "p" => paste_after, - // paste_all - "P" => paste_before, - - "Q" => record_macro, - "q" => replay_macro, - - ">" => indent, - "<" => unindent, - "=" => format_selections, - "J" => join_selections, - "K" => keep_selections, - "A-K" => remove_selections, - - "," => keep_primary_selection, - "A-," => remove_primary_selection, - - // "q" => record_macro, - // "Q" => replay_macro, - - "&" => align_selections, - "_" => trim_selections, - - "(" => rotate_selections_backward, - ")" => rotate_selections_forward, - "A-(" => rotate_selection_contents_backward, - "A-)" => rotate_selection_contents_forward, - - "A-:" => ensure_selections_forward, - - "esc" => normal_mode, - "C-b" | "pageup" => page_up, - "C-f" | "pagedown" => page_down, - "C-u" => half_page_up, - "C-d" => half_page_down, - - "C-w" => { "Window" - "C-w" | "w" => rotate_view, - "C-s" | "s" => hsplit, - "C-v" | "v" => vsplit, - "f" => goto_file_hsplit, - "F" => goto_file_vsplit, - "C-q" | "q" => wclose, - "C-o" | "o" => wonly, - "C-h" | "h" | "left" => jump_view_left, - "C-j" | "j" | "down" => jump_view_down, - "C-k" | "k" | "up" => jump_view_up, - "C-l" | "l" | "right" => jump_view_right, - "n" => { "New split scratch buffer" - "C-s" | "s" => hsplit_new, - "C-v" | "v" => vsplit_new, - }, - }, - - // move under c - "C-c" => toggle_comments, - - // z family for save/restore/combine from/to sels from register - - "tab" => jump_forward, // tab == - "C-o" => jump_backward, - "C-s" => save_selection, - - "space" => { "Space" - "f" => file_picker, - "b" => buffer_picker, - "s" => symbol_picker, - "S" => workspace_symbol_picker, - "a" => code_action, - "'" => last_picker, - "d" => { "Debug (experimental)" sticky=true - "l" => dap_launch, - "b" => dap_toggle_breakpoint, - "c" => dap_continue, - "h" => dap_pause, - "i" => dap_step_in, - "o" => dap_step_out, - "n" => dap_next, - "v" => dap_variables, - "t" => dap_terminate, - "C-c" => dap_edit_condition, - "C-l" => dap_edit_log, - "s" => { "Switch" - "t" => dap_switch_thread, - "f" => dap_switch_stack_frame, - // sl, sb - }, - "e" => dap_enable_exceptions, - "E" => dap_disable_exceptions, - }, - "w" => { "Window" - "C-w" | "w" => rotate_view, - "C-s" | "s" => hsplit, - "C-v" | "v" => vsplit, - "f" => goto_file_hsplit, - "F" => goto_file_vsplit, - "C-q" | "q" => wclose, - "C-o" | "o" => wonly, - "C-h" | "h" | "left" => jump_view_left, - "C-j" | "j" | "down" => jump_view_down, - "C-k" | "k" | "up" => jump_view_up, - "C-l" | "l" | "right" => jump_view_right, - "n" => { "New split scratch buffer" - "C-s" | "s" => hsplit_new, - "C-v" | "v" => vsplit_new, - }, - }, - "y" => yank_joined_to_clipboard, - "Y" => yank_main_selection_to_clipboard, - "p" => paste_clipboard_after, - "P" => paste_clipboard_before, - "R" => replace_selections_with_clipboard, - "/" => global_search, - "k" => hover, - "r" => rename_symbol, - "?" => command_palette, - }, - "z" => { "View" - "z" | "c" => align_view_center, - "t" => align_view_top, - "b" => align_view_bottom, - "m" => align_view_middle, - "k" | "up" => scroll_up, - "j" | "down" => scroll_down, - "C-b" | "pageup" => page_up, - "C-f" | "pagedown" => page_down, - "C-u" => half_page_up, - "C-d" => half_page_down, - }, - "Z" => { "View" sticky=true - "z" | "c" => align_view_center, - "t" => align_view_top, - "b" => align_view_bottom, - "m" => align_view_middle, - "k" | "up" => scroll_up, - "j" | "down" => scroll_down, - "C-b" | "pageup" => page_up, - "C-f" | "pagedown" => page_down, - "C-u" => half_page_up, - "C-d" => half_page_down, - }, - - "\"" => select_register, - "|" => shell_pipe, - "A-|" => shell_pipe_to, - "!" => shell_insert_output, - "A-!" => shell_append_output, - "$" => shell_keep_pipe, - "C-z" => suspend, - - "C-a" => increment, - "C-x" => decrement, - }); - let mut select = normal.clone(); - select.merge_nodes(keymap!({ "Select mode" - "h" | "left" => extend_char_left, - "j" | "down" => extend_line_down, - "k" | "up" => extend_line_up, - "l" | "right" => extend_char_right, - - "w" => extend_next_word_start, - "b" => extend_prev_word_start, - "e" => extend_next_word_end, - "W" => extend_next_long_word_start, - "B" => extend_prev_long_word_start, - "E" => extend_next_long_word_end, - - "n" => extend_search_next, - "N" => extend_search_prev, - - "t" => extend_till_char, - "f" => extend_next_char, - "T" => extend_till_prev_char, - "F" => extend_prev_char, - - "home" => extend_to_line_start, - "end" => extend_to_line_end, - "esc" => exit_select_mode, - - "v" => normal_mode, - })); - let insert = keymap!({ "Insert mode" - "esc" => normal_mode, - - "backspace" => delete_char_backward, - "C-h" => delete_char_backward, - "del" => delete_char_forward, - "C-d" => delete_char_forward, - "ret" => insert_newline, - "C-j" => insert_newline, - "tab" => insert_tab, - "C-w" => delete_word_backward, - "A-backspace" => delete_word_backward, - "A-d" => delete_word_forward, - - "left" => move_char_left, - "C-b" => move_char_left, - "down" => move_line_down, - "C-n" => move_line_down, - "up" => move_line_up, - "C-p" => move_line_up, - "right" => move_char_right, - "C-f" => move_char_right, - "A-b" => move_prev_word_end, - "A-left" => move_prev_word_end, - "A-f" => move_next_word_start, - "A-right" => move_next_word_start, - "A-<" => goto_file_start, - "A->" => goto_file_end, - "pageup" => page_up, - "pagedown" => page_down, - "home" => goto_line_start, - "C-a" => goto_line_start, - "end" => goto_line_end_newline, - "C-e" => goto_line_end_newline, - - "C-k" => kill_to_line_end, - "C-u" => kill_to_line_start, - - "C-x" => completion, - "C-r" => insert_register, - }); - Self::new(hashmap!( - Mode::Normal => Keymap::new(normal), - Mode::Select => Keymap::new(select), - Mode::Insert => Keymap::new(insert), - )) + Self::new(Box::new(ArcSwap::new(Arc::new(default())))) } } /// 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::take(&mut config.keys); - for (mode, keys) in &mut config.keys.map { - keys.merge(delta.map.remove(mode).unwrap_or_default()) + 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()) } config } #[cfg(test)] mod tests { + use super::macros::keymap; use super::*; + use arc_swap::access::Constant; + use helix_core::hashmap; #[test] #[should_panic] @@ -855,7 +395,7 @@ mod tests { #[test] fn merge_partial_keys() { let config = Config { - keys: Keymaps::new(hashmap! { + keys: hashmap! { Mode::Normal => Keymap::new( keymap!({ "Normal mode" "i" => normal_mode, @@ -867,13 +407,13 @@ mod tests { }, }) ) - }), + }, ..Default::default() }; let mut merged_config = merge_keys(config.clone()); assert_ne!(config, merged_config); - let keymap = &mut merged_config.keys; + 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), @@ -891,7 +431,7 @@ mod tests { "Leaf should replace node" ); - let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap(); + 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(), @@ -911,14 +451,14 @@ mod tests { "Old leaves in subnode should be present in merged node" ); - assert!(merged_config.keys.map.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.map.get(&Mode::Insert).unwrap().len() > 0); + 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: Keymaps::new(hashmap! { + keys: hashmap! { Mode::Normal => Keymap::new( keymap!({ "Normal mode" "space" => { "" @@ -929,12 +469,12 @@ mod tests { }, }) ) - }), + }, ..Default::default() }; let mut merged_config = merge_keys(config.clone()); assert_ne!(config, merged_config); - let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap(); + let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( keymap @@ -951,8 +491,8 @@ mod tests { #[test] fn aliased_modes_are_same_in_default_keymap() { - let keymaps = Keymaps::default(); - let root = keymaps.map.get(&Mode::Normal).unwrap().root(); + 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(), diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs new file mode 100644 index 00000000..b5685082 --- /dev/null +++ b/helix-term/src/keymap/default.rs @@ -0,0 +1,359 @@ +use std::collections::HashMap; + +use super::macros::keymap; +use super::{Keymap, Mode}; +use helix_core::hashmap; + +pub fn default() -> HashMap { + let normal = keymap!({ "Normal mode" + "h" | "left" => move_char_left, + "j" | "down" => move_line_down, + "k" | "up" => move_line_up, + "l" | "right" => move_char_right, + + "t" => find_till_char, + "f" => find_next_char, + "T" => till_prev_char, + "F" => find_prev_char, + "r" => replace, + "R" => replace_with_yanked, + "A-." => repeat_last_motion, + + "~" => switch_case, + "`" => switch_to_lowercase, + "A-`" => switch_to_uppercase, + + "home" => goto_line_start, + "end" => goto_line_end, + + "w" => move_next_word_start, + "b" => move_prev_word_start, + "e" => move_next_word_end, + + "W" => move_next_long_word_start, + "B" => move_prev_long_word_start, + "E" => move_next_long_word_end, + + "v" => select_mode, + "G" => goto_line, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_last_line, + "f" => goto_file, + "h" => goto_line_start, + "l" => goto_line_end, + "s" => goto_first_nonwhitespace, + "d" => goto_definition, + "y" => goto_type_definition, + "r" => goto_reference, + "i" => goto_implementation, + "t" => goto_window_top, + "c" => goto_window_center, + "b" => goto_window_bottom, + "a" => goto_last_accessed_file, + "m" => goto_last_modified_file, + "n" => goto_next_buffer, + "p" => goto_previous_buffer, + "." => goto_last_modification, + }, + ":" => command_mode, + + "i" => insert_mode, + "I" => prepend_to_line, + "a" => append_mode, + "A" => append_to_line, + "o" => open_below, + "O" => open_above, + + "d" => delete_selection, + "A-d" => delete_selection_noyank, + "c" => change_selection, + "A-c" => change_selection_noyank, + + "C" => copy_selection_on_next_line, + "A-C" => copy_selection_on_prev_line, + + + "s" => select_regex, + "A-s" => split_selection_on_newline, + "S" => split_selection, + ";" => collapse_selection, + "A-;" => flip_selections, + "A-k" | "A-up" => expand_selection, + "A-j" | "A-down" => shrink_selection, + "A-h" | "A-left" => select_prev_sibling, + "A-l" | "A-right" => select_next_sibling, + + "%" => select_all, + "x" => extend_line, + "X" => extend_to_line_bounds, + // crop_to_whole_line + + "m" => { "Match" + "m" => match_brackets, + "s" => surround_add, + "r" => surround_replace, + "d" => surround_delete, + "a" => select_textobject_around, + "i" => select_textobject_inner, + }, + "[" => { "Left bracket" + "d" => goto_prev_diag, + "D" => goto_first_diag, + "f" => goto_prev_function, + "c" => goto_prev_class, + "a" => goto_prev_parameter, + "o" => goto_prev_comment, + "space" => add_newline_above, + }, + "]" => { "Right bracket" + "d" => goto_next_diag, + "D" => goto_last_diag, + "f" => goto_next_function, + "c" => goto_next_class, + "a" => goto_next_parameter, + "o" => goto_next_comment, + "space" => add_newline_below, + }, + + "/" => search, + "?" => rsearch, + "n" => search_next, + "N" => search_prev, + "*" => search_selection, + + "u" => undo, + "U" => redo, + "A-u" => earlier, + "A-U" => later, + + "y" => yank, + // yank_all + "p" => paste_after, + // paste_all + "P" => paste_before, + + "Q" => record_macro, + "q" => replay_macro, + + ">" => indent, + "<" => unindent, + "=" => format_selections, + "J" => join_selections, + "K" => keep_selections, + "A-K" => remove_selections, + + "," => keep_primary_selection, + "A-," => remove_primary_selection, + + // "q" => record_macro, + // "Q" => replay_macro, + + "&" => align_selections, + "_" => trim_selections, + + "(" => rotate_selections_backward, + ")" => rotate_selections_forward, + "A-(" => rotate_selection_contents_backward, + "A-)" => rotate_selection_contents_forward, + + "A-:" => ensure_selections_forward, + + "esc" => normal_mode, + "C-b" | "pageup" => page_up, + "C-f" | "pagedown" => page_down, + "C-u" => half_page_up, + "C-d" => half_page_down, + + "C-w" => { "Window" + "C-w" | "w" => rotate_view, + "C-s" | "s" => hsplit, + "C-v" | "v" => vsplit, + "f" => goto_file_hsplit, + "F" => goto_file_vsplit, + "C-q" | "q" => wclose, + "C-o" | "o" => wonly, + "C-h" | "h" | "left" => jump_view_left, + "C-j" | "j" | "down" => jump_view_down, + "C-k" | "k" | "up" => jump_view_up, + "C-l" | "l" | "right" => jump_view_right, + "n" => { "New split scratch buffer" + "C-s" | "s" => hsplit_new, + "C-v" | "v" => vsplit_new, + }, + }, + + // move under c + "C-c" => toggle_comments, + + // z family for save/restore/combine from/to sels from register + + "tab" => jump_forward, // tab == + "C-o" => jump_backward, + "C-s" => save_selection, + + "space" => { "Space" + "f" => file_picker, + "b" => buffer_picker, + "s" => symbol_picker, + "S" => workspace_symbol_picker, + "a" => code_action, + "'" => last_picker, + "d" => { "Debug (experimental)" sticky=true + "l" => dap_launch, + "b" => dap_toggle_breakpoint, + "c" => dap_continue, + "h" => dap_pause, + "i" => dap_step_in, + "o" => dap_step_out, + "n" => dap_next, + "v" => dap_variables, + "t" => dap_terminate, + "C-c" => dap_edit_condition, + "C-l" => dap_edit_log, + "s" => { "Switch" + "t" => dap_switch_thread, + "f" => dap_switch_stack_frame, + // sl, sb + }, + "e" => dap_enable_exceptions, + "E" => dap_disable_exceptions, + }, + "w" => { "Window" + "C-w" | "w" => rotate_view, + "C-s" | "s" => hsplit, + "C-v" | "v" => vsplit, + "f" => goto_file_hsplit, + "F" => goto_file_vsplit, + "C-q" | "q" => wclose, + "C-o" | "o" => wonly, + "C-h" | "h" | "left" => jump_view_left, + "C-j" | "j" | "down" => jump_view_down, + "C-k" | "k" | "up" => jump_view_up, + "C-l" | "l" | "right" => jump_view_right, + "n" => { "New split scratch buffer" + "C-s" | "s" => hsplit_new, + "C-v" | "v" => vsplit_new, + }, + }, + "y" => yank_joined_to_clipboard, + "Y" => yank_main_selection_to_clipboard, + "p" => paste_clipboard_after, + "P" => paste_clipboard_before, + "R" => replace_selections_with_clipboard, + "/" => global_search, + "k" => hover, + "r" => rename_symbol, + "?" => command_palette, + }, + "z" => { "View" + "z" | "c" => align_view_center, + "t" => align_view_top, + "b" => align_view_bottom, + "m" => align_view_middle, + "k" | "up" => scroll_up, + "j" | "down" => scroll_down, + "C-b" | "pageup" => page_up, + "C-f" | "pagedown" => page_down, + "C-u" => half_page_up, + "C-d" => half_page_down, + }, + "Z" => { "View" sticky=true + "z" | "c" => align_view_center, + "t" => align_view_top, + "b" => align_view_bottom, + "m" => align_view_middle, + "k" | "up" => scroll_up, + "j" | "down" => scroll_down, + "C-b" | "pageup" => page_up, + "C-f" | "pagedown" => page_down, + "C-u" => half_page_up, + "C-d" => half_page_down, + }, + + "\"" => select_register, + "|" => shell_pipe, + "A-|" => shell_pipe_to, + "!" => shell_insert_output, + "A-!" => shell_append_output, + "$" => shell_keep_pipe, + "C-z" => suspend, + + "C-a" => increment, + "C-x" => decrement, + }); + let mut select = normal.clone(); + select.merge_nodes(keymap!({ "Select mode" + "h" | "left" => extend_char_left, + "j" | "down" => extend_line_down, + "k" | "up" => extend_line_up, + "l" | "right" => extend_char_right, + + "w" => extend_next_word_start, + "b" => extend_prev_word_start, + "e" => extend_next_word_end, + "W" => extend_next_long_word_start, + "B" => extend_prev_long_word_start, + "E" => extend_next_long_word_end, + + "n" => extend_search_next, + "N" => extend_search_prev, + + "t" => extend_till_char, + "f" => extend_next_char, + "T" => extend_till_prev_char, + "F" => extend_prev_char, + + "home" => extend_to_line_start, + "end" => extend_to_line_end, + "esc" => exit_select_mode, + + "v" => normal_mode, + })); + let insert = keymap!({ "Insert mode" + "esc" => normal_mode, + + "backspace" => delete_char_backward, + "C-h" => delete_char_backward, + "del" => delete_char_forward, + "C-d" => delete_char_forward, + "ret" => insert_newline, + "C-j" => insert_newline, + "tab" => insert_tab, + "C-w" => delete_word_backward, + "A-backspace" => delete_word_backward, + "A-d" => delete_word_forward, + + "left" => move_char_left, + "C-b" => move_char_left, + "down" => move_line_down, + "C-n" => move_line_down, + "up" => move_line_up, + "C-p" => move_line_up, + "right" => move_char_right, + "C-f" => move_char_right, + "A-b" => move_prev_word_end, + "A-left" => move_prev_word_end, + "A-f" => move_next_word_start, + "A-right" => move_next_word_start, + "A-<" => goto_file_start, + "A->" => goto_file_end, + "pageup" => page_up, + "pagedown" => page_down, + "home" => goto_line_start, + "C-a" => goto_line_start, + "end" => goto_line_end_newline, + "C-e" => goto_line_end_newline, + + "C-k" => kill_to_line_end, + "C-u" => kill_to_line_start, + + "C-x" => completion, + "C-r" => insert_register, + }); + hashmap!( + Mode::Normal => Keymap::new(normal), + Mode::Select => Keymap::new(select), + Mode::Insert => Keymap::new(insert), + ) +} diff --git a/helix-term/src/keymap/macros.rs b/helix-term/src/keymap/macros.rs new file mode 100644 index 00000000..c4a1bfbb --- /dev/null +++ b/helix-term/src/keymap/macros.rs @@ -0,0 +1,127 @@ +#[macro_export] +macro_rules! key { + ($key:ident) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::$key, + modifiers: ::helix_view::keyboard::KeyModifiers::NONE, + } + }; + ($($ch:tt)*) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::NONE, + } + }; +} + +#[macro_export] +macro_rules! shift { + ($key:ident) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::$key, + modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, + } + }; + ($($ch:tt)*) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT, + } + }; +} + +#[macro_export] +macro_rules! ctrl { + ($key:ident) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::$key, + modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, + } + }; + ($($ch:tt)*) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, + } + }; +} + +#[macro_export] +macro_rules! alt { + ($key:ident) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::$key, + modifiers: ::helix_view::keyboard::KeyModifiers::ALT, + } + }; + ($($ch:tt)*) => { + ::helix_view::input::KeyEvent { + code: ::helix_view::keyboard::KeyCode::Char($($ch)*), + modifiers: ::helix_view::keyboard::KeyModifiers::ALT, + } + }; +} + +/// Macro for defining the root of a `Keymap` object. Example: +/// +/// ``` +/// # use helix_core::hashmap; +/// # use helix_term::keymap; +/// # use helix_term::keymap::Keymap; +/// let normal_mode = keymap!({ "Normal mode" +/// "i" => insert_mode, +/// "g" => { "Goto" +/// "g" => goto_file_start, +/// "e" => goto_file_end, +/// }, +/// "j" | "down" => move_line_down, +/// }); +/// let keymap = Keymap::new(normal_mode); +/// ``` +#[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,)+ } + ) => { + // modified from the hashmap! macro + { + 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(); + let _duplicate = _map.insert( + _key, + 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); + $( _node.is_sticky = $sticky; )? + $crate::keymap::KeyTrie::Node(_node) + } + }; +} + +pub use alt; +pub use ctrl; +pub use key; +pub use keymap; +pub use shift; diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index fc8e934e..a945b20d 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -10,6 +10,7 @@ pub mod health; pub mod job; pub mod keymap; pub mod ui; +pub use keymap::macros::*; #[cfg(not(windows))] fn true_color() -> bool { diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index e554a21b..0385d92c 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,8 +1,7 @@ use anyhow::{Context, Error, Result}; use helix_term::application::Application; use helix_term::args::Args; -use helix_term::config::Config; -use helix_term::keymap::merge_keys; +use helix_term::config::{Config, ConfigLoadError}; use std::path::PathBuf; fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { @@ -60,7 +59,6 @@ ARGS: FLAGS: -h, --help Prints help information - --edit-config Opens the helix config file --tutor Loads the tutorial --health [LANG] Checks for potential errors in editor setup If given, checks for config errors in language LANG @@ -118,19 +116,24 @@ FLAGS: std::fs::create_dir_all(&conf_dir).ok(); } - let config = match std::fs::read_to_string(helix_loader::config_file()) { - Ok(config) => toml::from_str(&config) - .map(merge_keys) - .unwrap_or_else(|err| { - eprintln!("Bad config: {}", err); - eprintln!("Press to continue with default config"); - use std::io::Read; - // This waits for an enter press. - let _ = std::io::stdin().read(&mut []); - Config::default() - }), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), - Err(err) => return Err(Error::new(err)), + let config = match Config::load_default() { + Ok(config) => config, + Err(err) => { + match err { + ConfigLoadError::BadConfig(err) => { + eprintln!("Bad config: {}", err); + eprintln!("Press to continue with default config"); + use std::io::Read; + // This waits for an enter press. + let _ = std::io::stdin().read(&mut []); + Config::default() + } + ConfigLoadError::Error(err) if err.kind() == std::io::ErrorKind::NotFound => { + Config::default() + } + ConfigLoadError::Error(err) => return Err(Error::new(err)), + } + } }; setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 611d65fb..28665ec3 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -118,7 +118,7 @@ impl EditorView { let highlights: Box> = if is_focused { Box::new(syntax::merge( highlights, - Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape), + Self::doc_selection_highlights(doc, view, theme, &editor.config().cursor_shape), )) } else { Box::new(highlights) @@ -702,7 +702,6 @@ impl EditorView { cxt: &mut commands::Context, event: KeyEvent, ) -> Option { - cxt.editor.autoinfo = None; let key_result = self.keymaps.get(mode, event); cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); @@ -845,7 +844,7 @@ impl EditorView { pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { if self.completion.is_some() - || !cx.editor.config.auto_completion + || !cx.editor.config().auto_completion || doc!(cx.editor).mode != Mode::Insert { return EventResult::Ignored(None); @@ -871,6 +870,7 @@ impl EditorView { event: MouseEvent, cxt: &mut commands::Context, ) -> EventResult { + let config = cxt.editor.config(); match event { MouseEvent { kind: MouseEventKind::Down(MouseButton::Left), @@ -971,7 +971,7 @@ impl EditorView { None => return EventResult::Ignored(None), } - let offset = cxt.editor.config.scroll_lines.abs() as usize; + let offset = config.scroll_lines.abs() as usize; commands::scroll(cxt, offset, direction); cxt.editor.tree.focus = current_view; @@ -983,7 +983,7 @@ impl EditorView { kind: MouseEventKind::Up(MouseButton::Left), .. } => { - if !cxt.editor.config.middle_click_paste { + if !config.middle_click_paste { return EventResult::Ignored(None); } @@ -1039,7 +1039,7 @@ impl EditorView { .. } => { let editor = &mut cxt.editor; - if !editor.config.middle_click_paste { + if !config.middle_click_paste { return EventResult::Ignored(None); } @@ -1163,9 +1163,9 @@ impl Component for EditorView { if cx.editor.should_close() { return EventResult::Ignored(None); } - + let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff); // Store a history state if not in insert mode. This also takes care of // commiting changes when leaving insert mode. @@ -1206,7 +1206,7 @@ impl Component for EditorView { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // clear with background color surface.set_style(area, cx.editor.theme.get("ui.background")); - + let config = cx.editor.config(); // if the terminal size suddenly changed, we need to trigger a resize cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline @@ -1215,7 +1215,7 @@ impl Component for EditorView { self.render_view(cx.editor, doc, view, area, surface, is_focused); } - if cx.editor.config.auto_info { + if config.auto_info { if let Some(mut info) = cx.editor.autoinfo.take() { info.render(area, surface, cx); cx.editor.autoinfo = Some(info) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 2273477f..6242ea2e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -37,6 +37,7 @@ pub fn regex_prompt( let doc_id = view.doc; let snapshot = doc.selection(view.id).clone(); let offset_snapshot = view.offset; + let config = cx.editor.config(); let mut prompt = Prompt::new( prompt, @@ -65,7 +66,7 @@ pub fn regex_prompt( return; } - let case_insensitive = if cx.editor.config.search.smart_case { + let case_insensitive = if config.search.smart_case { !input.chars().any(char::is_uppercase) } else { false @@ -84,7 +85,7 @@ pub fn regex_prompt( fun(view, doc, regex, event); - view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff); } Err(_err) => (), // TODO: mark command line as error } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index a4fa256d..e54b0289 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -17,14 +17,16 @@ term = ["crossterm"] bitflags = "1.3" anyhow = "1" helix-core = { version = "0.6", path = "../helix-core" } -helix-lsp = { version = "0.6", path = "../helix-lsp"} -helix-dap = { version = "0.6", path = "../helix-dap"} +helix-lsp = { version = "0.6", path = "../helix-lsp" } +helix-dap = { version = "0.6", path = "../helix-dap" } crossterm = { version = "0.23", optional = true } # Conversion traits once_cell = "1.10" url = "2" +arc-swap = { version = "1.5.0" } + tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index adf0cdf3..8220deb7 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -13,7 +13,6 @@ use futures_util::future; use futures_util::stream::select_all::SelectAll; use tokio_stream::wrappers::UnboundedReceiverStream; -use log::debug; use std::{ borrow::Cow, collections::{BTreeMap, HashMap}, @@ -24,7 +23,10 @@ use std::{ sync::Arc, }; -use tokio::time::{sleep, Duration, Instant, Sleep}; +use tokio::{ + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + time::{sleep, Duration, Instant, Sleep}, +}; use anyhow::{bail, Error}; @@ -40,6 +42,8 @@ use helix_dap as dap; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; +use arc_swap::access::{DynAccess, DynGuard}; + fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -287,7 +291,6 @@ pub struct Breakpoint { pub log_message: Option, } -#[derive(Debug)] pub struct Editor { pub tree: Tree, pub next_document_id: DocumentId, @@ -311,7 +314,7 @@ pub struct Editor { pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option, - pub config: Config, + pub config: Box>, pub auto_pairs: Option, pub idle_timer: Pin>, @@ -321,6 +324,14 @@ pub struct Editor { pub last_completion: Option, pub exit_code: i32, + + pub config_events: (UnboundedSender, UnboundedReceiver), +} + +#[derive(Debug, Clone)] +pub enum ConfigEvent { + Refresh, + Update(Config), } #[derive(Debug, Clone)] @@ -342,12 +353,11 @@ impl Editor { mut area: Rect, theme_loader: Arc, syn_loader: Arc, - config: Config, + config: Box>, ) -> Self { let language_servers = helix_lsp::Registry::new(); - let auto_pairs = (&config.auto_pairs).into(); - - debug!("Editor config: {config:#?}"); + let conf = config.load(); + let auto_pairs = (&conf.auto_pairs).into(); // HAXX: offset the render area height by 1 to account for prompt/commandline area.height -= 1; @@ -370,16 +380,21 @@ impl Editor { clipboard_provider: get_clipboard_provider(), status_msg: None, autoinfo: None, - idle_timer: Box::pin(sleep(config.idle_timeout)), + idle_timer: Box::pin(sleep(conf.idle_timeout)), last_motion: None, last_completion: None, pseudo_pending: None, config, auto_pairs, exit_code: 0, + config_events: unbounded_channel(), } } + pub fn config(&self) -> DynGuard { + self.config.load() + } + pub fn clear_idle_timer(&mut self) { // equivalent to internal Instant::far_future() (30 years) self.idle_timer @@ -388,9 +403,10 @@ impl Editor { } pub fn reset_idle_timer(&mut self) { + let config = self.config(); self.idle_timer .as_mut() - .reset(Instant::now() + self.config.idle_timeout); + .reset(Instant::now() + config.idle_timeout); } pub fn clear_status(&mut self) { @@ -466,9 +482,10 @@ impl Editor { } fn _refresh(&mut self) { + let config = self.config(); for (view, _) in self.tree.views_mut() { let doc = &self.documents[&view.doc]; - view.ensure_cursor_in_view(doc, self.config.scrolloff) + view.ensure_cursor_in_view(doc, config.scrolloff) } } @@ -716,9 +733,10 @@ impl Editor { } pub fn ensure_cursor_in_view(&mut self, id: ViewId) { + let config = self.config(); let view = self.tree.get_mut(id); let doc = &self.documents[&view.doc]; - view.ensure_cursor_in_view(doc, self.config.scrolloff) + view.ensure_cursor_in_view(doc, config.scrolloff) } #[inline] @@ -752,6 +770,7 @@ impl Editor { } pub fn cursor(&self) -> (Option, CursorKind) { + let config = self.config(); let (view, doc) = current_ref!(self); let cursor = doc .selection(view.id) @@ -761,7 +780,7 @@ impl Editor { let inner = view.inner_area(); pos.col += inner.x as usize; pos.row += inner.y as usize; - let cursorkind = self.config.cursor_shape.from_mode(doc.mode()); + let cursorkind = config.cursor_shape.from_mode(doc.mode()); (Some(pos), cursorkind) } else { (None, CursorKind::default()) diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 6a77c41f..7327ed1a 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -60,7 +60,7 @@ pub fn line_number<'doc>( .text() .char_to_line(doc.selection(view.id).primary().cursor(text)); - let config = editor.config.line_number; + let line_number = editor.config().line_number; let mode = doc.mode; Box::new(move |line: usize, selected: bool, out: &mut String| { @@ -70,7 +70,7 @@ pub fn line_number<'doc>( } else { use crate::{document::Mode, editor::LineNumber}; - let relative = config == LineNumber::Relative + let relative = line_number == LineNumber::Relative && mode != Mode::Insert && is_focused && current_line != line;