use std::path::PathBuf;
use anyhow::Result;
use hjkl_buffer::View;
use hjkl_engine::{BufferEdit, DefaultHost, Editor, Options};
use hjkl_ex::ExEffect;
pub fn run(files: Vec<PathBuf>, commands: Vec<String>) -> Result<i32> {
crate::host::disable_clipboard_for_rpc();
if files.is_empty() && commands.is_empty() {
eprintln!("hjkl --headless: no commands or files; exiting");
return Ok(0);
}
let targets: Vec<Option<PathBuf>> = if files.is_empty() {
vec![None]
} else {
files.into_iter().map(Some).collect()
};
let mut exit_code = 0i32;
let banks = SharedBanks::new();
for maybe_path in targets {
let display_name = maybe_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<scratch>".to_string());
let mut buffer = View::new();
let mut is_new_file = false;
if let Some(ref path) = maybe_path {
match std::fs::read_to_string(path) {
Ok(content) => {
let content = content.strip_suffix('\n').unwrap_or(&content);
BufferEdit::replace_all(&mut buffer, content);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
is_new_file = true;
}
Err(e) => {
eprintln!("hjkl: {display_name}: {e}");
exit_code = 1;
continue;
}
}
}
let _ = is_new_file;
let host = DefaultHost::new();
let mut editor = hjkl_vim::vim_editor(buffer, host, Options::default());
banks.apply(&mut editor);
let mut current_filename: Option<PathBuf> = maybe_path.clone();
let mut wrote = false;
for cmd in &commands {
let cmd = cmd.strip_prefix(':').unwrap_or(cmd);
let reg = hjkl_ex::default_registry::<hjkl_engine::DefaultHost>();
let effect = hjkl_ex::try_dispatch(®, &mut editor, cmd)
.unwrap_or(ExEffect::Unknown(cmd.to_string()));
match effect {
ExEffect::None => {}
ExEffect::Quickfix(_) | ExEffect::Location(_) => {}
ExEffect::Ok => {}
ExEffect::Info(_) | ExEffect::InfoTitled { .. } => {
}
ExEffect::Substituted { .. } => {
}
ExEffect::Error(msg) => {
eprintln!("hjkl: {display_name}: {msg}");
exit_code = 1;
}
ExEffect::Unknown(name) => {
eprintln!("hjkl: {display_name}: unknown ex command: {name}");
exit_code = 1;
}
ExEffect::Save => {
if let Err(e) = write_buffer(&editor, ¤t_filename, &display_name) {
eprintln!("{e}");
exit_code = 1;
} else {
wrote = true;
}
}
ExEffect::SaveAs(path_str) => {
let new_path = PathBuf::from(&path_str);
if let Err(e) = write_buffer(&editor, &Some(new_path.clone()), &display_name) {
eprintln!("{e}");
exit_code = 1;
} else {
current_filename = Some(new_path);
wrote = true;
}
}
ExEffect::Quit { save, force: _ } => {
if save {
if let Err(e) = write_buffer(&editor, ¤t_filename, &display_name) {
eprintln!("{e}");
exit_code = 1;
} else {
wrote = true;
}
}
break;
}
ExEffect::EditFile { path, .. } => {
match std::fs::read_to_string(&path) {
Ok(content) => {
let content = content.strip_suffix('\n').unwrap_or(&content);
hjkl_engine::BufferEdit::replace_all(editor.buffer_mut(), content);
current_filename = Some(PathBuf::from(&path));
}
Err(e) => {
eprintln!("hjkl: {path}: {e}");
exit_code = 1;
}
}
}
ExEffect::BufferDelete { .. } => {
break;
}
ExEffect::PutRegister { .. } => {
}
ExEffect::SaveAndRename { path } => {
let new_path = PathBuf::from(&path);
if let Err(e) = write_buffer(&editor, &Some(new_path.clone()), &display_name) {
eprintln!("{e}");
exit_code = 1;
} else {
current_filename = Some(new_path);
wrote = true;
}
}
ExEffect::RenameBuffer { .. } => {
}
ExEffect::Cwd(_) => {
}
ExEffect::Redraw { .. } => {
}
ExEffect::Preserve => {
}
ExEffect::Recover(_) => {
}
ExEffect::SubstituteConfirm { matches } => {
let count = matches.len();
if count > 0 {
let accepted = vec![true; count];
hjkl_engine::apply_collected_matches(&mut editor, &matches, &accepted);
}
}
}
}
let _ = wrote; }
Ok(exit_code)
}
fn write_buffer(
editor: &Editor<View, DefaultHost>,
path: &Option<PathBuf>,
display_name: &str,
) -> Result<(), String> {
match path {
None => Err(format!("hjkl: {display_name}: E32: No file name")),
Some(p) => {
let joined = editor.buffer().content_joined();
let trailing_nl = !joined.is_empty();
crate::save::save_file_durable(p, joined.as_bytes(), trailing_nl)
.map_err(|e| format!("hjkl: {}: {e}", p.display()))
}
}
}
struct SharedBanks {
registers: std::sync::Arc<std::sync::Mutex<hjkl_engine::Registers>>,
global_marks: std::sync::Arc<std::sync::Mutex<hjkl_engine::GlobalMarks>>,
last_substitute: std::sync::Arc<std::sync::Mutex<Option<hjkl_engine::SubstituteCmd>>>,
abbrevs: std::sync::Arc<std::sync::Mutex<Vec<hjkl_engine::Abbrev>>>,
search: std::sync::Arc<std::sync::Mutex<hjkl_engine::SearchBank>>,
change_bank: std::sync::Arc<std::sync::Mutex<hjkl_engine::ChangeBank>>,
}
impl SharedBanks {
fn new() -> Self {
Self {
registers: std::sync::Arc::new(
std::sync::Mutex::new(hjkl_engine::Registers::default()),
),
global_marks: std::sync::Arc::new(std::sync::Mutex::new(
hjkl_engine::GlobalMarks::new(),
)),
last_substitute: std::sync::Arc::new(std::sync::Mutex::new(None)),
abbrevs: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
search: std::sync::Arc::new(std::sync::Mutex::new(hjkl_engine::SearchBank::default())),
change_bank: std::sync::Arc::new(std::sync::Mutex::new(
hjkl_engine::ChangeBank::default(),
)),
}
}
fn apply(&self, editor: &mut Editor<View, DefaultHost>) {
editor.set_registers_arc(self.registers.clone());
editor.set_global_marks_arc(self.global_marks.clone());
editor.set_last_substitute_arc(self.last_substitute.clone());
editor.set_abbrevs_arc(self.abbrevs.clone());
editor.set_search_arc(self.search.clone());
editor.set_change_bank_arc(self.change_bank.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shared_banks_carry_a_yank_across_separate_editors() {
let banks = SharedBanks::new();
let mut view_a = View::new();
BufferEdit::replace_all(&mut view_a, "hello from file a");
let mut editor_a = hjkl_vim::vim_editor(view_a, DefaultHost::new(), Options::default());
banks.apply(&mut editor_a);
for input in hjkl_engine::decode_macro("yy") {
hjkl_vim::dispatch_input(&mut editor_a, input);
}
let mut view_b = View::new();
BufferEdit::replace_all(&mut view_b, "line already in file b");
let mut editor_b = hjkl_vim::vim_editor(view_b, DefaultHost::new(), Options::default());
banks.apply(&mut editor_b);
for input in hjkl_engine::decode_macro("p") {
hjkl_vim::dispatch_input(&mut editor_b, input);
}
let content_b = editor_b.buffer().content_joined();
assert!(
content_b.contains("hello from file a"),
"pasting in editor B must see the yank from editor A when both \
are wired to the same SharedBanks (mirrors --headless processing \
file A, then file B); got: {content_b:?}"
);
}
#[test]
fn unshared_editors_do_not_carry_a_yank_across() {
let mut view_a = View::new();
BufferEdit::replace_all(&mut view_a, "hello from file a");
let mut editor_a = hjkl_vim::vim_editor(view_a, DefaultHost::new(), Options::default());
for input in hjkl_engine::decode_macro("yy") {
hjkl_vim::dispatch_input(&mut editor_a, input);
}
let mut view_b = View::new();
BufferEdit::replace_all(&mut view_b, "line already in file b");
let mut editor_b = hjkl_vim::vim_editor(view_b, DefaultHost::new(), Options::default());
for input in hjkl_engine::decode_macro("p") {
hjkl_vim::dispatch_input(&mut editor_b, input);
}
let content_b = editor_b.buffer().content_joined();
assert!(
!content_b.contains("hello from file a"),
"editors with independent (non-shared) registers must not see \
each other's yanks; got: {content_b:?}"
);
}
}