use crate::{
effect::ExEffect,
range::LineRange,
registry::{ArgKind, ExCommand, Registry},
};
use hjkl_engine::Host;
use crate::folds::{apply_fold_indent, apply_fold_syntax};
use crate::global::{global_match_handler, vglobal_handler};
fn quit_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: false,
save: false,
})
}
fn quit_force_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: true,
save: false,
})
}
fn write_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
let path = args.trim();
if path.is_empty() {
Some(ExEffect::Save)
} else {
Some(ExEffect::SaveAs(path.to_string()))
}
}
fn edit_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::EditFile {
path: args.trim().to_string(),
force: false,
})
}
fn edit_force_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::EditFile {
path: args.trim().to_string(),
force: true,
})
}
fn read_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
range: Option<LineRange>,
) -> Option<ExEffect> {
use hjkl_buffer::{Edit, Position};
let path = args.trim();
if path.is_empty() {
return None;
}
let content = if let Some(cmd) = path.strip_prefix('!') {
let cmd = cmd.trim();
if cmd.is_empty() {
return Some(ExEffect::Error(":r ! needs a shell command".into()));
}
match std::process::Command::new("sh").arg("-c").arg(cmd).output() {
Ok(out) if out.status.success() => match String::from_utf8(out.stdout) {
Ok(s) => s,
Err(_) => return Some(ExEffect::Error("command output was not UTF-8".into())),
},
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
let trimmed = stderr.trim();
let label = if trimmed.is_empty() {
"no stderr".to_string()
} else {
trimmed.to_string()
};
return Some(ExEffect::Error(format!(
"command exited {} ({label})",
out.status
.code()
.map(|c| c.to_string())
.unwrap_or_else(|| "?".into())
)));
}
Err(e) => return Some(ExEffect::Error(format!("cannot run `{cmd}`: {e}"))),
}
} else {
match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => return Some(ExEffect::Error(format!("cannot read `{path}`: {e}"))),
}
};
let trimmed = content.strip_suffix('\n').unwrap_or(&content);
editor.push_undo();
let row = match range {
Some(r) => r.end_one_based().saturating_sub(1),
None => editor.cursor().0,
};
let line_chars = editor
.buffer()
.line(row)
.map(|l| l.chars().count())
.unwrap_or(0);
let insert_text = format!("\n{trimmed}");
editor.mutate_edit(Edit::InsertStr {
at: Position::new(row, line_chars),
text: insert_text,
});
editor.jump_cursor(row + 1, 0);
editor.mark_content_dirty();
Some(ExEffect::Ok)
}
fn bdelete_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::BufferDelete {
force: false,
wipe: false,
})
}
fn bdelete_force_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::BufferDelete {
force: true,
wipe: false,
})
}
fn bwipeout_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::BufferDelete {
force: false,
wipe: true,
})
}
fn bwipeout_force_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::BufferDelete {
force: true,
wipe: true,
})
}
fn wall_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Save)
}
fn wq_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: false,
save: true,
})
}
fn wq_force_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: true,
save: true,
})
}
fn wqall_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: false,
save: true,
})
}
fn qall_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: false,
save: false,
})
}
fn qall_force_handler<H: Host>(
_editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Quit {
force: true,
save: false,
})
}
fn nohlsearch_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
editor.set_search_pattern(None);
Some(ExEffect::Ok)
}
fn undo_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
editor.undo();
Some(ExEffect::Ok)
}
fn redo_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
editor.redo();
Some(ExEffect::Ok)
}
fn registers_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Info(crate::listings::format_registers(editor)))
}
fn marks_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Info(crate::listings::format_marks(editor)))
}
fn jumps_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Info(crate::listings::format_jumps(editor)))
}
fn changes_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(ExEffect::Info(crate::listings::format_changes(editor)))
}
fn delete_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
_args: &str,
range: Option<LineRange>,
) -> Option<ExEffect> {
use hjkl_buffer::{Edit, MotionKind, Position};
let r = range.unwrap_or_else(|| LineRange::single(editor.cursor().0 + 1));
let start_row = r.start_one_based().saturating_sub(1);
let total = editor.buffer().row_count();
if total == 0 {
return Some(ExEffect::Ok);
}
let end_row = (r.end_one_based().saturating_sub(1)).min(total.saturating_sub(1));
if start_row > end_row {
return Some(ExEffect::Ok);
}
editor.push_undo();
for row in (start_row..=end_row).rev() {
if editor.buffer().row_count() == 1 {
let line_chars = editor
.buffer()
.line(0)
.map(|l| l.chars().count())
.unwrap_or(0);
if line_chars > 0 {
editor.mutate_edit(Edit::DeleteRange {
start: Position::new(0, 0),
end: Position::new(0, line_chars),
kind: MotionKind::Char,
});
}
continue;
}
editor.mutate_edit(Edit::DeleteRange {
start: Position::new(row, 0),
end: Position::new(row, 0),
kind: MotionKind::Line,
});
}
editor.mark_content_dirty();
Some(ExEffect::Ok)
}
fn sort_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
range: Option<LineRange>,
) -> Option<ExEffect> {
let trimmed = args.trim();
let mut reverse = false;
let mut unique = false;
let mut numeric = false;
let mut ignore_case = false;
for c in trimmed.chars() {
match c {
'!' => reverse = true,
'u' => unique = true,
'n' => numeric = true,
'i' => ignore_case = true,
' ' | '\t' => {}
other => return Some(ExEffect::Error(format!("bad :sort flag `{other}`"))),
}
}
let mut all_lines: Vec<String> = editor.buffer().lines().to_vec();
let total = all_lines.len();
if total == 0 {
return Some(ExEffect::Ok);
}
let (start_row, end_row) = match range {
Some(r) => {
let s = r.start_one_based().saturating_sub(1);
let e = (r.end_one_based().saturating_sub(1)).min(total - 1);
(s, e)
}
None => (0, total - 1),
};
if start_row > end_row {
return Some(ExEffect::Ok);
}
let mut slice: Vec<String> = all_lines[start_row..=end_row].to_vec();
if numeric {
slice.sort_by_key(|l| extract_leading_number(l));
} else if ignore_case {
slice.sort_by_key(|s| s.to_lowercase());
} else {
slice.sort();
}
if reverse {
slice.reverse();
}
if unique {
let cmp_key = |s: &str| -> String {
if ignore_case {
s.to_lowercase()
} else {
s.to_string()
}
};
let mut seen = std::collections::HashSet::new();
slice.retain(|line| seen.insert(cmp_key(line)));
}
let after: Vec<String> = all_lines.split_off(end_row + 1);
all_lines.truncate(start_row);
all_lines.extend(slice);
all_lines.extend(after);
editor.push_undo();
editor.restore(all_lines, (start_row, 0));
editor.mark_content_dirty();
Some(ExEffect::Ok)
}
fn extract_leading_number(line: &str) -> i64 {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() && !bytes[i].is_ascii_digit() && bytes[i] != b'-' {
i += 1;
}
if i >= bytes.len() {
return i64::MIN;
}
let mut j = i;
if bytes[j] == b'-' {
j += 1;
}
let start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j == start {
return i64::MIN;
}
line[i..j].parse().unwrap_or(i64::MIN)
}
fn substitute_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
range: Option<LineRange>,
) -> Option<ExEffect> {
use hjkl_engine::substitute::{apply_substitute, parse_substitute};
let cmd = match parse_substitute(args) {
Ok(c) => c,
Err(e) => return Some(ExEffect::Error(e.to_string())),
};
let r = match range {
Some(lr) => {
let start = lr.start_one_based().saturating_sub(1) as u32;
let end = lr.end_one_based().saturating_sub(1) as u32;
start..=end
}
None => {
let row = editor.cursor().0 as u32;
row..=row
}
};
match apply_substitute(editor, &cmd, r) {
Ok(out) => Some(ExEffect::Substituted {
count: out.replacements,
lines_changed: out.lines_changed,
}),
Err(e) => Some(ExEffect::Error(e.to_string())),
}
}
fn set_handler<H: Host>(
editor: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
args: &str,
_range: Option<LineRange>,
) -> Option<ExEffect> {
Some(crate::setopt::apply_set(editor, args))
}
pub(crate) fn register_builtins<H: Host>(reg: &mut Registry<H>) {
reg.add(ExCommand {
name: "quit",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 1,
run: quit_handler::<H>,
});
reg.add(ExCommand {
name: "quit!",
aliases: &["q!"],
arg_kind: ArgKind::None,
min_prefix: 2,
run: quit_force_handler::<H>,
});
reg.add(ExCommand {
name: "write",
aliases: &[],
arg_kind: ArgKind::Path,
min_prefix: 1,
run: write_handler::<H>,
});
reg.add(ExCommand {
name: "wall",
aliases: &["wa"],
arg_kind: ArgKind::None,
min_prefix: 2,
run: wall_handler::<H>,
});
reg.add(ExCommand {
name: "wq",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 2,
run: wq_handler::<H>,
});
reg.add(ExCommand {
name: "wq!",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 3,
run: wq_force_handler::<H>,
});
reg.add(ExCommand {
name: "x",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 1,
run: wq_handler::<H>,
});
reg.add(ExCommand {
name: "x!",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 2,
run: wq_force_handler::<H>,
});
reg.add(ExCommand {
name: "wqall",
aliases: &["wqa"],
arg_kind: ArgKind::None,
min_prefix: 3,
run: wqall_handler::<H>,
});
reg.add(ExCommand {
name: "wqall!",
aliases: &["wqa!"],
arg_kind: ArgKind::None,
min_prefix: 4,
run: wqall_handler::<H>,
});
reg.add(ExCommand {
name: "qall",
aliases: &["qa"],
arg_kind: ArgKind::None,
min_prefix: 2,
run: qall_handler::<H>,
});
reg.add(ExCommand {
name: "qall!",
aliases: &["qa!"],
arg_kind: ArgKind::None,
min_prefix: 3,
run: qall_force_handler::<H>,
});
reg.add(ExCommand {
name: "nohlsearch",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 3,
run: nohlsearch_handler::<H>,
});
reg.add(ExCommand {
name: "undo",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 1,
run: undo_handler::<H>,
});
reg.add(ExCommand {
name: "redo",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 3,
run: redo_handler::<H>,
});
reg.add(ExCommand {
name: "edit",
aliases: &[],
arg_kind: ArgKind::Path,
min_prefix: 1,
run: edit_handler::<H>,
});
reg.add(ExCommand {
name: "edit!",
aliases: &["e!"],
arg_kind: ArgKind::Path,
min_prefix: 2,
run: edit_force_handler::<H>,
});
reg.add(ExCommand {
name: "read",
aliases: &[],
arg_kind: ArgKind::Path,
min_prefix: 1,
run: read_handler::<H>,
});
reg.add(ExCommand {
name: "bdelete",
aliases: &["bd"],
arg_kind: ArgKind::None,
min_prefix: 2,
run: bdelete_handler::<H>,
});
reg.add(ExCommand {
name: "bdelete!",
aliases: &["bd!"],
arg_kind: ArgKind::None,
min_prefix: 3,
run: bdelete_force_handler::<H>,
});
reg.add(ExCommand {
name: "bwipeout",
aliases: &["bw"],
arg_kind: ArgKind::None,
min_prefix: 2,
run: bwipeout_handler::<H>,
});
reg.add(ExCommand {
name: "bwipeout!",
aliases: &["bw!"],
arg_kind: ArgKind::None,
min_prefix: 3,
run: bwipeout_force_handler::<H>,
});
reg.add(ExCommand {
name: "registers",
aliases: &["reg"],
arg_kind: ArgKind::None,
min_prefix: 3,
run: registers_handler::<H>,
});
reg.add(ExCommand {
name: "marks",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 5,
run: marks_handler::<H>,
});
reg.add(ExCommand {
name: "jumps",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 5,
run: jumps_handler::<H>,
});
reg.add(ExCommand {
name: "changes",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 7,
run: changes_handler::<H>,
});
reg.add(ExCommand {
name: "delete",
aliases: &["d"],
arg_kind: ArgKind::None,
min_prefix: 1,
run: delete_handler::<H>,
});
reg.add(ExCommand {
name: "sort",
aliases: &[],
arg_kind: ArgKind::Raw,
min_prefix: 3,
run: sort_handler::<H>,
});
reg.add(ExCommand {
name: "substitute",
aliases: &[],
arg_kind: ArgKind::Raw,
min_prefix: 1,
run: substitute_handler::<H>,
});
reg.add(ExCommand {
name: "set",
aliases: &[],
arg_kind: ArgKind::Setting,
min_prefix: 2,
run: set_handler::<H>,
});
reg.add(ExCommand {
name: "foldindent",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 5,
run: |editor, args, range| apply_fold_indent(editor, args, range),
});
reg.add(ExCommand {
name: "foldsyntax",
aliases: &[],
arg_kind: ArgKind::None,
min_prefix: 5,
run: |editor, args, range| apply_fold_syntax(editor, args, range),
});
reg.add(ExCommand {
name: "global",
aliases: &["g"],
arg_kind: ArgKind::Raw,
min_prefix: 1,
run: |editor, args, range| global_match_handler(editor, args, range),
});
reg.add(ExCommand {
name: "vglobal",
aliases: &["v"],
arg_kind: ArgKind::Raw,
min_prefix: 1,
run: |editor, args, range| vglobal_handler(editor, args, range),
});
}