feat(nvim): add nvim_lsp, nvim_ast, buffer undo/redo

This commit is contained in:
Riz Ashraf committed 2026-10-09 00:51:05 +01:00
1 parent 3765e6b05d
commit 1ed75ca156
1 file changed
+284 -1
+284 -1
View File
@@ -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!("{}<C-r>", 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,
}
}