on master help sign in
Add refresh-config and open-config command (#1803)
bee05ddJoe
Cargo.lock+2
@@ -435,6 +435,7 @@ name = "helix-term"
435version = "0.6.0"
435version = "0.6.0"
436dependencies = [
436dependencies = [
437 "anyhow",
437 "anyhow",
438+ "arc-swap",
438 "chrono",
439 "chrono",
439 "content_inspector",
440 "content_inspector",
440 "crossterm",
441 "crossterm",
@@ -483,6 +484,7 @@ name = "helix-view"
483version = "0.6.0"
484version = "0.6.0"
484dependencies = [
485dependencies = [
485 "anyhow",
486 "anyhow",
487+ "arc-swap",
486 "bitflags",
488 "bitflags",
487 "chardetng",
489 "chardetng",
488 "clipboard-win",
490 "clipboard-win",
book/src/configuration.md+1−1
@@ -5,7 +5,7 @@ To override global configuration parameters, create a `config.toml` file located
5* Linux and Mac: `~/.config/helix/config.toml`
5* Linux and Mac: `~/.config/helix/config.toml`
6* Windows: `%AppData%\helix\config.toml`
6* Windows: `%AppData%\helix\config.toml`
7
7
8−> Note: You may use `hx --edit-config` to create and edit the `config.toml` file.
8+> Hint: You can easily open the config file by typing `:config-open` within Helix normal mode.
9
9
10Example config:
10Example config:
11
11
book/src/generated/typable-cmd.md+2
@@ -55,3 +55,5 @@
55| `:sort` | Sort ranges in selection. |
55| `:sort` | Sort ranges in selection. |
56| `:rsort` | Sort ranges in selection in reverse order. |
56| `:rsort` | Sort ranges in selection in reverse order. |
57| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
57| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
58+| `:config-reload` | Refreshes helix's config. |
59+| `:config-open` | Open the helix config.toml file. |
helix-term/Cargo.toml+1
@@ -41,6 +41,7 @@ crossterm = { version = "0.23", features = ["event-stream"] }
41signal-hook = "0.3"
41signal-hook = "0.3"
42tokio-stream = "0.1"
42tokio-stream = "0.1"
43futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
43futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
44+arc-swap = { version = "1.5.0" }
44
45
45# Logging
46# Logging
46fern = "0.6"
47fern = "0.6"
helix-term/src/application.rs+72−12
@@ -1,10 +1,14 @@
1+use arc_swap::{access::Map, ArcSwap};
1use helix_core::{
2use helix_core::{
2 config::{default_syntax_loader, user_syntax_loader},
3 config::{default_syntax_loader, user_syntax_loader},
3 pos_at_coords, syntax, Selection,
4 pos_at_coords, syntax, Selection,
4};
5};
5use helix_dap::{self as dap, Payload, Request};
6use helix_dap::{self as dap, Payload, Request};
6use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
7use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
7−use helix_view::{editor::Breakpoint, theme, Editor};
8+use helix_view::{
9+ editor::{Breakpoint, ConfigEvent},
10+ theme, Editor,
11+};
8use serde_json::json;
12use serde_json::json;
9
13
10use crate::{
14use crate::{
@@ -13,6 +17,7 @@ use crate::{
13 compositor::Compositor,
17 compositor::Compositor,
14 config::Config,
18 config::Config,
15 job::Jobs,
19 job::Jobs,
20+ keymap::Keymaps,
16 ui::{self, overlay::overlayed},
21 ui::{self, overlay::overlayed},
17};
22};
18
23
@@ -42,8 +47,7 @@ pub struct Application {
42 compositor: Compositor,
47 compositor: Compositor,
43 editor: Editor,
48 editor: Editor,
44
49
45− // TODO: share an ArcSwap with Editor?
46− config: Config,
50+ config: Arc<ArcSwap<Config>>,
47
51
48 #[allow(dead_code)]
52 #[allow(dead_code)]
49 theme_loader: Arc<theme::Loader>,
53 theme_loader: Arc<theme::Loader>,
@@ -56,7 +60,7 @@ pub struct Application {
56}
60}
57
61
58impl Application {
62impl Application {
59− pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
63+ pub fn new(args: Args, config: Config) -> Result<Self, Error> {
60 use helix_view::editor::Action;
64 use helix_view::editor::Action;
61 let mut compositor = Compositor::new()?;
65 let mut compositor = Compositor::new()?;
62 let size = compositor.size();
66 let size = compositor.size();
@@ -98,14 +102,20 @@ impl Application {
98 });
102 });
99 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
103 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
100
104
105+ let config = Arc::new(ArcSwap::from_pointee(config));
101 let mut editor = Editor::new(
106 let mut editor = Editor::new(
102 size,
107 size,
103 theme_loader.clone(),
108 theme_loader.clone(),
104 syn_loader.clone(),
109 syn_loader.clone(),
110+ Box::new(Map::new(Arc::clone(&config), |config: &Config| {
105− config.editor.clone(),
111+ &config.editor
112+ })),
106 );
113 );
107
114
115+ let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
116+ &config.keys
117+ }));
108− let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
118+ let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
109 compositor.push(editor_view);
119 compositor.push(editor_view);
110
120
111 if args.load_tutor {
121 if args.load_tutor {
@@ -113,15 +123,12 @@ impl Application {
113 editor.open(path, Action::VerticalSplit)?;
123 editor.open(path, Action::VerticalSplit)?;
114 // Unset path to prevent accidentally saving to the original tutor file.
124 // Unset path to prevent accidentally saving to the original tutor file.
115 doc_mut!(editor).set_path(None)?;
125 doc_mut!(editor).set_path(None)?;
116− } else if args.edit_config {
117− let path = conf_dir.join("config.toml");
118− editor.open(path, Action::VerticalSplit)?;
119 } else if !args.files.is_empty() {
126 } else if !args.files.is_empty() {
120 let first = &args.files[0].0; // we know it's not empty
127 let first = &args.files[0].0; // we know it's not empty
121 if first.is_dir() {
128 if first.is_dir() {
122 std::env::set_current_dir(&first)?;
129 std::env::set_current_dir(&first)?;
123 editor.new_file(Action::VerticalSplit);
130 editor.new_file(Action::VerticalSplit);
124− let picker = ui::file_picker(".".into(), &config.editor);
131+ let picker = ui::file_picker(".".into(), &config.load().editor);
125 compositor.push(Box::new(overlayed(picker)));
132 compositor.push(Box::new(overlayed(picker)));
126 } else {
133 } else {
127 let nr_of_files = args.files.len();
134 let nr_of_files = args.files.len();
@@ -228,6 +235,10 @@ impl Application {
228 Some(payload) = self.editor.debugger_events.next() => {
235 Some(payload) = self.editor.debugger_events.next() => {
229 self.handle_debugger_message(payload).await;
236 self.handle_debugger_message(payload).await;
230 }
237 }
238+ Some(config_event) = self.editor.config_events.1.recv() => {
239+ self.handle_config_events(config_event);
240+ self.render();
241+ }
231 Some(callback) = self.jobs.futures.next() => {
242 Some(callback) = self.jobs.futures.next() => {
232 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
243 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
233 self.render();
244 self.render();
@@ -245,6 +256,55 @@ impl Application {
245 }
256 }
246 }
257 }
247
258
259+ pub fn handle_config_events(&mut self, config_event: ConfigEvent) {
260+ match config_event {
261+ ConfigEvent::Refresh => self.refresh_config(),
262+
263+ // Since only the Application can make changes to Editor's config,
264+ // the Editor must send up a new copy of a modified config so that
265+ // the Application can apply it.
266+ ConfigEvent::Update(editor_config) => {
267+ let mut app_config = (*self.config.load().clone()).clone();
268+ app_config.editor = editor_config;
269+ self.config.store(Arc::new(app_config));
270+ }
271+ }
272+ }
273+
274+ fn refresh_config(&mut self) {
275+ let config = Config::load(helix_loader::config_file()).unwrap_or_else(|err| {
276+ self.editor.set_error(err.to_string());
277+ Config::default()
278+ });
279+
280+ // Refresh theme
281+ if let Some(theme) = config.theme.clone() {
282+ let true_color = self.true_color();
283+ self.editor.set_theme(
284+ self.theme_loader
285+ .load(&theme)
286+ .map_err(|e| {
287+ log::warn!("failed to load theme `{}` - {}", theme, e);
288+ e
289+ })
290+ .ok()
291+ .filter(|theme| (true_color || theme.is_16_color()))
292+ .unwrap_or_else(|| {
293+ if true_color {
294+ self.theme_loader.default()
295+ } else {
296+ self.theme_loader.base16_default()
297+ }
298+ }),
299+ );
300+ }
301+ self.config.store(Arc::new(config));
302+ }
303+
304+ fn true_color(&self) -> bool {
305+ self.config.load().editor.true_color || crate::true_color()
306+ }
307+
248 #[cfg(windows)]
308 #[cfg(windows)]
249 // no signal handling available on windows
309 // no signal handling available on windows
250 pub async fn handle_signals(&mut self, _signal: ()) {}
310 pub async fn handle_signals(&mut self, _signal: ()) {}
@@ -700,7 +760,7 @@ impl Application {
700 self.lsp_progress.update(server_id, token, work);
760 self.lsp_progress.update(server_id, token, work);
701 }
761 }
702
762
703− if self.config.lsp.display_messages {
763+ if self.config.load().lsp.display_messages {
704 self.editor.set_status(status);
764 self.editor.set_status(status);
705 }
765 }
706 }
766 }
@@ -809,7 +869,7 @@ impl Application {
809 terminal::enable_raw_mode()?;
869 terminal::enable_raw_mode()?;
810 let mut stdout = stdout();
870 let mut stdout = stdout();
811 execute!(stdout, terminal::EnterAlternateScreen)?;
871 execute!(stdout, terminal::EnterAlternateScreen)?;
812− if self.config.editor.mouse {
872+ if self.config.load().editor.mouse {
813 execute!(stdout, EnableMouseCapture)?;
873 execute!(stdout, EnableMouseCapture)?;
814 }
874 }
815 Ok(())
875 Ok(())
helix-term/src/args.rs−2
@@ -13,7 +13,6 @@ pub struct Args {
13 pub build_grammars: bool,
13 pub build_grammars: bool,
14 pub verbosity: u64,
14 pub verbosity: u64,
15 pub files: Vec<(PathBuf, Position)>,
15 pub files: Vec<(PathBuf, Position)>,
16− pub edit_config: bool,
17}
16}
18
17
19impl Args {
18impl Args {
@@ -29,7 +28,6 @@ impl Args {
29 "--version" => args.display_version = true,
28 "--version" => args.display_version = true,
30 "--help" => args.display_help = true,
29 "--help" => args.display_help = true,
31 "--tutor" => args.load_tutor = true,
30 "--tutor" => args.load_tutor = true,
32− "--edit-config" => args.edit_config = true,
33 "--health" => {
31 "--health" => {
34 args.health = true;
32 args.health = true;
35 args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
33 args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
helix-term/src/commands.rs+21−14
@@ -842,6 +842,7 @@ fn align_selections(cx: &mut Context) {
842
842
843fn goto_window(cx: &mut Context, align: Align) {
843fn goto_window(cx: &mut Context, align: Align) {
844 let count = cx.count() - 1;
844 let count = cx.count() - 1;
845+ let config = cx.editor.config();
845 let (view, doc) = current!(cx.editor);
846 let (view, doc) = current!(cx.editor);
846
847
847 let height = view.inner_area().height as usize;
848 let height = view.inner_area().height as usize;
@@ -850,7 +851,7 @@ fn goto_window(cx: &mut Context, align: Align) {
850 // - 1 so we have at least one gap in the middle.
851 // - 1 so we have at least one gap in the middle.
851 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
852 // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
852 // as we type
853 // as we type
853− let scrolloff = cx.editor.config.scrolloff.min(height.saturating_sub(1) / 2);
854+ let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2);
854
855
855 let last_line = view.last_line(doc);
856 let last_line = view.last_line(doc);
856
857
@@ -1274,6 +1275,7 @@ fn switch_to_lowercase(cx: &mut Context) {
1274
1275
1275pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
1276pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
1276 use Direction::*;
1277 use Direction::*;
1278+ let config = cx.editor.config();
1277 let (view, doc) = current!(cx.editor);
1279 let (view, doc) = current!(cx.editor);
1278
1280
1279 let range = doc.selection(view.id).primary();
1281 let range = doc.selection(view.id).primary();
@@ -1292,7 +1294,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
1292
1294
1293 let height = view.inner_area().height;
1295 let height = view.inner_area().height;
1294
1296
1295− let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2);
1297+ let scrolloff = config.scrolloff.min(height as usize / 2);
1296
1298
1297 view.offset.row = match direction {
1299 view.offset.row = match direction {
1298 Forward => view.offset.row + offset,
1300 Forward => view.offset.row + offset,
@@ -1585,8 +1587,9 @@ fn rsearch(cx: &mut Context) {
1585
1587
1586fn searcher(cx: &mut Context, direction: Direction) {
1588fn searcher(cx: &mut Context, direction: Direction) {
1587 let reg = cx.register.unwrap_or('/');
1589 let reg = cx.register.unwrap_or('/');
1588− let scrolloff = cx.editor.config.scrolloff;
1590+ let config = cx.editor.config();
1591+ let scrolloff = config.scrolloff;
1589− let wrap_around = cx.editor.config.search.wrap_around;
1592+ let wrap_around = config.search.wrap_around;
1590
1593
1591 let doc = doc!(cx.editor);
1594 let doc = doc!(cx.editor);
1592
1595
@@ -1629,13 +1632,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
1629}
1632}
1630
1633
1631fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1634fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1632− let scrolloff = cx.editor.config.scrolloff;
1635+ let config = cx.editor.config();
1636+ let scrolloff = config.scrolloff;
1633 let (view, doc) = current!(cx.editor);
1637 let (view, doc) = current!(cx.editor);
1634 let registers = &cx.editor.registers;
1638 let registers = &cx.editor.registers;
1635 if let Some(query) = registers.read('/') {
1639 if let Some(query) = registers.read('/') {
1636 let query = query.last().unwrap();
1640 let query = query.last().unwrap();
1637 let contents = doc.text().slice(..).to_string();
1641 let contents = doc.text().slice(..).to_string();
1638− let search_config = &cx.editor.config.search;
1642+ let search_config = &config.search;
1639 let case_insensitive = if search_config.smart_case {
1643 let case_insensitive = if search_config.smart_case {
1640 !query.chars().any(char::is_uppercase)
1644 !query.chars().any(char::is_uppercase)
1641 } else {
1645 } else {
@@ -1695,8 +1699,9 @@ fn search_selection(cx: &mut Context) {
1695fn global_search(cx: &mut Context) {
1699fn global_search(cx: &mut Context) {
1696 let (all_matches_sx, all_matches_rx) =
1700 let (all_matches_sx, all_matches_rx) =
1697 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1701 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1698− let smart_case = cx.editor.config.search.smart_case;
1702+ let config = cx.editor.config();
1703+ let smart_case = config.search.smart_case;
1699− let file_picker_config = cx.editor.config.file_picker.clone();
1704+ let file_picker_config = config.file_picker.clone();
1700
1705
1701 let completions = search_completions(cx, None);
1706 let completions = search_completions(cx, None);
1702 let prompt = ui::regex_prompt(
1707 let prompt = ui::regex_prompt(
@@ -2028,7 +2033,7 @@ fn append_mode(cx: &mut Context) {
2028fn file_picker(cx: &mut Context) {
2033fn file_picker(cx: &mut Context) {
2029 // We don't specify language markers, root will be the root of the current git repo
2034 // We don't specify language markers, root will be the root of the current git repo
2030 let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
2035 let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
2031− let picker = ui::file_picker(root, &cx.editor.config);
2036+ let picker = ui::file_picker(root, &cx.editor.config());
2032 cx.push_layer(Box::new(overlayed(picker)));
2037 cx.push_layer(Box::new(overlayed(picker)));
2033}
2038}
2034
2039
@@ -2105,7 +2110,7 @@ pub fn command_palette(cx: &mut Context) {
2105 move |compositor: &mut Compositor, cx: &mut compositor::Context| {
2110 move |compositor: &mut Compositor, cx: &mut compositor::Context| {
2106 let doc = doc_mut!(cx.editor);
2111 let doc = doc_mut!(cx.editor);
2107 let keymap =
2112 let keymap =
2108− compositor.find::<ui::EditorView>().unwrap().keymaps.map[&doc.mode].reverse_map();
2113+ compositor.find::<ui::EditorView>().unwrap().keymaps.map()[&doc.mode].reverse_map();
2109
2114
2110 let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
2115 let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
2111 commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
2116 commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
@@ -2571,6 +2576,7 @@ pub mod insert {
2571 // It trigger completion when idle timer reaches deadline
2576 // It trigger completion when idle timer reaches deadline
2572 // Only trigger completion if the word under cursor is longer than n characters
2577 // Only trigger completion if the word under cursor is longer than n characters
2573 pub fn idle_completion(cx: &mut Context) {
2578 pub fn idle_completion(cx: &mut Context) {
2579+ let config = cx.editor.config();
2574 let (view, doc) = current!(cx.editor);
2580 let (view, doc) = current!(cx.editor);
2575 let text = doc.text().slice(..);
2581 let text = doc.text().slice(..);
2576 let cursor = doc.selection(view.id).primary().cursor(text);
2582 let cursor = doc.selection(view.id).primary().cursor(text);
@@ -2578,7 +2584,7 @@ pub mod insert {
2578 use helix_core::chars::char_is_word;
2584 use helix_core::chars::char_is_word;
2579 let mut iter = text.chars_at(cursor);
2585 let mut iter = text.chars_at(cursor);
2580 iter.reverse();
2586 iter.reverse();
2581− for _ in 0..cx.editor.config.completion_trigger_len {
2587+ for _ in 0..config.completion_trigger_len {
2582 match iter.next() {
2588 match iter.next() {
2583 Some(c) if char_is_word(c) => {}
2589 Some(c) if char_is_word(c) => {}
2584 _ => return,
2590 _ => return,
@@ -4154,7 +4160,7 @@ fn shell_keep_pipe(cx: &mut Context) {
4154 Some('|'),
4160 Some('|'),
4155 ui::completers::none,
4161 ui::completers::none,
4156 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4162 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4157− let shell = &cx.editor.config.shell;
4163+ let shell = &cx.editor.config().shell;
4158 if event != PromptEvent::Validate {
4164 if event != PromptEvent::Validate {
4159 return;
4165 return;
4160 }
4166 }
@@ -4250,7 +4256,8 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4250 Some('|'),
4256 Some('|'),
4251 ui::completers::none,
4257 ui::completers::none,
4252 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4258 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4253− let shell = &cx.editor.config.shell;
4259+ let config = cx.editor.config();
4260+ let shell = &config.shell;
4254 if event != PromptEvent::Validate {
4261 if event != PromptEvent::Validate {
4255 return;
4262 return;
4256 }
4263 }
@@ -4295,7 +4302,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4295
4302
4296 // after replace cursor may be out of bounds, do this to
4303 // after replace cursor may be out of bounds, do this to
4297 // make sure cursor is in view and update scroll as well
4304 // make sure cursor is in view and update scroll as well
4298− view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
4305+ view.ensure_cursor_in_view(doc, config.scrolloff);
4299 },
4306 },
4300 );
4307 );
4301
4308
helix-term/src/commands/typed.rs+41−4
@@ -1,6 +1,6 @@
1use super::*;
1use super::*;
2
2
3−use helix_view::editor::Action;
3+use helix_view::editor::{Action, ConfigEvent};
4use ui::completers::{self, Completer};
4use ui::completers::{self, Completer};
5
5
6#[derive(Clone)]
6#[derive(Clone)]
@@ -540,7 +540,7 @@ fn theme(
540 .theme_loader
540 .theme_loader
541 .load(theme)
541 .load(theme)
542 .with_context(|| format!("Failed setting theme {}", theme))?;
542 .with_context(|| format!("Failed setting theme {}", theme))?;
543− let true_color = cx.editor.config.true_color || crate::true_color();
543+ let true_color = cx.editor.config().true_color || crate::true_color();
544 if !(true_color || theme.is_16_color()) {
544 if !(true_color || theme.is_16_color()) {
545 bail!("Unsupported theme: theme requires true color support");
545 bail!("Unsupported theme: theme requires true color support");
546 }
546 }
@@ -894,7 +894,7 @@ fn setting(
894 let key_error = || anyhow::anyhow!("Unknown key `{key}`");
894 let key_error = || anyhow::anyhow!("Unknown key `{key}`");
895 let field_error = |_| anyhow::anyhow!("Could not parse field `{arg}`");
895 let field_error = |_| anyhow::anyhow!("Could not parse field `{arg}`");
896
896
897− let mut config = serde_json::to_value(&cx.editor.config).unwrap();
897+ let mut config = serde_json::to_value(&cx.editor.config().clone()).unwrap();
898 let pointer = format!("/{}", key.replace('.', "/"));
898 let pointer = format!("/{}", key.replace('.', "/"));
899 let value = config.pointer_mut(&pointer).ok_or_else(key_error)?;
899 let value = config.pointer_mut(&pointer).ok_or_else(key_error)?;
900
900
@@ -904,8 +904,12 @@ fn setting(
904 } else {
904 } else {
905 arg.parse().map_err(field_error)?
905 arg.parse().map_err(field_error)?
906 };
906 };
907− cx.editor.config = serde_json::from_value(config).map_err(field_error)?;
907+ let config = serde_json::from_value(config).map_err(field_error)?;
908
908
909+ cx.editor
910+ .config_events
911+ .0
912+ .send(ConfigEvent::Update(config))?;
909 Ok(())
913 Ok(())
910}
914}
911
915
@@ -995,6 +999,25 @@ fn tree_sitter_subtree(
995 Ok(())
999 Ok(())
996}
1000}
997
1001
1002+fn open_config(
1003+ cx: &mut compositor::Context,
1004+ _args: &[Cow<str>],
1005+ _event: PromptEvent,
1006+) -> anyhow::Result<()> {
1007+ cx.editor
1008+ .open(helix_loader::config_file(), Action::Replace)?;
1009+ Ok(())
1010+}
1011+
1012+fn refresh_config(
1013+ cx: &mut compositor::Context,
1014+ _args: &[Cow<str>],
1015+ _event: PromptEvent,
1016+) -> anyhow::Result<()> {
1017+ cx.editor.config_events.0.send(ConfigEvent::Refresh)?;
1018+ Ok(())
1019+}
1020+
998pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
1021pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
999 TypableCommand {
1022 TypableCommand {
1000 name: "quit",
1023 name: "quit",
@@ -1381,6 +1404,20 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
1381 fun: tree_sitter_subtree,
1404 fun: tree_sitter_subtree,
1382 completer: None,
1405 completer: None,
1383 },
1406 },
1407+ TypableCommand {
1408+ name: "config-reload",
1409+ aliases: &[],
1410+ doc: "Refreshes helix's config.",
1411+ fun: refresh_config,
1412+ completer: None,
1413+ },
1414+ TypableCommand {
1415+ name: "config-open",
1416+ aliases: &[],
1417+ doc: "Open the helix config.toml file.",
1418+ fun: open_config,
1419+ completer: None,
1420+ },
1384 ];
1421 ];
1385
1422
1386pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
1423pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
helix-term/src/config.rs+64−7
@@ -1,25 +1,71 @@
1+use crate::keymap::{default::default, merge_keys, Keymap};
2+use helix_view::document::Mode;
1use serde::Deserialize;
3use serde::Deserialize;
4+use std::collections::HashMap;
5+use std::fmt::Display;
6+use std::io::Error as IOError;
7+use std::path::PathBuf;
8+use toml::de::Error as TomlError;
2
9
3−use crate::keymap::Keymaps;
4−
5−#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
10+#[derive(Debug, Clone, PartialEq, Deserialize)]
6#[serde(deny_unknown_fields)]
11#[serde(deny_unknown_fields)]
7pub struct Config {
12pub struct Config {
8 pub theme: Option<String>,
13 pub theme: Option<String>,
9 #[serde(default)]
14 #[serde(default)]
10 pub lsp: LspConfig,
15 pub lsp: LspConfig,
11− #[serde(default)]
16+ #[serde(default = "default")]
12− pub keys: Keymaps,
17+ pub keys: HashMap<Mode, Keymap>,
13 #[serde(default)]
18 #[serde(default)]
14 pub editor: helix_view::editor::Config,
19 pub editor: helix_view::editor::Config,
15}
20}
16
21
22+impl Default for Config {
23+ fn default() -> Config {
24+ Config {
25+ theme: None,
26+ lsp: LspConfig::default(),
27+ keys: default(),
28+ editor: helix_view::editor::Config::default(),
29+ }
30+ }
31+}
32+
33+#[derive(Debug)]
34+pub enum ConfigLoadError {
35+ BadConfig(TomlError),
36+ Error(IOError),
37+}
38+
39+impl Display for ConfigLoadError {
40+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41+ match self {
42+ ConfigLoadError::BadConfig(err) => err.fmt(f),
43+ ConfigLoadError::Error(err) => err.fmt(f),
44+ }
45+ }
46+}
47+
17#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
48#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
18#[serde(rename_all = "kebab-case", deny_unknown_fields)]
49#[serde(rename_all = "kebab-case", deny_unknown_fields)]
19pub struct LspConfig {
50pub struct LspConfig {
20 pub display_messages: bool,
51 pub display_messages: bool,
21}
52}
22
53
54+impl Config {
55+ pub fn load(config_path: PathBuf) -> Result<Config, ConfigLoadError> {
56+ match std::fs::read_to_string(config_path) {
57+ Ok(config) => toml::from_str(&config)
58+ .map(merge_keys)
59+ .map_err(ConfigLoadError::BadConfig),
60+ Err(err) => Err(ConfigLoadError::Error(err)),
61+ }
62+ }
63+
64+ pub fn load_default() -> Result<Config, ConfigLoadError> {
65+ Config::load(helix_loader::config_file())
66+ }
67+}
68+
23#[cfg(test)]
69#[cfg(test)]
24mod tests {
70mod tests {
25 use super::*;
71 use super::*;
@@ -43,7 +89,7 @@ mod tests {
43 assert_eq!(
89 assert_eq!(
44 toml::from_str::<Config>(sample_keymaps).unwrap(),
90 toml::from_str::<Config>(sample_keymaps).unwrap(),
45 Config {
91 Config {
46− keys: Keymaps::new(hashmap! {
92+ keys: hashmap! {
47 Mode::Insert => Keymap::new(keymap!({ "Insert mode"
93 Mode::Insert => Keymap::new(keymap!({ "Insert mode"
48 "y" => move_line_down,
94 "y" => move_line_down,
49 "S-C-a" => delete_selection,
95 "S-C-a" => delete_selection,
@@ -51,9 +97,20 @@ mod tests {
51 Mode::Normal => Keymap::new(keymap!({ "Normal mode"
97 Mode::Normal => Keymap::new(keymap!({ "Normal mode"
52 "A-F12" => move_next_word_end,
98 "A-F12" => move_next_word_end,
53 })),
99 })),
54− }),
100+ },
55 ..Default::default()
101 ..Default::default()
56 }
102 }
57 );
103 );
58 }
104 }
105+
106+ #[test]
107+ fn keys_resolve_to_correct_defaults() {
108+ // From serde default
109+ let default_keys = toml::from_str::<Config>("").unwrap().keys;
110+ assert_eq!(default_keys, default());
111+
112+ // From the Default trait
113+ let default_keys = Config::default().keys;
114+ assert_eq!(default_keys, default());
115+ }
59}
116}
helix-term/src/keymap.rs+36−496
@@ -1,135 +1,23 @@
1+pub mod default;
2+pub mod macros;
3+
1pub use crate::commands::MappableCommand;
4pub use crate::commands::MappableCommand;
2use crate::config::Config;
5use crate::config::Config;
3−use helix_core::hashmap;
6+use arc_swap::{
7+ access::{DynAccess, DynGuard},
8+ ArcSwap,
9+};
4use helix_view::{document::Mode, info::Info, input::KeyEvent};
10use helix_view::{document::Mode, info::Info, input::KeyEvent};
5use serde::Deserialize;
11use serde::Deserialize;
6use std::{
12use std::{
7 borrow::Cow,
13 borrow::Cow,
8 collections::{BTreeSet, HashMap},
14 collections::{BTreeSet, HashMap},
9 ops::{Deref, DerefMut},
15 ops::{Deref, DerefMut},
16+ sync::Arc,
10};
17};
11
18
12−#[macro_export]
19+use default::default;
13−macro_rules! key {
20+use macros::key;
14− ($key:ident) => {
15− ::helix_view::input::KeyEvent {
16− code: ::helix_view::keyboard::KeyCode::$key,
17− modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
18− }
19− };
20− ($($ch:tt)*) => {
21− ::helix_view::input::KeyEvent {
22− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
23− modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
24− }
25− };
26−}
27−
28−#[macro_export]
29−macro_rules! shift {
30− ($key:ident) => {
31− ::helix_view::input::KeyEvent {
32− code: ::helix_view::keyboard::KeyCode::$key,
33− modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
34− }
35− };
36− ($($ch:tt)*) => {
37− ::helix_view::input::KeyEvent {
38− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
39− modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
40− }
41− };
42−}
43−
44−#[macro_export]
45−macro_rules! ctrl {
46− ($key:ident) => {
47− ::helix_view::input::KeyEvent {
48− code: ::helix_view::keyboard::KeyCode::$key,
49− modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
50− }
51− };
52− ($($ch:tt)*) => {
53− ::helix_view::input::KeyEvent {
54− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
55− modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
56− }
57− };
58−}
59−
60−#[macro_export]
61−macro_rules! alt {
62− ($key:ident) => {
63− ::helix_view::input::KeyEvent {
64− code: ::helix_view::keyboard::KeyCode::$key,
65− modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
66− }
67− };
68− ($($ch:tt)*) => {
69− ::helix_view::input::KeyEvent {
70− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
71− modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
72− }
73− };
74−}
75−
76−/// Macro for defining the root of a `Keymap` object. Example:
77−///
78−/// ```
79−/// # use helix_core::hashmap;
80−/// # use helix_term::keymap;
81−/// # use helix_term::keymap::Keymap;
82−/// let normal_mode = keymap!({ "Normal mode"
83−/// "i" => insert_mode,
84−/// "g" => { "Goto"
85−/// "g" => goto_file_start,
86−/// "e" => goto_file_end,
87−/// },
88−/// "j" | "down" => move_line_down,
89−/// });
90−/// let keymap = Keymap::new(normal_mode);
91−/// ```
92−#[macro_export]
93−macro_rules! keymap {
94− (@trie $cmd:ident) => {
95− $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
96− };
97−
98− (@trie
99− { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
100− ) => {
101− keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
102− };
103−
104− (@trie [$($cmd:ident),* $(,)?]) => {
105− $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
106− };
107−
108− (
109− { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
110− ) => {
111− // modified from the hashmap! macro
112− {
113− let _cap = hashmap!(@count $($($key),+),*);
114− let mut _map = ::std::collections::HashMap::with_capacity(_cap);
115− let mut _order = ::std::vec::Vec::with_capacity(_cap);
116− $(
117− $(
118− let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
119− let _duplicate = _map.insert(
120− _key,
121− keymap!(@trie $value)
122− );
123− assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
124− _order.push(_key);
125− )+
126− )*
127− let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
128− $( _node.is_sticky = $sticky; )?
129− $crate::keymap::KeyTrie::Node(_node)
130− }
131− };
132−}
133
21
134#[derive(Debug, Clone)]
22#[derive(Debug, Clone)]
135pub struct KeyTrieNode {
23pub struct KeyTrieNode {
@@ -381,23 +269,17 @@ impl Default for Keymap {
381 }
269 }
382}
270}
383
271
384−#[derive(Debug, Clone, PartialEq, Deserialize)]
385pub struct Keymaps {
272pub struct Keymaps {
386− #[serde(flatten)]
387− pub map: HashMap<Mode, Keymap>,
273+ pub map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>,
388−
389 /// Stores pending keys waiting for the next key. This is relative to a
274 /// Stores pending keys waiting for the next key. This is relative to a
390 /// sticky node if one is in use.
275 /// sticky node if one is in use.
391− #[serde(skip)]
392 state: Vec<KeyEvent>,
276 state: Vec<KeyEvent>,
393−
394 /// Stores the sticky node if one is activated.
277 /// Stores the sticky node if one is activated.
395− #[serde(skip)]
396 pub sticky: Option<KeyTrieNode>,
278 pub sticky: Option<KeyTrieNode>,
397}
279}
398
280
399impl Keymaps {
281impl Keymaps {
400− pub fn new(map: HashMap<Mode, Keymap>) -> Self {
282+ pub fn new(map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>) -> Self {
401 Self {
283 Self {
402 map,
284 map,
403 state: Vec::new(),
285 state: Vec::new(),
@@ -405,6 +287,10 @@ impl Keymaps {
405 }
287 }
406 }
288 }
407
289
290+ pub fn map(&self) -> DynGuard<HashMap<Mode, Keymap>> {
291+ self.map.load()
292+ }
293+
408 /// Returns list of keys waiting to be disambiguated in current mode.
294 /// Returns list of keys waiting to be disambiguated in current mode.
409 pub fn pending(&self) -> &[KeyEvent] {
295 pub fn pending(&self) -> &[KeyEvent] {
410 &self.state
296 &self.state
@@ -419,7 +305,8 @@ impl Keymaps {
419 /// sticky node is in use, it will be cleared.
305 /// sticky node is in use, it will be cleared.
420 pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
306 pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
421 // TODO: remove the sticky part and look up manually
307 // TODO: remove the sticky part and look up manually
422− let keymap = &self.map[&mode];
308+ let keymaps = &*self.map();
309+ let keymap = &keymaps[&mode];
423
310
424 if key!(Esc) == key {
311 if key!(Esc) == key {
425 if !self.state.is_empty() {
312 if !self.state.is_empty() {
@@ -470,372 +357,25 @@ impl Keymaps {
470
357
471impl Default for Keymaps {
358impl Default for Keymaps {
472 fn default() -> Self {
359 fn default() -> Self {
473− let normal = keymap!({ "Normal mode"
360+ Self::new(Box::new(ArcSwap::new(Arc::new(default()))))
474− "h" | "left" => move_char_left,
475− "j" | "down" => move_line_down,
476− "k" | "up" => move_line_up,
477− "l" | "right" => move_char_right,
478−
479− "t" => find_till_char,
480− "f" => find_next_char,
481− "T" => till_prev_char,
482− "F" => find_prev_char,
483− "r" => replace,
484− "R" => replace_with_yanked,
485− "A-." => repeat_last_motion,
486−
487− "~" => switch_case,
488− "`" => switch_to_lowercase,
489− "A-`" => switch_to_uppercase,
490−
491− "home" => goto_line_start,
492− "end" => goto_line_end,
493−
494− "w" => move_next_word_start,
495− "b" => move_prev_word_start,
496− "e" => move_next_word_end,
497−
498− "W" => move_next_long_word_start,
499− "B" => move_prev_long_word_start,
500− "E" => move_next_long_word_end,
501−
502− "v" => select_mode,
503− "G" => goto_line,
504− "g" => { "Goto"
505− "g" => goto_file_start,
506− "e" => goto_last_line,
507− "f" => goto_file,
508− "h" => goto_line_start,
509− "l" => goto_line_end,
510− "s" => goto_first_nonwhitespace,
511− "d" => goto_definition,
512− "y" => goto_type_definition,
513− "r" => goto_reference,
514− "i" => goto_implementation,
515− "t" => goto_window_top,
516− "c" => goto_window_center,
517− "b" => goto_window_bottom,
518− "a" => goto_last_accessed_file,
519− "m" => goto_last_modified_file,
520− "n" => goto_next_buffer,
521− "p" => goto_previous_buffer,
522− "." => goto_last_modification,
523− },
524− ":" => command_mode,
525−
526− "i" => insert_mode,
527− "I" => prepend_to_line,
528− "a" => append_mode,
529− "A" => append_to_line,
530− "o" => open_below,
531− "O" => open_above,
532−
533− "d" => delete_selection,
534− "A-d" => delete_selection_noyank,
535− "c" => change_selection,
536− "A-c" => change_selection_noyank,
537−
538− "C" => copy_selection_on_next_line,
539− "A-C" => copy_selection_on_prev_line,
540−
541−
542− "s" => select_regex,
543− "A-s" => split_selection_on_newline,
544− "S" => split_selection,
545− ";" => collapse_selection,
546− "A-;" => flip_selections,
547− "A-k" | "A-up" => expand_selection,
548− "A-j" | "A-down" => shrink_selection,
549− "A-h" | "A-left" => select_prev_sibling,
550− "A-l" | "A-right" => select_next_sibling,
551−
552− "%" => select_all,
553− "x" => extend_line,
554− "X" => extend_to_line_bounds,
555− // crop_to_whole_line
556−
557− "m" => { "Match"
558− "m" => match_brackets,
559− "s" => surround_add,
560− "r" => surround_replace,
561− "d" => surround_delete,
562− "a" => select_textobject_around,
563− "i" => select_textobject_inner,
564− },
565− "[" => { "Left bracket"
566− "d" => goto_prev_diag,
567− "D" => goto_first_diag,
568− "f" => goto_prev_function,
569− "c" => goto_prev_class,
570− "a" => goto_prev_parameter,
571− "o" => goto_prev_comment,
572− "space" => add_newline_above,
573− },
574− "]" => { "Right bracket"
575− "d" => goto_next_diag,
576− "D" => goto_last_diag,
577− "f" => goto_next_function,
578− "c" => goto_next_class,
579− "a" => goto_next_parameter,
580− "o" => goto_next_comment,
581− "space" => add_newline_below,
582− },
583−
584− "/" => search,
585− "?" => rsearch,
586− "n" => search_next,
587− "N" => search_prev,
588− "*" => search_selection,
589−
590− "u" => undo,
591− "U" => redo,
592− "A-u" => earlier,
593− "A-U" => later,
594−
595− "y" => yank,
596− // yank_all
597− "p" => paste_after,
598− // paste_all
599− "P" => paste_before,
600−
601− "Q" => record_macro,
602− "q" => replay_macro,
603−
604− ">" => indent,
605− "<" => unindent,
606− "=" => format_selections,
607− "J" => join_selections,
608− "K" => keep_selections,
609− "A-K" => remove_selections,
610−
611− "," => keep_primary_selection,
612− "A-," => remove_primary_selection,
613−
614− // "q" => record_macro,
615− // "Q" => replay_macro,
616−
617− "&" => align_selections,
618− "_" => trim_selections,
619−
620− "(" => rotate_selections_backward,
621− ")" => rotate_selections_forward,
622− "A-(" => rotate_selection_contents_backward,
623− "A-)" => rotate_selection_contents_forward,
624−
625− "A-:" => ensure_selections_forward,
626−
627− "esc" => normal_mode,
628− "C-b" | "pageup" => page_up,
629− "C-f" | "pagedown" => page_down,
630− "C-u" => half_page_up,
631− "C-d" => half_page_down,
632−
633− "C-w" => { "Window"
634− "C-w" | "w" => rotate_view,
635− "C-s" | "s" => hsplit,
636− "C-v" | "v" => vsplit,
637− "f" => goto_file_hsplit,
638− "F" => goto_file_vsplit,
639− "C-q" | "q" => wclose,
640− "C-o" | "o" => wonly,
641− "C-h" | "h" | "left" => jump_view_left,
642− "C-j" | "j" | "down" => jump_view_down,
643− "C-k" | "k" | "up" => jump_view_up,
644− "C-l" | "l" | "right" => jump_view_right,
645− "n" => { "New split scratch buffer"
646− "C-s" | "s" => hsplit_new,
647− "C-v" | "v" => vsplit_new,
648− },
649− },
650−
651− // move under <space>c
652− "C-c" => toggle_comments,
653−
654− // z family for save/restore/combine from/to sels from register
655−
656− "tab" => jump_forward, // tab == <C-i>
657− "C-o" => jump_backward,
658− "C-s" => save_selection,
659−
660− "space" => { "Space"
661− "f" => file_picker,
662− "b" => buffer_picker,
663− "s" => symbol_picker,
664− "S" => workspace_symbol_picker,
665− "a" => code_action,
666− "'" => last_picker,
667− "d" => { "Debug (experimental)" sticky=true
668− "l" => dap_launch,
669− "b" => dap_toggle_breakpoint,
670− "c" => dap_continue,
671− "h" => dap_pause,
672− "i" => dap_step_in,
673− "o" => dap_step_out,
674− "n" => dap_next,
675− "v" => dap_variables,
676− "t" => dap_terminate,
677− "C-c" => dap_edit_condition,
678− "C-l" => dap_edit_log,
679− "s" => { "Switch"
680− "t" => dap_switch_thread,
681− "f" => dap_switch_stack_frame,
682− // sl, sb
683− },
684− "e" => dap_enable_exceptions,
685− "E" => dap_disable_exceptions,
686− },
687− "w" => { "Window"
688− "C-w" | "w" => rotate_view,
689− "C-s" | "s" => hsplit,
690− "C-v" | "v" => vsplit,
691− "f" => goto_file_hsplit,
692− "F" => goto_file_vsplit,
693− "C-q" | "q" => wclose,
694− "C-o" | "o" => wonly,
695− "C-h" | "h" | "left" => jump_view_left,
696− "C-j" | "j" | "down" => jump_view_down,
697− "C-k" | "k" | "up" => jump_view_up,
698− "C-l" | "l" | "right" => jump_view_right,
699− "n" => { "New split scratch buffer"
700− "C-s" | "s" => hsplit_new,
701− "C-v" | "v" => vsplit_new,
702− },
703− },
704− "y" => yank_joined_to_clipboard,
705− "Y" => yank_main_selection_to_clipboard,
706− "p" => paste_clipboard_after,
707− "P" => paste_clipboard_before,
708− "R" => replace_selections_with_clipboard,
709− "/" => global_search,
710− "k" => hover,
711− "r" => rename_symbol,
712− "?" => command_palette,
713− },
714− "z" => { "View"
715− "z" | "c" => align_view_center,
716− "t" => align_view_top,
717− "b" => align_view_bottom,
718− "m" => align_view_middle,
719− "k" | "up" => scroll_up,
720− "j" | "down" => scroll_down,
721− "C-b" | "pageup" => page_up,
722− "C-f" | "pagedown" => page_down,
723− "C-u" => half_page_up,
724− "C-d" => half_page_down,
725− },
726− "Z" => { "View" sticky=true
727− "z" | "c" => align_view_center,
728− "t" => align_view_top,
729− "b" => align_view_bottom,
730− "m" => align_view_middle,
731− "k" | "up" => scroll_up,
732− "j" | "down" => scroll_down,
733− "C-b" | "pageup" => page_up,
734− "C-f" | "pagedown" => page_down,
735− "C-u" => half_page_up,
736− "C-d" => half_page_down,
737− },
738−
739− "\"" => select_register,
740− "|" => shell_pipe,
741− "A-|" => shell_pipe_to,
742− "!" => shell_insert_output,
743− "A-!" => shell_append_output,
744− "$" => shell_keep_pipe,
745− "C-z" => suspend,
746−
747− "C-a" => increment,
748− "C-x" => decrement,
749− });
750− let mut select = normal.clone();
751− select.merge_nodes(keymap!({ "Select mode"
752− "h" | "left" => extend_char_left,
753− "j" | "down" => extend_line_down,
754− "k" | "up" => extend_line_up,
755− "l" | "right" => extend_char_right,
756−
757− "w" => extend_next_word_start,
758− "b" => extend_prev_word_start,
759− "e" => extend_next_word_end,
760− "W" => extend_next_long_word_start,
761− "B" => extend_prev_long_word_start,
762− "E" => extend_next_long_word_end,
763−
764− "n" => extend_search_next,
765− "N" => extend_search_prev,
766−
767− "t" => extend_till_char,
768− "f" => extend_next_char,
769− "T" => extend_till_prev_char,
770− "F" => extend_prev_char,
771−
772− "home" => extend_to_line_start,
773− "end" => extend_to_line_end,
774− "esc" => exit_select_mode,
775−
776− "v" => normal_mode,
777− }));
778− let insert = keymap!({ "Insert mode"
779− "esc" => normal_mode,
780−
781− "backspace" => delete_char_backward,
782− "C-h" => delete_char_backward,
783− "del" => delete_char_forward,
784− "C-d" => delete_char_forward,
785− "ret" => insert_newline,
786− "C-j" => insert_newline,
787− "tab" => insert_tab,
788− "C-w" => delete_word_backward,
789− "A-backspace" => delete_word_backward,
790− "A-d" => delete_word_forward,
791−
792− "left" => move_char_left,
793− "C-b" => move_char_left,
794− "down" => move_line_down,
795− "C-n" => move_line_down,
796− "up" => move_line_up,
797− "C-p" => move_line_up,
798− "right" => move_char_right,
799− "C-f" => move_char_right,
800− "A-b" => move_prev_word_end,
801− "A-left" => move_prev_word_end,
802− "A-f" => move_next_word_start,
803− "A-right" => move_next_word_start,
804− "A-<" => goto_file_start,
805− "A->" => goto_file_end,
806− "pageup" => page_up,
807− "pagedown" => page_down,
808− "home" => goto_line_start,
809− "C-a" => goto_line_start,
810− "end" => goto_line_end_newline,
811− "C-e" => goto_line_end_newline,
812−
813− "C-k" => kill_to_line_end,
814− "C-u" => kill_to_line_start,
815−
816− "C-x" => completion,
817− "C-r" => insert_register,
818− });
819− Self::new(hashmap!(
820− Mode::Normal => Keymap::new(normal),
821− Mode::Select => Keymap::new(select),
822− Mode::Insert => Keymap::new(insert),
823− ))
824 }
361 }
825}
362}
826
363
827/// Merge default config keys with user overwritten keys for custom user config.
364/// Merge default config keys with user overwritten keys for custom user config.
828pub fn merge_keys(mut config: Config) -> Config {
365pub fn merge_keys(mut config: Config) -> Config {
829− let mut delta = std::mem::take(&mut config.keys);
366+ let mut delta = std::mem::replace(&mut config.keys, default());
830− for (mode, keys) in &mut config.keys.map {
367+ for (mode, keys) in &mut config.keys {
831− keys.merge(delta.map.remove(mode).unwrap_or_default())
368+ keys.merge(delta.remove(mode).unwrap_or_default())
832 }
369 }
833 config
370 config
834}
371}
835
372
836#[cfg(test)]
373#[cfg(test)]
837mod tests {
374mod tests {
375+ use super::macros::keymap;
838 use super::*;
376 use super::*;
377+ use arc_swap::access::Constant;
378+ use helix_core::hashmap;
839
379
840 #[test]
380 #[test]
841 #[should_panic]
381 #[should_panic]
@@ -855,7 +395,7 @@ mod tests {
855 #[test]
395 #[test]
856 fn merge_partial_keys() {
396 fn merge_partial_keys() {
857 let config = Config {
397 let config = Config {
858− keys: Keymaps::new(hashmap! {
398+ keys: hashmap! {
859 Mode::Normal => Keymap::new(
399 Mode::Normal => Keymap::new(
860 keymap!({ "Normal mode"
400 keymap!({ "Normal mode"
861 "i" => normal_mode,
401 "i" => normal_mode,
@@ -867,13 +407,13 @@ mod tests {
867 },
407 },
868 })
408 })
869 )
409 )
870− }),
410+ },
871 ..Default::default()
411 ..Default::default()
872 };
412 };
873 let mut merged_config = merge_keys(config.clone());
413 let mut merged_config = merge_keys(config.clone());
874 assert_ne!(config, merged_config);
414 assert_ne!(config, merged_config);
875
415
876− let keymap = &mut merged_config.keys;
416+ let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone())));
877 assert_eq!(
417 assert_eq!(
878 keymap.get(Mode::Normal, key!('i')),
418 keymap.get(Mode::Normal, key!('i')),
879 KeymapResult::Matched(MappableCommand::normal_mode),
419 KeymapResult::Matched(MappableCommand::normal_mode),
@@ -891,7 +431,7 @@ mod tests {
891 "Leaf should replace node"
431 "Leaf should replace node"
892 );
432 );
893
433
894− let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap();
434+ let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
895 // Assumes that `g` is a node in default keymap
435 // Assumes that `g` is a node in default keymap
896 assert_eq!(
436 assert_eq!(
897 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
437 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
@@ -911,14 +451,14 @@ mod tests {
911 "Old leaves in subnode should be present in merged node"
451 "Old leaves in subnode should be present in merged node"
912 );
452 );
913
453
914− assert!(merged_config.keys.map.get(&Mode::Normal).unwrap().len() > 1);
454+ assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1);
915− assert!(merged_config.keys.map.get(&Mode::Insert).unwrap().len() > 0);
455+ assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0);
916 }
456 }
917
457
918 #[test]
458 #[test]
919 fn order_should_be_set() {
459 fn order_should_be_set() {
920 let config = Config {
460 let config = Config {
921− keys: Keymaps::new(hashmap! {
461+ keys: hashmap! {
922 Mode::Normal => Keymap::new(
462 Mode::Normal => Keymap::new(
923 keymap!({ "Normal mode"
463 keymap!({ "Normal mode"
924 "space" => { ""
464 "space" => { ""
@@ -929,12 +469,12 @@ mod tests {
929 },
469 },
930 })
470 })
931 )
471 )
932− }),
472+ },
933 ..Default::default()
473 ..Default::default()
934 };
474 };
935 let mut merged_config = merge_keys(config.clone());
475 let mut merged_config = merge_keys(config.clone());
936 assert_ne!(config, merged_config);
476 assert_ne!(config, merged_config);
937− let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap();
477+ let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
938 // Make sure mapping works
478 // Make sure mapping works
939 assert_eq!(
479 assert_eq!(
940 keymap
480 keymap
@@ -951,8 +491,8 @@ mod tests {
951
491
952 #[test]
492 #[test]
953 fn aliased_modes_are_same_in_default_keymap() {
493 fn aliased_modes_are_same_in_default_keymap() {
954− let keymaps = Keymaps::default();
494+ let keymaps = Keymaps::default().map();
955− let root = keymaps.map.get(&Mode::Normal).unwrap().root();
495+ let root = keymaps.get(&Mode::Normal).unwrap().root();
956 assert_eq!(
496 assert_eq!(
957 root.search(&[key!(' '), key!('w')]).unwrap(),
497 root.search(&[key!(' '), key!('w')]).unwrap(),
958 root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
498 root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
helix-term/src/keymap/default.rsnew file+359
@@ -0,0 +1,359 @@
1+use std::collections::HashMap;
2+
3+use super::macros::keymap;
4+use super::{Keymap, Mode};
5+use helix_core::hashmap;
6+
7+pub fn default() -> HashMap<Mode, Keymap> {
8+ let normal = keymap!({ "Normal mode"
9+ "h" | "left" => move_char_left,
10+ "j" | "down" => move_line_down,
11+ "k" | "up" => move_line_up,
12+ "l" | "right" => move_char_right,
13+
14+ "t" => find_till_char,
15+ "f" => find_next_char,
16+ "T" => till_prev_char,
17+ "F" => find_prev_char,
18+ "r" => replace,
19+ "R" => replace_with_yanked,
20+ "A-." => repeat_last_motion,
21+
22+ "~" => switch_case,
23+ "`" => switch_to_lowercase,
24+ "A-`" => switch_to_uppercase,
25+
26+ "home" => goto_line_start,
27+ "end" => goto_line_end,
28+
29+ "w" => move_next_word_start,
30+ "b" => move_prev_word_start,
31+ "e" => move_next_word_end,
32+
33+ "W" => move_next_long_word_start,
34+ "B" => move_prev_long_word_start,
35+ "E" => move_next_long_word_end,
36+
37+ "v" => select_mode,
38+ "G" => goto_line,
39+ "g" => { "Goto"
40+ "g" => goto_file_start,
41+ "e" => goto_last_line,
42+ "f" => goto_file,
43+ "h" => goto_line_start,
44+ "l" => goto_line_end,
45+ "s" => goto_first_nonwhitespace,
46+ "d" => goto_definition,
47+ "y" => goto_type_definition,
48+ "r" => goto_reference,
49+ "i" => goto_implementation,
50+ "t" => goto_window_top,
51+ "c" => goto_window_center,
52+ "b" => goto_window_bottom,
53+ "a" => goto_last_accessed_file,
54+ "m" => goto_last_modified_file,
55+ "n" => goto_next_buffer,
56+ "p" => goto_previous_buffer,
57+ "." => goto_last_modification,
58+ },
59+ ":" => command_mode,
60+
61+ "i" => insert_mode,
62+ "I" => prepend_to_line,
63+ "a" => append_mode,
64+ "A" => append_to_line,
65+ "o" => open_below,
66+ "O" => open_above,
67+
68+ "d" => delete_selection,
69+ "A-d" => delete_selection_noyank,
70+ "c" => change_selection,
71+ "A-c" => change_selection_noyank,
72+
73+ "C" => copy_selection_on_next_line,
74+ "A-C" => copy_selection_on_prev_line,
75+
76+
77+ "s" => select_regex,
78+ "A-s" => split_selection_on_newline,
79+ "S" => split_selection,
80+ ";" => collapse_selection,
81+ "A-;" => flip_selections,
82+ "A-k" | "A-up" => expand_selection,
83+ "A-j" | "A-down" => shrink_selection,
84+ "A-h" | "A-left" => select_prev_sibling,
85+ "A-l" | "A-right" => select_next_sibling,
86+
87+ "%" => select_all,
88+ "x" => extend_line,
89+ "X" => extend_to_line_bounds,
90+ // crop_to_whole_line
91+
92+ "m" => { "Match"
93+ "m" => match_brackets,
94+ "s" => surround_add,
95+ "r" => surround_replace,
96+ "d" => surround_delete,
97+ "a" => select_textobject_around,
98+ "i" => select_textobject_inner,
99+ },
100+ "[" => { "Left bracket"
101+ "d" => goto_prev_diag,
102+ "D" => goto_first_diag,
103+ "f" => goto_prev_function,
104+ "c" => goto_prev_class,
105+ "a" => goto_prev_parameter,
106+ "o" => goto_prev_comment,
107+ "space" => add_newline_above,
108+ },
109+ "]" => { "Right bracket"
110+ "d" => goto_next_diag,
111+ "D" => goto_last_diag,
112+ "f" => goto_next_function,
113+ "c" => goto_next_class,
114+ "a" => goto_next_parameter,
115+ "o" => goto_next_comment,
116+ "space" => add_newline_below,
117+ },
118+
119+ "/" => search,
120+ "?" => rsearch,
121+ "n" => search_next,
122+ "N" => search_prev,
123+ "*" => search_selection,
124+
125+ "u" => undo,
126+ "U" => redo,
127+ "A-u" => earlier,
128+ "A-U" => later,
129+
130+ "y" => yank,
131+ // yank_all
132+ "p" => paste_after,
133+ // paste_all
134+ "P" => paste_before,
135+
136+ "Q" => record_macro,
137+ "q" => replay_macro,
138+
139+ ">" => indent,
140+ "<" => unindent,
141+ "=" => format_selections,
142+ "J" => join_selections,
143+ "K" => keep_selections,
144+ "A-K" => remove_selections,
145+
146+ "," => keep_primary_selection,
147+ "A-," => remove_primary_selection,
148+
149+ // "q" => record_macro,
150+ // "Q" => replay_macro,
151+
152+ "&" => align_selections,
153+ "_" => trim_selections,
154+
155+ "(" => rotate_selections_backward,
156+ ")" => rotate_selections_forward,
157+ "A-(" => rotate_selection_contents_backward,
158+ "A-)" => rotate_selection_contents_forward,
159+
160+ "A-:" => ensure_selections_forward,
161+
162+ "esc" => normal_mode,
163+ "C-b" | "pageup" => page_up,
164+ "C-f" | "pagedown" => page_down,
165+ "C-u" => half_page_up,
166+ "C-d" => half_page_down,
167+
168+ "C-w" => { "Window"
169+ "C-w" | "w" => rotate_view,
170+ "C-s" | "s" => hsplit,
171+ "C-v" | "v" => vsplit,
172+ "f" => goto_file_hsplit,
173+ "F" => goto_file_vsplit,
174+ "C-q" | "q" => wclose,
175+ "C-o" | "o" => wonly,
176+ "C-h" | "h" | "left" => jump_view_left,
177+ "C-j" | "j" | "down" => jump_view_down,
178+ "C-k" | "k" | "up" => jump_view_up,
179+ "C-l" | "l" | "right" => jump_view_right,
180+ "n" => { "New split scratch buffer"
181+ "C-s" | "s" => hsplit_new,
182+ "C-v" | "v" => vsplit_new,
183+ },
184+ },
185+
186+ // move under <space>c
187+ "C-c" => toggle_comments,
188+
189+ // z family for save/restore/combine from/to sels from register
190+
191+ "tab" => jump_forward, // tab == <C-i>
192+ "C-o" => jump_backward,
193+ "C-s" => save_selection,
194+
195+ "space" => { "Space"
196+ "f" => file_picker,
197+ "b" => buffer_picker,
198+ "s" => symbol_picker,
199+ "S" => workspace_symbol_picker,
200+ "a" => code_action,
201+ "'" => last_picker,
202+ "d" => { "Debug (experimental)" sticky=true
203+ "l" => dap_launch,
204+ "b" => dap_toggle_breakpoint,
205+ "c" => dap_continue,
206+ "h" => dap_pause,
207+ "i" => dap_step_in,
208+ "o" => dap_step_out,
209+ "n" => dap_next,
210+ "v" => dap_variables,
211+ "t" => dap_terminate,
212+ "C-c" => dap_edit_condition,
213+ "C-l" => dap_edit_log,
214+ "s" => { "Switch"
215+ "t" => dap_switch_thread,
216+ "f" => dap_switch_stack_frame,
217+ // sl, sb
218+ },
219+ "e" => dap_enable_exceptions,
220+ "E" => dap_disable_exceptions,
221+ },
222+ "w" => { "Window"
223+ "C-w" | "w" => rotate_view,
224+ "C-s" | "s" => hsplit,
225+ "C-v" | "v" => vsplit,
226+ "f" => goto_file_hsplit,
227+ "F" => goto_file_vsplit,
228+ "C-q" | "q" => wclose,
229+ "C-o" | "o" => wonly,
230+ "C-h" | "h" | "left" => jump_view_left,
231+ "C-j" | "j" | "down" => jump_view_down,
232+ "C-k" | "k" | "up" => jump_view_up,
233+ "C-l" | "l" | "right" => jump_view_right,
234+ "n" => { "New split scratch buffer"
235+ "C-s" | "s" => hsplit_new,
236+ "C-v" | "v" => vsplit_new,
237+ },
238+ },
239+ "y" => yank_joined_to_clipboard,
240+ "Y" => yank_main_selection_to_clipboard,
241+ "p" => paste_clipboard_after,
242+ "P" => paste_clipboard_before,
243+ "R" => replace_selections_with_clipboard,
244+ "/" => global_search,
245+ "k" => hover,
246+ "r" => rename_symbol,
247+ "?" => command_palette,
248+ },
249+ "z" => { "View"
250+ "z" | "c" => align_view_center,
251+ "t" => align_view_top,
252+ "b" => align_view_bottom,
253+ "m" => align_view_middle,
254+ "k" | "up" => scroll_up,
255+ "j" | "down" => scroll_down,
256+ "C-b" | "pageup" => page_up,
257+ "C-f" | "pagedown" => page_down,
258+ "C-u" => half_page_up,
259+ "C-d" => half_page_down,
260+ },
261+ "Z" => { "View" sticky=true
262+ "z" | "c" => align_view_center,
263+ "t" => align_view_top,
264+ "b" => align_view_bottom,
265+ "m" => align_view_middle,
266+ "k" | "up" => scroll_up,
267+ "j" | "down" => scroll_down,
268+ "C-b" | "pageup" => page_up,
269+ "C-f" | "pagedown" => page_down,
270+ "C-u" => half_page_up,
271+ "C-d" => half_page_down,
272+ },
273+
274+ "\"" => select_register,
275+ "|" => shell_pipe,
276+ "A-|" => shell_pipe_to,
277+ "!" => shell_insert_output,
278+ "A-!" => shell_append_output,
279+ "$" => shell_keep_pipe,
280+ "C-z" => suspend,
281+
282+ "C-a" => increment,
283+ "C-x" => decrement,
284+ });
285+ let mut select = normal.clone();
286+ select.merge_nodes(keymap!({ "Select mode"
287+ "h" | "left" => extend_char_left,
288+ "j" | "down" => extend_line_down,
289+ "k" | "up" => extend_line_up,
290+ "l" | "right" => extend_char_right,
291+
292+ "w" => extend_next_word_start,
293+ "b" => extend_prev_word_start,
294+ "e" => extend_next_word_end,
295+ "W" => extend_next_long_word_start,
296+ "B" => extend_prev_long_word_start,
297+ "E" => extend_next_long_word_end,
298+
299+ "n" => extend_search_next,
300+ "N" => extend_search_prev,
301+
302+ "t" => extend_till_char,
303+ "f" => extend_next_char,
304+ "T" => extend_till_prev_char,
305+ "F" => extend_prev_char,
306+
307+ "home" => extend_to_line_start,
308+ "end" => extend_to_line_end,
309+ "esc" => exit_select_mode,
310+
311+ "v" => normal_mode,
312+ }));
313+ let insert = keymap!({ "Insert mode"
314+ "esc" => normal_mode,
315+
316+ "backspace" => delete_char_backward,
317+ "C-h" => delete_char_backward,
318+ "del" => delete_char_forward,
319+ "C-d" => delete_char_forward,
320+ "ret" => insert_newline,
321+ "C-j" => insert_newline,
322+ "tab" => insert_tab,
323+ "C-w" => delete_word_backward,
324+ "A-backspace" => delete_word_backward,
325+ "A-d" => delete_word_forward,
326+
327+ "left" => move_char_left,
328+ "C-b" => move_char_left,
329+ "down" => move_line_down,
330+ "C-n" => move_line_down,
331+ "up" => move_line_up,
332+ "C-p" => move_line_up,
333+ "right" => move_char_right,
334+ "C-f" => move_char_right,
335+ "A-b" => move_prev_word_end,
336+ "A-left" => move_prev_word_end,
337+ "A-f" => move_next_word_start,
338+ "A-right" => move_next_word_start,
339+ "A-<" => goto_file_start,
340+ "A->" => goto_file_end,
341+ "pageup" => page_up,
342+ "pagedown" => page_down,
343+ "home" => goto_line_start,
344+ "C-a" => goto_line_start,
345+ "end" => goto_line_end_newline,
346+ "C-e" => goto_line_end_newline,
347+
348+ "C-k" => kill_to_line_end,
349+ "C-u" => kill_to_line_start,
350+
351+ "C-x" => completion,
352+ "C-r" => insert_register,
353+ });
354+ hashmap!(
355+ Mode::Normal => Keymap::new(normal),
356+ Mode::Select => Keymap::new(select),
357+ Mode::Insert => Keymap::new(insert),
358+ )
359+}
helix-term/src/keymap/macros.rsnew file+127
@@ -0,0 +1,127 @@
1+#[macro_export]
2+macro_rules! key {
3+ ($key:ident) => {
4+ ::helix_view::input::KeyEvent {
5+ code: ::helix_view::keyboard::KeyCode::$key,
6+ modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
7+ }
8+ };
9+ ($($ch:tt)*) => {
10+ ::helix_view::input::KeyEvent {
11+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
12+ modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
13+ }
14+ };
15+}
16+
17+#[macro_export]
18+macro_rules! shift {
19+ ($key:ident) => {
20+ ::helix_view::input::KeyEvent {
21+ code: ::helix_view::keyboard::KeyCode::$key,
22+ modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
23+ }
24+ };
25+ ($($ch:tt)*) => {
26+ ::helix_view::input::KeyEvent {
27+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
28+ modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
29+ }
30+ };
31+}
32+
33+#[macro_export]
34+macro_rules! ctrl {
35+ ($key:ident) => {
36+ ::helix_view::input::KeyEvent {
37+ code: ::helix_view::keyboard::KeyCode::$key,
38+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
39+ }
40+ };
41+ ($($ch:tt)*) => {
42+ ::helix_view::input::KeyEvent {
43+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
44+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
45+ }
46+ };
47+}
48+
49+#[macro_export]
50+macro_rules! alt {
51+ ($key:ident) => {
52+ ::helix_view::input::KeyEvent {
53+ code: ::helix_view::keyboard::KeyCode::$key,
54+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
55+ }
56+ };
57+ ($($ch:tt)*) => {
58+ ::helix_view::input::KeyEvent {
59+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
60+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
61+ }
62+ };
63+}
64+
65+/// Macro for defining the root of a `Keymap` object. Example:
66+///
67+/// ```
68+/// # use helix_core::hashmap;
69+/// # use helix_term::keymap;
70+/// # use helix_term::keymap::Keymap;
71+/// let normal_mode = keymap!({ "Normal mode"
72+/// "i" => insert_mode,
73+/// "g" => { "Goto"
74+/// "g" => goto_file_start,
75+/// "e" => goto_file_end,
76+/// },
77+/// "j" | "down" => move_line_down,
78+/// });
79+/// let keymap = Keymap::new(normal_mode);
80+/// ```
81+#[macro_export]
82+macro_rules! keymap {
83+ (@trie $cmd:ident) => {
84+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
85+ };
86+
87+ (@trie
88+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
89+ ) => {
90+ keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
91+ };
92+
93+ (@trie [$($cmd:ident),* $(,)?]) => {
94+ $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
95+ };
96+
97+ (
98+ { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
99+ ) => {
100+ // modified from the hashmap! macro
101+ {
102+ let _cap = hashmap!(@count $($($key),+),*);
103+ let mut _map = ::std::collections::HashMap::with_capacity(_cap);
104+ let mut _order = ::std::vec::Vec::with_capacity(_cap);
105+ $(
106+ $(
107+ let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
108+ let _duplicate = _map.insert(
109+ _key,
110+ keymap!(@trie $value)
111+ );
112+ assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
113+ _order.push(_key);
114+ )+
115+ )*
116+ let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
117+ $( _node.is_sticky = $sticky; )?
118+ $crate::keymap::KeyTrie::Node(_node)
119+ }
120+ };
121+}
122+
123+pub use alt;
124+pub use ctrl;
125+pub use key;
126+pub use keymap;
127+pub use shift;
helix-term/src/lib.rs+1
@@ -10,6 +10,7 @@ pub mod health;
10pub mod job;
10pub mod job;
11pub mod keymap;
11pub mod keymap;
12pub mod ui;
12pub mod ui;
13+pub use keymap::macros::*;
13
14
14#[cfg(not(windows))]
15#[cfg(not(windows))]
15fn true_color() -> bool {
16fn true_color() -> bool {
helix-term/src/main.rs+19−16
@@ -1,8 +1,7 @@
1use anyhow::{Context, Error, Result};
1use anyhow::{Context, Error, Result};
2use helix_term::application::Application;
2use helix_term::application::Application;
3use helix_term::args::Args;
3use helix_term::args::Args;
4−use helix_term::config::Config;
4+use helix_term::config::{Config, ConfigLoadError};
5−use helix_term::keymap::merge_keys;
6use std::path::PathBuf;
5use std::path::PathBuf;
7
6
8fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
7fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
@@ -60,7 +59,6 @@ ARGS:
60
59
61FLAGS:
60FLAGS:
62 -h, --help Prints help information
61 -h, --help Prints help information
63− --edit-config Opens the helix config file
64 --tutor Loads the tutorial
62 --tutor Loads the tutorial
65 --health [LANG] Checks for potential errors in editor setup
63 --health [LANG] Checks for potential errors in editor setup
66 If given, checks for config errors in language LANG
64 If given, checks for config errors in language LANG
@@ -118,19 +116,24 @@ FLAGS:
118 std::fs::create_dir_all(&conf_dir).ok();
116 std::fs::create_dir_all(&conf_dir).ok();
119 }
117 }
120
118
121− let config = match std::fs::read_to_string(helix_loader::config_file()) {
119+ let config = match Config::load_default() {
122− Ok(config) => toml::from_str(&config)
120+ Ok(config) => config,
123− .map(merge_keys)
121+ Err(err) => {
124− .unwrap_or_else(|err| {
122+ match err {
123+ ConfigLoadError::BadConfig(err) => {
125− eprintln!("Bad config: {}", err);
124+ eprintln!("Bad config: {}", err);
126− eprintln!("Press <ENTER> to continue with default config");
125+ eprintln!("Press <ENTER> to continue with default config");
127− use std::io::Read;
126+ use std::io::Read;
128− // This waits for an enter press.
127+ // This waits for an enter press.
129− let _ = std::io::stdin().read(&mut []);
128+ let _ = std::io::stdin().read(&mut []);
130− Config::default()
129+ Config::default()
131− }),
130+ }
132− Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
131+ ConfigLoadError::Error(err) if err.kind() == std::io::ErrorKind::NotFound => {
132+ Config::default()
133+ }
133− Err(err) => return Err(Error::new(err)),
134+ ConfigLoadError::Error(err) => return Err(Error::new(err)),
135+ }
136+ }
134 };
137 };
135
138
136 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
139 setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
helix-term/src/ui/editor.rs+10−10
@@ -118,7 +118,7 @@ impl EditorView {
118 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
118 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
119 Box::new(syntax::merge(
119 Box::new(syntax::merge(
120 highlights,
120 highlights,
121− Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape),
121+ Self::doc_selection_highlights(doc, view, theme, &editor.config().cursor_shape),
122 ))
122 ))
123 } else {
123 } else {
124 Box::new(highlights)
124 Box::new(highlights)
@@ -702,7 +702,6 @@ impl EditorView {
702 cxt: &mut commands::Context,
702 cxt: &mut commands::Context,
703 event: KeyEvent,
703 event: KeyEvent,
704 ) -> Option<KeymapResult> {
704 ) -> Option<KeymapResult> {
705− cxt.editor.autoinfo = None;
706 let key_result = self.keymaps.get(mode, event);
705 let key_result = self.keymaps.get(mode, event);
707 cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
706 cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
708
707
@@ -845,7 +844,7 @@ impl EditorView {
845
844
846 pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
845 pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
847 if self.completion.is_some()
846 if self.completion.is_some()
848− || !cx.editor.config.auto_completion
847+ || !cx.editor.config().auto_completion
849 || doc!(cx.editor).mode != Mode::Insert
848 || doc!(cx.editor).mode != Mode::Insert
850 {
849 {
851 return EventResult::Ignored(None);
850 return EventResult::Ignored(None);
@@ -871,6 +870,7 @@ impl EditorView {
871 event: MouseEvent,
870 event: MouseEvent,
872 cxt: &mut commands::Context,
871 cxt: &mut commands::Context,
873 ) -> EventResult {
872 ) -> EventResult {
873+ let config = cxt.editor.config();
874 match event {
874 match event {
875 MouseEvent {
875 MouseEvent {
876 kind: MouseEventKind::Down(MouseButton::Left),
876 kind: MouseEventKind::Down(MouseButton::Left),
@@ -971,7 +971,7 @@ impl EditorView {
971 None => return EventResult::Ignored(None),
971 None => return EventResult::Ignored(None),
972 }
972 }
973
973
974− let offset = cxt.editor.config.scroll_lines.abs() as usize;
974+ let offset = config.scroll_lines.abs() as usize;
975 commands::scroll(cxt, offset, direction);
975 commands::scroll(cxt, offset, direction);
976
976
977 cxt.editor.tree.focus = current_view;
977 cxt.editor.tree.focus = current_view;
@@ -983,7 +983,7 @@ impl EditorView {
983 kind: MouseEventKind::Up(MouseButton::Left),
983 kind: MouseEventKind::Up(MouseButton::Left),
984 ..
984 ..
985 } => {
985 } => {
986− if !cxt.editor.config.middle_click_paste {
986+ if !config.middle_click_paste {
987 return EventResult::Ignored(None);
987 return EventResult::Ignored(None);
988 }
988 }
989
989
@@ -1039,7 +1039,7 @@ impl EditorView {
1039 ..
1039 ..
1040 } => {
1040 } => {
1041 let editor = &mut cxt.editor;
1041 let editor = &mut cxt.editor;
1042− if !editor.config.middle_click_paste {
1042+ if !config.middle_click_paste {
1043 return EventResult::Ignored(None);
1043 return EventResult::Ignored(None);
1044 }
1044 }
1045
1045
@@ -1163,9 +1163,9 @@ impl Component for EditorView {
1163 if cx.editor.should_close() {
1163 if cx.editor.should_close() {
1164 return EventResult::Ignored(None);
1164 return EventResult::Ignored(None);
1165 }
1165 }
1166−
1166+ let config = cx.editor.config();
1167 let (view, doc) = current!(cx.editor);
1167 let (view, doc) = current!(cx.editor);
1168− view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
1168+ view.ensure_cursor_in_view(doc, config.scrolloff);
1169
1169
1170 // Store a history state if not in insert mode. This also takes care of
1170 // Store a history state if not in insert mode. This also takes care of
1171 // commiting changes when leaving insert mode.
1171 // commiting changes when leaving insert mode.
@@ -1206,7 +1206,7 @@ impl Component for EditorView {
1206 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
1206 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
1207 // clear with background color
1207 // clear with background color
1208 surface.set_style(area, cx.editor.theme.get("ui.background"));
1208 surface.set_style(area, cx.editor.theme.get("ui.background"));
1209−
1209+ let config = cx.editor.config();
1210 // if the terminal size suddenly changed, we need to trigger a resize
1210 // if the terminal size suddenly changed, we need to trigger a resize
1211 cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
1211 cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
1212
1212
@@ -1215,7 +1215,7 @@ impl Component for EditorView {
1215 self.render_view(cx.editor, doc, view, area, surface, is_focused);
1215 self.render_view(cx.editor, doc, view, area, surface, is_focused);
1216 }
1216 }
1217
1217
1218− if cx.editor.config.auto_info {
1218+ if config.auto_info {
1219 if let Some(mut info) = cx.editor.autoinfo.take() {
1219 if let Some(mut info) = cx.editor.autoinfo.take() {
1220 info.render(area, surface, cx);
1220 info.render(area, surface, cx);
1221 cx.editor.autoinfo = Some(info)
1221 cx.editor.autoinfo = Some(info)
helix-term/src/ui/mod.rs+3−2
@@ -37,6 +37,7 @@ pub fn regex_prompt(
37 let doc_id = view.doc;
37 let doc_id = view.doc;
38 let snapshot = doc.selection(view.id).clone();
38 let snapshot = doc.selection(view.id).clone();
39 let offset_snapshot = view.offset;
39 let offset_snapshot = view.offset;
40+ let config = cx.editor.config();
40
41
41 let mut prompt = Prompt::new(
42 let mut prompt = Prompt::new(
42 prompt,
43 prompt,
@@ -65,7 +66,7 @@ pub fn regex_prompt(
65 return;
66 return;
66 }
67 }
67
68
68− let case_insensitive = if cx.editor.config.search.smart_case {
69+ let case_insensitive = if config.search.smart_case {
69 !input.chars().any(char::is_uppercase)
70 !input.chars().any(char::is_uppercase)
70 } else {
71 } else {
71 false
72 false
@@ -84,7 +85,7 @@ pub fn regex_prompt(
84
85
85 fun(view, doc, regex, event);
86 fun(view, doc, regex, event);
86
87
87− view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
88+ view.ensure_cursor_in_view(doc, config.scrolloff);
88 }
89 }
89 Err(_err) => (), // TODO: mark command line as error
90 Err(_err) => (), // TODO: mark command line as error
90 }
91 }
helix-view/Cargo.toml+4−2
@@ -17,14 +17,16 @@ term = ["crossterm"]
17bitflags = "1.3"
17bitflags = "1.3"
18anyhow = "1"
18anyhow = "1"
19helix-core = { version = "0.6", path = "../helix-core" }
19helix-core = { version = "0.6", path = "../helix-core" }
20−helix-lsp = { version = "0.6", path = "../helix-lsp"}
20+helix-lsp = { version = "0.6", path = "../helix-lsp" }
21−helix-dap = { version = "0.6", path = "../helix-dap"}
21+helix-dap = { version = "0.6", path = "../helix-dap" }
22crossterm = { version = "0.23", optional = true }
22crossterm = { version = "0.23", optional = true }
23
23
24# Conversion traits
24# Conversion traits
25once_cell = "1.10"
25once_cell = "1.10"
26url = "2"
26url = "2"
27
27
28+arc-swap = { version = "1.5.0" }
29+
28tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
30tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
29tokio-stream = "0.1"
31tokio-stream = "0.1"
30futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
32futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
helix-view/src/editor.rs+32−13
@@ -13,7 +13,6 @@ use futures_util::future;
13use futures_util::stream::select_all::SelectAll;
13use futures_util::stream::select_all::SelectAll;
14use tokio_stream::wrappers::UnboundedReceiverStream;
14use tokio_stream::wrappers::UnboundedReceiverStream;
15
15
16−use log::debug;
17use std::{
16use std::{
18 borrow::Cow,
17 borrow::Cow,
19 collections::{BTreeMap, HashMap},
18 collections::{BTreeMap, HashMap},
@@ -24,7 +23,10 @@ use std::{
24 sync::Arc,
23 sync::Arc,
25};
24};
26
25
26+use tokio::{
27+ sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
27−use tokio::time::{sleep, Duration, Instant, Sleep};
28+ time::{sleep, Duration, Instant, Sleep},
29+};
28
30
29use anyhow::{bail, Error};
31use anyhow::{bail, Error};
30
32
@@ -40,6 +42,8 @@ use helix_dap as dap;
40
42
41use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
43use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
42
44
45+use arc_swap::access::{DynAccess, DynGuard};
46+
43fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
47fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
44where
48where
45 D: serde::Deserializer<'de>,
49 D: serde::Deserializer<'de>,
@@ -287,7 +291,6 @@ pub struct Breakpoint {
287 pub log_message: Option<String>,
291 pub log_message: Option<String>,
288}
292}
289
293
290−#[derive(Debug)]
291pub struct Editor {
294pub struct Editor {
292 pub tree: Tree,
295 pub tree: Tree,
293 pub next_document_id: DocumentId,
296 pub next_document_id: DocumentId,
@@ -311,7 +314,7 @@ pub struct Editor {
311 pub status_msg: Option<(Cow<'static, str>, Severity)>,
314 pub status_msg: Option<(Cow<'static, str>, Severity)>,
312 pub autoinfo: Option<Info>,
315 pub autoinfo: Option<Info>,
313
316
314− pub config: Config,
317+ pub config: Box<dyn DynAccess<Config>>,
315 pub auto_pairs: Option<AutoPairs>,
318 pub auto_pairs: Option<AutoPairs>,
316
319
317 pub idle_timer: Pin<Box<Sleep>>,
320 pub idle_timer: Pin<Box<Sleep>>,
@@ -321,6 +324,14 @@ pub struct Editor {
321 pub last_completion: Option<CompleteAction>,
324 pub last_completion: Option<CompleteAction>,
322
325
323 pub exit_code: i32,
326 pub exit_code: i32,
327+
328+ pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
329+}
330+
331+#[derive(Debug, Clone)]
332+pub enum ConfigEvent {
333+ Refresh,
334+ Update(Config),
324}
335}
325
336
326#[derive(Debug, Clone)]
337#[derive(Debug, Clone)]
@@ -342,12 +353,11 @@ impl Editor {
342 mut area: Rect,
353 mut area: Rect,
343 theme_loader: Arc<theme::Loader>,
354 theme_loader: Arc<theme::Loader>,
344 syn_loader: Arc<syntax::Loader>,
355 syn_loader: Arc<syntax::Loader>,
345− config: Config,
356+ config: Box<dyn DynAccess<Config>>,
346 ) -> Self {
357 ) -> Self {
347 let language_servers = helix_lsp::Registry::new();
358 let language_servers = helix_lsp::Registry::new();
359+ let conf = config.load();
348− let auto_pairs = (&config.auto_pairs).into();
360+ let auto_pairs = (&conf.auto_pairs).into();
349−
350− debug!("Editor config: {config:#?}");
351
361
352 // HAXX: offset the render area height by 1 to account for prompt/commandline
362 // HAXX: offset the render area height by 1 to account for prompt/commandline
353 area.height -= 1;
363 area.height -= 1;
@@ -370,16 +380,21 @@ impl Editor {
370 clipboard_provider: get_clipboard_provider(),
380 clipboard_provider: get_clipboard_provider(),
371 status_msg: None,
381 status_msg: None,
372 autoinfo: None,
382 autoinfo: None,
373− idle_timer: Box::pin(sleep(config.idle_timeout)),
383+ idle_timer: Box::pin(sleep(conf.idle_timeout)),
374 last_motion: None,
384 last_motion: None,
375 last_completion: None,
385 last_completion: None,
376 pseudo_pending: None,
386 pseudo_pending: None,
377 config,
387 config,
378 auto_pairs,
388 auto_pairs,
379 exit_code: 0,
389 exit_code: 0,
390+ config_events: unbounded_channel(),
380 }
391 }
381 }
392 }
382
393
394+ pub fn config(&self) -> DynGuard<Config> {
395+ self.config.load()
396+ }
397+
383 pub fn clear_idle_timer(&mut self) {
398 pub fn clear_idle_timer(&mut self) {
384 // equivalent to internal Instant::far_future() (30 years)
399 // equivalent to internal Instant::far_future() (30 years)
385 self.idle_timer
400 self.idle_timer
@@ -388,9 +403,10 @@ impl Editor {
388 }
403 }
389
404
390 pub fn reset_idle_timer(&mut self) {
405 pub fn reset_idle_timer(&mut self) {
406+ let config = self.config();
391 self.idle_timer
407 self.idle_timer
392 .as_mut()
408 .as_mut()
393− .reset(Instant::now() + self.config.idle_timeout);
409+ .reset(Instant::now() + config.idle_timeout);
394 }
410 }
395
411
396 pub fn clear_status(&mut self) {
412 pub fn clear_status(&mut self) {
@@ -466,9 +482,10 @@ impl Editor {
466 }
482 }
467
483
468 fn _refresh(&mut self) {
484 fn _refresh(&mut self) {
485+ let config = self.config();
469 for (view, _) in self.tree.views_mut() {
486 for (view, _) in self.tree.views_mut() {
470 let doc = &self.documents[&view.doc];
487 let doc = &self.documents[&view.doc];
471− view.ensure_cursor_in_view(doc, self.config.scrolloff)
488+ view.ensure_cursor_in_view(doc, config.scrolloff)
472 }
489 }
473 }
490 }
474
491
@@ -716,9 +733,10 @@ impl Editor {
716 }
733 }
717
734
718 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
735 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
736+ let config = self.config();
719 let view = self.tree.get_mut(id);
737 let view = self.tree.get_mut(id);
720 let doc = &self.documents[&view.doc];
738 let doc = &self.documents[&view.doc];
721− view.ensure_cursor_in_view(doc, self.config.scrolloff)
739+ view.ensure_cursor_in_view(doc, config.scrolloff)
722 }
740 }
723
741
724 #[inline]
742 #[inline]
@@ -752,6 +770,7 @@ impl Editor {
752 }
770 }
753
771
754 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
772 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
773+ let config = self.config();
755 let (view, doc) = current_ref!(self);
774 let (view, doc) = current_ref!(self);
756 let cursor = doc
775 let cursor = doc
757 .selection(view.id)
776 .selection(view.id)
@@ -761,7 +780,7 @@ impl Editor {
761 let inner = view.inner_area();
780 let inner = view.inner_area();
762 pos.col += inner.x as usize;
781 pos.col += inner.x as usize;
763 pos.row += inner.y as usize;
782 pos.row += inner.y as usize;
764− let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
783+ let cursorkind = config.cursor_shape.from_mode(doc.mode());
765 (Some(pos), cursorkind)
784 (Some(pos), cursorkind)
766 } else {
785 } else {
767 (None, CursorKind::default())
786 (None, CursorKind::default())
helix-view/src/gutter.rs+2−2
@@ -60,7 +60,7 @@ pub fn line_number<'doc>(
60 .text()
60 .text()
61 .char_to_line(doc.selection(view.id).primary().cursor(text));
61 .char_to_line(doc.selection(view.id).primary().cursor(text));
62
62
63− let config = editor.config.line_number;
63+ let line_number = editor.config().line_number;
64 let mode = doc.mode;
64 let mode = doc.mode;
65
65
66 Box::new(move |line: usize, selected: bool, out: &mut String| {
66 Box::new(move |line: usize, selected: bool, out: &mut String| {
@@ -70,7 +70,7 @@ pub fn line_number<'doc>(
70 } else {
70 } else {
71 use crate::{document::Mode, editor::LineNumber};
71 use crate::{document::Mode, editor::LineNumber};
72
72
73− let relative = config == LineNumber::Relative
73+ let relative = line_number == LineNumber::Relative
74 && mode != Mode::Insert
74 && mode != Mode::Insert
75 && is_focused
75 && is_focused
76 && current_line != line;
76 && current_line != line;