From 0bf8a6bf7dffd713a6aa67b5732a2c5778c4cb68 Mon Sep 17 00:00:00 2001 From: Hsiu-Chieh Lee Date: Wed, 11 Mar 2026 03:44:45 +0800 Subject: [PATCH] feat: Ensure ClaudeCodeAdd correctly escapes file path arg --- lua/claudecode/init.lua | 42 +++++++++++- tests/unit/claudecode_add_command_spec.lua | 80 ++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index c4b7744e..ce83c867 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -900,6 +900,46 @@ function M._create_commands() desc = "Add selected file(s) from tree explorer to Claude Code context (supports visual selection)", }) + --- Parse a command-line string respecting backslash-escaped spaces. + --- Treats `\ ` as a literal space within a token and unescaped whitespace as delimiters. + --- @param input string + --- @return string[] + local function parse_escaped_args(input) + local args = {} + local current = {} + local i = 1 + local len = #input + while i <= len do + local ch = input:sub(i, i) + if ch == "\\" and i < len then + local next_ch = input:sub(i + 1, i + 1) + if next_ch == " " then + current[#current + 1] = " " + i = i + 2 + elseif next_ch == "\\" then + current[#current + 1] = "\\" + i = i + 2 + else + current[#current + 1] = ch + i = i + 1 + end + elseif ch:match("%s") then + if #current > 0 then + args[#args + 1] = table.concat(current) + current = {} + end + i = i + 1 + else + current[#current + 1] = ch + i = i + 1 + end + end + if #current > 0 then + args[#args + 1] = table.concat(current) + end + return args + end + vim.api.nvim_create_user_command("ClaudeCodeAdd", function(opts) if not M.state.server then logger.error("command", "ClaudeCodeAdd: Claude Code integration is not running.") @@ -911,7 +951,7 @@ function M._create_commands() return end - local args = vim.split(opts.args, "%s+") + local args = parse_escaped_args(opts.args) local file_path = args[1] local start_line = args[2] and tonumber(args[2]) or nil local end_line = args[3] and tonumber(args[3]) or nil diff --git a/tests/unit/claudecode_add_command_spec.lua b/tests/unit/claudecode_add_command_spec.lua index 5f98f652..1f3cb4e2 100644 --- a/tests/unit/claudecode_add_command_spec.lua +++ b/tests/unit/claudecode_add_command_spec.lua @@ -187,6 +187,86 @@ describe("ClaudeCodeAdd command", function() end) end) + describe("escaped path handling", function() + it("should parse backslash-escaped spaces in file paths", function() + vim.fn.filereadable = spy.new(function(path) + return path == "file name.lua" and 1 or 0 + end) + vim.fn.expand = spy.new(function(path) + return path + end) + + command_handler({ args = "file\\ name.lua" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "file name.lua", + lineStart = nil, + lineEnd = nil, + }) + end) + + it("should not treat 'file\\ 1' as path with line number", function() + vim.fn.filereadable = spy.new(function(path) + return path == "file 1" and 1 or 0 + end) + vim.fn.expand = spy.new(function(path) + return path + end) + + command_handler({ args = "file\\ 1" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "file 1", + lineStart = nil, + lineEnd = nil, + }) + end) + + it("should parse escaped path with line range", function() + vim.fn.filereadable = spy.new(function(path) + return path == "my test file.lua" and 1 or 0 + end) + vim.fn.expand = spy.new(function(path) + return path + end) + + command_handler({ args = "my\\ test\\ file.lua 10 20" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "my test file.lua", + lineStart = 9, + lineEnd = 19, + }) + end) + + it("should parse escaped path with start line only", function() + vim.fn.filereadable = spy.new(function(path) + return path == "file name.lua" and 1 or 0 + end) + vim.fn.expand = spy.new(function(path) + return path + end) + + command_handler({ args = "file\\ name.lua 5" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "file name.lua", + lineStart = 4, + lineEnd = nil, + }) + end) + + it("should still handle paths without spaces", function() + command_handler({ args = "/existing/file.lua 10" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "/existing/file.lua", + lineStart = 9, + lineEnd = nil, + }) + end) + end) + describe("path handling", function() it("should expand tilde paths", function() command_handler({ args = "~/test.lua" })