diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index cc99a5e..d9d77fb 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -1092,11 +1092,12 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { "properties": { "action": { "type": "string", - "enum": ["get_active", "read", "open", "create_scratch", "save", "reload", "close", "list", "search", "edit"], + "enum": ["get_active", "read", "open", "create_scratch", "save", "reload", "close", "list", "search", "edit", "undo", "redo"], "description": "Action to perform on the buffer" }, "file": { "type": "string", "description": "Target file path (for open, read, search)" }, "buf_id": { "type": "integer", "description": "Buffer ID (for close, reload, or split)" }, + "steps": { "type": "integer", "description": "Number of undo/redo steps to apply (default: 1)" }, "content": { "type": "string", "description": "Initial text content (for create_scratch)" }, "name": { "type": "string", "description": "Buffer display name (for create_scratch)" }, "filetype": { "type": "string", "description": "Filetype syntax (for open, create_scratch)" }, @@ -1245,6 +1246,43 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { }, "required": ["action"] } + }, + { + "name": "nvim_lsp", + "description": "Language Server Protocol integration for semantic queries and safe workspace-wide refactoring.", + "inputSchema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["rename", "definition", "references", "hover", "code_action"], + "description": "LSP action to perform" + }, + "file": { "type": "string", "description": "Target file path" }, + "line": { "type": "integer", "description": "1-indexed line number" }, + "col": { "type": "integer", "description": "0-indexed column number" }, + "new_name": { "type": "string", "description": "New name for rename action" } + }, + "required": ["action"] + } + }, + { + "name": "nvim_ast", + "description": "Tree-sitter AST queries for semantic code exploration and outlining.", + "inputSchema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["query", "outline"], + "description": "AST action to perform" + }, + "file": { "type": "string", "description": "Target file path" }, + "query": { "type": "string", "description": "Tree-sitter query string" }, + "preset": { "type": "string", "description": "Query preset (e.g., 'functions', 'classes')" } + }, + "required": ["action"] + } } ] })), @@ -1456,6 +1494,28 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { execute_nvim_lua_with_args(code, vec![args_rmp]) ) } + "undo" | "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 { + format!("{}u", steps) + } else { + format!("{}", steps) + }; + let code = format!( + " + local pcall_ok, err = pcall(function() + vim.cmd('normal! {}') + end) + if not pcall_ok then + error('Failed to {}: ' .. tostring(err)) + end + return '{} applied {} step(s)' + ", + cmd, action, action, steps + ); + handle_lua_result!(id, execute_nvim_lua(&code)); + } "open" => { let args_rmp = json_to_rmpv(args); let code = " @@ -2100,6 +2160,229 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { } }, + "nvim_lsp" => { + let args_rmp = json_to_rmpv(args); + let lua_code = match action { + "hover" => { + r#" + local args = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" end + local params = vim.lsp.util.make_position_params(0) + params.position.line = (tonumber(args.line) or 1) - 1 + params.position.character = tonumber(args.col) or 0 + params.textDocument.uri = vim.uri_from_bufnr(bufnr) + local res, err = vim.lsp.buf_request_sync(bufnr, 'textDocument/hover', params, 2000) + if err or not res then return "Error or timeout" end + for client_id, response in pairs(res) do + if response.result and response.result.contents then + local contents = response.result.contents + if type(contents) == 'table' and contents.value then return contents.value end + if type(contents) == 'string' then return contents end + end + end + return "No hover info" + "# + } + "definition" => { + r#" + local args = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" end + local params = vim.lsp.util.make_position_params(0) + params.position.line = (tonumber(args.line) or 1) - 1 + params.position.character = tonumber(args.col) or 0 + params.textDocument.uri = vim.uri_from_bufnr(bufnr) + local res, err = vim.lsp.buf_request_sync(bufnr, 'textDocument/definition', params, 2000) + if err or not res then return "Error or timeout" end + local locations = {} + for client_id, response in pairs(res) do + if response.result then + local res_locs = vim.tbl_islist(response.result) and response.result or {response.result} + for _, loc in ipairs(res_locs) do + local uri = loc.uri or loc.targetUri + local range = loc.range or loc.targetSelectionRange + table.insert(locations, vim.uri_to_fname(uri) .. ":" .. (range.start.line + 1)) + end + end + end + if #locations == 0 then return "No definition found" end + return table.concat(locations, "\n") + "# + } + "references" => { + r#" + local args = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" end + local params = vim.lsp.util.make_position_params(0) + params.position.line = (tonumber(args.line) or 1) - 1 + params.position.character = tonumber(args.col) or 0 + params.context = { includeDeclaration = true } + params.textDocument.uri = vim.uri_from_bufnr(bufnr) + local res, err = vim.lsp.buf_request_sync(bufnr, 'textDocument/references', params, 5000) + if err or not res then return "Error or timeout" end + local locations = {} + for client_id, response in pairs(res) do + if response.result then + for _, loc in ipairs(response.result) do + table.insert(locations, vim.uri_to_fname(loc.uri) .. ":" .. (loc.range.start.line + 1)) + end + end + end + if #locations == 0 then return "No references found" end + return table.concat(locations, "\n") + "# + } + "rename" => { + r#" + local args = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" end + local new_name = args.new_name + if not new_name then return "Missing new_name" end + local params = vim.lsp.util.make_position_params(0) + params.position.line = (tonumber(args.line) or 1) - 1 + params.position.character = tonumber(args.col) or 0 + params.newName = new_name + params.textDocument.uri = vim.uri_from_bufnr(bufnr) + local res, err = vim.lsp.buf_request_sync(bufnr, 'textDocument/rename', params, 2000) + if err or not res then return "Error or timeout" end + local applied = false + for client_id, response in pairs(res) do + if response.result then + vim.lsp.util.apply_workspace_edit(response.result, vim.lsp.get_client_by_id(client_id).offset_encoding) + applied = true + end + end + return applied and "Rename applied" or "No rename edits" + "# + } + "code_action" => { + r#" + local args = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" end + local params = vim.lsp.util.make_range_params(0) + params.position = nil + params.range.start.line = (tonumber(args.line) or 1) - 1 + params.range.start.character = tonumber(args.col) or 0 + params.range['end'] = params.range.start + params.context = { diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr, params.range.start.line) } + params.textDocument.uri = vim.uri_from_bufnr(bufnr) + local res, err = vim.lsp.buf_request_sync(bufnr, 'textDocument/codeAction', params, 2000) + if err or not res then return "Error or timeout" end + + local actions = {} + for client_id, response in pairs(res) do + if response.result then + for _, action in ipairs(response.result) do + if action.edit then + vim.lsp.util.apply_workspace_edit(action.edit, vim.lsp.get_client_by_id(client_id).offset_encoding) + table.insert(actions, "Applied: " .. (action.title or "unnamed")) + elseif action.command then + table.insert(actions, "Found command (not auto-applied): " .. (action.title or "unnamed")) + end + end + end + end + if #actions == 0 then return "No code actions available or applied" end + 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 = ... + local bufnr = vim.fn.bufnr(args.file) + if bufnr == -1 then return "Buffer not found" 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 lang = parser:lang() + + local q_str = "" + if lang == "rust" then + q_str = "(function_item name: (identifier) @name) (impl_item type: (type_identifier) @name) (struct_item name: (type_identifier) @name) (trait_item name: (type_identifier) @name)" + elseif lang == "python" then + q_str = "(function_definition name: (identifier) @name) (class_definition name: (identifier) @name)" + elseif lang == "javascript" or lang == "typescript" then + q_str = "(function_declaration name: (identifier) @name) (class_declaration name: (identifier) @name) (method_definition name: (property_identifier) @name)" + else + return "Outline preset not supported for language: " .. lang + end + + local query = vim.treesitter.query.parse(lang, q_str) + local results = {} + for id, node, metadata in query:iter_captures(root, bufnr, 0, -1) do + local name_text = vim.treesitter.get_node_text(node, bufnr) + local parent = node:parent() + local row1, col1, row2, col2 = parent:range() + table.insert(results, name_text .. " (Line " .. tostring(row1 + 1) .. "-" .. tostring(row2 + 1) .. ")") + end + return table.concat(results, "\n") + "# + } + _ => "", + }; + + if lua_code.is_empty() { + send_error( + id, + -32602, + &format!("Invalid nvim_ast action: {}", action), + ) + .await; + } else { + handle_lua_result!( + id, + execute_nvim_lua_with_args(lua_code, vec![args_rmp]) + ); + } + } _ => send_error(id, -32601, "Method not found").await, } }