Skip to main content

nvrim/
layout.rs

1use nvim_oxi::Dictionary;
2use nvim_oxi::api::Buffer;
3use serde::Deserialize;
4use ytil_noxi::mru_buffers::BufferKind;
5
6/// [`Dictionary`] of Rust tests utilities.
7pub fn dict() -> Dictionary {
8    dict! {
9        "focus_vsplit": fn_from!(focus_vsplit),
10        "smart_close_buffer": fn_from!(smart_close_buffer),
11        "toggle_alternate_buffer": fn_from!(toggle_alternate_buffer),
12    }
13}
14
15pub fn create_autocmd() {
16    crate::cmds::create_lua_autocmd(
17        &["BufEnter", "WinEnter", "TermOpen"],
18        "TerminalAutoInsertMode",
19        Some(&["term://*"]),
20        "vim.cmd('startinsert')",
21    );
22}
23
24#[derive(Clone, Copy, Deserialize)]
25#[serde(rename_all(deserialize = "snake_case"))]
26pub enum SplitKind {
27    Term,
28    Buffer,
29}
30
31impl SplitKind {
32    pub const fn is_term(self) -> bool {
33        match self {
34            Self::Term => true,
35            Self::Buffer => false,
36        }
37    }
38}
39
40ytil_noxi::impl_nvim_deserializable!(SplitKind);
41
42pub fn compute_width(perc: i32) -> Option<i32> {
43    let total_width: i32 = crate::vim_opts::get("columns", &crate::vim_opts::global_scope())?;
44    Some((total_width.saturating_mul(perc)) / 100)
45}
46
47fn focus_vsplit((split_kind, width_perc): (SplitKind, i32)) -> Option<()> {
48    // Single MRU fetch - source of truth for all terminal buffer lookups.
49    let mru_bufs = ytil_noxi::mru_buffers::get().unwrap_or_default();
50    let term_bufs: Vec<Buffer> = mru_bufs.iter().filter(|b| b.is_term()).map(Buffer::from).collect();
51
52    let current_buf = nvim_oxi::api::get_current_buf();
53
54    let is_term = split_kind.is_term();
55
56    // If current buffer IS terminal OR file buffer (based on the supplied `term` value).
57    if (is_term && term_bufs.contains(&current_buf)) || (!is_term && !term_bufs.contains(&current_buf)) {
58        ytil_noxi::common::exec_vim_script("only", None)?;
59        return Some(());
60    }
61
62    // Prioritize fzf-lua floating windows over underlying splits.
63    if !is_term && let Some(float_win) = ytil_noxi::window::find_focusable_float(&["fzf"]) {
64        ytil_noxi::window::set_current(&float_win)?;
65        return Some(());
66    }
67
68    // If there is a VISIBLE terminal OR file buffer (based on the supplied `term` value).
69    // Floating windows (e.g. fidget notifications) are excluded so they don't capture focus.
70    if let Some(win) = nvim_oxi::api::list_wins().find(|win| {
71        !ytil_noxi::window::is_floating(win)
72            && ytil_noxi::window::get_buffer(win)
73                .is_some_and(|buf| (is_term && term_bufs.contains(&buf)) || (!is_term && !term_bufs.contains(&buf)))
74    }) {
75        ytil_noxi::window::set_current(&win)?;
76        return Some(());
77    }
78
79    let width = compute_width(width_perc)?;
80    let (leftabove, split_new_cmd) = if is_term { ("leftabove ", "term") } else { ("", "enew") };
81
82    // If there is a NON-VISIBLE listed terminal OR file buffer (based on the supplied `term` value).
83    if let Some(mru_buf) = mru_bufs
84        .iter()
85        .find(|b| (is_term && b.is_term()) || (!is_term && matches!(b.kind, BufferKind::Path | BufferKind::NoName)))
86    {
87        ytil_noxi::common::exec_vim_script(
88            &format!("{leftabove}vsplit | vertical resize {width} | buffer {}", mru_buf.id),
89            None,
90        );
91        return Some(());
92    }
93
94    // If there is NO terminal buffer OR file buffer at all (based on the supplied `term` value).
95    ytil_noxi::common::exec_vim_script(
96        &format!("{leftabove}vsplit | vertical resize {width} | {split_new_cmd}"),
97        None,
98    );
99
100    Some(())
101}
102
103fn toggle_alternate_buffer(_: ()) -> Option<()> {
104    // Single MRU fetch: "ls t" returns listed buffers in most-recently-used order.
105    // The first entry is the current buffer, so skip it and find the first file buffer.
106    let mru_bufs = ytil_noxi::mru_buffers::get().unwrap_or_default();
107
108    if let Some(target) = mru_bufs.iter().skip(1).find(|b| matches!(b.kind, BufferKind::Path)) {
109        ytil_noxi::buffer::set_current(&Buffer::from(target))?;
110    }
111
112    Some(())
113}
114
115fn smart_close_buffer(force_close: Option<bool>) -> Option<()> {
116    let mru_buffers = ytil_noxi::mru_buffers::get()?;
117
118    let Some(current_buffer) = mru_buffers.first() else {
119        return Some(());
120    };
121
122    let force = if force_close.is_some_and(std::convert::identity) {
123        "!"
124    } else {
125        ""
126    };
127
128    match current_buffer.kind {
129        BufferKind::Term | BufferKind::NoName => return Some(()),
130        BufferKind::GrugFar => {}
131        BufferKind::Path => {
132            let new_current_buffer = if let Some(mru_buffer) = mru_buffers.get(1)
133                && matches!(mru_buffer.kind, BufferKind::Path | BufferKind::NoName)
134            {
135                Buffer::from(mru_buffer.id)
136            } else {
137                ytil_noxi::buffer::create()?
138            };
139
140            ytil_noxi::buffer::set_current(&new_current_buffer)?;
141        }
142    }
143
144    ytil_noxi::common::exec_vim_script(&format!("bd{force} {}", current_buffer.id), Option::default())?;
145
146    Some(())
147}