feat(nvim): update nvim_buffer edit to handle multiple block edits
This commit is contained in:
1 parent
d6acd885dd
commit
b4431a4dcd
1 file changed
+95
-25
+95
-25
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user