diff --git a/nvim-core/src/headless.rs b/nvim-core/src/headless.rs index 313f190..2caca4b 100644 --- a/nvim-core/src/headless.rs +++ b/nvim-core/src/headless.rs @@ -8,7 +8,11 @@ pub static HEADLESS_PROC: LazyLock Result { - if std::process::Command::new("nvim").arg("--version").output().is_err() { + 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()); } diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index df15c84..a66c497 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -64,7 +64,7 @@ async fn is_interactive(socket_path: &str) -> bool { if socket_path.contains("agy-headless-nvim") { return false; } - + if let Ok(output) = tokio::process::Command::new("nvim") .arg("--server") .arg(socket_path) @@ -169,18 +169,22 @@ static NVIM_STATE: LazyLock>> = LazyLock::new(|| Arc::new(std::sync::Mutex::new(NvimState::default()))); fn handle_nvim_notification(params: &[rmpv::Value]) { - if params.is_empty() { return; } + if params.is_empty() { + return; + } if let rmpv::Value::String(event) = ¶ms[0] { match event.as_str().unwrap_or("") { "CursorMoved" => { if params.len() > 1 && let rmpv::Value::Array(pos) = ¶ms[1] - && pos.len() >= 4 - && let (rmpv::Value::Integer(row), rmpv::Value::Integer(col)) = (&pos[1], &pos[2]) { - let mut state = NVIM_STATE.lock().unwrap_or_else(|e| e.into_inner()); - state.cursor = format!("Line: {}, Column: {}", row, col); - } - }, + && pos.len() >= 4 + && let (rmpv::Value::Integer(row), rmpv::Value::Integer(col)) = + (&pos[1], &pos[2]) + { + let mut state = NVIM_STATE.lock().unwrap_or_else(|e| e.into_inner()); + state.cursor = format!("Line: {}, Column: {}", row, col); + } + } "TextChanged" => { tracing::debug!("Shadow buffer text changed event received"); } @@ -195,28 +199,32 @@ async fn get_nvim_connection() -> Result, String> { { 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(sender) = conn_lock.as_ref() + && !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()); + 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()); } } } @@ -238,7 +246,10 @@ async fn get_nvim_connection() -> Result, String> { spawn_headless_nvim().await? }; - tracing::info!("Establishing new persistent connection to Neovim at {}", socket_path); + tracing::info!( + "Establishing new persistent connection to Neovim at {}", + socket_path + ); #[cfg(windows)] let stream = { @@ -308,12 +319,14 @@ async fn get_nvim_connection() -> Result, String> { if let Some((_, reply_sender)) = pending_clone2.remove(&msgid) { let _ = reply_sender.send(Ok(val)); } - } else if arr.len() >= 3 && arr[0] == rmpv::Value::Integer(2.into()) + } else if arr.len() >= 3 + && arr[0] == rmpv::Value::Integer(2.into()) && let rmpv::Value::String(method) = &arr[1] - && method.as_str().unwrap_or("") == "mcp_event" - && let rmpv::Value::Array(params) = &arr[2] { - handle_nvim_notification(params); - } + && method.as_str().unwrap_or("") == "mcp_event" + && let rmpv::Value::Array(params) = &arr[2] + { + handle_nvim_notification(params); + } } continue; } @@ -407,11 +420,13 @@ async fn get_nvim_connection() -> Result, String> { ]), ]); let (reply_tx, _reply_rx) = tokio::sync::oneshot::channel(); - let _ = tx_clone.send(NvimRequest { - msgid, - req, - reply: reply_tx, - }).await; + let _ = tx_clone + .send(NvimRequest { + msgid, + req, + reply: reply_tx, + }) + .await; tracing::info!("Injected bidirectional event tracking autocmds into Neovim"); }); @@ -781,7 +796,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { id, result: Some(json!({"resources": []})), error: None, - }).await; + }) + .await; } "resources/templates/list" => { send_response(JsonRpcResponse { @@ -789,7 +805,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { id, result: Some(json!({"resourceTemplates": []})), error: None, - }).await; + }) + .await; } "prompts/list" => { send_response(JsonRpcResponse { @@ -797,7 +814,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { id, result: Some(json!({"prompts": []})), error: None, - }).await; + }) + .await; } "tools/list" => { @@ -1428,12 +1446,14 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { Err(e) => send_error(id, -32603, &e).await, } } - + "nvim_read_file" => { if let Some(file) = args.get("file").and_then(|v| v.as_str()) { - let start_line = args.get("start_line").and_then(|v| v.as_i64()).unwrap_or(1); - let end_line = args.get("end_line").and_then(|v| v.as_i64()).unwrap_or(-1); - + let start_line = + args.get("start_line").and_then(|v| v.as_i64()).unwrap_or(1); + let end_line = + args.get("end_line").and_then(|v| v.as_i64()).unwrap_or(-1); + let escaped_file = file.replace('\\', "\\\\").replace('"', "\\\""); let code = format!(" local file = \"{escaped_file}\" @@ -1459,14 +1479,15 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { send_error(id, -32602, "Missing 'file'").await; } } - + "nvim_search_file" => { if let (Some(file), Some(pattern)) = ( args.get("file").and_then(|v| v.as_str()), args.get("pattern").and_then(|v| v.as_str()), ) { let escaped_file = file.replace('\\', "\\\\").replace('"', "\\\""); - let escaped_pattern = pattern.replace('\\', "\\\\").replace('"', "\\\""); + let escaped_pattern = + pattern.replace('\\', "\\\\").replace('"', "\\\""); let code = format!(" local file = \"{escaped_file}\" local pattern = \"{escaped_pattern}\" @@ -1545,7 +1566,9 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { let exe_path = std::env::current_exe() .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| "unknown".to_string()); - let socket = get_socket_path().await.unwrap_or_else(|e| format!("Error: {}", e)); + let socket = get_socket_path() + .await + .unwrap_or_else(|e| format!("Error: {}", e)); let info = format!( "Server: {}\nVersion: {}\nOS: {}\nExecutable: {}\nBackend Socket: {}", app_name, app_version, os, exe_path, socket diff --git a/server/src/api/events.rs b/server/src/api/events.rs index 2f89278..94b989c 100644 --- a/server/src/api/events.rs +++ b/server/src/api/events.rs @@ -17,10 +17,10 @@ pub async fn wait_for_event_handler( loop { match rx.recv().await { Ok(event) => { - let topic_matches = topic.as_ref().map_or(true, |t| t == &event.topic); + let topic_matches = topic.as_ref().is_none_or(|t| t == &event.topic); let session_matches = session_id .as_ref() - .map_or(true, |s| Some(s) == event.session_id.as_ref()); + .is_none_or(|s| Some(s) == event.session_id.as_ref()); if topic_matches && session_matches { return axum::Json(event); diff --git a/server/src/api/telemetry.rs b/server/src/api/telemetry.rs index 28266bf..b29d286 100644 --- a/server/src/api/telemetry.rs +++ b/server/src/api/telemetry.rs @@ -50,17 +50,17 @@ pub async fn nvim_telemetry_handler( let _ = tx.try_send(msg_str.clone()); } - if payload.event == "BufWritePost" { - if let Some(ref file_path) = payload.file { - let normalized_file = file_path.replace("\\", "/"); - let topic = format!("nvim:save:{}", normalized_file); - let event = crate::state::GenericEvent { - topic, - session_id: Some(payload.session_id.clone()), - payload: serde_json::json!(&payload), - }; - let _ = state.handler.state.event_bus_tx.send(event); - } + if payload.event == "BufWritePost" + && let Some(ref file_path) = payload.file + { + let normalized_file = file_path.replace("\\", "/"); + let topic = format!("nvim:save:{}", normalized_file); + let event = crate::state::GenericEvent { + topic, + session_id: Some(payload.session_id.clone()), + payload: serde_json::json!(&payload), + }; + let _ = state.handler.state.event_bus_tx.send(event); } axum::Json(serde_json::json!({"status": "ok"})) diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index 84ea99d..d2702bd 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -274,10 +274,9 @@ impl McpTool for ReadPreferencesHandler { } async fn execute(&self, _args: Value, state: Arc) -> Result { - let data = state + state .prefs - .read_with(|prefs| serde_json::to_string(prefs).map_err(|e| e.to_string())); - data + .read_with(|prefs| serde_json::to_string(prefs).map_err(|e| e.to_string())) } } diff --git a/server/src/handlers/workspaces.rs b/server/src/handlers/workspaces.rs index e45723b..eccf1db 100644 --- a/server/src/handlers/workspaces.rs +++ b/server/src/handlers/workspaces.rs @@ -275,7 +275,7 @@ impl McpTool for ListContextWorkspacesHandler { let data = state.context_workspaces.read_with(|ws| { let filtered: Vec<_> = ws .iter() - .filter(|w| req.namespace.as_ref().map_or(true, |ns| &w.namespace == ns)) + .filter(|w| req.namespace.as_ref().is_none_or(|ns| &w.namespace == ns)) .collect(); serde_json::to_string(&filtered).map_err(|e| e.to_string()) })?; diff --git a/server/src/router.rs b/server/src/router.rs index 234e58e..cab112c 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -587,8 +587,16 @@ mod tests { }); let res_list = handler.handle_request(req_list_res).await.unwrap(); let resources_arr = res_list["result"]["resources"].as_array().unwrap(); - assert!(resources_arr.iter().any(|r| r["uri"] == "memory://tasks/active")); - assert!(resources_arr.iter().any(|r| r["uri"] == "memory://pinned_files")); + assert!( + resources_arr + .iter() + .any(|r| r["uri"] == "memory://tasks/active") + ); + assert!( + resources_arr + .iter() + .any(|r| r["uri"] == "memory://pinned_files") + ); // Test resources/read let req_read_res = json!({ @@ -600,8 +608,16 @@ mod tests { } }); let res_read = handler.handle_request(req_read_res).await.unwrap(); - assert_eq!(res_read["result"]["contents"][0]["uri"], "memory://tasks/active"); - assert!(res_read["result"]["contents"][0]["text"].as_str().unwrap().contains("[]")); // Empty tasks + assert_eq!( + res_read["result"]["contents"][0]["uri"], + "memory://tasks/active" + ); + assert!( + res_read["result"]["contents"][0]["text"] + .as_str() + .unwrap() + .contains("[]") + ); // Empty tasks // Test prompts/list let req_list_prompts = json!({ @@ -627,7 +643,12 @@ mod tests { let res_get = handler.handle_request(req_get_prompt).await.unwrap(); let messages = res_get["result"]["messages"].as_array().unwrap(); assert_eq!(messages[0]["role"], "user"); - assert!(messages[0]["content"]["text"].as_str().unwrap().contains("standup report")); + assert!( + messages[0]["content"]["text"] + .as_str() + .unwrap() + .contains("standup report") + ); } #[tokio::test] diff --git a/server/src/store.rs b/server/src/store.rs index 0869358..e2dd4a8 100644 --- a/server/src/store.rs +++ b/server/src/store.rs @@ -28,7 +28,9 @@ impl let key_inner = key_clone.clone(); let json_data = { let lock = cache_clone.read().unwrap_or_else(|e| e.into_inner()); - serde_json::to_vec(&*lock).map_err(|e| tracing::error!("Failed to serialize memory store: {}", e)).ok() + serde_json::to_vec(&*lock) + .map_err(|e| tracing::error!("Failed to serialize memory store: {}", e)) + .ok() }; if let Some(json_data) = json_data { diff --git a/win-nvim/tests/integration_test.rs b/win-nvim/tests/integration_test.rs index 51287ba..dda2725 100644 --- a/win-nvim/tests/integration_test.rs +++ b/win-nvim/tests/integration_test.rs @@ -1,5 +1,5 @@ use serde_json::{Value, json}; -use std::io::{BufRead, BufReader, Write, Read}; +use std::io::{BufRead, BufReader, Read, Write}; use std::process::{Command, Stdio}; fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) { @@ -161,7 +161,11 @@ fn test_mcp_initialization_and_tools_list() { send_message(&mut stdin, call_req); let call_resp = read_message(&mut stdout).expect("Failed to read tools/call response"); - println!("Response for {}: {}", tool_name, serde_json::to_string(&call_resp).unwrap()); + println!( + "Response for {}: {}", + tool_name, + serde_json::to_string(&call_resp).unwrap() + ); assert_eq!(call_resp["jsonrpc"], "2.0"); assert_eq!(call_resp["id"], req_id); } @@ -200,7 +204,11 @@ fn test_mcp_initialization_and_tools_list() { let _ = stderr.read_to_string(&mut stderr_output); println!("Child STDERR: {}", stderr_output); } - + let status = child.wait().expect("Failed to wait on child"); - assert!(status.success(), "Child process did not exit successfully. Stderr: {}", stderr_output); + assert!( + status.success(), + "Child process did not exit successfully. Stderr: {}", + stderr_output + ); }