From 7707f682fab8295f24c45d1350af2e48928ce649 Mon Sep 17 00:00:00 2001 From: Tait van Strien Date: Fri, 21 Aug 2026 15:56:46 +0200 Subject: [PATCH] fix(command): handle spaces in ClaudeCodeAdd file paths Vim expands `%`/`#`/etc. in the Ex command line before the user-command callback runs, so `:ClaudeCodeAdd %` on a buffer whose path contains a space arrived as an already-expanded path string. The naive whitespace split then broke it into bogus path/line-number tokens, producing "Too many arguments" or "Invalid start line number" errors. Replace the split with a parser that treats trailing numeric tokens as line numbers and rejoins the rest as the path, and additionally supports a quoted path when a line range follows. Fixes #314 --- README.md | 2 +- lua/claudecode/init.lua | 77 ++++++++++++++++++---- tests/mocks/vim.lua | 4 ++ tests/unit/claudecode_add_command_spec.lua | 52 +++++++++++++++ 4 files changed, 120 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4e3f10f2..904eaf61 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ Configure the plugin with the detected path: - `:ClaudeCodeSelectModel` - Select Claude model and open terminal with optional arguments - `:ClaudeCodeSend` - Send current visual selection to Claude - `:ClaudeCodeSendText {text}` - Send text to the open Claude terminal and submit it (`!` to insert without submitting; `native`/`snacks` providers only) -- `:ClaudeCodeAdd [start-line] [end-line]` - Add specific file to Claude context with optional line range +- `:ClaudeCodeAdd [start-line] [end-line]` - Add specific file to Claude context with optional line range. A path containing spaces works unquoted (`:ClaudeCodeAdd my file.lua`) as long as no line range follows; add quotes when you also pass line numbers (`:ClaudeCodeAdd "my file.lua" 10 20`). - `:ClaudeCodeDiffAccept` - Accept diff changes - `:ClaudeCodeDiffDeny` - Reject diff changes - `:ClaudeCodeCloseAllDiffs` - Close pending Claude diffs (leaves accepted/saved diffs intact) diff --git a/lua/claudecode/init.lua b/lua/claudecode/init.lua index 58dc4659..085c2539 100644 --- a/lua/claudecode/init.lua +++ b/lua/claudecode/init.lua @@ -303,6 +303,55 @@ local function fire_send_complete(file_path, start_line, end_line, context) }) end +---Parse the raw argument string of `:ClaudeCodeAdd` into a file path and up to +---two line-number tokens. A quoted path (`"my file.lua" 10 20`) is honored +---verbatim; otherwise trailing tokens that parse as integers (at most two) are +---treated as line numbers and the remaining tokens are rejoined with spaces as +---the path, so unquoted paths containing spaces (e.g. `:ClaudeCodeAdd my file.lua`) +---still resolve correctly instead of being split apart (issue #314). +---@param raw string Raw `opts.args` string from the user command +---@return string|nil file_path +---@return string|nil start_arg +---@return string|nil end_arg +---@return string|nil error_msg +local function parse_add_command_args(raw) + raw = vim.trim(raw or "") + local file_path, rest + + local quote = raw:sub(1, 1) + if quote == '"' or quote == "'" then + local close = raw:find(quote, 2, true) + if not close then + return nil, nil, nil, "Unterminated quote in file path" + end + file_path = raw:sub(2, close - 1) + rest = vim.trim(raw:sub(close + 1)) + else + local tokens = {} + for tok in raw:gmatch("%S+") do + tokens[#tokens + 1] = tok + end + + local line_count = 0 + while line_count < 2 and #tokens - line_count > 1 and tonumber(tokens[#tokens - line_count]) do + line_count = line_count + 1 + end + + file_path = table.concat(tokens, " ", 1, #tokens - line_count) + rest = table.concat(tokens, " ", #tokens - line_count + 1, #tokens) + end + + local line_args = {} + for tok in rest:gmatch("%S+") do + line_args[#line_args + 1] = tok + end + if #line_args > 2 then + return nil, nil, nil, "Too many arguments. Usage: ClaudeCodeAdd [start-line] [end-line]" + end + + return file_path, line_args[1], line_args[2], nil +end + ---Send @ mention to Claude Code, handling connection state automatically ---@param file_path string The file path to send ---@param start_line number|nil Start line (0-indexed for Claude) @@ -989,26 +1038,26 @@ function M._create_commands() return end - local args = vim.split(opts.args, "%s+") - 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 - - if #args > 3 then - logger.error( - "command", - "ClaudeCodeAdd: Too many arguments. Usage: ClaudeCodeAdd [start-line] [end-line]" - ) + local file_path, start_arg, end_arg, parse_err = parse_add_command_args(opts.args) + if parse_err then + logger.error("command", "ClaudeCodeAdd: " .. parse_err) return end + if not file_path or file_path == "" then + logger.error("command", "ClaudeCodeAdd: No file path provided") + return + end + + local start_line = start_arg and tonumber(start_arg) or nil + local end_line = end_arg and tonumber(end_arg) or nil - if args[2] and not start_line then - logger.error("command", "ClaudeCodeAdd: Invalid start line number: " .. args[2]) + if start_arg and not start_line then + logger.error("command", "ClaudeCodeAdd: Invalid start line number: " .. start_arg) return end - if args[3] and not end_line then - logger.error("command", "ClaudeCodeAdd: Invalid end line number: " .. args[3]) + if end_arg and not end_line then + logger.error("command", "ClaudeCodeAdd: Invalid end line number: " .. end_arg) return end diff --git a/tests/mocks/vim.lua b/tests/mocks/vim.lua index a9f2d7b6..ad0b4d65 100644 --- a/tests/mocks/vim.lua +++ b/tests/mocks/vim.lua @@ -829,6 +829,10 @@ local vim = { end, }, + trim = function(str) + return (str:gsub("^%s+", ""):gsub("%s+$", "")) + end, + split = function(str, sep, opts) local plain = opts and opts.plain local result = {} diff --git a/tests/unit/claudecode_add_command_spec.lua b/tests/unit/claudecode_add_command_spec.lua index ef77838e..c11fcd03 100644 --- a/tests/unit/claudecode_add_command_spec.lua +++ b/tests/unit/claudecode_add_command_spec.lua @@ -452,6 +452,58 @@ describe("ClaudeCodeAdd command", function() end) end) + describe("paths containing spaces (issue #314)", function() + it("should keep an unquoted path with spaces intact when no line numbers are given", function() + vim.fn.filereadable = spy.new(function(path) + return path == "/existing/my file.lua" and 1 or 0 + end) + + command_handler({ args = "/existing/my file.lua" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "/existing/my file.lua", + lineStart = nil, + lineEnd = nil, + }) + assert.spy(mock_logger.error).was_not_called() + end) + + it("should split trailing numeric tokens off an unquoted path with spaces", function() + vim.fn.filereadable = spy.new(function(path) + return path == "/existing/my file.lua" and 1 or 0 + end) + + command_handler({ args = "/existing/my file.lua 10 20" }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "/existing/my file.lua", + lineStart = 9, + lineEnd = 19, + }) + end) + + it("should honor a quoted path containing spaces", function() + vim.fn.filereadable = spy.new(function(path) + return path == "/existing/my file.lua" and 1 or 0 + end) + + command_handler({ args = '"/existing/my file.lua" 10 20' }) + + assert.spy(mock_server.broadcast).was_called_with("at_mentioned", { + filePath = "/existing/my file.lua", + lineStart = 9, + lineEnd = 19, + }) + end) + + it("should error on an unterminated quote", function() + command_handler({ args = '"/existing/my file.lua' }) + + assert.spy(mock_logger.error).was_called() + assert.spy(mock_server.broadcast).was_not_called() + end) + end) + describe("path expansion with line ranges", function() it("should expand tilde paths with line numbers", function() command_handler({ args = "~/test.lua 10 20" })