use hjkl_buffer::Buffer;
use hjkl_engine::{Editor, Host, Options};
use std::path::PathBuf;
use super::{App, DiskState, STATUS_LINE_HEIGHT};
use crate::host::TuiHost;
impl App {
pub(crate) fn switch_to(&mut self, idx: usize) {
let current_slot = self.focused_slot_idx();
if idx != current_slot {
self.prev_active = Some(current_slot);
let regs = self.slots[current_slot].editor.registers().clone();
*self.slots[idx].editor.registers_mut() = regs;
}
let fw = self.focused_window();
self.windows[fw].as_mut().expect("focused_window open").slot = idx;
if let Ok(size) = crossterm::terminal::size() {
let vp = self.active_mut().editor.host_mut().viewport_mut();
vp.width = size.0;
vp.height = size.1.saturating_sub(STATUS_LINE_HEIGHT);
}
let buffer_id = self.active().buffer_id;
let (vp_top, vp_height) = {
let vp = self.active().editor.host().viewport();
(vp.top_row, vp.height as usize)
};
let cached_spans_installed = {
let current_dg = self.slots[idx].editor.buffer().dirty_gen();
let any_stale = [
&self.slots[idx].viewport_render_output,
&self.slots[idx].top_render_output,
&self.slots[idx].bottom_render_output,
]
.iter()
.any(|c| c.as_ref().is_some_and(|o| o.key.0 != current_dg));
if any_stale {
self.slots[idx].viewport_render_output = None;
self.slots[idx].top_render_output = None;
self.slots[idx].bottom_render_output = None;
false
} else {
let has_any = self.slots[idx].viewport_render_output.is_some()
|| self.slots[idx].top_render_output.is_some()
|| self.slots[idx].bottom_render_output.is_some();
if has_any {
if let Some(ref vp) = self.slots[idx].viewport_render_output {
let signs = vp.signs.clone();
self.slots[idx].diag_signs = signs;
}
self.install_merged_spans_for_slot(idx);
}
has_any
}
};
if !cached_spans_installed
&& let Some(out) = self.syntax.preview_render(
buffer_id,
self.active().editor.buffer(),
vp_top,
vp_height,
)
{
self.active_mut()
.editor
.install_ratatui_syntax_spans(out.spans);
}
self.active_mut().last_recompute_key = None;
self.recompute_and_install();
self.refresh_git_signs_force();
}
pub(crate) fn buffer_next(&mut self) {
if !self.require_multi_buffer() {
return;
}
let next = (self.focused_slot_idx() + 1) % self.slots.len();
self.switch_to(next);
}
pub(crate) fn buffer_prev(&mut self) {
if !self.require_multi_buffer() {
return;
}
let prev = (self.focused_slot_idx() + self.slots.len() - 1) % self.slots.len();
self.switch_to(prev);
}
pub(crate) fn buffer_alt(&mut self) {
if !self.require_multi_buffer() {
return;
}
match self.prev_active {
Some(i) if i < self.slots.len() => {
self.switch_to(i);
}
_ => {
self.status_message = Some("no alternate buffer".into());
}
}
}
pub(crate) fn buffer_delete(&mut self, force: bool) {
if !force && self.active().dirty {
self.status_message =
Some("E89: No write since last change (add ! to override)".into());
return;
}
let active_slot = self.focused_slot_idx();
if self.slots.len() == 1 {
self.lsp_detach_buffer(active_slot);
let old_id = self.active().buffer_id;
self.syntax.forget(old_id);
let new_id = self.next_buffer_id;
self.next_buffer_id += 1;
let host = TuiHost::new();
let mut editor = Editor::new(Buffer::new(), host, Options::default());
if let Ok(size) = crossterm::terminal::size() {
let vp = editor.host_mut().viewport_mut();
vp.width = size.0;
vp.height = size.1.saturating_sub(STATUS_LINE_HEIGHT);
}
let _ = editor.take_content_edits();
let _ = editor.take_content_reset();
let slot = &mut self.slots[0];
slot.buffer_id = new_id;
slot.editor = editor;
slot.filename = None;
slot.dirty = false;
slot.is_new_file = false;
slot.is_untracked = false;
slot.diag_signs.clear();
slot.git_signs.clear();
slot.last_git_dirty_gen = None;
slot.last_recompute_key = None;
slot.viewport_render_output = None;
slot.top_render_output = None;
slot.bottom_render_output = None;
slot.saved_hash = 0;
slot.saved_len = 0;
slot.disk_mtime = None;
slot.disk_len = None;
slot.disk_state = DiskState::Synced;
slot.snapshot_saved();
for win in self.windows.iter_mut().flatten() {
win.slot = 0;
}
self.status_message = Some("buffer closed (replaced with [No Name])".into());
return;
}
self.lsp_detach_buffer(active_slot);
let removed = self.slots.remove(active_slot);
self.syntax.forget(removed.buffer_id);
let slot_count = self.slots.len();
for win in self.windows.iter_mut().flatten() {
if win.slot == active_slot {
win.slot = if active_slot > 0 { active_slot - 1 } else { 0 };
} else if win.slot > active_slot {
win.slot -= 1;
}
win.slot = win.slot.min(slot_count.saturating_sub(1));
}
let target = self.focused_slot_idx();
self.switch_to(target);
self.prev_active = None;
let name = removed
.filename
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "[No Name]".into());
self.status_message = Some(format!("buffer closed: \"{name}\""));
}
pub(crate) fn buffer_wipe(&mut self, force: bool) {
if !force && self.active().dirty {
self.status_message =
Some("E89: No write since last change (add ! to override)".into());
return;
}
let active_slot = self.focused_slot_idx();
if self.slots.len() == 1 {
{
let editor = &mut self.slots[0].editor;
let mark_chars: Vec<char> = editor.marks().map(|(c, _)| c).collect();
for c in mark_chars {
editor.clear_mark(c);
}
editor.jump_back_list_mut().clear();
editor.jump_fwd_list_mut().clear();
}
{
let slot = &mut self.slots[0];
slot.lsp_diags.clear();
slot.diag_signs_lsp.clear();
}
self.lsp_detach_buffer(active_slot);
let old_id = self.active().buffer_id;
self.syntax.forget(old_id);
let new_id = self.next_buffer_id;
self.next_buffer_id += 1;
let host = TuiHost::new();
let mut editor = Editor::new(Buffer::new(), host, Options::default());
if let Ok(size) = crossterm::terminal::size() {
let vp = editor.host_mut().viewport_mut();
vp.width = size.0;
vp.height = size.1.saturating_sub(STATUS_LINE_HEIGHT);
}
let _ = editor.take_content_edits();
let _ = editor.take_content_reset();
let slot = &mut self.slots[0];
slot.buffer_id = new_id;
slot.editor = editor;
slot.filename = None;
slot.dirty = false;
slot.is_new_file = false;
slot.is_untracked = false;
slot.diag_signs.clear();
slot.git_signs.clear();
slot.last_git_dirty_gen = None;
slot.last_recompute_key = None;
slot.viewport_render_output = None;
slot.top_render_output = None;
slot.bottom_render_output = None;
slot.saved_hash = 0;
slot.saved_len = 0;
slot.disk_mtime = None;
slot.disk_len = None;
slot.disk_state = DiskState::Synced;
slot.snapshot_saved();
for win in self.windows.iter_mut().flatten() {
win.slot = 0;
}
self.status_message = Some("buffer wiped (replaced with [No Name])".into());
return;
}
self.lsp_detach_buffer(active_slot);
let removed = self.slots.remove(active_slot);
self.syntax.forget(removed.buffer_id);
let slot_count = self.slots.len();
for win in self.windows.iter_mut().flatten() {
if win.slot == active_slot {
win.slot = if active_slot > 0 { active_slot - 1 } else { 0 };
} else if win.slot > active_slot {
win.slot -= 1;
}
win.slot = win.slot.min(slot_count.saturating_sub(1));
}
let target = self.focused_slot_idx();
self.switch_to(target);
self.prev_active = None;
let name = removed
.filename
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "[No Name]".into());
self.status_message = Some(format!("buffer wiped: \"{name}\""));
}
pub(crate) fn require_multi_buffer(&mut self) -> bool {
if self.slots.len() <= 1 {
self.status_message = Some("only one buffer open".into());
return false;
}
true
}
pub(crate) fn list_buffers(&self) -> String {
let active_slot = self.focused_slot_idx();
let mut parts = Vec::with_capacity(self.slots.len());
for (i, slot) in self.slots.iter().enumerate() {
let active = if i == active_slot { '%' } else { ' ' };
let modf = if slot.dirty { '+' } else { ' ' };
let name = slot
.filename
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "[No Name]".into());
parts.push(format!("{}:{active}{modf} \"{name}\"", i + 1));
}
parts.join(" | ")
}
pub(crate) fn open_new_slot(&mut self, path: PathBuf) -> Result<usize, String> {
let buffer_id = self.next_buffer_id;
self.next_buffer_id += 1;
let slot = super::build_slot(&mut self.syntax, buffer_id, Some(path), &self.config)?;
self.slots.push(slot);
let idx = self.slots.len() - 1;
self.lsp_attach_buffer(idx);
Ok(idx)
}
pub(crate) fn dispatch_buffer_action(
&mut self,
action: crate::keymap_actions::AppAction,
count: usize,
) {
use crate::keymap_actions::AppAction;
match action {
AppAction::Tabnext => {
for _ in 0..count {
self.dispatch_ex("tabnext");
}
}
AppAction::Tabprev => {
for _ in 0..count {
self.dispatch_ex("tabprev");
}
}
AppAction::BufferNext => self.buffer_next(),
AppAction::BufferPrev => self.buffer_prev(),
AppAction::BufferAlt => self.buffer_alt(),
AppAction::BufferCycleH => {
if self.slots.len() > 1 {
self.buffer_prev();
} else {
let n = self.pending_count.take_or(1) as usize;
self.active_mut()
.editor
.apply_motion(hjkl_vim::MotionKind::ViewportTop, n);
}
}
AppAction::BufferCycleL => {
if self.slots.len() > 1 {
self.buffer_next();
} else {
let n = self.pending_count.take_or(1) as usize;
self.active_mut()
.editor
.apply_motion(hjkl_vim::MotionKind::ViewportBottom, n);
}
}
_ => {}
}
}
}