Skip to content

Commit de1729a

Browse files
committed
fix: visual selection on send
1 parent c0b46e2 commit de1729a

2 files changed

Lines changed: 151 additions & 1 deletion

File tree

lua/codex/init.lua

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ local send_dispatch_mod = require("codex.runtime.send_dispatch")
88
local mention_mod = require("codex.context.mention")
99
local prompt_submit = require("codex.context.prompt_submit")
1010

11+
local CTRL_V = string.char(22)
1112
local SAVED_PROMPT_NOTIFY_MSG = "Saved current prompt to unnamed register"
1213
local COULD_NOT_SAVE_PROMPT_NOTIFY_MSG = "Could not save existing prompt before clearing"
1314

@@ -377,6 +378,104 @@ local function log_selection_failure(deps, subject, err)
377378
deps.logger.error("failed to collect %s: %s", target, err or "unknown error")
378379
end
379380

381+
---Check whether a value is an integer >= 1.
382+
---@param value any
383+
---@return boolean
384+
local function is_positive_integer(value)
385+
return type(value) == "number" and value >= 1 and math.floor(value) == value
386+
end
387+
388+
---Check whether a value is an integer >= 0.
389+
---@param value any
390+
---@return boolean
391+
local function is_non_negative_integer(value)
392+
return type(value) == "number" and value >= 0 and math.floor(value) == value
393+
end
394+
395+
---Return a shallow copy of a possibly nil table.
396+
---@param opts table|nil
397+
---@return table
398+
local function copy_opts(opts)
399+
local copied = {}
400+
for key, value in pairs(opts or {}) do
401+
copied[key] = value
402+
end
403+
return copied
404+
end
405+
406+
---Resolve active visual selection metadata when explicit range opts are missing.
407+
---This supports first-use lazy-key visual mappings where visual marks may be unset.
408+
---@param deps table
409+
---@param opts? codex.SelectionOpts
410+
---@return codex.SelectionOpts
411+
local function resolve_selection_opts(deps, opts)
412+
local resolved = copy_opts(opts)
413+
if is_positive_integer(resolved.line1) and is_positive_integer(resolved.line2) then
414+
return resolved
415+
end
416+
417+
local fn = deps.vim.fn or {}
418+
if type(fn.mode) ~= "function" then
419+
return resolved
420+
end
421+
422+
local ok_mode, visual_mode = pcall(fn.mode, 1)
423+
if not ok_mode then
424+
return resolved
425+
end
426+
if visual_mode ~= "v" and visual_mode ~= "V" and visual_mode ~= CTRL_V then
427+
return resolved
428+
end
429+
430+
if resolved.visual_mode == nil then
431+
resolved.visual_mode = visual_mode
432+
end
433+
434+
if type(fn.getpos) ~= "function" then
435+
return resolved
436+
end
437+
local api = deps.vim.api or {}
438+
if type(api.nvim_win_get_cursor) ~= "function" then
439+
return resolved
440+
end
441+
442+
local ok_anchor, anchor = pcall(fn.getpos, "v")
443+
local ok_cursor, cursor = pcall(api.nvim_win_get_cursor, 0)
444+
if not ok_anchor or type(anchor) ~= "table" then
445+
return resolved
446+
end
447+
if not ok_cursor or type(cursor) ~= "table" then
448+
return resolved
449+
end
450+
451+
local anchor_line = anchor[2]
452+
local anchor_col = anchor[3]
453+
local cursor_line = cursor[1]
454+
local cursor_col = cursor[2]
455+
456+
if not is_positive_integer(anchor_line) or not is_positive_integer(cursor_line) then
457+
return resolved
458+
end
459+
if not is_positive_integer(anchor_col) or not is_non_negative_integer(cursor_col) then
460+
return resolved
461+
end
462+
463+
if not is_positive_integer(resolved.line1) then
464+
resolved.line1 = anchor_line
465+
end
466+
if not is_positive_integer(resolved.line2) then
467+
resolved.line2 = cursor_line
468+
end
469+
if not is_non_negative_integer(resolved.start_col) then
470+
resolved.start_col = anchor_col - 1
471+
end
472+
if not is_non_negative_integer(resolved.end_col) then
473+
resolved.end_col = cursor_col
474+
end
475+
476+
return resolved
477+
end
478+
380479
---Formats current buffer reference and sends it as bracketed paste.
381480
---@param opts? codex.SelectionOpts Buffer override via `opts.bufnr`.
382481
---@return codex.SendResult ok True when buffer payload is sent.
@@ -404,7 +503,8 @@ end
404503
function M.send_selection(opts)
405504
ensure_setup()
406505
local deps = get_deps()
407-
local spec, err = deps.selection.get_visual_selection(deps.vim, opts)
506+
local spec, err =
507+
deps.selection.get_visual_selection(deps.vim, resolve_selection_opts(deps, opts))
408508
if not spec then
409509
log_selection_failure(deps, "selection", err)
410510
return false, err

tests/unit/init_send_selection_spec.lua

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local helpers = require("tests.unit.helpers.init_spec_helpers")
22
local setup_with_deps = helpers.setup_with_deps
33
local run_deferred = helpers.run_deferred
4+
local CTRL_V = string.char(22)
45

56
describe("codex.init public api send_selection", function()
67
before_each(function()
@@ -125,9 +126,58 @@ describe("codex.init public api send_selection", function()
125126

126127
it("send_selection forwards explicit range options to selection extractor", function()
127128
local env = setup_with_deps()
129+
env.fake_vim.fn.mode = function()
130+
return "v"
131+
end
132+
env.fake_vim.fn.getpos = function()
133+
return { 0, 8, 6, 0 }
134+
end
135+
env.fake_vim._set_buf_cursor(1, 0, 10, 4)
128136

129137
env.codex.send_selection({ line1 = 3, line2 = 5 })
130138

131139
assert.same({ line1 = 3, line2 = 5 }, env.selection.calls[1].opts)
132140
end)
141+
142+
it("send_selection derives visual range when called in visual mode without opts", function()
143+
local env = setup_with_deps()
144+
env.fake_vim.fn.mode = function()
145+
return "v"
146+
end
147+
env.fake_vim.fn.getpos = function()
148+
return { 0, 4, 3, 0 }
149+
end
150+
env.fake_vim._set_buf_cursor(1, 0, 7, 9)
151+
152+
env.codex.send_selection()
153+
154+
assert.same({
155+
line1 = 4,
156+
line2 = 7,
157+
start_col = 2,
158+
end_col = 9,
159+
visual_mode = "v",
160+
}, env.selection.calls[1].opts)
161+
end)
162+
163+
it("send_selection derives blockwise mode for visual fallback", function()
164+
local env = setup_with_deps()
165+
env.fake_vim.fn.mode = function()
166+
return CTRL_V
167+
end
168+
env.fake_vim.fn.getpos = function()
169+
return { 0, 6, 4, 0 }
170+
end
171+
env.fake_vim._set_buf_cursor(1, 0, 9, 12)
172+
173+
env.codex.send_selection()
174+
175+
assert.same({
176+
line1 = 6,
177+
line2 = 9,
178+
start_col = 3,
179+
end_col = 12,
180+
visual_mode = CTRL_V,
181+
}, env.selection.calls[1].opts)
182+
end)
133183
end)

0 commit comments

Comments
 (0)