From 914cecbaf2ee055b5d27b1f9e8637f65d3bcf15b Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Wed, 23 Sep 2026 04:53:09 +0100 Subject: [PATCH] feat(nvim): implement auto-spawning headless instance and interactive preemption --- nvim-core/src/headless.rs | 42 +++++++++++++++++++++++++++ nvim-core/src/lib.rs | 61 ++++++++++++++++++++++++++++++++++----- 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 nvim-core/src/headless.rs diff --git a/nvim-core/src/headless.rs b/nvim-core/src/headless.rs new file mode 100644 index 0000000..7cfff74 --- /dev/null +++ b/nvim-core/src/headless.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; +use std::sync::LazyLock; + +pub static ACTIVE_SOCKET: LazyLock>>> = + LazyLock::new(|| Arc::new(std::sync::Mutex::new(None))); + +pub static HEADLESS_PROC: LazyLock>>> = + LazyLock::new(|| Arc::new(std::sync::Mutex::new(None))); + +pub async fn spawn_headless_nvim() -> Result { + if std::process::Command::new("nvim").arg("--version").output().is_err() { + return Err("Error: Neovim is not installed or not in PATH. Cannot execute nvim tool. Fallback to basic terminal tools.".to_string()); + } + + #[cfg(windows)] + let socket_name = format!(r"\\.\pipe\agy-headless-nvim-{}", std::process::id()); + #[cfg(unix)] + let socket_name = format!("/tmp/agy-headless-nvim-{}.sock", std::process::id()); + + let child = tokio::process::Command::new("nvim") + .arg("--headless") + .arg("--listen") + .arg(&socket_name) + .kill_on_drop(true) + .spawn() + .map_err(|e| format!("Failed to spawn headless Neovim: {}", e))?; + + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + + let mut old_child = { + let mut lock = HEADLESS_PROC.lock().unwrap_or_else(|e| e.into_inner()); + let old = lock.take(); + *lock = Some(child); + old + }; + + if let Some(mut old) = old_child { + let _ = old.kill().await; + } + + Ok(socket_name) +} diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index 614602c..3c41c97 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -1,3 +1,5 @@ +mod headless; +use headless::*; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -160,17 +162,55 @@ fn handle_nvim_notification(params: &[rmpv::Value]) { } async fn get_nvim_connection() -> Result, String> { + let interactive_socket = get_socket_path().await.ok(); + let mut should_kill_headless = false; + { - let conn_lock = NVIM_CONN.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(sender) = conn_lock.as_ref() - && !sender.is_closed() - { - return Ok(sender.clone()); + let mut conn_lock = NVIM_CONN.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(sender) = conn_lock.as_ref() { + if !sender.is_closed() { + let current_socket = { + let active_lock = ACTIVE_SOCKET.lock().unwrap_or_else(|e| e.into_inner()); + active_lock.clone() + }; + + if let Some(ref interactive) = interactive_socket { + if Some(interactive.clone()) != current_socket { + tracing::info!("Interactive Preemption: New Neovim instance detected at {}. Disconnecting from current.", interactive); + should_kill_headless = true; + *conn_lock = None; + { + let mut active_lock2 = ACTIVE_SOCKET.lock().unwrap_or_else(|e| e.into_inner()); + *active_lock2 = None; + } + } else { + return Ok(sender.clone()); + } + } else { + return Ok(sender.clone()); + } + } } } - tracing::info!("Establishing new persistent connection to Neovim"); - let socket_path = get_socket_path().await?; + if should_kill_headless { + let mut child_to_kill = { + let mut proc_lock = HEADLESS_PROC.lock().unwrap_or_else(|e| e.into_inner()); + proc_lock.take() + }; + if let Some(mut child) = child_to_kill { + let _ = child.kill().await; + } + } + + let socket_path = if let Some(p) = interactive_socket { + p + } else { + tracing::info!("No interactive Neovim detected. Spawning headless instance."); + spawn_headless_nvim().await? + }; + + tracing::info!("Establishing new persistent connection to Neovim at {}", socket_path); #[cfg(windows)] let stream = { @@ -183,11 +223,16 @@ async fn get_nvim_connection() -> Result, String> { #[cfg(unix)] let stream = { use tokio::net::UnixStream; - UnixStream::connect(socket_path) + UnixStream::connect(&socket_path) .await .map_err(|e| e.to_string())? }; + { + let mut active_lock = ACTIVE_SOCKET.lock().unwrap_or_else(|e| e.into_inner()); + *active_lock = Some(socket_path.clone()); + } + let (mut read_half, mut write_half) = tokio::io::split(stream); let (tx, mut rx) = mpsc::channel::(32); type PendingRequestsMap =