aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorNathan Vegdahl2021-07-26 18:19:10 +0000
committerNathan Vegdahl2021-07-26 18:19:10 +0000
commitf62ec6e51e99dce9e93600801c3637c196c592b6 (patch)
tree8821ee23ade21c0da4fbb5a70143717ac42a2bc1 /helix-term/src
parent5ee6ba5b38ebeb86006bb2e42734a2285eb354df (diff)
parent88d6f652390922b389667f469b6d308db569bdaf (diff)
Merge branch 'master' into great_line_ending_and_cursor_range_cleanup
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs610
-rw-r--r--helix-term/src/config.rs34
-rw-r--r--helix-term/src/keymap.rs692
-rw-r--r--helix-term/src/ui/editor.rs76
-rw-r--r--helix-term/src/ui/info.rs2
-rw-r--r--helix-term/src/ui/mod.rs1
-rw-r--r--helix-term/src/ui/picker.rs1
-rw-r--r--helix-term/src/ui/prompt.rs58
8 files changed, 878 insertions, 596 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 89042acb..a37d2d26 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -13,7 +13,6 @@ use helix_core::{
use helix_view::{
document::{IndentStyle, Mode},
editor::Action,
- info::Info,
input::KeyEvent,
keyboard::KeyCode,
view::{View, PADDING},
@@ -36,7 +35,6 @@ use crate::{
use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, TryFutureExt};
-use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::{fmt, future::Future};
@@ -45,7 +43,7 @@ use std::{
path::{Path, PathBuf},
};
-use once_cell::sync::{Lazy, OnceCell};
+use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer};
pub struct Context<'a> {
@@ -75,18 +73,6 @@ impl<'a> Context<'a> {
}
#[inline]
- pub fn on_next_key_mode(&mut self, map: HashMap<KeyEvent, fn(&mut Context)>) {
- let count = self.count;
- self.on_next_key(move |cx, event| {
- cx.count = count;
- cx.editor.autoinfo = None;
- if let Some(func) = map.get(&event) {
- func(cx);
- }
- });
- }
-
- #[inline]
pub fn callback<T, F>(
&mut self,
call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
@@ -139,13 +125,21 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
/// A command is composed of a static name, and a function that takes the current state plus a count,
/// and does a side-effect on the state (usually by creating and applying a transaction).
#[derive(Copy, Clone)]
-pub struct Command(&'static str, fn(cx: &mut Context));
+pub struct Command {
+ name: &'static str,
+ fun: fn(cx: &mut Context),
+ doc: &'static str,
+}
macro_rules! commands {
- ( $($name:ident),* ) => {
+ ( $($name:ident, $doc:literal),* ) => {
$(
#[allow(non_upper_case_globals)]
- pub const $name: Self = Self(stringify!($name), $name);
+ pub const $name: Self = Self {
+ name: stringify!($name),
+ fun: $name,
+ doc: $doc
+ };
)*
pub const COMMAND_LIST: &'static [Self] = &[
@@ -156,144 +150,159 @@ macro_rules! commands {
impl Command {
pub fn execute(&self, cx: &mut Context) {
- (self.1)(cx);
+ (self.fun)(cx);
}
pub fn name(&self) -> &'static str {
- self.0
+ self.name
+ }
+
+ pub fn doc(&self) -> &'static str {
+ self.doc
}
+ #[rustfmt::skip]
commands!(
- move_char_left,
- move_char_right,
- move_line_up,
- move_line_down,
- move_next_word_start,
- move_prev_word_start,
- move_next_word_end,
- move_next_long_word_start,
- move_prev_long_word_start,
- move_next_long_word_end,
- extend_next_word_start,
- extend_prev_word_start,
- extend_next_word_end,
- find_till_char,
- find_next_char,
- extend_till_char,
- extend_next_char,
- till_prev_char,
- find_prev_char,
- extend_till_prev_char,
- extend_prev_char,
- replace,
- switch_case,
- switch_to_uppercase,
- switch_to_lowercase,
- page_up,
- page_down,
- half_page_up,
- half_page_down,
- extend_char_left,
- extend_char_right,
- extend_line_up,
- extend_line_down,
- select_all,
- select_regex,
- split_selection,
- split_selection_on_newline,
- search,
- search_next,
- extend_search_next,
- search_selection,
- extend_line,
- extend_to_line_bounds,
- delete_selection,
- change_selection,
- collapse_selection,
- flip_selections,
- insert_mode,
- append_mode,
- command_mode,
- file_picker,
- buffer_picker,
- symbol_picker,
- last_picker,
- prepend_to_line,
- append_to_line,
- open_below,
- open_above,
- normal_mode,
- goto_mode,
- select_mode,
- exit_select_mode,
- goto_definition,
- goto_type_definition,
- goto_implementation,
- goto_file_start,
- goto_file_end,
- goto_reference,
- goto_first_diag,
- goto_last_diag,
- goto_next_diag,
- goto_prev_diag,
- goto_line_start,
- goto_line_end,
- goto_line_end_newline,
- goto_first_nonwhitespace,
- signature_help,
- insert_tab,
- insert_newline,
- delete_char_backward,
- delete_char_forward,
- delete_word_backward,
- undo,
- redo,
- yank,
- yank_joined_to_clipboard,
- yank_main_selection_to_clipboard,
- replace_with_yanked,
- replace_selections_with_clipboard,
- paste_after,
- paste_before,
- paste_clipboard_after,
- paste_clipboard_before,
- indent,
- unindent,
- format_selections,
- join_selections,
- keep_selections,
- keep_primary_selection,
- completion,
- hover,
- toggle_comments,
- expand_selection,
- match_brackets,
- jump_forward,
- jump_backward,
- window_mode,
- rotate_view,
- hsplit,
- vsplit,
- wclose,
- select_register,
- space_mode,
- view_mode,
- left_bracket_mode,
- right_bracket_mode,
- match_mode
+ move_char_left, "Move left",
+ move_char_right, "Move right",
+ move_line_up, "Move up",
+ move_line_down, "Move down",
+ extend_char_left, "Extend left",
+ extend_char_right, "Extend right",
+ extend_line_up, "Extend up",
+ extend_line_down, "Extend down",
+ move_next_word_start, "Move to beginning of next word",
+ move_prev_word_start, "Move to beginning of previous word",
+ move_next_word_end, "Move to end of next word",
+ move_next_long_word_start, "Move to beginning of next long word",
+ move_prev_long_word_start, "Move to beginning of previous long word",
+ move_next_long_word_end, "Move to end of next long word",
+ extend_next_word_start, "Extend to beginning of next word",
+ extend_prev_word_start, "Extend to beginning of previous word",
+ extend_next_word_end, "Extend to end of next word",
+ find_till_char, "Move till next occurance of char",
+ find_next_char, "Move to next occurance of char",
+ extend_till_char, "Extend till next occurance of char",
+ extend_next_char, "Extend to next occurance of char",
+ till_prev_char, "Move till previous occurance of char",
+ find_prev_char, "Move to previous occurance of char",
+ extend_till_prev_char, "Extend till previous occurance of char",
+ extend_prev_char, "Extend to previous occurance of char",
+ replace, "Replace with new char",
+ switch_case, "Switch (toggle) case",
+ switch_to_uppercase, "Switch to uppercase",
+ switch_to_lowercase, "Switch to lowercase",
+ page_up, "Move page up",
+ page_down, "Move page down",
+ half_page_up, "Move half page up",
+ half_page_down, "Move half page down",
+ select_all, "Select whole document",
+ select_regex, "Select all regex matches inside selections",
+ split_selection, "Split selection into subselections on regex matches",
+ split_selection_on_newline, "Split selection on newlines",
+ search, "Search for regex pattern",
+ search_next, "Select next search match",
+ extend_search_next, "Add next search match to selection",
+ search_selection, "Use current selection as search pattern",
+ extend_line, "Select current line, if already selected, extend to next line",
+ extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
+ delete_selection, "Delete selection",
+ change_selection, "Change selection (delete and enter insert mode)",
+ collapse_selection, "Collapse selection onto a single cursor",
+ flip_selections, "Flip selection cursor and anchor",
+ insert_mode, "Insert before selection",
+ append_mode, "Insert after selection (append)",
+ command_mode, "Enter command mode",
+ file_picker, "Open file picker",
+ code_action, "Perform code action",
+ buffer_picker, "Open buffer picker",
+ symbol_picker, "Open symbol picker",
+ last_picker, "Open last picker",
+ prepend_to_line, "Insert at start of line",
+ append_to_line, "Insert at end of line",
+ open_below, "Open new line below selection",
+ open_above, "Open new line above selection",
+ normal_mode, "Enter normal mode",
+ select_mode, "Enter selection extend mode",
+ exit_select_mode, "Exit selection mode",
+ goto_definition, "Goto definition",
+ goto_type_definition, "Goto type definition",
+ goto_implementation, "Goto implementation",
+ goto_file_start, "Goto file start",
+ goto_file_end, "Goto file end",
+ goto_reference, "Goto references",
+ goto_window_top, "Goto window top",
+ goto_window_middle, "Goto window middle",
+ goto_window_bottom, "Goto window bottom",
+ goto_last_accessed_file, "Goto last accessed file",
+ goto_first_diag, "Goto first diagnostic",
+ goto_last_diag, "Goto last diagnostic",
+ goto_next_diag, "Goto next diagnostic",
+ goto_prev_diag, "Goto previous diagnostic",
+ goto_line_start, "Goto line start",
+ goto_line_end, "Goto line end",
+ // TODO: different description ?
+ goto_line_end_newline, "Goto line end",
+ goto_first_nonwhitespace, "Goto first non-blank in line",
+ signature_help, "Show signature help",
+ insert_tab, "Insert tab char",
+ insert_newline, "Insert newline char",
+ delete_char_backward, "Delete previous char",
+ delete_char_forward, "Delete next char",
+ delete_word_backward, "Delete previous word",
+ undo, "Undo change",
+ redo, "Redo change",
+ yank, "Yank selection",
+ yank_joined_to_clipboard, "Join and yank selections to clipboard",
+ yank_main_selection_to_clipboard, "Yank main selection to clipboard",
+ replace_with_yanked, "Replace with yanked text",
+ replace_selections_with_clipboard, "Replace selections by clipboard content",
+ paste_after, "Paste after selection",
+ paste_before, "Paste before selection",
+ paste_clipboard_after, "Paste clipboard after selections",
+ paste_clipboard_before, "Paste clipboard before selections",
+ indent, "Indent selection",
+ unindent, "Unindent selection",
+ format_selections, "Format selection",
+ join_selections, "Join lines inside selection",
+ keep_selections, "Keep selections matching regex",
+ keep_primary_selection, "Keep primary selection",
+ completion, "Invoke completion popup",
+ hover, "Show docs for item under cursor",
+ toggle_comments, "Comment/uncomment selections",
+ expand_selection, "Expand selection to parent syntax node",
+ jump_forward, "Jump forward on jumplist",
+ jump_backward, "Jump backward on jumplist",
+ rotate_view, "Goto next window",
+ hsplit, "Horizontal bottom split",
+ vsplit, "Vertical right split",
+ wclose, "Close window",
+ select_register, "Select register",
+ align_view_middle, "Align view middle",
+ align_view_top, "Align view top",
+ align_view_center, "Align view center",
+ align_view_bottom, "Align view bottom",
+ scroll_up, "Scroll view up",
+ scroll_down, "Scroll view down",
+ match_brackets, "Goto matching bracket",
+ surround_add, "Surround add",
+ surround_replace, "Surround replace",
+ surround_delete, "Surround delete",
+ select_textobject_around, "Select around object",
+ select_textobject_inner, "Select inside object"
);
}
impl fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command(name, _) = self;
+ let Command { name, .. } = self;
f.debug_tuple("Command").field(name).finish()
}
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command(name, _) = self;
+ let Command { name, .. } = self;
f.write_str(name)
}
}
@@ -305,7 +314,7 @@ impl std::str::FromStr for Command {
Command::COMMAND_LIST
.iter()
.copied()
- .find(|cmd| cmd.0 == s)
+ .find(|cmd| cmd.name == s)
.ok_or_else(|| anyhow!("No command named '{}'", s))
}
}
@@ -1171,7 +1180,7 @@ fn delete_selection(cx: &mut Context) {
let reg_name = cx.selected_register.name();
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
- let reg = registers.get_or_insert(reg_name);
+ let reg = registers.get_mut(reg_name);
delete_selection_impl(reg, doc, view.id);
doc.append_changes_to_history(view.id);
@@ -1184,7 +1193,7 @@ fn change_selection(cx: &mut Context) {
let reg_name = cx.selected_register.name();
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
- let reg = registers.get_or_insert(reg_name);
+ let reg = registers.get_mut(reg_name);
delete_selection_impl(reg, doc, view.id);
enter_insert_mode(doc);
}
@@ -1974,6 +1983,7 @@ mod cmd {
fn command_mode(cx: &mut Context) {
let mut prompt = Prompt::new(
":".to_owned(),
+ Some(':'),
|input: &str| {
// we use .this over split_whitespace() because we care about empty segments
let parts = input.split(' ').collect::<Vec<&str>>();
@@ -2147,6 +2157,112 @@ fn symbol_picker(cx: &mut Context) {
)
}
+pub fn code_action(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+
+ let language_server = match doc.language_server() {
+ Some(language_server) => language_server,
+ None => return,
+ };
+
+ let range = range_to_lsp_range(
+ doc.text(),
+ doc.selection(view.id).primary(),
+ language_server.offset_encoding(),
+ );
+
+ let future = language_server.code_actions(doc.identifier(), range);
+ let offset_encoding = language_server.offset_encoding();
+
+ cx.callback(
+ future,
+ move |_editor: &mut Editor,
+ compositor: &mut Compositor,
+ response: Option<lsp::CodeActionResponse>| {
+ if let Some(actions) = response {
+ let picker = Picker::new(
+ actions,
+ |action| match action {
+ lsp::CodeActionOrCommand::CodeAction(action) => {
+ action.title.as_str().into()
+ }
+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
+ },
+ move |editor, code_action, _action| match code_action {
+ lsp::CodeActionOrCommand::Command(command) => {
+ log::debug!("code action command: {:?}", command);
+ editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ }
+ lsp::CodeActionOrCommand::CodeAction(code_action) => {
+ log::debug!("code action: {:?}", code_action);
+ if let Some(ref workspace_edit) = code_action.edit {
+ apply_workspace_edit(editor, offset_encoding, workspace_edit)
+ }
+ }
+ },
+ );
+ compositor.push(Box::new(picker))
+ }
+ },
+ )
+}
+
+fn apply_workspace_edit(
+ editor: &mut Editor,
+ offset_encoding: OffsetEncoding,
+ workspace_edit: &lsp::WorkspaceEdit,
+) {
+ if let Some(ref changes) = workspace_edit.changes {
+ log::debug!("workspace changes: {:?}", changes);
+ editor.set_error(String::from("Handling workspace changesis not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ return;
+ // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
+ // TODO: find some example that uses workspace changes, and test it
+ // for (url, edits) in changes.iter() {
+ // let file_path = url.origin().ascii_serialization();
+ // let file_path = std::path::PathBuf::from(file_path);
+ // let file = std::fs::File::open(file_path).unwrap();
+ // let mut text = Rope::from_reader(file).unwrap();
+ // let transaction = edits_to_changes(&text, edits);
+ // transaction.apply(&mut text);
+ // }
+ }
+
+ if let Some(ref document_changes) = workspace_edit.document_changes {
+ match document_changes {
+ lsp::DocumentChanges::Edits(document_edits) => {
+ for document_edit in document_edits {
+ let (view, doc) = current!(editor);
+ assert_eq!(doc.url().unwrap(), document_edit.text_document.uri);
+ let edits = document_edit
+ .edits
+ .iter()
+ .map(|edit| match edit {
+ lsp::OneOf::Left(text_edit) => text_edit,
+ lsp::OneOf::Right(annotated_text_edit) => {
+ &annotated_text_edit.text_edit
+ }
+ })
+ .cloned()
+ .collect();
+
+ let transaction = helix_lsp::util::generate_transaction_from_edits(
+ doc.text(),
+ edits,
+ offset_encoding,
+ );
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ }
+ lsp::DocumentChanges::Operations(operations) => {
+ log::debug!("document changes - operations: {:?}", operations);
+ editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ }
+ }
+ }
+}
+
fn last_picker(cx: &mut Context) {
// TODO: last picker does not seemed to work well with buffer_picker
cx.callback = Some(Box::new(|compositor: &mut Compositor| {
@@ -2360,20 +2476,6 @@ fn exit_select_mode(cx: &mut Context) {
doc_mut!(cx.editor).mode = Mode::Normal;
}
-fn goto_prehook(cx: &mut Context) -> bool {
- if let Some(count) = cx.count {
- push_jump(cx.editor);
-
- let (view, doc) = current!(cx.editor);
- let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(1));
- let pos = doc.text().line_to_char(line_idx);
- doc.set_selection(view.id, Selection::point(pos));
- true
- } else {
- false
- }
-}
-
fn goto_impl(
editor: &mut Editor,
compositor: &mut Compositor,
@@ -3823,199 +3925,3 @@ fn surround_delete(cx: &mut Context) {
}
})
}
-
-/// Do nothing, just for modeinfo.
-fn noop(_cx: &mut Context) -> bool {
- false
-}
-
-/// Generate modeinfo.
-///
-/// If prehook returns true then it will stop the rest.
-macro_rules! mode_info {
- // TODO: reuse $mode for $stat
- (@join $first:expr $(,$rest:expr)*) => {
- concat!($first, $(", ", $rest),*)
- };
- (@name #[doc = $name:literal] $(#[$rest:meta])*) => {
- $name
- };
- {
- #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident,
- $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
- } => {
- mode_info! {
- #[doc = $name]
- $(#[$doc])*
- $mode, $stat, noop,
- $(
- #[doc = $desc]
- $($key)|+ => $func
- ),+,
- }
- };
- {
- #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr,
- $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
- } => {
- #[doc = $name]
- $(#[$doc])*
- #[doc = ""]
- #[doc = "<table><tr><th>key</th><th>desc</th></tr><tbody>"]
- $(
- #[doc = "<tr><td>"]
- // TODO switch to this once we use rust 1.54
- // right now it will produce multiple rows
- // #[doc = mode_info!(@join $($key),+)]
- $(
- #[doc = $key]
- )+
- // <-
- #[doc = "</td><td>"]
- #[doc = $desc]
- #[doc = "</td></tr>"]
- )+
- #[doc = "</tbody></table>"]
- pub fn $mode(cx: &mut Context) {
- if $prehook(cx) {
- return;
- }
- static $stat: OnceCell<Info> = OnceCell::new();
- cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key(
- $name.trim(),
- vec![$((&[$($key.parse().unwrap()),+], $desc)),+],
- )));
- use helix_core::hashmap;
- // TODO: try and convert this to match later
- let map = hashmap! {
- $($($key.parse::<KeyEvent>().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),*
- };
- cx.on_next_key_mode(map);
- }
- };
-}
-
-mode_info! {
- /// space mode
- space_mode, SPACE_MODE,
- /// resume last picker
- "'" => last_picker,
- /// file picker
- "f" => file_picker,
- /// buffer picker
- "b" => buffer_picker,
- /// symbol picker
- "s" => symbol_picker,
- /// window mode
- "w" => window_mode,
- /// yank joined to clipboard
- "y" => yank_joined_to_clipboard,
- /// yank main selection to clipboard
- "Y" => yank_main_selection_to_clipboard,
- /// paste system clipboard after selections
- "p" => paste_clipboard_after,
- /// paste system clipboard before selections
- "P" => paste_clipboard_before,
- /// replace selections with clipboard
- "R" => replace_selections_with_clipboard,
- /// keep primary selection
- "space" => keep_primary_selection,
-}
-
-mode_info! {
- /// goto
- ///
- /// When specified with a count, it will go to that line without entering the mode.
- goto_mode, GOTO_MODE, goto_prehook,
- /// file start
- "g" => goto_file_start,
- /// file end
- "e" => goto_file_end,
- /// line start
- "h" => goto_line_start,
- /// line end
- "l" => goto_line_end,
- /// line first non blank
- "s" => goto_first_nonwhitespace,
- /// definition
- "d" => goto_definition,
- /// type references
- "y" => goto_type_definition,
- /// references
- "r" => goto_reference,
- /// implementation
- "i" => goto_implementation,
- /// window top
- "t" => goto_window_top,
- /// window middle
- "m" => goto_window_middle,
- /// window bottom
- "b" => goto_window_bottom,
- /// last accessed file
- "a" => goto_last_accessed_file,
-}
-
-mode_info! {
- /// window
- window_mode, WINDOW_MODE,
- /// rotate
- "w" | "C-w" => rotate_view,
- /// horizontal split
- "h" => hsplit,
- /// vertical split
- "v" => vsplit,
- /// close
- "q" => wclose,
-}
-
-mode_info! {
- /// match
- match_mode, MATCH_MODE,
- /// matching character
- "m" => match_brackets,
- /// surround add
- "s" => surround_add,
- /// surround replace
- "r" => surround_replace,
- /// surround delete
- "d" => surround_delete,
- /// around object
- "a" => select_textobject_around,
- /// inside object
- "i" => select_textobject_inner,
-}
-
-mode_info! {
- /// select to previous
- left_bracket_mode, LEFT_BRACKET_MODE,
- /// previous diagnostic
- "d" => goto_prev_diag,
- /// diagnostic (first)
- "D" => goto_first_diag,
-}
-
-mode_info! {
- /// select to next
- right_bracket_mode, RIGHT_BRACKET_MODE,
- /// diagnostic
- "d" => goto_next_diag,
- /// diagnostic (last)
- "D" => goto_last_diag,
-}
-
-mode_info! {
- /// view
- view_mode, VIEW_MODE,
- /// align view top
- "t" => align_view_top,
- /// align view center
- "z" | "c" => align_view_center,
- /// align view bottom
- "b" => align_view_bottom,
- /// align view middle
- "m" => align_view_middle,
- /// scroll up
- "k" => scroll_up,
- /// scroll down
- "j" => scroll_down,
-}
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index b5ccbdfb..f3f0ba53 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -2,9 +2,6 @@ use serde::Deserialize;
use crate::keymap::Keymaps;
-#[cfg(test)]
-use crate::commands::Command;
-
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
pub struct Config {
pub theme: Option<String>,
@@ -22,12 +19,10 @@ pub struct LspConfig {
#[test]
fn parsing_keymaps_config_file() {
+ use crate::keymap;
+ use crate::keymap::Keymap;
use helix_core::hashmap;
- use helix_view::{
- document::Mode,
- input::KeyEvent,
- keyboard::{KeyCode, KeyModifiers},
- };
+ use helix_view::document::Mode;
let sample_keymaps = r#"
[keys.insert]
@@ -42,22 +37,13 @@ fn parsing_keymaps_config_file() {
toml::from_str::<Config>(sample_keymaps).unwrap(),
Config {
keys: Keymaps(hashmap! {
- Mode::Insert => hashmap! {
- KeyEvent {
- code: KeyCode::Char('y'),
- modifiers: KeyModifiers::NONE,
- } => Command::move_line_down,
- KeyEvent {
- code: KeyCode::Char('a'),
- modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
- } => Command::delete_selection,
- },
- Mode::Normal => hashmap! {
- KeyEvent {
- code: KeyCode::F(12),
- modifiers: KeyModifiers::ALT,
- } => Command::move_next_word_end,
- },
+ Mode::Insert => Keymap::new(keymap!({ "Insert mode"
+ "y" => move_line_down,
+ "S-C-a" => delete_selection,
+ })),
+ Mode::Normal => Keymap::new(keymap!({ "Normal mode"
+ "A-F12" => move_next_word_end,
+ })),
}),
..Default::default()
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 32994c37..93cc5328 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,7 +1,7 @@
pub use crate::commands::Command;
use crate::config::Config;
use helix_core::hashmap;
-use helix_view::{document::Mode, input::KeyEvent};
+use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize;
use std::{
collections::HashMap,
@@ -24,30 +24,276 @@ macro_rules! key {
};
}
-macro_rules! ctrl {
- ($($ch:tt)*) => {
- KeyEvent {
- code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
- modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
+/// 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::Command::$cmd)
+ };
+
+ (@trie
+ { $label:literal $($($key:literal)|+ => $value:tt,)+ }
+ ) => {
+ keymap!({ $label $($($key)|+ => $value,)+ })
+ };
+
+ (
+ { $label: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();
+ _map.insert(
+ _key,
+ keymap!(@trie $value)
+ );
+ _order.push(_key);
+ )+
+ )*
+ $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
}
};
}
-macro_rules! alt {
- ($($ch:tt)*) => {
- KeyEvent {
- code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
- modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
+#[derive(Debug, Clone, Deserialize)]
+pub struct KeyTrieNode {
+ /// A label for keys coming under this node, like "Goto mode"
+ #[serde(skip)]
+ name: String,
+ #[serde(flatten)]
+ map: HashMap<KeyEvent, KeyTrie>,
+ #[serde(skip)]
+ order: Vec<KeyEvent>,
+}
+
+impl KeyTrieNode {
+ pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
+ Self {
+ name: name.to_string(),
+ map,
+ order,
}
- };
+ }
+
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ /// Merge another Node in. Leaves and subnodes from the other node replace
+ /// corresponding keyevent in self, except when both other and self have
+ /// subnodes for same key. In that case the merge is recursive.
+ pub fn merge(&mut self, mut other: Self) {
+ for (key, trie) in std::mem::take(&mut other.map) {
+ if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) {
+ if let KeyTrie::Node(other_node) = trie {
+ node.merge(other_node);
+ continue;
+ }
+ }
+ self.map.insert(key, trie);
+ }
+
+ for &key in self.map.keys() {
+ if !self.order.contains(&key) {
+ self.order.push(key);
+ }
+ }
+ }
+}
+
+impl From<KeyTrieNode> for Info {
+ fn from(node: KeyTrieNode) -> Self {
+ let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
+ for (&key, trie) in node.iter() {
+ let desc = match trie {
+ KeyTrie::Leaf(cmd) => cmd.doc(),
+ KeyTrie::Node(n) => n.name(),
+ };
+ match body.iter().position(|(d, _)| d == &desc) {
+ // FIXME: multiple keys are ordered randomly (use BTreeSet)
+ Some(pos) => body[pos].1.push(key),
+ None => body.push((desc, vec![key])),
+ }
+ }
+ body.sort_unstable_by_key(|(_, keys)| {
+ node.order.iter().position(|&k| k == keys[0]).unwrap()
+ });
+ let prefix = format!("{} ", node.name());
+ if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
+ body = body
+ .into_iter()
+ .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
+ .collect();
+ }
+ Info::key(node.name(), body)
+ }
+}
+
+impl Default for KeyTrieNode {
+ fn default() -> Self {
+ Self::new("", HashMap::new(), Vec::new())
+ }
+}
+
+impl PartialEq for KeyTrieNode {
+ fn eq(&self, other: &Self) -> bool {
+ self.map == other.map
+ }
+}
+
+impl Deref for KeyTrieNode {
+ type Target = HashMap<KeyEvent, KeyTrie>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.map
+ }
+}
+
+impl DerefMut for KeyTrieNode {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.map
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[serde(untagged)]
+pub enum KeyTrie {
+ Leaf(Command),
+ Node(KeyTrieNode),
+}
+
+impl KeyTrie {
+ pub fn node(&self) -> Option<&KeyTrieNode> {
+ match *self {
+ KeyTrie::Node(ref node) => Some(node),
+ KeyTrie::Leaf(_) => None,
+ }
+ }
+
+ pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
+ match *self {
+ KeyTrie::Node(ref mut node) => Some(node),
+ KeyTrie::Leaf(_) => None,
+ }
+ }
+
+ /// Merge another KeyTrie in, assuming that this KeyTrie and the other
+ /// are both Nodes. Panics otherwise.
+ pub fn merge_nodes(&mut self, mut other: Self) {
+ let node = std::mem::take(other.node_mut().unwrap());
+ self.node_mut().unwrap().merge(node);
+ }
+
+ pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> {
+ let mut trie = self;
+ for key in keys {
+ trie = match trie {
+ KeyTrie::Node(map) => map.get(key),
+ // leaf encountered while keys left to process
+ KeyTrie::Leaf(_) => None,
+ }?
+ }
+ Some(trie)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum KeymapResult {
+ /// Needs more keys to execute a command. Contains valid keys for next keystroke.
+ Pending(KeyTrieNode),
+ Matched(Command),
+ /// Key was not found in the root keymap
+ NotFound,
+ /// Key is invalid in combination with previous keys. Contains keys leading upto
+ /// and including current (invalid) key.
+ Cancelled(Vec<KeyEvent>),
+}
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct Keymap {
+ /// Always a Node
+ #[serde(flatten)]
+ root: KeyTrie,
+ #[serde(skip)]
+ state: Vec<KeyEvent>,
+}
+
+impl Keymap {
+ pub fn new(root: KeyTrie) -> Self {
+ Keymap {
+ root,
+ state: Vec::new(),
+ }
+ }
+
+ pub fn root(&self) -> &KeyTrie {
+ &self.root
+ }
+
+ /// Lookup `key` in the keymap to try and find a command to execute
+ pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
+ let &first = self.state.get(0).unwrap_or(&key);
+ let trie = match self.root.search(&[first]) {
+ Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
+ None => return KeymapResult::NotFound,
+ Some(t) => t,
+ };
+ self.state.push(key);
+ match trie.search(&self.state[1..]) {
+ Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
+ Some(&KeyTrie::Leaf(command)) => {
+ self.state.clear();
+ KeymapResult::Matched(command)
+ }
+ None => KeymapResult::Cancelled(self.state.drain(..).collect()),
+ }
+ }
+
+ pub fn merge(&mut self, other: Self) {
+ self.root.merge_nodes(other.root);
+ }
+}
+
+impl Deref for Keymap {
+ type Target = KeyTrieNode;
+
+ fn deref(&self) -> &Self::Target {
+ &self.root.node().unwrap()
+ }
+}
+
+impl Default for Keymap {
+ fn default() -> Self {
+ Self::new(KeyTrie::Node(KeyTrieNode::default()))
+ }
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(transparent)]
-pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
+pub struct Keymaps(pub HashMap<Mode, Keymap>);
impl Deref for Keymaps {
- type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
+ type Target = HashMap<Mode, Keymap>;
fn deref(&self) -> &Self::Target {
&self.0
@@ -62,252 +308,298 @@ impl DerefMut for Keymaps {
impl Default for Keymaps {
fn default() -> Keymaps {
- let normal = hashmap!(
- key!('h') => Command::move_char_left,
- key!('j') => Command::move_line_down,
- key!('k') => Command::move_line_up,
- key!('l') => Command::move_char_right,
-
- key!(Left) => Command::move_char_left,
- key!(Down) => Command::move_line_down,
- key!(Up) => Command::move_line_up,
- key!(Right) => Command::move_char_right,
-
- key!('t') => Command::find_till_char,
- key!('f') => Command::find_next_char,
- key!('T') => Command::till_prev_char,
- key!('F') => Command::find_prev_char,
- // and matching set for select mode (extend)
- //
- key!('r') => Command::replace,
- key!('R') => Command::replace_with_yanked,
-
- key!('~') => Command::switch_case,
- alt!('`') => Command::switch_to_uppercase,
- key!('`') => Command::switch_to_lowercase,
-
- key!(Home) => Command::goto_line_start,
- key!(End) => Command::goto_line_end,
-
- key!('w') => Command::move_next_word_start,
- key!('b') => Command::move_prev_word_start,
- key!('e') => Command::move_next_word_end,
-
- key!('W') => Command::move_next_long_word_start,
- key!('B') => Command::move_prev_long_word_start,
- key!('E') => Command::move_next_long_word_end,
-
- key!('v') => Command::select_mode,
- key!('g') => Command::goto_mode,
- key!(':') => Command::command_mode,
-
- key!('i') => Command::insert_mode,
- key!('I') => Command::prepend_to_line,
- key!('a') => Command::append_mode,
- key!('A') => Command::append_to_line,
- key!('o') => Command::open_below,
- key!('O') => Command::open_above,
+ 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,
+
+ "~" => 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"
+ "g" => goto_file_start,
+ "e" => goto_file_end,
+ "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,
+ "m" => goto_window_middle,
+ "b" => goto_window_bottom,
+ "a" => goto_last_accessed_file,
+ },
+ ":" => command_mode,
+
+ "i" => insert_mode,
+ "I" => prepend_to_line,
+ "a" => append_mode,
+ "A" => append_to_line,
+ "o" => open_below,
+ "O" => open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit)
-
- key!('d') => Command::delete_selection,
+ "d" => delete_selection,
// TODO: also delete without yanking
- key!('c') => Command::change_selection,
+ "c" => change_selection,
// TODO: also change delete without yanking
- // key!('r') => Command::replace_with_char,
-
- key!('s') => Command::select_regex,
- alt!('s') => Command::split_selection_on_newline,
- key!('S') => Command::split_selection,
- key!(';') => Command::collapse_selection,
- alt!(';') => Command::flip_selections,
- key!('%') => Command::select_all,
- key!('x') => Command::extend_line,
- key!('X') => Command::extend_to_line_bounds,
+ "s" => select_regex,
+ "A-s" => split_selection_on_newline,
+ "S" => split_selection,
+ ";" => collapse_selection,
+ "A-;" => flip_selections,
+ "%" => 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,
+ },
+ "]" => { "Right bracket"
+ "d" => goto_next_diag,
+ "D" => goto_last_diag,
+ },
- key!('m') => Command::match_mode,
- key!('[') => Command::left_bracket_mode,
- key!(']') => Command::right_bracket_mode,
-
- key!('/') => Command::search,
+ "/" => search,
// ? for search_reverse
- key!('n') => Command::search_next,
- key!('N') => Command::extend_search_next,
+ "n" => search_next,
+ "N" => extend_search_next,
// N for search_prev
- key!('*') => Command::search_selection,
+ "*" => search_selection,
- key!('u') => Command::undo,
- key!('U') => Command::redo,
+ "u" => undo,
+ "U" => redo,
- key!('y') => Command::yank,
+ "y" => yank,
// yank_all
- key!('p') => Command::paste_after,
+ "p" => paste_after,
// paste_all
- key!('P') => Command::paste_before,
+ "P" => paste_before,
- key!('>') => Command::indent,
- key!('<') => Command::unindent,
- key!('=') => Command::format_selections,
- key!('J') => Command::join_selections,
+ ">" => indent,
+ "<" => unindent,
+ "=" => format_selections,
+ "J" => join_selections,
// TODO: conflicts hover/doc
- key!('K') => Command::keep_selections,
+ "K" => keep_selections,
// TODO: and another method for inverse
// TODO: clashes with space mode
- key!(' ') => Command::keep_primary_selection,
+ "space" => keep_primary_selection,
- // key!('q') => Command::record_macro,
- // key!('Q') => Command::replay_macro,
+ // "q" => record_macro,
+ // "Q" => replay_macro,
- // ~ / apostrophe => change case
// & align selections
// _ trim selections
// C / altC = copy (repeat) selections on prev/next lines
- key!(Esc) => Command::normal_mode,
- key!(PageUp) => Command::page_up,
- key!(PageDown) => Command::page_down,
- ctrl!('b') => Command::page_up,
- ctrl!('f') => Command::page_down,
- ctrl!('u') => Command::half_page_up,
- ctrl!('d') => Command::half_page_down,
-
- ctrl!('w') => Command::window_mode,
+ "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-h" | "h" => hsplit,
+ "C-v" | "v" => vsplit,
+ "C-q" | "q" => wclose,
+ },
// move under <space>c
- ctrl!('c') => Command::toggle_comments,
- key!('K') => Command::hover,
+ "C-c" => toggle_comments,
+ "K" => hover,
// z family for save/restore/combine from/to sels from register
- // supposedly ctrl!('i') but did not work
- key!(Tab) => Command::jump_forward,
- ctrl!('o') => Command::jump_backward,
- // ctrl!('s') => Command::save_selection,
-
- key!(' ') => Command::space_mode,
- key!('z') => Command::view_mode,
+ // supposedly "C-i" but did not work
+ "tab" => jump_forward,
+ "C-o" => jump_backward,
+ // "C-s" => save_selection,
+
+ "space" => { "Space"
+ "f" => file_picker,
+ "b" => buffer_picker,
+ "s" => symbol_picker,
+ "a" => code_action,
+ "'" => last_picker,
+ "w" => { "Window"
+ "C-w" | "w" => rotate_view,
+ "C-h" | "h" => hsplit,
+ "C-v" | "v" => vsplit,
+ "C-q" | "q" => wclose,
+ },
+ "y" => yank_joined_to_clipboard,
+ "Y" => yank_main_selection_to_clipboard,
+ "p" => paste_clipboard_after,
+ "P" => paste_clipboard_before,
+ "R" => replace_selections_with_clipboard,
+ "space" => keep_primary_selection,
+ },
+ "z" => { "View"
+ "z" | "c" => align_view_center,
+ "t" => align_view_top,
+ "b" => align_view_bottom,
+ "m" => align_view_middle,
+ "k" => scroll_up,
+ "j" => scroll_down,
+ },
- key!('"') => Command::select_register,
- );
+ "\"" => select_register,
+ });
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
// because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone();
- select.extend(
- hashmap!(
- key!('h') => Command::extend_char_left,
- key!('j') => Command::extend_line_down,
- key!('k') => Command::extend_line_up,
- key!('l') => Command::extend_char_right,
-
- key!(Left) => Command::extend_char_left,
- key!(Down) => Command::extend_line_down,
- key!(Up) => Command::extend_line_up,
- key!(Right) => Command::extend_char_right,
-
- key!('w') => Command::extend_next_word_start,
- key!('b') => Command::extend_prev_word_start,
- key!('e') => Command::extend_next_word_end,
-
- key!('t') => Command::extend_till_char,
- key!('f') => Command::extend_next_char,
-
- key!('T') => Command::extend_till_prev_char,
- key!('F') => Command::extend_prev_char,
- key!(Home) => Command::goto_line_start,
- key!(End) => Command::goto_line_end,
- key!(Esc) => Command::exit_select_mode,
- )
- .into_iter(),
- );
-
+ 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,
+
+ "t" => extend_till_char,
+ "f" => extend_next_char,
+ "T" => extend_till_prev_char,
+ "F" => extend_prev_char,
+
+ "home" => goto_line_start,
+ "end" => goto_line_end,
+ "esc" => exit_select_mode,
+ }));
+ let insert = keymap!({ "Insert mode"
+ "esc" => normal_mode,
+
+ "backspace" => delete_char_backward,
+ "del" => delete_char_forward,
+ "ret" => insert_newline,
+ "tab" => insert_tab,
+ "C-w" => delete_word_backward,
+
+ "left" => move_char_left,
+ "down" => move_line_down,
+ "up" => move_line_up,
+ "right" => move_char_right,
+ "pageup" => page_up,
+ "pagedown" => page_down,
+ "home" => goto_line_start,
+ "end" => goto_line_end_newline,
+
+ "C-x" => completion,
+ });
Keymaps(hashmap!(
- // as long as you cast the first item, rust is able to infer the other cases
- // TODO: select could be normal mode with some bindings merged over
- Mode::Normal => normal,
- Mode::Select => select,
- Mode::Insert => hashmap!(
- key!(Esc) => Command::normal_mode as Command,
- key!(Backspace) => Command::delete_char_backward,
- key!(Delete) => Command::delete_char_forward,
- key!(Enter) => Command::insert_newline,
- key!(Tab) => Command::insert_tab,
- key!(Left) => Command::move_char_left,
- key!(Down) => Command::move_line_down,
- key!(Up) => Command::move_line_up,
- key!(Right) => Command::move_char_right,
- key!(PageUp) => Command::page_up,
- key!(PageDown) => Command::page_down,
- key!(Home) => Command::goto_line_start,
- key!(End) => Command::goto_line_end_newline,
- ctrl!('x') => Command::completion,
- ctrl!('w') => Command::delete_word_backward,
- ),
+ Mode::Normal => Keymap::new(normal),
+ Mode::Select => Keymap::new(select),
+ Mode::Insert => Keymap::new(insert),
))
}
}
-/// Merge default config keys with user overwritten keys for custom
-/// user config.
+/// 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 {
- keys.extend(delta.remove(mode).unwrap_or_default());
+ keys.merge(delta.remove(mode).unwrap_or_default())
}
config
}
#[test]
fn merge_partial_keys() {
- use helix_view::keyboard::{KeyCode, KeyModifiers};
let config = Config {
keys: Keymaps(hashmap! {
- Mode::Normal => hashmap! {
- KeyEvent {
- code: KeyCode::Char('i'),
- modifiers: KeyModifiers::NONE,
- } => Command::normal_mode,
- KeyEvent { // key that does not exist
- code: KeyCode::Char('无'),
- modifiers: KeyModifiers::NONE,
- } => Command::insert_mode,
- },
+ Mode::Normal => Keymap::new(
+ keymap!({ "Normal mode"
+ "i" => normal_mode,
+ "无" => insert_mode,
+ "z" => jump_backward,
+ "g" => { "Merge into goto mode"
+ "$" => goto_line_end,
+ "g" => delete_char_forward,
+ },
+ })
+ )
}),
..Default::default()
};
- let merged_config = merge_keys(config.clone());
+ let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
+
+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
- *merged_config
- .keys
- .0
- .get(&Mode::Normal)
- .unwrap()
- .get(&KeyEvent {
- code: KeyCode::Char('i'),
- modifiers: KeyModifiers::NONE
- })
- .unwrap(),
- Command::normal_mode
+ keymap.get(key!('i')),
+ KeymapResult::Matched(Command::normal_mode),
+ "Leaf should replace leaf"
);
assert_eq!(
- *merged_config
- .keys
- .0
- .get(&Mode::Normal)
- .unwrap()
- .get(&KeyEvent {
- code: KeyCode::Char('无'),
- modifiers: KeyModifiers::NONE
- })
- .unwrap(),
- Command::insert_mode
+ keymap.get(key!('无')),
+ KeymapResult::Matched(Command::insert_mode),
+ "New leaf should be present in merged keymap"
);
+ // Assumes that z is a node in the default keymap
+ assert_eq!(
+ keymap.get(key!('z')),
+ KeymapResult::Matched(Command::jump_backward),
+ "Leaf should replace node"
+ );
+ // Assumes that `g` is a node in default keymap
+ assert_eq!(
+ keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
+ &KeyTrie::Leaf(Command::goto_line_end),
+ "Leaf should be present in merged subnode"
+ );
+ // Assumes that `gg` is in default keymap
+ assert_eq!(
+ keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
+ &KeyTrie::Leaf(Command::delete_char_forward),
+ "Leaf should replace old leaf in merged subnode"
+ );
+ // Assumes that `ge` is in default keymap
+ assert_eq!(
+ keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
+ &KeyTrie::Leaf(Command::goto_file_end),
+ "Old leaves in subnode should be present in merged node"
+ );
+
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 482a4117..3e131bf1 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -2,7 +2,7 @@ use crate::{
commands,
compositor::{Component, Context, EventResult},
key,
- keymap::Keymaps,
+ keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners},
};
@@ -15,6 +15,7 @@ use helix_core::{
use helix_view::{
document::Mode,
graphics::{CursorKind, Modifier, Rect, Style},
+ info::Info,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
@@ -30,6 +31,7 @@ pub struct EditorView {
last_insert: (commands::Command, Vec<KeyEvent>),
completion: Option<Completion>,
spinners: ProgressSpinners,
+ pub autoinfo: Option<Info>,
}
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -48,6 +50,7 @@ impl EditorView {
last_insert: (commands::Command::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
+ autoinfo: None,
}
}
@@ -594,19 +597,53 @@ impl EditorView {
);
}
- fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
- if let Some(command) = self.keymaps[&Mode::Insert].get(&event) {
- command.execute(cx);
- } else if let KeyEvent {
- code: KeyCode::Char(ch),
- ..
- } = event
- {
- commands::insert::insert_char(cx, ch);
+ /// Handle events by looking them up in `self.keymaps`. Returns None
+ /// if event was handled (a command was executed or a subkeymap was
+ /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
+ /// otherwise.
+ fn handle_keymap_event(
+ &mut self,
+ mode: Mode,
+ cxt: &mut commands::Context,
+ event: KeyEvent,
+ ) -> Option<KeymapResult> {
+ self.autoinfo = None;
+ match self.keymaps.get_mut(&mode).unwrap().get(event) {
+ KeymapResult::Matched(command) => command.execute(cxt),
+ KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
+ k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
}
+ None
}
- fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
+ fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
+ if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
+ match keyresult {
+ KeymapResult::NotFound => {
+ if let Some(ch) = event.char() {
+ commands::insert::insert_char(cx, ch)
+ }
+ }
+ KeymapResult::Cancelled(pending) => {
+ for ev in pending {
+ match ev.char() {
+ Some(ch) => commands::insert::insert_char(cx, ch),
+ None => {
+ if let KeymapResult::Matched(command) =
+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
+ {
+ command.execute(cx);
+ }
+ }
+ }
+ }
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+
+ fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
match event {
// count handling
key!(i @ '0'..='9') => {
@@ -619,8 +656,8 @@ impl EditorView {
// first execute whatever put us into insert mode
self.last_insert.0.execute(cxt);
// then replay the inputs
- for key in &self.last_insert.1 {
- self.insert_mode(cxt, *key)
+ for &key in &self.last_insert.1.clone() {
+ self.insert_mode(cxt, key)
}
}
_ => {
@@ -633,9 +670,7 @@ impl EditorView {
// set the register
cxt.selected_register = cxt.editor.selected_register.take();
- if let Some(command) = self.keymaps[&mode].get(&event) {
- command.execute(cxt);
- }
+ self.handle_keymap_event(mode, cxt, event);
}
}
}
@@ -749,7 +784,11 @@ impl Component for EditorView {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
- self.last_insert.0 = self.keymaps[&mode][&key];
+ self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
+ KeymapResult::Matched(command) => command,
+ // FIXME: insert mode can only be entered through single KeyCodes
+ _ => unimplemented!(),
+ };
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {
@@ -787,9 +826,8 @@ impl Component for EditorView {
);
}
- if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) {
+ if let Some(ref info) = self.autoinfo {
info.render(area, surface, cx);
- cx.editor.autoinfo = Some(info);
}
// render status msg
diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs
index e5f20562..36b096db 100644
--- a/helix-term/src/ui/info.rs
+++ b/helix-term/src/ui/info.rs
@@ -8,7 +8,7 @@ impl Component for Info {
fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.popup");
let block = Block::default()
- .title(self.title)
+ .title(self.title.as_str())
.borders(Borders::ALL)
.border_style(style);
let Info { width, height, .. } = self;
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 288d3d2e..9e71cfe7 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -36,6 +36,7 @@ pub fn regex_prompt(
Prompt::new(
prompt,
+ None,
|_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 733be2fc..0b67cd9c 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -43,6 +43,7 @@ impl<T> Picker<T> {
) -> Self {
let prompt = Prompt::new(
"".to_string(),
+ None,
|_pattern: &str| Vec::new(),
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
//
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 2df1e281..57daef3a 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -20,6 +20,8 @@ pub struct Prompt {
cursor: usize,
completion: Vec<Completion>,
selection: Option<usize>,
+ history_register: Option<char>,
+ history_pos: Option<usize>,
completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>,
callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
@@ -54,6 +56,7 @@ pub enum Movement {
impl Prompt {
pub fn new(
prompt: String,
+ history_register: Option<char>,
mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static,
callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
) -> Self {
@@ -63,6 +66,8 @@ impl Prompt {
cursor: 0,
completion: completion_fn(""),
selection: None,
+ history_register,
+ history_pos: None,
completion_fn: Box::new(completion_fn),
callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None),
@@ -226,6 +231,28 @@ impl Prompt {
self.exit_selection();
}
+ pub fn change_history(&mut self, register: &[String], direction: CompletionDirection) {
+ if register.is_empty() {
+ return;
+ }
+
+ let end = register.len().saturating_sub(1);
+
+ let index = match direction {
+ CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
+ CompletionDirection::Backward => {
+ self.history_pos.unwrap_or(register.len()).saturating_sub(1)
+ }
+ }
+ .min(end);
+
+ self.line = register[index].clone();
+
+ self.history_pos = Some(index);
+
+ self.move_end();
+ }
+
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
if self.completion.is_empty() {
return;
@@ -468,10 +495,41 @@ impl Component for Prompt {
self.exit_selection();
} else {
(self.callback_fn)(cx, &self.line, PromptEvent::Validate);
+
+ if let Some(register) = self.history_register {
+ // store in history
+ let register = cx.editor.registers.get_mut(register);
+ register.push(self.line.clone());
+ }
return close_fn;
}
}
KeyEvent {
+ code: KeyCode::Char('p'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::Up, ..
+ } => {
+ if let Some(register) = self.history_register {
+ let register = cx.editor.registers.get_mut(register);
+ self.change_history(register.read(), CompletionDirection::Backward);
+ }
+ }
+ KeyEvent {
+ code: KeyCode::Char('n'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
+ code: KeyCode::Down,
+ ..
+ } => {
+ if let Some(register) = self.history_register {
+ let register = cx.editor.registers.get_mut(register);
+ self.change_history(register.read(), CompletionDirection::Forward);
+ }
+ }
+ KeyEvent {
code: KeyCode::Tab, ..
} => self.change_completion_selection(CompletionDirection::Forward),
KeyEvent {