From ea735003e290b0b9e486606fb260942d8f629752 Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Tue, 22 Sep 2026 22:32:37 +0100 Subject: [PATCH] feat(nvim-core): implement bidirectional event streaming and real-time shadow buffer - Created global thread-safe NvimState to shadow Neovim context. - Injected Lua autocmds (CursorMoved, TextChanged) on connection to stream events back to MCP. - Upgraded msgpack RPC loop to intercept and parse 'mcp_event' notifications alongside standard responses. - Optimized nvim_get_cursor to pull instantly from the local shadow buffer, bypassing RPC round-trips. --- Cargo.lock | 22 +++++++++ nvim-core/src/lib.rs | 103 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 113 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6ffe97..5d5d094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,6 +497,20 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.11.1" @@ -807,6 +821,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -1375,6 +1395,8 @@ dependencies = [ name = "nvim-core" version = "0.1.0" dependencies = [ + "bytes", + "dashmap", "dirs", "mcp-stdio", "rmcp", diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index 12f3693..2631d66 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -129,6 +129,36 @@ static NEXT_MSGID: AtomicU64 = AtomicU64::new(1); static NVIM_CONN: LazyLock>>>> = LazyLock::new(|| Arc::new(std::sync::Mutex::new(None))); +#[derive(Debug, Default, Clone)] +pub struct NvimState { + pub cursor: String, + pub active_buffer_id: u64, +} + +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 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); + } + }, + "TextChanged" => { + tracing::debug!("Shadow buffer text changed event received"); + } + _ => {} + } + } +} + async fn get_nvim_connection() -> Result, String> { { let conn_lock = NVIM_CONN.lock().unwrap_or_else(|e| e.into_inner()); @@ -196,18 +226,21 @@ async fn get_nvim_connection() -> Result, String> { let parsed_len = cursor.position() as usize; resp_buf.advance(parsed_len); - if let rmpv::Value::Array(ref arr) = val - && arr.len() >= 4 - && arr[0] == rmpv::Value::Integer(1.into()) - { - let msgid = match &arr[1] { - rmpv::Value::Integer(i) => i.as_u64().unwrap_or(0), - _ => 0, - }; - - if let Some((_, reply_sender)) = pending_clone2.remove(&msgid) { - let _ = reply_sender.send(Ok(val)); - } + if let rmpv::Value::Array(ref arr) = val { + if arr.len() >= 4 && arr[0] == rmpv::Value::Integer(1.into()) { + let msgid = match &arr[1] { + rmpv::Value::Integer(i) => i.as_u64().unwrap_or(0), + _ => 0, + }; + 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()) + && 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); + } } continue; } @@ -272,6 +305,45 @@ async fn get_nvim_connection() -> Result, String> { return Ok(existing_sender.clone()); } *conn_lock = Some(tx.clone()); + drop(conn_lock); + + let tx_clone = tx.clone(); + tokio::spawn(async move { + let setup_code = r#" + local channel = vim.api.nvim_get_api_info()[1] + vim.api.nvim_create_augroup("MCP_Tracking", { clear = true }) + vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { + group = "MCP_Tracking", + callback = function() + pcall(vim.rpcnotify, channel, "mcp_event", "CursorMoved", vim.fn.getpos('.')) + end + }) + vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI", "BufEnter"}, { + group = "MCP_Tracking", + callback = function() + pcall(vim.rpcnotify, channel, "mcp_event", "TextChanged", vim.api.nvim_get_current_buf()) + end + }) + "#; + let msgid = NEXT_MSGID.fetch_add(1, Ordering::SeqCst); + let req = rmpv::Value::Array(vec![ + rmpv::Value::Integer(0.into()), + rmpv::Value::Integer(msgid.into()), + rmpv::Value::String("nvim_exec_lua".into()), + rmpv::Value::Array(vec![ + rmpv::Value::String(setup_code.into()), + rmpv::Value::Array(vec![]), + ]), + ]); + let (reply_tx, _reply_rx) = tokio::sync::oneshot::channel(); + let _ = tx_clone.send(NvimRequest { + msgid, + req, + reply: reply_tx, + }).await; + tracing::info!("Injected bidirectional event tracking autocmds into Neovim"); + }); + Ok(tx) } @@ -363,6 +435,13 @@ async fn get_nvim_active_buffer() -> Result { } async fn get_nvim_cursor() -> Result { + { + let state = NVIM_STATE.lock().unwrap_or_else(|e| e.into_inner()); + if !state.cursor.is_empty() { + return Ok(state.cursor.clone()); + } + } + let result = call_nvim_method("nvim_win_get_cursor", vec![rmpv::Value::Integer(0.into())]).await?;