aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs84
-rw-r--r--helix-term/src/args.rs2
-rw-r--r--helix-term/src/commands.rs35
-rw-r--r--helix-term/src/commands/typed.rs45
-rw-r--r--helix-term/src/config.rs71
-rw-r--r--helix-term/src/keymap.rs532
-rw-r--r--helix-term/src/keymap/default.rs359
-rw-r--r--helix-term/src/keymap/macros.rs127
-rw-r--r--helix-term/src/lib.rs1
-rw-r--r--helix-term/src/main.rs35
-rw-r--r--helix-term/src/ui/editor.rs20
-rw-r--r--helix-term/src/ui/mod.rs5
12 files changed, 753 insertions, 563 deletions
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<ArcSwap<Config>>,
#[allow(dead_code)]
theme_loader: Arc<theme::Loader>,
@@ -56,7 +60,7 @@ pub struct Application {
}
impl Application {
- pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
+ pub fn new(args: Args, config: Config) -> Result<Self, Error> {
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::<ui::EditorView>().unwrap().keymaps.map[&doc.mode].reverse_map();
+ compositor.find::<ui::EditorView>().unwrap().keymaps.map()[&doc.mode].reverse_map();
let mut commands: Vec<MappableCommand> = 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<str>],
+ _event: PromptEvent,
+) -> anyhow::Result<()> {
+ cx.editor
+ .open(helix_loader::config_file(), Action::Replace)?;
+ Ok(())
+}
+
+fn refresh_config(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _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<HashMap<&'static str, &'static TypableCommand>> =
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<String>,
#[serde(default)]
pub lsp: LspConfig,
- #[serde(default)]
- pub keys: Keymaps,
+ #[serde(default = "default")]
+ pub keys: HashMap<Mode, Keymap>,
#[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<Config, ConfigLoadError> {
+ 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, ConfigLoadError> {
+ Config::load(helix_loader::config_file())
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -43,7 +89,7 @@ mod tests {
assert_eq!(
toml::from_str::<Config>(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::<Config>("").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<Mode, Keymap>,
-
+ pub map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>,
/// Stores pending keys waiting for the next key. This is relative to a
/// sticky node if one is in use.
- #[serde(skip)]
state: Vec<KeyEvent>,
-
/// Stores the sticky node if one is activated.
- #[serde(skip)]
pub sticky: Option<KeyTrieNode>,
}
impl Keymaps {
- pub fn new(map: HashMap<Mode, Keymap>) -> Self {
+ pub fn new(map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>) -> Self {
Self {
map,
state: Vec::new(),
@@ -405,6 +287,10 @@ impl Keymaps {
}
}
+ pub fn map(&self) -> DynGuard<HashMap<Mode, Keymap>> {
+ 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 <space>c
- "C-c" => toggle_comments,
-
- // z family for save/restore/combine from/to sels from register
-
- "tab" => jump_forward, // tab == <C-i>
- "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::<KeyEvent>().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<Mode, Keymap> {
+ 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 <space>c
+ "C-c" => toggle_comments,
+
+ // z family for save/restore/combine from/to sels from register
+
+ "tab" => jump_forward, // tab == <C-i>
+ "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 <ENTER> 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 <ENTER> 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<dyn Iterator<Item = HighlightEvent>> = 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<KeymapResult> {
- 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
}