From 4ea8f861cc60212402a8cb1a691a1b9352fe65af Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Fri, 9 Oct 2026 06:43:05 +0100 Subject: [PATCH] refactor(nvim-core): map requests directly to mega-tools and remove legacy routing --- nvim-core/src/lib.rs | 1048 ++++++++---------------------------------- 1 file changed, 196 insertions(+), 852 deletions(-) diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index 4e0ab13..f9f458c 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -742,112 +742,6 @@ pub async fn send_nvim_command(cmd: &str) -> Result<(), String> { Ok(()) } -async fn get_nvim_active_buffer() -> Result { - let result = call_nvim_method( - "nvim_buf_get_lines", - vec![ - rmpv::Value::Integer(0.into()), - rmpv::Value::Integer(0.into()), - rmpv::Value::Integer((-1).into()), - rmpv::Value::Boolean(true), - ], - ) - .await?; - - if let rmpv::Value::Array(lines) = result { - let mut text = String::new(); - for line in lines { - if let rmpv::Value::String(s) = line - && let Some(s) = s.as_str() - { - text.push_str(s); - text.push('\n'); - } - } - return Ok(text); - } - Err("Invalid response format".to_string()) -} - -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?; - - if let rmpv::Value::Array(pos) = result - && pos.len() == 2 - && let (rmpv::Value::Integer(row), rmpv::Value::Integer(col)) = (&pos[0], &pos[1]) - { - return Ok(format!("Line: {row}, Column: {col}")); - } - Err("Invalid response format".to_string()) -} - -async fn get_nvim_visual_selection() -> Result { - 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 - "#; - - let result = call_nvim_method( - "nvim_exec_lua", - vec![ - rmpv::Value::String(lua_script.into()), - rmpv::Value::Array(vec![]), - ], - ) - .await?; - - if let rmpv::Value::String(s) = result - && let Some(text) = s.as_str() - { - return Ok(text.to_string()); - } - Err("Invalid response format".to_string()) -} - -async fn set_nvim_diagnostics(line: i64, message: &str) -> Result<(), String> { - let lua_script = r#" - local line, message = ... - local ns = vim.api.nvim_create_namespace("gemini_diagnostics") - local diagnostics = { - { - lnum = line - 1, - col = 0, - severity = vim.diagnostic.severity.WARN, - message = message, - } - } - vim.diagnostic.set(ns, 0, diagnostics, {}) - "#; - - call_nvim_method( - "nvim_exec_lua", - vec![ - rmpv::Value::String(lua_script.into()), - rmpv::Value::Array(vec![ - rmpv::Value::Integer(line.into()), - rmpv::Value::String(message.into()), - ]), - ], - ) - .await?; - - Ok(()) -} - async fn set_nvim_extmark(line: i64, text: &str, hl_group: &str) -> Result<(), String> { let lua_script = r#" local line, text, hl_group = ... @@ -1214,48 +1108,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { let action = args.get("action").and_then(|v| v.as_str()).unwrap_or(""); - let (mapped_name, mapped_action) = match (name, action) { - ("nvim_buffer", "read") => ("nvim_buffer", "read"), - ("nvim_buffer", "replace") => ("nvim_buffer", "edit"), - ("nvim_buffer", "save") => ("nvim_buffer", "save"), - ("nvim_buffer", "undo") => ("nvim_buffer", "undo"), - ("nvim_buffer", "redo") => ("nvim_buffer", "redo"), - ("nvim_buffer", "create_scratch") => ("nvim_buffer", "create_scratch"), - - ("nvim_workspace", "list_buffers") => ("nvim_buffer", "list"), - ("nvim_workspace", "list_windows") => ("nvim_window", "list"), - ("nvim_workspace", "focus") => ("nvim_window", "focus"), - ("nvim_workspace", "split") => ("nvim_window", "split"), - ("nvim_workspace", "cwd") => ("nvim_system", "cwd"), - - ("nvim_intelligence", "hover") => ("nvim_lsp", "hover"), - ("nvim_intelligence", "definition") => ("nvim_lsp", "definition"), - ("nvim_intelligence", "references") => ("nvim_lsp", "references"), - ("nvim_intelligence", "rename") => ("nvim_lsp", "rename"), - ("nvim_intelligence", "code_action") => ("nvim_lsp", "code_action"), - ("nvim_intelligence", "outline") => ("nvim_ast", "outline"), - ("nvim_intelligence", "query") => ("nvim_ast", "query"), - ("nvim_intelligence", "diagnostics") => ("nvim_diagnostics", "get"), - - ("nvim_ui", "highlight") => ("nvim_visual", "highlight"), - ("nvim_ui", "ghost_text") => ("nvim_visual", "extmark"), - ("nvim_ui", "clear") => ("nvim_visual", "clear_highlight"), - - ("nvim_exec", "lua") => ("nvim_execute_lua", ""), - ("nvim_exec", "vimscript") => ("nvim_system", "vimscript"), - ("nvim_exec", "terminal") => ("nvim_system", "send_to_terminal"), - - (n, a) => (n, a), - }; - - match mapped_name { - "nvim_buffer" => match mapped_action { - "get_active" => match get_nvim_active_buffer().await { - Ok(content) => { - send_text_result!(id.clone(), content); - } - Err(e) => send_error(id, -32603, &e).await, - }, + match name { + "nvim_buffer" => match action { "read" => { let args_rmp = json_to_rmpv(args); let code = " @@ -1285,54 +1139,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { execute_nvim_lua_with_args(code, vec![args_rmp]) ) } - "search" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local file = args.file - local pattern = args.pattern - if not file or file == '' or file == vim.NIL then - error(\"Missing 'file'\") - end - if not pattern or pattern == '' or pattern == vim.NIL then - error(\"Missing 'pattern'\") - end - local bufnr = vim.fn.bufnr(file, true) - if vim.fn.bufloaded(bufnr) == 0 then - vim.fn.bufload(bufnr) - end - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local matches = {} - - local pcall_ok, _ = pcall(function() - for i, line in ipairs(lines) do - if vim.fn.match(line, '\\v' .. pattern) >= 0 then - table.insert(matches, tostring(i) .. ': ' .. line) - end - end - end) - - if not pcall_ok or #matches == 0 then - matches = {} - for i, line in ipairs(lines) do - if string.find(line, pattern, 1, true) then - table.insert(matches, tostring(i) .. ': ' .. line) - end - end - if #matches > 0 then - table.insert(matches, 1, 'Regex failed or no results, but found these literal matches:') - else - return 'No matches found.' - end - end - return table.concat(matches, '\\n') - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - "edit" => { + "replace" => { let mut modified_args = args.clone(); if let Some(edits) = modified_args .get_mut("edits") @@ -1445,7 +1252,15 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { execute_nvim_lua_with_args(code, vec![args_rmp]) ) } - "undo" | "redo" => { + "save" => { + let code = " + vim.cmd('write') + return 'Saved current buffer' + "; + handle_lua_result!(id, execute_nvim_lua(code)); + } + + "redo" => { let is_undo = action == "undo"; let steps = args.get("steps").and_then(|v| v.as_i64()).unwrap_or(1); let cmd = if is_undo { @@ -1467,112 +1282,21 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { ); handle_lua_result!(id, execute_nvim_lua(&code)); } - "open" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local target_file = (args.file and args.file ~= '' and args.file ~= 'null') and args.file - or (args.file_path and args.file_path ~= '' and args.file_path ~= 'null') and args.file_path - or (args.path and args.path ~= '' and args.path ~= 'null') and args.path - or (args.target and args.target ~= '' and args.target ~= 'null') and args.target - if not target_file or target_file == vim.NIL then - return 'Error: No valid file path provided' - end - local curr_buf = vim.api.nvim_get_current_buf() - local is_empty_unnamed = vim.api.nvim_buf_get_name(curr_buf) == '' - and vim.api.nvim_buf_get_option(curr_buf, 'modified') == false - and vim.api.nvim_buf_line_count(curr_buf) <= 1 - and (vim.api.nvim_buf_get_lines(curr_buf, 0, 1, false)[1] or '') == '' - vim.cmd('edit ' .. vim.fn.fnameescape(target_file)) - local new_buf = vim.api.nvim_get_current_buf() - if is_empty_unnamed and curr_buf ~= new_buf and vim.api.nvim_buf_is_valid(curr_buf) then - pcall(vim.api.nvim_buf_delete, curr_buf, { force = true }) - end - if args.filetype and args.filetype ~= '' then - vim.bo.filetype = args.filetype - end - return 'Opened file ' .. target_file - "; - handle_lua_result!( + + _ => { + send_error( id, - execute_nvim_lua_with_args(code, vec![args_rmp]) + -32602, + &format!( + "Invalid or missing action '{}' for nvim_buffer", + action + ), ) + .await } - "create_scratch" | "open_buffer" => { - let mut modified_args = args.clone(); - if let Some(content) = modified_args.get_mut("content") - && let Some(s) = content.as_str() - { - let lines: Vec = s - .split('\n') - .map(|l| l.trim_end_matches('\r').to_string()) - .collect(); - *content = serde_json::json!(lines); - } - let args_rmp = json_to_rmpv(&modified_args); - let code = " - local args = ... - local name = (args.name and args.name ~= '') and args.name or '[Antigravity Scratch]' - local listed = args.listed == true - local buf = vim.api.nvim_create_buf(listed, true) - pcall(vim.api.nvim_buf_set_name, buf, name) - vim.bo[buf].buftype = 'nofile' - vim.bo[buf].bufhidden = 'hide' - if args.content then - vim.api.nvim_buf_set_lines(buf, 0, -1, false, args.content) - end - if args.filetype and args.filetype ~= '' then - vim.bo[buf].filetype = args.filetype - end - vim.api.nvim_win_set_buf(0, buf) - return 'Opened buffer ' .. tostring(buf) .. ' (' .. name .. ')' - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - "close" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local buf = args.buf_id or vim.api.nvim_get_current_buf() - local force = args.force or false - vim.api.nvim_buf_delete(buf, { force = force }) - return 'Closed buffer ' .. tostring(buf) - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - "reload" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local buf = args.buf_id or vim.api.nvim_get_current_buf() - vim.api.nvim_buf_call(buf, function() - if args.force then - vim.cmd('edit!') - else - vim.cmd('edit') - end - end) - return 'Reloaded buffer ' .. tostring(buf) - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - "save" => { - let code = " - vim.cmd('write') - return 'Saved current buffer' - "; - handle_lua_result!(id, execute_nvim_lua(code)); - } - "list" => { + }, + "nvim_workspace" => match action { + "list_buffers" => { let lua_code = r" local bufs = vim.api.nvim_list_bufs() local result = {} @@ -1586,58 +1310,7 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { "; handle_lua_result!(id, execute_nvim_lua(lua_code)) } - _ => { - send_error( - id, - -32602, - &format!( - "Invalid or missing action '{}' for nvim_buffer", - action - ), - ) - .await - } - }, - "nvim_window" => match action { - "list" => { - let lua_code = r" - local wins = vim.api.nvim_list_wins() - local result = {} - for _, win in ipairs(wins) do - if vim.api.nvim_win_is_valid(win) then - local buf = vim.api.nvim_win_get_buf(win) - local name = vim.api.nvim_buf_get_name(buf) - table.insert(result, { id = win, buf_id = buf, buf_name = name }) - end - end - return vim.fn.json_encode(result) - "; - handle_lua_result!(id, execute_nvim_lua(lua_code)) - } - "get_active" => { - let lua_code = r" - local win = vim.api.nvim_get_current_win() - return tostring(win) - "; - handle_lua_result!(id, execute_nvim_lua(lua_code)) - } - "focus" | "set_active" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - if args.win_id then - vim.api.nvim_set_current_win(args.win_id) - return 'Active window set to ' .. tostring(args.win_id) - else - error('Missing win_id') - end - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } "split" => { let args_rmp = json_to_rmpv(args); let code = " @@ -1656,448 +1329,6 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { execute_nvim_lua_with_args(code, vec![args_rmp]) ) } - "close" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local win = args.win_id or vim.api.nvim_get_current_win() - local force = args.force or false - vim.api.nvim_win_close(win, force) - return 'Closed window ' .. tostring(win) - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - _ => { - send_error( - id, - -32602, - &format!( - "Invalid or missing action '{}' for nvim_window", - action - ), - ) - .await - } - }, - - "nvim_view" => match action { - "goto_line" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local file = args.file - local line = tonumber(args.line) or 1 - if not file or file == '' or file == vim.NIL then - error(\"Missing or empty 'file'\") - end - vim.cmd('edit ' .. vim.fn.fnameescape(file)) - pcall(vim.api.nvim_win_set_cursor, 0, {line, 0}) - vim.cmd('normal! zz') - return string.format('Successfully jumped to %s line %d', file, line) - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - "get_cursor" => match get_nvim_cursor().await { - Ok(content) => { - send_text_result!(id.clone(), content); - } - Err(e) => send_error(id, -32603, &e).await, - }, - "get_viewport" => { - let code = " - local first = vim.fn.line('w0') - local last = vim.fn.line('w$') - local total = vim.api.nvim_buf_line_count(0) - local lines = vim.api.nvim_buf_get_lines(0, first - 1, last, false) - local res = {} - for i, line in ipairs(lines) do - table.insert(res, tostring(first + i - 1) .. ': ' .. line) - end - local content = table.concat(res, '\\n') - - local pct = math.floor(((last - first + 1) / total) * 100) - local result = { - viewport_start = first, - viewport_end = last, - total_buffer_lines = total, - warning = string.format('You are only seeing %d%% of this file. Do not assume variables are undefined just because they are not in the viewport.', pct), - content = content - } - return result - "; - match execute_nvim_lua(code).await { - Ok(res) => send_text_result!(id.clone(), res), - Err(e) => send_error(id, -32603, &e).await, - } - } - "get_selection" => match get_nvim_visual_selection().await { - Ok(content) => { - send_text_result!(id.clone(), content); - } - Err(e) => send_error(id, -32603, &e).await, - }, - _ => { - send_error( - id, - -32602, - &format!( - "Invalid or missing action '{}' for nvim_view", - action - ), - ) - .await - } - }, - - "nvim_diagnostics" => match action { - "get" => { - let lua_code = r" - local bufnr = vim.api.nvim_get_current_buf() - local diagnostics = vim.diagnostic.get(bufnr) - local result = {} - for _, d in ipairs(diagnostics) do - table.insert(result, { - lnum = d.lnum, - col = d.col, - severity = d.severity, - message = d.message, - source = d.source - }) - end - return vim.fn.json_encode(result) - "; - handle_lua_result!(id, execute_nvim_lua(lua_code)) - } - "set" => { - if let (Some(line), Some(message)) = ( - args.get("line").and_then(serde_json::Value::as_i64), - args.get("message").and_then(|v| v.as_str()), - ) { - match set_nvim_diagnostics(line, message).await { - Ok(()) => { - send_text_result!( - id.clone(), - format!( - "Successfully set diagnostic on line {}", - line - ) - ); - } - Err(e) => send_error(id, -32603, &e).await, - } - } else { - send_error(id, -32602, "Missing 'line' or 'message'").await; - } - } - "set_quickfix" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local items = args.items or {} - local action = ' ' - if args.action == 'append' or args.qf_action == 'append' then action = 'a' end - if args.action == 'prepend' or args.qf_action == 'prepend' then action = 'p' end - if args.action == 'replace' or args.qf_action == 'replace' then action = 'r' end - vim.fn.setqflist(items, action) - vim.cmd('copen') - return 'Populated quickfix with ' .. tostring(#items) .. ' items' - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - _ => { - send_error( - id, - -32602, - &format!( - "Invalid or missing action '{}' for nvim_diagnostics", - action - ), - ) - .await - } - }, - - "nvim_visual" => match action { - "preview" => { - if let ( - Some(file), - Some(start_line), - Some(end_line), - Some(preview_content), - ) = ( - args.get("file_path") - .or_else(|| args.get("file")) - .and_then(|v| v.as_str()), - args.get("start_line").and_then(|v| v.as_i64()), - args.get("end_line").and_then(|v| v.as_i64()), - args.get("preview_content") - .or_else(|| args.get("content")) - .and_then(|v| v.as_str()), - ) { - use std::time::{SystemTime, UNIX_EPOCH}; - let diff_id = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() - .to_string(); - - let lines_arr: Vec = preview_content - .lines() - .map(|line| rmpv::Value::String(line.into())) - .collect(); - - let lua_code = r#" - local diff_id, file, start_line, end_line, lines = ... - if _G.gemini and _G.gemini.preview_diff then - local bufnr = vim.fn.bufnr(file) - if bufnr == -1 then - bufnr = 0 - end - _G.gemini.preview_diff(diff_id, bufnr, start_line, end_line, lines) - return { status = "Preview added", diff_id = diff_id } - else - error("gemini UI not loaded") - end - "#; - handle_lua_result!( - id, - execute_nvim_lua_with_args( - lua_code, - vec![ - rmpv::Value::String(diff_id.into()), - rmpv::Value::String(file.into()), - rmpv::Value::Integer(start_line.into()), - rmpv::Value::Integer(end_line.into()), - rmpv::Value::Array(lines_arr), - ] - ) - ); - } else { - send_error(id, -32602, "Missing arguments: requires file_path, start_line, end_line, and preview_content").await; - } - } - "prompt" | "prompt_user" => { - let prompt_text = args - .get("prompt") - .or_else(|| args.get("prompt_text")) - .and_then(|v| v.as_str()); - if let Some(prompt) = prompt_text { - use std::time::{SystemTime, UNIX_EPOCH}; - let request_id = args - .get("request_id") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() - .to_string() - }); - - let options_arr = - match args.get("options").and_then(|v| v.as_array()) { - Some(arr) => { - let rmp_arr: Vec = arr - .iter() - .filter_map(|item| item.as_str()) - .map(|s| rmpv::Value::String(s.into())) - .collect(); - rmpv::Value::Array(rmp_arr) - } - None => rmpv::Value::Nil, - }; - - let lua_code = r#" - local prompt_text, options, request_id = ... - if _G.gemini and _G.gemini.prompt_user then - _G.gemini.prompt_user(prompt_text, options, request_id) - return { status = "Prompt displayed", request_id = request_id } - else - error("gemini UI not loaded") - end - "#; - handle_lua_result!( - id, - execute_nvim_lua_with_args( - lua_code, - vec![ - rmpv::Value::String(prompt.into()), - options_arr, - rmpv::Value::String(request_id.into()), - ] - ) - ); - } else { - send_error(id, -32602, "Missing 'prompt' or 'prompt_text'") - .await; - } - } - "extmark" => { - if let (Some(line), Some(text)) = ( - args.get("line").and_then(serde_json::Value::as_i64), - args.get("text").and_then(|v| v.as_str()), - ) { - let hl_group = args - .get("highlight_group") - .and_then(|v| v.as_str()) - .unwrap_or("Comment"); - match set_nvim_extmark(line, text, hl_group).await { - Ok(()) => { - send_text_result!( - id.clone(), - format!( - "Successfully set extmark on line {}", - line - ) - ); - } - Err(e) => send_error(id, -32603, &e).await, - } - } else { - send_error(id, -32602, "Missing 'line' or 'text'").await; - } - } - "highlight" | "clear_highlight" => { - let mut modified_args = args.clone(); - if action == "clear_highlight" - && let Some(obj) = modified_args.as_object_mut() - { - obj.insert("clear_only".to_string(), json!(true)); - } - let args_rmp = json_to_rmpv(&modified_args); - let code = " - local args = ... - local buf = args.buf_id or vim.api.nvim_get_current_buf() - local group = args.group or args.highlight_group or 'IncSearch' - local ns = vim.api.nvim_create_namespace('antigravity_highlight') - - if args.clear_only then - vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) - return 'Cleared highlights' - end - - vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) - local start_line = tonumber(args.start_line) or 1 - local end_line = tonumber(args.end_line) or start_line - for i = start_line - 1, end_line - 1 do - pcall(vim.api.nvim_buf_add_highlight, buf, ns, group, i, 0, -1) - end - - local duration = args.duration_ms or 5000 - if duration > 0 then - vim.defer_fn(function() - pcall(vim.api.nvim_buf_clear_namespace, buf, ns, 0, -1) - end, duration) - end - return 'Highlighted lines ' .. tostring(start_line) .. ' to ' .. tostring(end_line) - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - _ => { - send_error( - id, - -32602, - &format!( - "Invalid or missing action '{}' for nvim_visual", - action - ), - ) - .await - } - }, - - "nvim_execute_lua" => { - if let Some(code) = args.get("code").and_then(|v| v.as_str()) { - // READ-ONLY ENFORCEMENT: Block any Lua code that attempts to mutate state. - let lower_code = code.to_lowercase(); - if lower_code.contains("vim.cmd") - || lower_code.contains("nvim_buf_set_lines") - || lower_code.contains("nvim_buf_set_text") - || lower_code.contains("nvim_command") - || lower_code.contains("nvim_set_current") - || lower_code.contains("nvim_win_set") - || lower_code.contains("nvim_buf_set_name") - { - send_error(id, -32600, "CRITICAL ERROR: nvim_execute_lua is restricted to READ-ONLY queries to prevent agents from bypassing the specialized visual tools (nvim_buffer, nvim_visual). Use the designated tools to mutate editor state.").await; - return; - } - - handle_lua_result!(id, execute_nvim_lua(code)); - } else { - send_error(id, -32602, "Missing 'code'").await; - } - } - - "nvim_system" => match action { - "get_info" => { - let os = if cfg!(windows) { "Windows" } else { "Linux" }; - 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 info = format!( - "Server: {}\nVersion: {}\nOS: {}\nExecutable: {}\nBackend Socket: {}", - app_name, app_version, os, exe_path, socket - ); - send_text_result!(id, info); - } - "get_messages" => { - let args_rmp = json_to_rmpv(args); - let code = " - local args = ... - local msg = vim.fn.execute('messages') - local lines = vim.split(msg, '\\n') - if args.tail and args.tail > 0 and #lines > args.tail then - local tail_lines = {} - for i = #lines - args.tail + 1, #lines do - table.insert(tail_lines, lines[i]) - end - return table.concat(tail_lines, '\\n') - end - return msg - "; - handle_lua_result!( - id, - execute_nvim_lua_with_args(code, vec![args_rmp]) - ) - } - "send_to_terminal" => { - if let Some(cmd) = args.get("command").and_then(|v| v.as_str()) { - let lua_code = r#" - local cmd = ... - if _G.gemini and _G.gemini.send_to_terminal then - _G.gemini.send_to_terminal(cmd) - return "Command sent to terminal" - else - error("gemini UI not loaded") - end - "#; - handle_lua_result!( - id, - execute_nvim_lua_with_args( - lua_code, - vec![rmpv::Value::String(cmd.into())] - ) - ); - } else { - send_error(id, -32602, "Missing 'command'").await; - } - } "cwd" => { let lua_code = if let Some(path) = args.get("path").and_then(|v| v.as_str()) { @@ -2110,30 +1341,19 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { }; handle_lua_result!(id, execute_nvim_lua(&lua_code)); } - "vimscript" => { - if let Some(code) = args.get("code").and_then(|v| v.as_str()) { - let lua_code = - format!("vim.cmd([=[{}]=]); return 'Success'", code); - handle_lua_result!(id, execute_nvim_lua(&lua_code)); - } else { - send_error(id, -32602, "Missing 'code'").await; - } - } _ => { send_error( id, -32602, &format!( - "Invalid or missing action '{}' for nvim_system", + "Invalid or missing action '{}' for nvim_workspace", action ), ) .await } }, - - "nvim_lsp" => { - let args_rmp = json_to_rmpv(args); + "nvim_intelligence" => { let lua_code = match action { "hover" => { r#" @@ -2262,49 +1482,6 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { return table.concat(actions, "\n") "# } - _ => "", - }; - - if lua_code.is_empty() { - send_error( - id, - -32602, - &format!("Invalid nvim_lsp action: {}", action), - ) - .await; - } else { - handle_lua_result!( - id, - execute_nvim_lua_with_args(lua_code, vec![args_rmp]) - ); - } - } - - "nvim_ast" => { - let args_rmp = json_to_rmpv(args); - let lua_code = match action { - "query" => { - r#" - local args = ... - local bufnr = vim.fn.bufnr(args.file) - if bufnr == -1 then return "Buffer not found" end - local query_str = args.query - if not query_str then return "Missing query string" end - local parser = vim.treesitter.get_parser(bufnr) - if not parser then return "No TS parser for buffer" end - local tree = parser:parse()[1] - local root = tree:root() - local query = vim.treesitter.query.parse(parser:lang(), query_str) - local results = {} - for id, node, metadata in query:iter_captures(root, bufnr, 0, -1) do - local name = query.captures[id] - local row1, col1, row2, col2 = node:range() - local text = vim.treesitter.get_node_text(node, bufnr) - table.insert(results, name .. " at line " .. tostring(row1 + 1) .. ":\n" .. text) - end - return table.concat(results, "\n\n") - "# - } "outline" => { r#" local args = ... @@ -2338,24 +1515,191 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { return table.concat(results, "\n") "# } + "query" => { + r#" + local args = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" end + local query_str = args.query + if not query_str then return "Missing query string" end + local parser = vim.treesitter.get_parser(bufnr) + if not parser then return "No TS parser for buffer" end + local tree = parser:parse()[1] + local root = tree:root() + local query = vim.treesitter.query.parse(parser:lang(), query_str) + local results = {} + for id, node, metadata in query:iter_captures(root, bufnr, 0, -1) do + local name = query.captures[id] + local row1, col1, row2, col2 = node:range() + local text = vim.treesitter.get_node_text(node, bufnr) + table.insert(results, name .. " at line " .. tostring(row1 + 1) .. ":\n" .. text) + end + return table.concat(results, "\n\n") + "# + } + _ => "", }; - if lua_code.is_empty() { send_error( id, -32602, - &format!("Invalid nvim_ast action: {}", action), + &format!( + "Invalid or missing action '{}' for nvim_intelligence", + action + ), ) .await; } else { + let args_rmp = json_to_rmpv(args); handle_lua_result!( id, execute_nvim_lua_with_args(lua_code, vec![args_rmp]) ); } } - _ => send_error(id, -32601, "Method not found").await, + "nvim_ui" => match action { + "ghost_text" => { + if let (Some(line), Some(text)) = ( + args.get("line").and_then(serde_json::Value::as_i64), + args.get("text").and_then(|v| v.as_str()), + ) { + let hl_group = args + .get("highlight_group") + .and_then(|v| v.as_str()) + .unwrap_or("Comment"); + match set_nvim_extmark(line, text, hl_group).await { + Ok(()) => { + send_text_result!( + id.clone(), + format!( + "Successfully set extmark on line {}", + line + ) + ); + } + Err(e) => send_error(id, -32603, &e).await, + } + } else { + send_error(id, -32602, "Missing 'line' or 'text'").await; + } + } + "clear" => { + let mut modified_args = args.clone(); + if action == "clear_highlight" + && let Some(obj) = modified_args.as_object_mut() + { + obj.insert("clear_only".to_string(), json!(true)); + } + let args_rmp = json_to_rmpv(&modified_args); + let code = " + local args = ... + local buf = args.buf_id or vim.api.nvim_get_current_buf() + local group = args.group or args.highlight_group or 'IncSearch' + local ns = vim.api.nvim_create_namespace('antigravity_highlight') + + if args.clear_only then + vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) + return 'Cleared highlights' + end + + vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) + local start_line = tonumber(args.start_line) or 1 + local end_line = tonumber(args.end_line) or start_line + for i = start_line - 1, end_line - 1 do + pcall(vim.api.nvim_buf_add_highlight, buf, ns, group, i, 0, -1) + end + + local duration = args.duration_ms or 5000 + if duration > 0 then + vim.defer_fn(function() + pcall(vim.api.nvim_buf_clear_namespace, buf, ns, 0, -1) + end, duration) + end + return 'Highlighted lines ' .. tostring(start_line) .. ' to ' .. tostring(end_line) + "; + handle_lua_result!( + id, + execute_nvim_lua_with_args(code, vec![args_rmp]) + ) + } + _ => { + send_error( + id, + -32602, + &format!("Invalid or missing action '{}' for nvim_ui", action), + ) + .await + } + }, + "nvim_exec" => match action { + "lua" => { + if let Some(code) = args.get("code").and_then(|v| v.as_str()) { + // READ-ONLY ENFORCEMENT: Block any Lua code that attempts to mutate state. + let lower_code = code.to_lowercase(); + if lower_code.contains("vim.cmd") + || lower_code.contains("nvim_buf_set_lines") + || lower_code.contains("nvim_buf_set_text") + || lower_code.contains("nvim_command") + || lower_code.contains("nvim_set_current") + || lower_code.contains("nvim_win_set") + || lower_code.contains("nvim_buf_set_name") + { + send_error(id, -32600, "CRITICAL ERROR: nvim_exec is restricted to READ-ONLY queries to prevent agents from bypassing the specialized visual tools (nvim_buffer, nvim_ui). Use the designated tools to mutate editor state.").await; + return; + } + + handle_lua_result!(id, execute_nvim_lua(code)); + } else { + send_error(id, -32602, "Missing 'code'").await; + } + } + "vimscript" => { + if let Some(code) = args.get("code").and_then(|v| v.as_str()) { + let lua_code = + format!("vim.cmd([=[{}]=]); return 'Success'", code); + handle_lua_result!(id, execute_nvim_lua(&lua_code)); + } else { + send_error(id, -32602, "Missing 'code'").await; + } + } + "terminal" => { + if let Some(cmd) = args.get("command").and_then(|v| v.as_str()) { + let lua_code = r#" + local cmd = ... + if _G.gemini and _G.gemini.send_to_terminal then + _G.gemini.send_to_terminal(cmd) + return "Command sent to terminal" + else + error("gemini UI not loaded") + end + "#; + handle_lua_result!( + id, + execute_nvim_lua_with_args( + lua_code, + vec![rmpv::Value::String(cmd.into())] + ) + ); + } else { + send_error(id, -32602, "Missing 'command'").await; + } + } + _ => { + send_error( + id, + -32602, + &format!( + "Invalid or missing action '{}' for nvim_exec", + action + ), + ) + .await + } + }, + _ => { + send_error(id, -32601, &format!("Method not found: {}", name)).await; + } } } _ => {