Skip to main content

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        &[Mode::Normal],
79        "<leader>yf",
80        r#"yy:let @+ = expand("%") . ":" . line(".")<cr>"#,
81        &default_opts,
82    );
83    set(
84        &[Mode::Visual],
85        "<leader>yf",
86        concat!(
87            r#"y:let @+ = expand("%") . ":" . "#,
88            r#"min([line("'<"), line("'>")])"#,
89            r#" . (line("'<") == line("'>") ? "" : ":" . max([line("'<"), line("'>")]))<cr>"#,
90        ),
91        &default_opts,
92    );
93    set(&[Mode::Visual], "y", "ygv<esc>", &default_opts);
94    set(&[Mode::Visual], "p", r#""_dP"#, &default_opts);
95
96    set(&[Mode::Visual], ">", ">gv", &default_opts);
97    set(&[Mode::Visual], "<", "<gv", &default_opts);
98    set(&[Mode::Normal], ">", ">>", &default_opts);
99    set(&[Mode::Normal], "<", "<<", &default_opts);
100    set(&NV_MODE, "U", "<c-r>", &default_opts);
101
102    set(&NV_MODE, "<leader><leader>", ":silent :w!<cr>", &default_opts);
103    set(&NV_MODE, "<leader>x", ":bd<cr>", &default_opts);
104    set(&NV_MODE, "<leader>X", ":bd!<cr>", &default_opts);
105    set(&NV_MODE, "<leader>q", ":q<cr>", &default_opts);
106    set(&NV_MODE, "<leader>Q", ":q!<cr>", &default_opts);
107
108    set(&NV_MODE, "<c-;>", ":set wrap!<cr>", &default_opts);
109    set(&[Mode::Normal], "<esc>", r#":noh<cr>:echo""<cr>"#, &default_opts);
110}
111
112/// Return the RHS for a smart normal-mode `i` mapping.
113///
114/// If the current line is blank, returns `"_cc` (replace line without yanking);
115/// otherwise returns `i`.
116///
117/// Intended to be used with an *expr* mapping and `.expr(true)` in [`SetKeymapOpts`].
118fn smart_ident_on_blank_line(_: ()) -> String {
119    apply_on_current_line_or_unwrap(|line| if line.is_empty() { r#""_cc"# } else { "i" }, "i")
120}
121
122/// Return the RHS for a smart `dd` mapping that skips yanking blank lines.
123///
124/// Produces `"_dd` when the current line is entirely whitespace; otherwise `dd`.
125///
126/// Intended for an *expr* mapping.
127fn smart_dd_no_yank_empty_line(_: ()) -> String {
128    apply_on_current_line_or_unwrap(
129        |line| {
130            if line.chars().all(char::is_whitespace) {
131                r#""_dd"#
132            } else {
133                "dd"
134            }
135        },
136        "dd",
137    )
138}
139
140/// Return the RHS for a visual-mode `<Esc>` *expr* mapping that reselects the
141/// visual range (direction aware) and then applies [`NORMAL_ESC`].
142fn visual_esc(_: ()) -> String {
143    let current_line: i64 = nvim_oxi::api::call_function("line", (".",))
144        .inspect_err(|err| {
145            ytil_noxi::notify::error(format!("error getting current line | error={err:#?}"));
146        })
147        .unwrap_or(0);
148    let visual_line: i64 = nvim_oxi::api::call_function("line", ("v",))
149        .inspect_err(|err| {
150            ytil_noxi::notify::error(format!("error getting visual line | error={err:#?}"));
151        })
152        .unwrap_or(0);
153    format!(
154        ":<c-u>'{}<cr>{}",
155        if current_line < visual_line { "<" } else { ">" },
156        NORMAL_ESC
157    )
158}
159
160/// Apply a closure to the current line or fall back to `default`.
161///
162/// Used by the smart *expr* mapping helpers.
163///
164/// In case of errors, they are notified to Nvim and a `default` value is returned.
165fn apply_on_current_line_or_unwrap<'a, F: FnOnce(String) -> &'a str>(fun: F, default: &'a str) -> String {
166    ytil_noxi::buffer::get_current_line().map_or(default, fun).to_string()
167}