use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio};
use anyhow::{Context as _, Result, anyhow};
use nvim_rs::{Handler, compat::tokio::Compat, create::tokio as create};
use tokio::io::WriteHalf;
use tracing::{debug, info, warn};
use crate::matcher::vim_path as vim_path_fn;
use crate::platform;
#[cfg(unix)]
type NvimConnection = tokio::net::UnixStream;
#[cfg(windows)]
type NvimConnection = tokio::net::windows::named_pipe::NamedPipeClient;
type NvimWriter = Compat<WriteHalf<NvimConnection>>;
#[derive(Clone)]
struct DummyHandler;
impl Handler for DummyHandler {
type Writer = NvimWriter;
}
#[derive(Debug, Clone)]
pub struct NeovimBackend {
pub command: String,
pub listen: String,
pub args_remote: Vec<String>,
pub args_new: Vec<String>,
pub passthrough: Vec<String>,
pub gui: bool,
#[cfg_attr(not(unix), allow(dead_code))]
pub can_exec: bool,
}
impl NeovimBackend {
pub async fn dispatch(&self, files: &[PathBuf], mode: &str, sync: bool) -> Result<()> {
match (mode, sync) {
("remote", false) => self.dispatch_remote(files).await,
("new", false) => self.spawn_detached_fresh(files),
("new", true) => self.spawn_sync(files),
("remote", true) => {
warn!("neovim remote+sync is not implemented yet; falling back to new+sync");
self.spawn_sync(files)
}
(other, s) => {
warn!(
mode = other,
"unknown mode for neovim backend; treating as remote"
);
if s {
self.spawn_sync(files)
} else {
self.dispatch_remote(files).await
}
}
}
}
async fn dispatch_remote(&self, files: &[PathBuf]) -> Result<()> {
if !self.passthrough.is_empty() {
warn!(
passthrough = ?self.passthrough,
"passthrough flags are ignored for remote-mode neovim (RPC is post-startup)",
);
}
match create::new_path(self.listen.as_str(), DummyHandler).await {
Ok((nvim, _io_handle)) => {
info!(pipe = %self.listen, count = files.len(), "connected to existing nvim");
if files.is_empty() {
debug!(cmd = "enew", "sending RPC");
nvim.command("enew").await.context("failed to send :enew")?;
} else {
for f in files {
let vim_cmd = format!("edit {}", vim_path(f));
debug!(cmd = %vim_cmd, "sending RPC");
nvim.command(&vim_cmd)
.await
.with_context(|| format!("failed to send :{vim_cmd}"))?;
}
}
Ok(())
}
Err(e) => {
info!(pipe = %self.listen, reason = %e, "no listener; starting nvim with --listen");
self.start_with_listen(files)
}
}
}
fn start_with_listen(&self, files: &[PathBuf]) -> Result<()> {
let mut cmd = StdCommand::new(&self.command);
for p in &self.passthrough {
cmd.arg(p);
}
for f in files {
cmd.arg(f);
}
for a in &self.args_remote {
cmd.arg(a);
}
cmd.arg("--listen").arg(&self.listen);
#[cfg(unix)]
if !self.gui && self.can_exec {
use std::os::unix::process::CommandExt;
let err = cmd.exec();
return Err(
anyhow::Error::from(err).context(format!("failed to exec {}", self.command))
);
}
platform::spawn_detached(
&mut cmd,
self.gui,
files.first().map(PathBuf::as_path).unwrap_or(Path::new("")),
)
.with_context(|| format!("failed to spawn {}", self.command))?;
Ok(())
}
fn spawn_detached_fresh(&self, files: &[PathBuf]) -> Result<()> {
let mut cmd = StdCommand::new(&self.command);
for a in &self.args_new {
cmd.arg(a);
}
for p in &self.passthrough {
cmd.arg(p);
}
for f in files {
cmd.arg(f);
}
platform::spawn_detached(
&mut cmd,
self.gui,
files.first().map(PathBuf::as_path).unwrap_or(Path::new("")),
)
.with_context(|| format!("failed to spawn {}", self.command))?;
Ok(())
}
fn spawn_sync(&self, files: &[PathBuf]) -> Result<()> {
let mut cmd = StdCommand::new(&self.command);
for a in &self.args_new {
cmd.arg(a);
}
for p in &self.passthrough {
cmd.arg(p);
}
for f in files {
cmd.arg(f);
}
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = cmd
.status()
.with_context(|| format!("failed to run {}", self.command))?;
if status.success() {
Ok(())
} else {
Err(anyhow!("{} exited with status {}", self.command, status))
}
}
}
fn vim_path(p: &Path) -> String {
vim_path_fn(p)
}