use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use hjkl_buffer::Buffer;
use hjkl_engine::{BufferEdit, DefaultHost, Editor, Options, VimMode};
use hjkl_ex::ExEffect;
use rmpv::Value;
const BUFFER_EXT: i8 = 0;
const WINDOW_EXT: i8 = 1;
fn encode_id(id: u64) -> Vec<u8> {
let mut buf = Vec::new();
rmpv::encode::write_value(&mut buf, &Value::from(id)).expect("encode ext id");
buf
}
fn buf_handle() -> Value {
Value::Ext(BUFFER_EXT, encode_id(1))
}
fn win_handle() -> Value {
Value::Ext(WINDOW_EXT, encode_id(1))
}
fn write_response(stdout: &mut impl Write, msgid: u32, error: Value, result: Value) -> Result<()> {
let msg = Value::Array(vec![
Value::from(1u64), Value::from(msgid as u64),
error,
result,
]);
rmpv::encode::write_value(stdout, &msg)?;
stdout.flush()?;
Ok(())
}
fn ok(stdout: &mut impl Write, msgid: u32, result: Value) -> Result<()> {
write_response(stdout, msgid, Value::Nil, result)
}
fn err(stdout: &mut impl Write, msgid: u32, msg: &str) -> Result<()> {
write_response(
stdout,
msgid,
Value::Array(vec![
Value::from(0i64), Value::from(msg),
]),
Value::Nil,
)
}
fn build_editor(
maybe_path: Option<&PathBuf>,
) -> Result<(Editor<Buffer, DefaultHost>, Option<PathBuf>)> {
let mut buffer = Buffer::new();
if let Some(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 => {}
Err(e) => return Err(anyhow::anyhow!("hjkl: {}: {e}", path.display())),
}
}
let host = DefaultHost::new();
let editor = Editor::new(buffer, host, Options::default());
Ok((editor, maybe_path.cloned()))
}
fn as_array(params: &Value) -> std::result::Result<&[Value], String> {
match params {
Value::Array(v) => Ok(v.as_slice()),
_ => Err("params must be an array".to_string()),
}
}
fn param_i64(params: &[Value], idx: usize) -> std::result::Result<i64, String> {
match params.get(idx) {
Some(Value::Integer(n)) => n
.as_i64()
.ok_or_else(|| format!("params[{idx}] out of i64 range")),
Some(other) => Err(format!("params[{idx}] must be integer, got {other:?}")),
None => Err(format!("params[{idx}] missing")),
}
}
fn param_bool(params: &[Value], idx: usize) -> std::result::Result<bool, String> {
match params.get(idx) {
Some(Value::Boolean(b)) => Ok(*b),
Some(_) | None => Ok(false), }
}
fn param_str(params: &[Value], idx: usize) -> std::result::Result<String, String> {
match params.get(idx) {
Some(Value::String(s)) => s
.as_str()
.map(|s| s.to_owned())
.ok_or_else(|| format!("params[{idx}] not valid UTF-8")),
Some(other) => Err(format!("params[{idx}] must be string, got {other:?}")),
None => Err(format!("params[{idx}] missing")),
}
}
fn param_string_array(params: &[Value], idx: usize) -> std::result::Result<Vec<String>, String> {
match params.get(idx) {
Some(Value::Array(arr)) => arr
.iter()
.enumerate()
.map(|(i, v)| match v {
Value::String(s) => s
.as_str()
.map(|s| s.to_owned())
.ok_or_else(|| format!("params[{idx}][{i}] not valid UTF-8")),
other => Err(format!("params[{idx}][{i}] must be string, got {other:?}")),
})
.collect(),
Some(other) => Err(format!("params[{idx}] must be array, got {other:?}")),
None => Err(format!("params[{idx}] missing")),
}
}
fn mode_code(editor: &Editor<Buffer, DefaultHost>) -> &'static str {
match editor.vim_mode() {
VimMode::Normal => "n",
VimMode::Insert => "i",
VimMode::Visual => "v",
VimMode::VisualLine => "V",
VimMode::VisualBlock => "\x16",
}
}
fn resolve_line_range(
lines: &[String],
start: i64,
end: i64,
) -> std::result::Result<(usize, usize), String> {
let n = lines.len() as i64;
let s = if start < 0 {
(n + start).max(0) as usize
} else {
start as usize
};
let e = if end < 0 {
(n + end + 1).max(0) as usize
} else {
end as usize
};
let e = e.min(lines.len());
if s > e {
return Err(format!(
"line range out of order: start={start} end={end} (resolved {s}..{e})"
));
}
Ok((s, e))
}
fn dispatch(
editor: &mut Editor<Buffer, DefaultHost>,
current_filename: &mut Option<PathBuf>,
should_quit: &mut bool,
method: &str,
params: &Value,
stdout: &mut impl Write,
msgid: u32,
) -> Result<()> {
match method {
"nvim_get_current_buf" => ok(stdout, msgid, buf_handle()),
"nvim_get_current_win" => ok(stdout, msgid, win_handle()),
"nvim_buf_set_lines" => {
let p = match as_array(params) {
Ok(p) => p,
Err(e) => return err(stdout, msgid, &e),
};
let start = match param_i64(p, 1) {
Ok(v) => v,
Err(e) => return err(stdout, msgid, &e),
};
let end = match param_i64(p, 2) {
Ok(v) => v,
Err(e) => return err(stdout, msgid, &e),
};
let _strict = param_bool(p, 3).unwrap_or(false);
let new_lines = match param_string_array(p, 4) {
Ok(v) => v,
Err(e) => return err(stdout, msgid, &e),
};
let current_lines = editor.buffer().lines().to_vec();
let (s, e) = match resolve_line_range(¤t_lines, start, end) {
Ok(r) => r,
Err(msg) => return err(stdout, msgid, &msg),
};
let mut result: Vec<String> = Vec::new();
result.extend_from_slice(¤t_lines[..s]);
result.extend(new_lines);
result.extend_from_slice(¤t_lines[e..]);
let content = result.join("\n");
editor.set_content(&content);
ok(stdout, msgid, Value::Nil)
}
"nvim_buf_get_lines" => {
let p = match as_array(params) {
Ok(p) => p,
Err(e) => return err(stdout, msgid, &e),
};
let start = match param_i64(p, 1) {
Ok(v) => v,
Err(e) => return err(stdout, msgid, &e),
};
let end = match param_i64(p, 2) {
Ok(v) => v,
Err(e) => return err(stdout, msgid, &e),
};
let _strict = param_bool(p, 3).unwrap_or(false);
let lines = editor.buffer().lines();
let (s, e) = match resolve_line_range(&lines, start, end) {
Ok(r) => r,
Err(msg) => return err(stdout, msgid, &msg),
};
let result: Vec<Value> = lines[s..e]
.iter()
.map(|l| Value::from(l.as_str()))
.collect();
ok(stdout, msgid, Value::Array(result))
}
"nvim_win_set_cursor" => {
let p = match as_array(params) {
Ok(p) => p,
Err(e) => return err(stdout, msgid, &e),
};
let pair = match p.get(1) {
Some(Value::Array(arr)) => arr.as_slice(),
Some(other) => {
return err(
stdout,
msgid,
&format!("params[1] must be [row, col] array, got {other:?}"),
);
}
None => return err(stdout, msgid, "params[1] missing"),
};
let row_1based = match pair.first() {
Some(Value::Integer(n)) => n.as_i64().unwrap_or(1),
_ => return err(stdout, msgid, "cursor row must be integer"),
};
let col = match pair.get(1) {
Some(Value::Integer(n)) => n.as_i64().unwrap_or(0),
_ => return err(stdout, msgid, "cursor col must be integer"),
};
let row = (row_1based - 1).max(0) as usize;
let char_col = {
let lines = editor.buffer().lines();
if let Some(line) = lines.get(row) {
let byte_offset = (col as usize).min(line.len());
line[..byte_offset].chars().count()
} else {
0
}
};
editor.jump_cursor(row, char_col);
ok(stdout, msgid, Value::Nil)
}
"nvim_win_get_cursor" => {
let (row, char_col) = editor.cursor();
let byte_col = {
let lines = editor.buffer().lines();
if let Some(line) = lines.get(row) {
line.chars()
.take(char_col)
.map(|c| c.len_utf8())
.sum::<usize>()
} else {
char_col
}
};
let result = Value::Array(vec![
Value::from((row + 1) as i64), Value::from(byte_col as i64),
]);
ok(stdout, msgid, result)
}
"nvim_input" => {
let p = match as_array(params) {
Ok(p) => p,
Err(e) => return err(stdout, msgid, &e),
};
let keys = match param_str(p, 0) {
Ok(s) => s,
Err(e) => return err(stdout, msgid, &e),
};
let len = keys.len() as i64;
let inputs = hjkl_engine::decode_macro(&keys);
for input in inputs {
hjkl_vim::dispatch_input(editor, input);
}
ok(stdout, msgid, Value::from(len))
}
"nvim_command" => {
let p = match as_array(params) {
Ok(p) => p,
Err(e) => return err(stdout, msgid, &e),
};
let cmd_raw = match param_str(p, 0) {
Ok(s) => s,
Err(e) => return err(stdout, msgid, &e),
};
let cmd = cmd_raw.strip_prefix(':').unwrap_or(&cmd_raw).to_string();
let reg = hjkl_ex::default_registry::<hjkl_engine::DefaultHost>();
let effect =
hjkl_ex::try_dispatch(®, editor, &cmd).unwrap_or(ExEffect::Unknown(cmd.clone()));
match effect {
ExEffect::None
| ExEffect::Ok
| ExEffect::Info(_)
| ExEffect::InfoTitled { .. }
| ExEffect::Substituted { .. } => ok(stdout, msgid, Value::Nil),
ExEffect::Error(msg) | ExEffect::Unknown(msg) => err(stdout, msgid, &msg),
ExEffect::Save => {
if let Err(e) = write_buffer(editor, current_filename) {
err(stdout, msgid, &e)
} else {
ok(stdout, msgid, Value::Nil)
}
}
ExEffect::SaveAs(path_str) => {
let new_path = PathBuf::from(&path_str);
if let Err(e) = write_buffer(editor, &Some(new_path.clone())) {
err(stdout, msgid, &e)
} else {
*current_filename = Some(new_path);
ok(stdout, msgid, Value::Nil)
}
}
ExEffect::Quit { save, force: _ } => {
if save && let Err(e) = write_buffer(editor, current_filename) {
return err(stdout, msgid, &e);
}
*should_quit = true;
ok(stdout, msgid, Value::Nil)
}
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));
ok(stdout, msgid, Value::Nil)
}
Err(e) => err(stdout, msgid, &format!("{path}: {e}")),
}
}
ExEffect::BufferDelete { .. } => {
*should_quit = true;
ok(stdout, msgid, Value::Nil)
}
}
}
"nvim_get_mode" => {
let code = mode_code(editor);
let map = Value::Map(vec![
(Value::from("mode"), Value::from(code)),
(Value::from("blocking"), Value::Boolean(false)),
]);
ok(stdout, msgid, map)
}
"nvim_call_function" => {
let p = match as_array(params) {
Ok(p) => p,
Err(e) => return err(stdout, msgid, &e),
};
let fn_name = match param_str(p, 0) {
Ok(s) => s,
Err(e) => return err(stdout, msgid, &e),
};
if fn_name != "getreg" {
return err(
stdout,
msgid,
&format!("nvim_call_function: unsupported function: {fn_name}"),
);
}
let fn_args = match p.get(1) {
Some(Value::Array(arr)) => arr.as_slice(),
_ => return err(stdout, msgid, "nvim_call_function: params[1] must be array"),
};
let reg_name = match fn_args.first() {
Some(Value::String(s)) => s.as_str().unwrap_or("\"").to_owned(),
_ => "\"".to_owned(),
};
let reg_char = reg_name.chars().next().unwrap_or('"');
let text = match editor.registers().read(reg_char) {
Some(slot) => slot.text.clone(),
None => String::new(),
};
ok(stdout, msgid, Value::from(text.as_str()))
}
_ => err(stdout, msgid, &format!("method not implemented: {method}")),
}
}
fn write_buffer(
editor: &Editor<Buffer, DefaultHost>,
path: &Option<PathBuf>,
) -> std::result::Result<(), String> {
match path {
None => Err("E32: No file name".to_string()),
Some(p) => {
let lines = editor.buffer().lines();
let content = if lines.is_empty() {
String::new()
} else {
let mut s = lines.join("\n");
s.push('\n');
s
};
std::fs::write(p, &content).map_err(|e| format!("hjkl: {}: {e}", p.display()))
}
}
}
pub fn run(files: Vec<PathBuf>) -> Result<i32> {
let first_file = files.into_iter().next();
let (mut editor, mut current_filename) = build_editor(first_file.as_ref())?;
let mut should_quit = false;
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut stdin_lock = stdin.lock();
let mut stdout_lock = stdout.lock();
let mut reader = std::io::BufReader::new(&mut stdin_lock);
loop {
let msg = match rmpv::decode::read_value(&mut reader) {
Ok(v) => v,
Err(e) => {
use rmpv::decode::Error;
match e {
Error::InvalidMarkerRead(io) | Error::InvalidDataRead(io)
if io.kind() == std::io::ErrorKind::UnexpectedEof =>
{
break;
}
_ => {
eprintln!("hjkl --nvim-api: decode error: {e}");
continue;
}
}
}
};
let arr = match &msg {
Value::Array(a) => a.as_slice(),
_ => {
eprintln!("hjkl --nvim-api: expected array, got {msg:?}");
continue;
}
};
let msg_type = match arr.first() {
Some(Value::Integer(n)) => n.as_u64().unwrap_or(99),
_ => {
eprintln!("hjkl --nvim-api: bad message type");
continue;
}
};
match msg_type {
0 => {
let msgid = match arr.get(1) {
Some(Value::Integer(n)) => n.as_u64().unwrap_or(0) as u32,
_ => {
eprintln!("hjkl --nvim-api: missing msgid");
continue;
}
};
let method = match arr.get(2) {
Some(Value::String(s)) => s.as_str().unwrap_or("").to_owned(),
_ => {
let _ = err(&mut stdout_lock, msgid, "missing method");
continue;
}
};
let params = arr.get(3).cloned().unwrap_or(Value::Array(vec![]));
dispatch(
&mut editor,
&mut current_filename,
&mut should_quit,
&method,
¶ms,
&mut stdout_lock,
msgid,
)?;
if should_quit {
break;
}
}
2 => {
let method = match arr.get(1) {
Some(Value::String(s)) => s.as_str().unwrap_or("").to_owned(),
_ => continue,
};
let params = arr.get(2).cloned().unwrap_or(Value::Array(vec![]));
let mut dev_null = std::io::sink();
dispatch(
&mut editor,
&mut current_filename,
&mut should_quit,
&method,
¶ms,
&mut dev_null,
0,
)?;
if should_quit {
break;
}
}
_ => {
eprintln!("hjkl --nvim-api: unexpected message type {msg_type}");
}
}
}
Ok(0)
}