feat(nvim): update nvim_buffer edit to handle multiple block edits

This commit is contained in:
Riz Ashraf committed 2026-10-09 00:18:01 +01:00
1 parent d6acd885dd
commit b4431a4dcd
1 file changed
+95 -25
+95 -25
View File
@@ -1091,16 +1091,30 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
"enum": ["get_active", "read", "open", "create_scratch", "save", "reload", "close", "list", "search", "edit"],
"description": "Action to perform on the buffer"
},
"file": { "type": "string", "description": "Target file path (for open, read, search, edit)" },
"file": { "type": "string", "description": "Target file path (for open, read, search)" },
"buf_id": { "type": "integer", "description": "Buffer ID (for close, reload, or split)" },
"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)" },
"start_line": { "type": "integer", "description": "1-indexed start line (for read, edit)" },
"end_line": { "type": "integer", "description": "1-indexed end line (for read, edit)" },
"replacement_content": { "type": "string", "description": "New content for the specified line range (for edit)" },
"start_line": { "type": "integer", "description": "1-indexed start line (for read)" },
"end_line": { "type": "integer", "description": "1-indexed end line (for read)" },
"pattern": { "type": "string", "description": "Regex or substring pattern to search for (for search)" },
"force": { "type": "boolean", "description": "Force reload or close (for reload, close)" }
"force": { "type": "boolean", "description": "Force reload or close (for reload, close)" },
"edits": {
"type": "array",
"description": "Array of edits to apply sequentially (for edit). Grouped by file, applied in descending order.",
"items": {
"type": "object",
"properties": {
"file": { "type": "string", "description": "Target file path" },
"start_line": { "type": "integer", "description": "1-indexed start line" },
"end_line": { "type": "integer", "description": "1-indexed end line" },
"replacement_content": { "type": "string", "description": "New content" },
"expected_content": { "type": "string", "description": "Optional: exact content expected in the replacement range to prevent line drift corruption" }
},
"required": ["file", "start_line", "end_line", "replacement_content"]
}
}
},
"required": ["action"]
}
@@ -1327,7 +1341,16 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
}
"edit" => {
let mut modified_args = args.clone();
if let Some(content) = modified_args.get_mut("replacement_content") {
if let Some(edits) = modified_args.get_mut("edits").and_then(|v| v.as_array_mut()) {
for edit in edits.iter_mut() {
if let Some(content) = edit.get_mut("replacement_content") {
if let Some(s) = content.as_str() {
let lines: Vec<String> = s.split('\n').map(|l| l.trim_end_matches('\r').to_string()).collect();
*content = serde_json::json!(lines);
}
}
}
} else if let Some(content) = modified_args.get_mut("replacement_content") {
if let Some(s) = content.as_str() {
let lines: Vec<String> = s.split('\n').map(|l| l.trim_end_matches('\r').to_string()).collect();
*content = serde_json::json!(lines);
@@ -1336,36 +1359,83 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
let args_rmp = json_to_rmpv(&modified_args);
let code = "
local args = ...
local file = args.file
if not file or file == '' or file == vim.NIL then
error(\"Missing 'file'\")
local edits = args.edits
if not edits or type(edits) ~= 'table' then
if args.file and args.start_line and args.end_line and args.replacement_content then
edits = {
{
file = args.file,
start_line = tonumber(args.start_line),
end_line = tonumber(args.end_line),
replacement_content = args.replacement_content,
expected_content = args.expected_content
}
}
else
error(\"Missing 'edits' array or valid single edit parameters\")
end
local start_line = tonumber(args.start_line)
if not start_line then
error(\"Missing 'start_line'\")
end
local end_line = tonumber(args.end_line)
if not end_line then
error(\"Missing 'end_line'\")
end
local content_lines = args.replacement_content
if not content_lines or content_lines == vim.NIL then
error(\"Missing 'replacement_content'\")
end
local grouped_edits = {}
for i, edit in ipairs(edits) do
local file = edit.file
if not file or file == '' or file == vim.NIL then
error(\"Edit missing 'file'\")
end
if not grouped_edits[file] then
grouped_edits[file] = {}
end
table.insert(grouped_edits[file], {
start_line = tonumber(edit.start_line),
end_line = tonumber(edit.end_line),
replacement_content = edit.replacement_content,
expected_content = edit.expected_content,
original_index = i
})
end
local results = {}
for file, file_edits in pairs(grouped_edits) do
local bufnr = vim.fn.bufnr(file, true)
if vim.fn.bufloaded(bufnr) == 0 then
vim.fn.bufload(bufnr)
end
vim.api.nvim_buf_set_lines(bufnr, start_line - 1, end_line, false, content_lines)
table.sort(file_edits, function(a, b)
return a.start_line > b.start_line
end)
local win = vim.fn.bufwinid(bufnr)
if win ~= -1 then
pcall(vim.api.nvim_win_set_cursor, win, {start_line, 0})
for _, edit in ipairs(file_edits) do
local sl = edit.start_line
local el = edit.end_line
local rc = edit.replacement_content
local ec = edit.expected_content
if not sl then error(\"Edit missing 'start_line'\") end
if not el then error(\"Edit missing 'end_line'\") end
if not rc or rc == vim.NIL then error(\"Edit missing 'replacement_content'\") end
if ec and type(ec) == 'string' and ec ~= vim.NIL and ec ~= '' then
local current_lines = vim.api.nvim_buf_get_lines(bufnr, sl - 1, el, false)
local current_text = table.concat(current_lines, '\\n')
local ec_clean = string.gsub(ec, '\\r', '')
if current_text ~= ec_clean then
error(\"Safety check failed in \" .. file .. \": Expected content at lines \" .. tostring(sl) .. \"-\" .. tostring(el) .. \" does not match actual content.\\nExpected:\\n\" .. ec_clean .. \"\\nActual:\\n\" .. current_text)
end
end
return 'Successfully edited lines ' .. tostring(start_line) .. ' to ' .. tostring(end_line) .. ' in ' .. file
vim.api.nvim_buf_set_lines(bufnr, sl - 1, el, false, rc)
end
local win = vim.fn.bufwinid(bufnr)
if win ~= -1 and #file_edits > 0 then
pcall(vim.api.nvim_win_set_cursor, win, {file_edits[#file_edits].start_line, 0})
end
table.insert(results, \"Edited \" .. file .. \" (\" .. tostring(#file_edits) .. \" blocks)\")
end
return table.concat(results, ', ')
";
handle_lua_result!(
id,