feat(nvim): update nvim_buffer edit to handle multiple block edits
This commit is contained in:
1 parent
d6acd885dd
commit
b4431a4dcd
1 file changed
+99
-29
+99
-29
@@ -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"],
|
"enum": ["get_active", "read", "open", "create_scratch", "save", "reload", "close", "list", "search", "edit"],
|
||||||
"description": "Action to perform on the buffer"
|
"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)" },
|
"buf_id": { "type": "integer", "description": "Buffer ID (for close, reload, or split)" },
|
||||||
"content": { "type": "string", "description": "Initial text content (for create_scratch)" },
|
"content": { "type": "string", "description": "Initial text content (for create_scratch)" },
|
||||||
"name": { "type": "string", "description": "Buffer display name (for create_scratch)" },
|
"name": { "type": "string", "description": "Buffer display name (for create_scratch)" },
|
||||||
"filetype": { "type": "string", "description": "Filetype syntax (for open, create_scratch)" },
|
"filetype": { "type": "string", "description": "Filetype syntax (for open, create_scratch)" },
|
||||||
"start_line": { "type": "integer", "description": "1-indexed start line (for read, edit)" },
|
"start_line": { "type": "integer", "description": "1-indexed start line (for read)" },
|
||||||
"end_line": { "type": "integer", "description": "1-indexed end line (for read, edit)" },
|
"end_line": { "type": "integer", "description": "1-indexed end line (for read)" },
|
||||||
"replacement_content": { "type": "string", "description": "New content for the specified line range (for edit)" },
|
|
||||||
"pattern": { "type": "string", "description": "Regex or substring pattern to search for (for search)" },
|
"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"]
|
"required": ["action"]
|
||||||
}
|
}
|
||||||
@@ -1327,7 +1341,16 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
"edit" => {
|
"edit" => {
|
||||||
let mut modified_args = args.clone();
|
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() {
|
if let Some(s) = content.as_str() {
|
||||||
let lines: Vec<String> = s.split('\n').map(|l| l.trim_end_matches('\r').to_string()).collect();
|
let lines: Vec<String> = s.split('\n').map(|l| l.trim_end_matches('\r').to_string()).collect();
|
||||||
*content = serde_json::json!(lines);
|
*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 args_rmp = json_to_rmpv(&modified_args);
|
||||||
let code = "
|
let code = "
|
||||||
local args = ...
|
local args = ...
|
||||||
local file = args.file
|
local edits = args.edits
|
||||||
if not file or file == '' or file == vim.NIL then
|
if not edits or type(edits) ~= 'table' then
|
||||||
error(\"Missing 'file'\")
|
if args.file and args.start_line and args.end_line and args.replacement_content then
|
||||||
end
|
edits = {
|
||||||
local start_line = tonumber(args.start_line)
|
{
|
||||||
if not start_line then
|
file = args.file,
|
||||||
error(\"Missing 'start_line'\")
|
start_line = tonumber(args.start_line),
|
||||||
end
|
end_line = tonumber(args.end_line),
|
||||||
local end_line = tonumber(args.end_line)
|
replacement_content = args.replacement_content,
|
||||||
if not end_line then
|
expected_content = args.expected_content
|
||||||
error(\"Missing 'end_line'\")
|
}
|
||||||
end
|
}
|
||||||
local content_lines = args.replacement_content
|
else
|
||||||
if not content_lines or content_lines == vim.NIL then
|
error(\"Missing 'edits' array or valid single edit parameters\")
|
||||||
error(\"Missing 'replacement_content'\")
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local bufnr = vim.fn.bufnr(file, true)
|
local grouped_edits = {}
|
||||||
if vim.fn.bufloaded(bufnr) == 0 then
|
for i, edit in ipairs(edits) do
|
||||||
vim.fn.bufload(bufnr)
|
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
|
end
|
||||||
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, start_line - 1, end_line, false, content_lines)
|
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
|
||||||
|
|
||||||
local win = vim.fn.bufwinid(bufnr)
|
table.sort(file_edits, function(a, b)
|
||||||
if win ~= -1 then
|
return a.start_line > b.start_line
|
||||||
pcall(vim.api.nvim_win_set_cursor, win, {start_line, 0})
|
end)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
return 'Successfully edited lines ' .. tostring(start_line) .. ' to ' .. tostring(end_line) .. ' in ' .. file
|
return table.concat(results, ', ')
|
||||||
";
|
";
|
||||||
handle_lua_result!(
|
handle_lua_result!(
|
||||||
id,
|
id,
|
||||||
|
|||||||
Reference in new issue
Block a user