use hjkl_editor::runtime::ex::{self, ExEffect};
use hjkl_engine::{Host, Query};
use hjkl_tree_sitter::DotFallbackTheme;
use std::path::PathBuf;
use std::sync::Arc;
use super::App;
impl App {
pub(crate) fn dispatch_ex(&mut self, cmd: &str) {
let canon = ex::canonical_command_name(cmd);
let cmd: &str = canon.as_ref();
if cmd == "perf" {
self.perf_overlay = !self.perf_overlay;
self.recompute_hits = 0;
self.recompute_throttled = 0;
self.recompute_runs = 0;
self.status_message = Some(if self.perf_overlay {
"perf overlay: on (counters reset)".into()
} else {
"perf overlay: off".into()
});
return;
}
if let Some(rest) = cmd.strip_prefix("set background=") {
match rest.trim() {
"dark" => {
self.syntax.set_theme(Arc::new(DotFallbackTheme::dark()));
self.active_mut().last_recompute_key = None;
self.recompute_and_install();
self.status_message = Some("background=dark".into());
return;
}
"light" => {
self.syntax.set_theme(Arc::new(DotFallbackTheme::light()));
self.active_mut().last_recompute_key = None;
self.recompute_and_install();
self.status_message = Some("background=light".into());
return;
}
other => {
self.status_message = Some(format!("E: unknown background value: {other}"));
return;
}
}
}
if cmd == "picker" {
self.open_picker();
return;
}
if cmd == "rg" || cmd.starts_with("rg ") {
let pattern = cmd.strip_prefix("rg ").map(str::trim);
self.open_grep_picker(pattern);
return;
}
if cmd == "b" || cmd.starts_with("b ") {
let arg = cmd.strip_prefix("b ").map(str::trim).unwrap_or("").trim();
if arg.is_empty() {
self.status_message = Some("E94: No matching buffer".into());
} else if arg.chars().all(|c| c.is_ascii_digit()) {
let n: usize = arg.parse().unwrap_or(0);
if n == 0 || n > self.slots.len() {
self.status_message = Some(format!("E86: Buffer {n} does not exist"));
} else {
self.switch_to(n - 1);
}
} else {
let arg_lower = arg.to_lowercase();
let matches: Vec<usize> = self
.slots
.iter()
.enumerate()
.filter(|(_, s)| {
s.filename
.as_ref()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(|n| n.to_lowercase().contains(&arg_lower))
.unwrap_or(false)
})
.map(|(i, _)| i)
.collect();
match matches.len() {
0 => {
self.status_message = Some(format!("E94: No matching buffer for {arg}"));
}
1 => {
self.switch_to(matches[0]);
}
_ => {
self.status_message = Some(format!("E93: More than one match for {arg}"));
}
}
}
return;
}
match cmd {
"bnext" => {
self.buffer_next();
return;
}
"bprevious" | "bNext" => {
self.buffer_prev();
return;
}
"bdelete" => {
self.buffer_delete(false);
return;
}
"bdelete!" => {
self.buffer_delete(true);
return;
}
"bfirst" => {
self.switch_to(0);
return;
}
"blast" => {
let last = self.slots.len().saturating_sub(1);
self.switch_to(last);
return;
}
"buffers" | "ls" | "files" => {
self.status_message = Some(self.list_buffers());
return;
}
"b#" => {
self.buffer_alt();
return;
}
"wall" => {
self.write_all();
return;
}
"qall" => {
self.quit_all(false);
return;
}
"qall!" => {
self.quit_all(true);
return;
}
"wqall" => {
self.write_quit_all(false);
return;
}
"wqall!" => {
self.write_quit_all(true);
return;
}
"bpicker" => {
self.open_buffer_picker();
return;
}
_ => {}
}
if cmd == "edit" || cmd == "edit!" || cmd.starts_with("edit ") || cmd.starts_with("edit!") {
let force = cmd.starts_with("edit!");
let arg = if let Some(rest) = cmd.strip_prefix("edit!") {
rest.trim()
} else if let Some(rest) = cmd.strip_prefix("edit ") {
rest.trim()
} else {
""
};
self.do_edit(arg, force);
return;
}
match ex::run(&mut self.slots[self.active].editor, cmd) {
ExEffect::None => {}
ExEffect::Ok => {}
ExEffect::Save => {
self.do_save(None);
}
ExEffect::SaveAs(path) => {
self.do_save(Some(PathBuf::from(path)));
}
ExEffect::Quit { force, save } => {
if save {
self.do_save(None);
}
if self.slots.len() > 1 {
self.buffer_delete(force);
return;
}
if force || save {
self.exit_requested = true;
} else if self.active().dirty {
self.status_message =
Some("E37: No write since last change (add ! to override)".into());
} else {
self.exit_requested = true;
}
}
ExEffect::Substituted { count } => {
if self.slots[self.active].editor.take_dirty() {
let elapsed = self.slots[self.active].refresh_dirty_against_saved();
self.last_signature_us = elapsed;
let buffer_id = self.slots[self.active].buffer_id;
if self.slots[self.active].editor.take_content_reset() {
self.syntax.reset(buffer_id);
}
let edits = self.slots[self.active].editor.take_content_edits();
if !edits.is_empty() {
self.syntax.apply_edits(buffer_id, &edits);
}
self.recompute_and_install();
}
self.status_message = Some(format!("{count} substitution(s)"));
}
ExEffect::Info(msg) => {
if msg.contains('\n') {
self.info_popup = Some(msg);
} else {
self.status_message = Some(msg);
}
}
ExEffect::Error(msg) => {
self.status_message = Some(format!("E: {msg}"));
}
ExEffect::Unknown(c) => {
self.status_message = Some(format!("E492: Not an editor command: :{c}"));
}
}
}
pub(crate) fn do_save(&mut self, path: Option<PathBuf>) {
let idx = self.active;
self.save_slot(idx, path);
}
fn save_slot(&mut self, idx: usize, path: Option<PathBuf>) {
if self.slots[idx].editor.is_readonly() {
self.status_message = Some("E45: 'readonly' option is set (add ! to override)".into());
return;
}
let target = path.or_else(|| self.slots[idx].filename.clone());
match target {
None => {
self.status_message = Some("E32: No file name".into());
}
Some(p) => {
let lines = self.slots[idx].editor.buffer().lines();
let content = if lines.is_empty() {
String::new()
} else {
let mut s = lines.join("\n");
s.push('\n');
s
};
match std::fs::write(&p, &content) {
Ok(()) => {
let line_count = lines.len();
let byte_count = content.len();
self.status_message = Some(format!(
"\"{}\" {}L, {}B written",
p.display(),
line_count,
byte_count,
));
self.slots[idx].filename = Some(p);
self.slots[idx].is_new_file = false;
self.slots[idx].snapshot_saved();
if idx == self.active {
self.refresh_git_signs_force();
}
}
Err(e) => {
self.status_message = Some(format!("E: {}: {e}", p.display()));
}
}
}
}
}
fn write_all(&mut self) {
let mut written = 0usize;
let mut skipped = 0usize;
for i in 0..self.slots.len() {
if self.slots[i].filename.is_none() {
skipped += 1;
continue;
}
if !self.slots[i].dirty {
continue;
}
self.save_slot(i, None);
written += 1;
}
self.status_message = Some(format!("{written} buffer(s) written, {skipped} skipped"));
}
fn quit_all(&mut self, force: bool) {
if !force && let Some(idx) = self.slots.iter().position(|s| s.dirty) {
let name = self.slots[idx]
.filename
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "[No Name]".into());
self.status_message = Some(format!(
"E37: No write since last change for buffer \"{name}\" (add ! to override)"
));
return;
}
self.exit_requested = true;
}
fn write_quit_all(&mut self, force: bool) {
self.write_all();
self.quit_all(force);
}
pub(crate) fn do_edit(&mut self, arg: &str, force: bool) {
if arg.is_empty() {
self.reload_current(force);
return;
}
let path_str = if arg.contains('%') {
let curr = match self.active().filename.as_ref().and_then(|p| p.to_str()) {
Some(s) => s,
None => {
self.status_message = Some("E499: Empty file name for '%'".into());
return;
}
};
arg.replace('%', curr)
} else {
arg.to_string()
};
let path = PathBuf::from(&path_str);
let target = super::canon_for_match(&path);
if let Some(idx) = self
.slots
.iter()
.position(|s| s.filename.as_deref().map(super::canon_for_match) == Some(target.clone()))
{
if idx == self.active {
self.reload_current(force);
return;
}
self.switch_to(idx);
self.status_message = Some(format!(
"switched to buffer {}: \"{}\"",
idx + 1,
self.active()
.filename
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default()
));
return;
}
match self.open_new_slot(path) {
Ok(idx) => {
self.prev_active = Some(self.active);
self.active = idx;
let line_count = self.active().editor.buffer().line_count() as usize;
let path_display = self
.active()
.filename
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default();
self.status_message = Some(format!("\"{path_display}\" {line_count}L"));
self.refresh_git_signs_force();
}
Err(msg) => {
self.status_message = Some(msg);
}
}
}
fn reload_current(&mut self, force: bool) {
let path = match self.active().filename.clone() {
Some(p) => p,
None => {
self.status_message = Some("E32: No file name".into());
return;
}
};
if !force && self.active().dirty {
self.status_message =
Some("E37: No write since last change (add ! to override)".into());
return;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
self.status_message =
Some(format!("E484: Can't open file {}: {e}", path.display()));
return;
}
};
let trimmed = content.strip_suffix('\n').unwrap_or(&content);
let line_count = trimmed.lines().count();
let byte_count = content.len();
self.active_mut().editor.set_content(trimmed);
self.active_mut().editor.goto_line(1);
{
let vp = self.active_mut().editor.host_mut().viewport_mut();
vp.top_row = 0;
vp.top_col = 0;
}
self.active_mut().is_new_file = false;
let buffer_id = self.active().buffer_id;
self.syntax.set_language_for_path(buffer_id, &path);
self.syntax.reset(buffer_id);
self.active_mut().last_recompute_key = None;
self.active_mut()
.editor
.install_ratatui_syntax_spans(Vec::new());
let (vp_top, vp_height) = {
let vp = self.active().editor.host().viewport();
(vp.top_row, vp.height as usize)
};
if 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.recompute_and_install();
self.active_mut().snapshot_saved();
self.refresh_git_signs_force();
self.status_message = Some(format!(
"\"{}\" {line_count}L, {byte_count}B",
path.display()
));
}
}