feat: Dual-OS Neovim MCP Integration
- Created linux-nvim and win-nvim MCP servers for deep Neovim Msgpack-RPC integration. - Implemented tools: nvim_goto_line, nvim_get_active_buffer, nvim_get_cursor, nvim_get_visual_selection, nvim_set_diagnostics. - Added gemini-integration.lua for active UI socket discovery. - Updated build.cmd to cross-compile for both Windows (gnu) and Linux (musl). - Updated README, design.md, and instructions.md with architecture and tool capabilities.
This commit is contained in:
1 parent
28f70aafdb
commit
67b6a0407e
14 files changed
+1271
-19
No files matched your search
Generated
+90
-3
@@ -1031,6 +1031,18 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "mcp-memory-linux-nvim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rmp-serde",
|
||||
"rmpv",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-memory-server"
|
||||
version = "0.1.0"
|
||||
@@ -1045,7 +1057,7 @@ dependencies = [
|
||||
"glob",
|
||||
"redb",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"schemars 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tantivy",
|
||||
@@ -1068,6 +1080,18 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-memory-win-nvim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rmp-serde",
|
||||
"rmpv",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "measure_time"
|
||||
version = "0.9.0"
|
||||
@@ -1522,6 +1546,34 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||
dependencies = [
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmpv"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417"
|
||||
dependencies = [
|
||||
"rmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
@@ -1598,6 +1650,18 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.2"
|
||||
@@ -1606,11 +1670,23 @@ checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"schemars_derive 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals 0.29.1",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.2.2"
|
||||
@@ -1619,7 +1695,7 @@ checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"serde_derive_internals 0.30.0",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
@@ -1659,6 +1735,17 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.30.0"
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"server",
|
||||
"stub"
|
||||
"stub",
|
||||
"win-nvim",
|
||||
"linux-nvim"
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -82,3 +82,18 @@ You can also programmatically query these live metrics via the API endpoint:
|
||||
|
||||
## Further Reading
|
||||
For a deep dive into the architecture, Write-Ahead Logging (WAL), locking mechanisms, and the HTTP SSE event loop, consult the design.md file in this repository.
|
||||
|
||||
## Neovim Integration
|
||||
The `linux-nvim` and `win-nvim` MCP servers provide direct Msgpack-RPC communication with Neovim.
|
||||
For this to work, you must add the provided `gemini-integration.lua` file to your Neovim `lua/` configuration directory and require it in your `init.lua`:
|
||||
```lua
|
||||
require('gemini-integration')
|
||||
```
|
||||
This script allows Neovim to broadcast its active socket to `~/.gemini/active_nvim.txt` when it gains focus, allowing the MCP servers to bridge the connection.
|
||||
|
||||
These MCP servers provide the following two-way integration tools:
|
||||
* **`nvim_goto_line`**: Open files and jump cursors directly from the LLM.
|
||||
* **`nvim_set_diagnostics`**: Push inline code review warnings as virtual text.
|
||||
* **`nvim_get_active_buffer`**: Read live, unsaved buffer contents.
|
||||
* **`nvim_get_cursor`**: Fetch precise line/column coordinates.
|
||||
* **`nvim_get_visual_selection`**: Read highlighted code blocks.
|
||||
@@ -1,12 +1,47 @@
|
||||
@echo off
|
||||
echo Building mcp-memory in release mode...
|
||||
cargo build --release
|
||||
setlocal
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo Build successful!
|
||||
echo Executable is located at: target\release\mcp-memory.exe
|
||||
) else (
|
||||
echo.
|
||||
echo Build failed with error code %ERRORLEVEL%.
|
||||
echo Building mcp-memory (server and stub) for Windows...
|
||||
rustup run stable cargo build --release
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo Windows build failed!
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
|
||||
echo Building mcp-memory (server and stub) for Linux (WSL)...
|
||||
rustup run stable cargo zigbuild --release --target x86_64-unknown-linux-musl
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo Linux build failed!
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build successful! Gracefully shutting down running servers...
|
||||
:: Attempt shutdown via CLI flag
|
||||
if exist "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" (
|
||||
"C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe" --exit >nul 2>&1
|
||||
)
|
||||
:: Fallback shutdown via HTTP endpoint
|
||||
curl -X POST http://127.0.0.1:3000/shutdown -s -o nul >nul 2>&1
|
||||
|
||||
:: Give the server a moment to flush checkpoints and release file locks
|
||||
timeout /t 2 /nobreak >nul
|
||||
|
||||
echo.
|
||||
echo Copying Windows executables...
|
||||
copy /Y target\release\mcp-memory-server.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-server.exe"
|
||||
copy /Y target\release\mcp-memory-stub.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-stub.exe"
|
||||
copy /Y target\x86_64-pc-windows-gnu\release\mcp-memory-win-nvim.exe "C:\Users\reazul.ashraf\.local\bin\mcp-memory-win-nvim.exe"
|
||||
|
||||
echo Copying Linux executables to WSL...
|
||||
wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-server /home/riz/.local/bin/mcp-memory-server && chmod +x /home/riz/.local/bin/mcp-memory-server"
|
||||
wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-stub /home/riz/.local/bin/mcp-memory-stub && chmod +x /home/riz/.local/bin/mcp-memory-stub"
|
||||
wsl -d Ubuntu -e bash -c "cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/target/x86_64-unknown-linux-musl/release/mcp-memory-linux-nvim /home/riz/.local/bin/mcp-memory-linux-nvim && chmod +x /home/riz/.local/bin/mcp-memory-linux-nvim"
|
||||
|
||||
echo Copying instructions.md...
|
||||
copy /Y instructions.md "C:\Users\reazul.ashraf\.gemini\antigravity-cli\mcp\memory\instructions.md"
|
||||
wsl -d Ubuntu -e bash -c "mkdir -p /home/riz/.gemini/antigravity-cli/mcp/memory && cp /mnt/c/Users/reazul.ashraf/workspace/rust/mcp-memory/instructions.md /home/riz/.gemini/antigravity-cli/mcp/memory/instructions.md"
|
||||
|
||||
echo.
|
||||
echo Build and deployment complete!
|
||||
endlocal
|
||||
@@ -114,3 +114,4 @@ To optimize for different environments, the codebase is structured as a Cargo Wo
|
||||
eqwest and okio).
|
||||
* **Role:** A dedicated, OS-agnostic proxy binary used strictly for routing stdio JSON-RPC traffic over HTTP to a remote Leader. Windows gy clients point directly to this binary to bypass loading the heavy Server daemon into memory.
|
||||
* **WSL Behavior:** Compiled as a Linux native binary (x86_64-unknown-linux-musl). When executed by WSL agy, it acts as a transparent proxy to http://127.0.0.1:3000. It can also execute wake_cmd (e.g., WSL interop) to silently wake the Windows host if the Leader is offline.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- gemini-integration.lua
|
||||
-- Registers the active Neovim instance for the Antigravity MCP Server
|
||||
vim.api.nvim_create_autocmd({"VimEnter", "FocusGained"}, {
|
||||
callback = function()
|
||||
-- Only register if Neovim has an active UI attached (ignore headless/embedded instances)
|
||||
if #vim.api.nvim_list_uis() > 0 then
|
||||
local server_name = vim.v.servername
|
||||
if server_name then
|
||||
local home = os.getenv("HOME")
|
||||
if not home then
|
||||
home = os.getenv("USERPROFILE")
|
||||
end
|
||||
if home then
|
||||
os.execute("mkdir -p " .. home .. "/.gemini")
|
||||
local path = home .. "/.gemini/active_nvim.txt"
|
||||
local f = io.open(path, "w")
|
||||
if f then
|
||||
f:write(server_name)
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
desc = "Register active Neovim instance for Gemini CLI",
|
||||
})
|
||||
@@ -131,3 +131,10 @@ When starting a new session, get a numerical aggregate of the project's current
|
||||
To maintain absolute traceability, we link memory items directly to the exact git commits they occurred on.
|
||||
- When calling **log_code_change**, **log_error_fix**, or **log_tech_debt**, you should execute git rev-parse HEAD and git branch --show-current in the project directory first (if it's a git repo).
|
||||
- Pass the resulting hash and branch name into the git_commit and git_branch arguments of those tools to permanently link the memory item to the VCS state.
|
||||
|
||||
## 13. Server Lifecycle & Updates (Graceful Shutdown)
|
||||
When instructed to restart, update, or stop the mcp-memory-server binary, never use brute-force OS kill commands (e.g. Stop-Process, pkill, kill).
|
||||
- Why: The server uses asynchronous checkpointing. Force-killing it bypasses the shutdown flush, risking data loss in the master JSON graph, and can lead to lingering file locks.
|
||||
- How: Always use the built-in graceful shutdown mechanisms:
|
||||
1. CLI Flag: mcp-memory-server --exit (or --restart)
|
||||
2. HTTP Endpoint: POST http://127.0.0.1:3000/shutdown
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "mcp-memory-linux-nvim"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rmp-serde = "1.1"
|
||||
rmpv = "1.0"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
schemars = "0.8"
|
||||
@@ -0,0 +1,469 @@
|
||||
mod mcp;
|
||||
|
||||
use mcp::{read_message, send_response, send_error, JsonRpcResponse};
|
||||
use serde_json::{json, Value};
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
loop {
|
||||
let msg = match read_message() {
|
||||
Some(m) => m,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let id = msg.id.unwrap_or(json!(null));
|
||||
|
||||
match msg.method.as_str() {
|
||||
"initialize" => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"capabilities": {},
|
||||
"serverInfo": {
|
||||
"name": "mcp-memory-linux-nvim",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
"tools/list" => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"tools": [
|
||||
{
|
||||
"name": "nvim_goto_line",
|
||||
"description": "Open a file and jump to a specific line",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"line": { "type": "integer" }
|
||||
},
|
||||
"required": ["file", "line"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_get_active_buffer",
|
||||
"description": "Get the contents of the currently active Neovim buffer",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_get_cursor",
|
||||
"description": "Get the current cursor position (line and column) in the active Neovim buffer",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_get_visual_selection",
|
||||
"description": "Get the text that is currently highlighted or was last highlighted in Visual mode",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_set_diagnostics",
|
||||
"description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"line": { "type": "integer" },
|
||||
"message": { "type": "string" }
|
||||
},
|
||||
"required": ["line", "message"]
|
||||
}
|
||||
}
|
||||
]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
"tools/call" => {
|
||||
let params = msg.params.unwrap_or(json!({}));
|
||||
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
||||
|
||||
match name {
|
||||
"nvim_goto_line" => {
|
||||
let file = args.get("file").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
|
||||
match send_nvim_command(&format!("edit +{} {}", line, file)).await {
|
||||
Ok(_) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_get_active_buffer" => {
|
||||
match get_nvim_active_buffer().await {
|
||||
Ok(content) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": content}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_get_cursor" => {
|
||||
match get_nvim_cursor().await {
|
||||
Ok(content) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": content}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_get_visual_selection" => {
|
||||
match get_nvim_visual_selection().await {
|
||||
Ok(content) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": content}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_set_diagnostics" => {
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
match set_nvim_diagnostics(line, message).await {
|
||||
Ok(_) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
send_error(id, -32601, "Tool not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore other methods
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_socket_path() -> Result<String, String> {
|
||||
if let Ok(content) = std::fs::read_to_string(format!("{}/.gemini/active_nvim.txt", std::env::var("HOME").unwrap_or("/home/riz".into()))) {
|
||||
let p = content.trim().to_string();
|
||||
if !p.is_empty() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search /tmp/nvim.*/0
|
||||
if let Ok(entries) = std::fs::read_dir("/tmp") {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(name) = entry.file_name().into_string() {
|
||||
if name.starts_with("nvim.") {
|
||||
let path = entry.path().join("0");
|
||||
if path.exists() {
|
||||
return Ok(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Could not find Neovim socket".to_string())
|
||||
}
|
||||
|
||||
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(1.into()),
|
||||
RmpValue::String("nvim_command".into()),
|
||||
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_nvim_active_buffer() -> Result<String, String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(2.into()),
|
||||
RmpValue::String("nvim_buf_get_lines".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer((-1).into()),
|
||||
RmpValue::Boolean(true),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 1024 * 1024];
|
||||
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
if let RmpValue::Array(lines) = &arr[3] {
|
||||
let mut text = String::new();
|
||||
for line in lines {
|
||||
if let RmpValue::String(s) = line {
|
||||
if let Some(s) = s.as_str() {
|
||||
text.push_str(s);
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
|
||||
async fn get_nvim_cursor() -> Result<String, String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(2.into()),
|
||||
RmpValue::String("nvim_win_get_cursor".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 8192];
|
||||
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
if let RmpValue::Array(pos) = &arr[3] {
|
||||
if pos.len() == 2 {
|
||||
if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) {
|
||||
return Ok(format!("Line: {}, Column: {}", row, col));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
|
||||
async fn get_nvim_visual_selection() -> Result<String, String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let lua_script = r#"
|
||||
local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
|
||||
local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>"))
|
||||
local lines = vim.fn.getline(csrow, cerow)
|
||||
if type(lines) == "table" then
|
||||
return table.concat(lines, "\n")
|
||||
else
|
||||
return lines
|
||||
end
|
||||
"#;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(3.into()),
|
||||
RmpValue::String("nvim_exec_lua".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::String(lua_script.into()),
|
||||
RmpValue::Array(vec![]),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 1024 * 1024];
|
||||
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
if let RmpValue::String(s) = &arr[3] {
|
||||
if let Some(text) = s.as_str() {
|
||||
return Ok(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
|
||||
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut stream = UnixStream::connect(socket_path).await.map_err(|e| e.to_string())?;
|
||||
|
||||
// We escape backslashes and double quotes in the message
|
||||
let escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
|
||||
let lua_script = format!(r#"
|
||||
local ns = vim.api.nvim_create_namespace("gemini_diagnostics")
|
||||
local diagnostics = {{{{
|
||||
lnum = {} - 1,
|
||||
col = 0,
|
||||
severity = vim.diagnostic.severity.WARN,
|
||||
message = "{}",
|
||||
}}}}
|
||||
vim.diagnostic.set(ns, 0, diagnostics, {{}})
|
||||
"#, line, escaped_message);
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(4.into()),
|
||||
RmpValue::String("nvim_exec_lua".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::String(lua_script.into()),
|
||||
RmpValue::Array(vec![]),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
stream.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 8192];
|
||||
let n = stream.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct JsonRpcRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct JsonRpcResponse {
|
||||
pub jsonrpc: String,
|
||||
pub id: Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<Value>,
|
||||
}
|
||||
|
||||
pub fn read_message() -> Option<JsonRpcRequest> {
|
||||
use std::io::{BufRead, Read};
|
||||
let stdin = std::io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
let mut length = 0;
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
if handle.read_line(&mut line).unwrap_or(0) == 0 {
|
||||
return None;
|
||||
}
|
||||
let line = line.trim_end();
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
|
||||
length = len_str.parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
if length == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buffer = vec![0; length];
|
||||
handle.read_exact(&mut buffer).unwrap_or(());
|
||||
serde_json::from_slice(&buffer).ok()
|
||||
}
|
||||
|
||||
pub fn send_response(response: JsonRpcResponse) {
|
||||
let msg = serde_json::to_string(&response).unwrap();
|
||||
print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
|
||||
use std::io::Write;
|
||||
std::io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn send_error(id: Value, code: i32, message: &str) {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(serde_json::json!({"code": code, "message": message})),
|
||||
});
|
||||
}
|
||||
+8
-6
@@ -7,10 +7,12 @@ use tokio::sync::mpsc;
|
||||
use tokio_util::io::StreamReader;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "mcp-memory-stub")]
|
||||
#[command(name = "mcp-memory-stub", author, version, about = "Antigravity MCP Memory Stub / Proxy", long_about = None)]
|
||||
struct Cli {
|
||||
/// Target URL for the stub to proxy messages to
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
target: String,
|
||||
/// Optional command to execute if the target server is unreachable
|
||||
#[arg(long)]
|
||||
wake_cmd: Option<String>,
|
||||
}
|
||||
@@ -56,11 +58,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if res.is_ok() {
|
||||
break;
|
||||
if let Ok(resp) = res { if resp.status().is_success() { break; } }
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
attempts += 1;
|
||||
if attempts % 10 == 0 {
|
||||
eprintln!("[PROXY] Waiting for server to accept messages...");
|
||||
@@ -132,7 +132,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
*post_url.write().await = String::new();
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
Err(_) => {
|
||||
if let Some(ref cmd) = wake_cmd {
|
||||
@@ -150,3 +150,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "mcp-memory-win-nvim"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rmp-serde = "1.1"
|
||||
rmpv = "1.0"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
schemars = "0.8"
|
||||
@@ -0,0 +1,460 @@
|
||||
mod mcp;
|
||||
|
||||
use mcp::{read_message, send_response, send_error, JsonRpcResponse};
|
||||
use serde_json::{json, Value};
|
||||
use tokio::net::windows::named_pipe::ClientOptions;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
loop {
|
||||
let msg = match read_message() {
|
||||
Some(m) => m,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let id = msg.id.unwrap_or(json!(null));
|
||||
|
||||
match msg.method.as_str() {
|
||||
"initialize" => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"capabilities": {},
|
||||
"serverInfo": {
|
||||
"name": "mcp-memory-win-nvim",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
"tools/list" => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"tools": [
|
||||
{
|
||||
"name": "nvim_goto_line",
|
||||
"description": "Open a file and jump to a specific line",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"line": { "type": "integer" }
|
||||
},
|
||||
"required": ["file", "line"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_get_active_buffer",
|
||||
"description": "Get the contents of the currently active Neovim buffer",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_get_cursor",
|
||||
"description": "Get the current cursor position (line and column) in the active Neovim buffer",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_get_visual_selection",
|
||||
"description": "Get the text that is currently highlighted or was last highlighted in Visual mode",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nvim_set_diagnostics",
|
||||
"description": "Push a diagnostic message (like an LSP warning) to a specific line in the active buffer",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"line": { "type": "integer" },
|
||||
"message": { "type": "string" }
|
||||
},
|
||||
"required": ["line", "message"]
|
||||
}
|
||||
}
|
||||
]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
"tools/call" => {
|
||||
let params = msg.params.unwrap_or(json!({}));
|
||||
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
||||
|
||||
match name {
|
||||
"nvim_goto_line" => {
|
||||
let file = args.get("file").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
|
||||
match send_nvim_command(&format!("edit +{} {}", line, file)).await {
|
||||
Ok(_) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": format!("Successfully jumped to {}:{}", file, line)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_get_active_buffer" => {
|
||||
match get_nvim_active_buffer().await {
|
||||
Ok(content) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": content}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_get_cursor" => {
|
||||
match get_nvim_cursor().await {
|
||||
Ok(content) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": content}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_get_visual_selection" => {
|
||||
match get_nvim_visual_selection().await {
|
||||
Ok(content) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": content}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
"nvim_set_diagnostics" => {
|
||||
let line = args.get("line").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
match set_nvim_diagnostics(line, message).await {
|
||||
Ok(_) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"content": [{"type": "text", "text": format!("Successfully pushed diagnostic to line {}", line)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: Some(json!({
|
||||
"isError": true,
|
||||
"content": [{"type": "text", "text": format!("Error: {}", e)}]
|
||||
})),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
send_error(id, -32601, "Tool not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore other methods
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_socket_path() -> Result<String, String> {
|
||||
let profile = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
|
||||
let path = format!("{}\\.gemini\\active_nvim.txt", profile);
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
let p = content.trim().to_string();
|
||||
if !p.is_empty() {
|
||||
return Ok(p);
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not find active Windows Neovim named pipe in active_nvim.txt".to_string())
|
||||
}
|
||||
|
||||
async fn send_nvim_command(cmd: &str) -> Result<(), String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(1.into()),
|
||||
RmpValue::String("nvim_command".into()),
|
||||
RmpValue::Array(vec![RmpValue::String(cmd.into())]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_nvim_active_buffer() -> Result<String, String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(2.into()),
|
||||
RmpValue::String("nvim_buf_get_lines".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer((-1).into()),
|
||||
RmpValue::Boolean(true),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 1024 * 1024]; // 1MB buffer
|
||||
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
if let RmpValue::Array(lines) = &arr[3] {
|
||||
let mut text = String::new();
|
||||
for line in lines {
|
||||
if let RmpValue::String(s) = line {
|
||||
if let Some(s) = s.as_str() {
|
||||
text.push_str(s);
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
|
||||
async fn get_nvim_cursor() -> Result<String, String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(2.into()),
|
||||
RmpValue::String("nvim_win_get_cursor".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 8192];
|
||||
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
if let RmpValue::Array(pos) = &arr[3] {
|
||||
if pos.len() == 2 {
|
||||
if let (RmpValue::Integer(row), RmpValue::Integer(col)) = (&pos[0], &pos[1]) {
|
||||
return Ok(format!("Line: {}, Column: {}", row, col));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
|
||||
async fn get_nvim_visual_selection() -> Result<String, String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
|
||||
|
||||
let lua_script = r#"
|
||||
local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
|
||||
local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>"))
|
||||
local lines = vim.fn.getline(csrow, cerow)
|
||||
if type(lines) == "table" then
|
||||
return table.concat(lines, "\n")
|
||||
else
|
||||
return lines
|
||||
end
|
||||
"#;
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(3.into()),
|
||||
RmpValue::String("nvim_exec_lua".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::String(lua_script.into()),
|
||||
RmpValue::Array(vec![]),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 1024 * 1024];
|
||||
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
if let RmpValue::String(s) = &arr[3] {
|
||||
if let Some(text) = s.as_str() {
|
||||
return Ok(text.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
|
||||
async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> {
|
||||
let socket_path = get_socket_path().await?;
|
||||
let mut client = ClientOptions::new().open(&socket_path).map_err(|e| e.to_string())?;
|
||||
|
||||
let escaped_message = message.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
|
||||
let lua_script = format!(r#"
|
||||
local ns = vim.api.nvim_create_namespace("gemini_diagnostics")
|
||||
local diagnostics = {{{{
|
||||
lnum = {} - 1,
|
||||
col = 0,
|
||||
severity = vim.diagnostic.severity.WARN,
|
||||
message = "{}",
|
||||
}}}}
|
||||
vim.diagnostic.set(ns, 0, diagnostics, {{}})
|
||||
"#, line, escaped_message);
|
||||
|
||||
use rmpv::Value as RmpValue;
|
||||
let req = RmpValue::Array(vec![
|
||||
RmpValue::Integer(0.into()),
|
||||
RmpValue::Integer(4.into()),
|
||||
RmpValue::String("nvim_exec_lua".into()),
|
||||
RmpValue::Array(vec![
|
||||
RmpValue::String(lua_script.into()),
|
||||
RmpValue::Array(vec![]),
|
||||
]),
|
||||
]);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
rmpv::encode::write_value(&mut buf, &req).map_err(|e| e.to_string())?;
|
||||
client.write_all(&buf).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut resp_buf = [0u8; 8192];
|
||||
let n = client.read(&mut resp_buf).await.map_err(|e| e.to_string())?;
|
||||
let mut cursor = std::io::Cursor::new(&resp_buf[..n]);
|
||||
let resp = rmpv::decode::read_value(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
if let RmpValue::Array(arr) = resp {
|
||||
if arr.len() >= 4 {
|
||||
if !arr[2].is_nil() {
|
||||
return Err(format!("Neovim error: {:?}", arr[2]));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err("Invalid response from Neovim".to_string())
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct JsonRpcRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct JsonRpcResponse {
|
||||
pub jsonrpc: String,
|
||||
pub id: Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<Value>,
|
||||
}
|
||||
|
||||
pub fn read_message() -> Option<JsonRpcRequest> {
|
||||
use std::io::{BufRead, Read};
|
||||
let stdin = std::io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
let mut length = 0;
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
if handle.read_line(&mut line).unwrap_or(0) == 0 {
|
||||
return None;
|
||||
}
|
||||
let line = line.trim_end();
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
|
||||
length = len_str.parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
if length == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buffer = vec![0; length];
|
||||
handle.read_exact(&mut buffer).unwrap_or(());
|
||||
serde_json::from_slice(&buffer).ok()
|
||||
}
|
||||
|
||||
pub fn send_response(response: JsonRpcResponse) {
|
||||
let msg = serde_json::to_string(&response).unwrap();
|
||||
print!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
|
||||
use std::io::Write;
|
||||
std::io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
pub fn send_error(id: Value, code: i32, message: &str) {
|
||||
send_response(JsonRpcResponse {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(serde_json::json!({"code": code, "message": message})),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user