nvrim/
keymaps.rs

1//! Keymap helpers and expr RHS generators.
2//!
3//! Provides `keymaps.dict()` offering bulk keymap setup (`set_all`) plus smart editing helpers.
4//! Core mappings target Normal / Visual / Operator modes; failures in individual definitions are
5//! logged without aborting subsequent mappings.
6
7use ytil_noxi::Dictionary;
8use ytil_noxi::api::opts::SetKeymapOpts;
9use ytil_noxi::api::opts::SetKeymapOptsBuilder;
10use ytil_noxi::api::types::Mode;
11
12const NV_MODE: [Mode; 2] = [Mode::Normal, Mode::Visual];
13const NVOP_MODE: [Mode; 1] = [Mode::NormalVisualOperator];
14
15/// RHS used for the normal-mode `<Esc>` mapping (clear search + empty echo).
16pub const NORMAL_ESC: &str = r#":noh<cr>:echo""<cr>"#;
17
18/// [`Dictionary`] of keymap helpers and expr RHS generators.
19pub fn dict() -> Dictionary {
20    dict! {
21        "set_all": fn_from!(set_all),
22        "smart_ident_on_blank_line": fn_from!(smart_ident_on_blank_line),
23        "smart_dd_no_yank_empty_line": fn_from!(smart_dd_no_yank_empty_line),
24        "normal_esc": NORMAL_ESC,
25        "visual_esc": fn_from!(visual_esc),
26    }
27}
28
29/// Set a keymap for each provided [`Mode`].
30///
31/// Errors are reported (not propagated) via `ytil_noxi::notify::error`.
32pub fn set(modes: &[Mode], lhs: &str, rhs: &str, opts: &SetKeymapOpts) {
33    for mode in modes {
34        if let Err(err) = nvim_oxi::api::set_keymap(*mode, lhs, rhs, opts) {
35            ytil_noxi::notify::error(format!(
36                "cannot set keymap | mode={mode:#?} lhs={lhs} rhs={rhs} opts={opts:#?} error={err:#?}"
37            ));
38        }
39    }
40}
41
42/// Build the default [`SetKeymapOpts`]: silent + non-recursive.
43///
44/// Unlike Lua's `vim.keymap.set`, the default for [`SetKeymapOpts`] does *not*
45/// enable `noremap` so we do it explicitly.
46pub fn default_opts() -> SetKeymapOpts {
47    SetKeymapOptsBuilder::default().silent(true).noremap(true).build()
48}
49
50/// Sets the core (non‑plugin) keymaps ported from the Lua `M.setup` function.
51///
52/// All mappings are set with the default non-recursive, silent options returned
53/// by [`default_opts`].
54///
55/// Failures are reported internally by the [`set`] helper via `ytil_noxi::notify::error`.
56fn set_all(_: ()) {
57    let default_opts = default_opts();
58
59    ytil_noxi::common::set_g_var("mapleader", " ");
60    ytil_noxi::common::set_g_var("maplocalleader", " ");
61
62    set(&[Mode::Terminal], "<Esc>", "<c-\\><c-n>", &default_opts);
63    set(&[Mode::Insert], "<c-a>", "<esc>^i", &default_opts);
64    set(&[Mode::Normal], "<c-a>", "^i", &default_opts);
65    set(&[Mode::Insert], "<c-e>", "<end>", &default_opts);
66    set(&[Mode::Normal], "<c-e>", "$a", &default_opts);
67
68    set(&NVOP_MODE, "gn", ":bn<cr>", &default_opts);
69    set(&NVOP_MODE, "gp", ":bp<cr>", &default_opts);
70    set(&NV_MODE, "gh", "0", &default_opts);
71    set(&NV_MODE, "gl", "$", &default_opts);
72    set(&NV_MODE, "gs", "_", &default_opts);
73
74    set(&NV_MODE, "x", r#""_x"#, &default_opts);
75    set(&NV_MODE, "X", r#""_X"#, &default_opts);
76
77    set(
78        &NV_MODE,
79        "<leader>yf",
80        r#":let @+ = expand("%") . ":" . line(".")<cr>"#,
81        &default_opts,
82    );
83    set(&[Mode::Visual], "y", "ygv<esc>", &default_opts);
84    set(&[Mode::Visual], "p", r#""_dP"#, &default_opts);
85
86    set(&[Mode::Visual], ">", ">gv", &default_opts);
87    set(&[Mode::Visual], "<", "<gv", &default_opts);
88    set(&[Mode::Normal], ">", ">>", &default_opts);
89    set(&[Mode::Normal], "<", "<<", &default_opts);
90    set(&NV_MODE, "U", "<c-r>", &default_opts);
91
92    set(&NV_MODE, "<leader><leader>", ":silent :w!<cr>", &default_opts);
93    set(&NV_MODE, "<leader>x", ":bd<cr>", &default_opts);
94    set(&NV_MODE, "<leader>X", ":bd!<cr>", &default_opts);
95    set(&NV_MODE, "<leader>q", ":q<cr>", &default_opts);
96    set(&NV_MODE, "<leader>Q", ":q!<cr>", &default_opts);
97
98    set(&NV_MODE, "<c-;>", ":set wrap!<cr>", &default_opts);
99    set(&[Mode::Normal], "<esc>", r#":noh<cr>:echo""<cr>"#, &default_opts);
100}
101
102/// Return the RHS for a smart normal-mode `i` mapping.
103///
104/// If the current line is blank, returns `"_cc` (replace line without yanking);
105/// otherwise returns `i`.
106///
107/// Intended to be used with an *expr* mapping and `.expr(true)` in [`SetKeymapOpts`].
108fn smart_ident_on_blank_line(_: ()) -> String {
109    apply_on_current_line_or_unwrap(|line| if line.is_empty() { r#""_cc"# } else { "i" }, "i")
110}
111
112/// Return the RHS for a smart `dd` mapping that skips yanking blank lines.
113///
114/// Produces `"_dd` when the current line is entirely whitespace; otherwise `dd`.
115///
116/// Intended for an *expr* mapping.
117fn smart_dd_no_yank_empty_line(_: ()) -> String {
118    apply_on_current_line_or_unwrap(
119        |line| {
120            if line.chars().all(char::is_whitespace) {
121                r#""_dd"#
122            } else {
123                "dd"
124            }
125        },
126        "dd",
127    )
128}
129
130/// Return the RHS for a visual-mode `<Esc>` *expr* mapping that reselects the
131/// visual range (direction aware) and then applies [`NORMAL_ESC`].
132fn visual_esc(_: ()) -> String {
133    let current_line: i64 = nvim_oxi::api::call_function("line", (".",))
134        .inspect_err(|err| {
135            ytil_noxi::notify::error(format!("error getting current line | error={err:#?}"));
136        })
137        .unwrap_or(0);
138    let visual_line: i64 = nvim_oxi::api::call_function("line", ("v",))
139        .inspect_err(|err| {
140            ytil_noxi::notify::error(format!("error getting visual line | error={err:#?}"));
141        })
142        .unwrap_or(0);
143    format!(
144        ":<c-u>'{}<cr>{}",
145        if current_line < visual_line { "<" } else { ">" },
146        NORMAL_ESC
147    )
148}
149
150/// Apply a closure to the current line or fall back to `default`.
151///
152/// Used by the smart *expr* mapping helpers.
153///
154/// In case of errors, they are notified to Nvim and a `default` value is returned.
155fn apply_on_current_line_or_unwrap<'a, F: FnOnce(String) -> &'a str>(fun: F, default: &'a str) -> String {
156    ytil_noxi::buffer::get_current_line().map_or(default, fun).to_string()
157}