Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 74 additions & 39 deletions tests/integration/selection_held_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,40 @@ describe("a held selection with the real plugin", function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), "xt", false)
end

-- Lets the real timers run: the debounce is 100 ms.
-- Lets the real timers run for at least ms. Use it only where the time is the point of the step.
local function wait(ms)
vim.wait(ms, function()
return false
end, 10)
end

-- Waits until the plugin has finished what it started: no debounce timer is pending, no demotion timer is pending
-- (unless keep_demotion), and every vim.schedule callback that was queued before the call has run (the check after
-- an edit is one). A fixed wait() cannot say that. When the main loop stalls for 300 ms under load, vim.wait returns
-- in the iteration that fires the timers, before their vim.schedule_wrap callbacks run, and a debounce that fires
-- late arms a demotion that is still pending. The step after the wait then reads a plugin that is not done: the
-- debounce has not read the cursor, the demotion has not fired, or it fires after the step has renamed the buffer.
local function settle(keep_demotion)
local drained = false
vim.schedule(function()
drained = true
end)
local done = vim.wait(10000, function()
return drained
and selection.state.debounce_timer == nil
and (keep_demotion or selection.state.demotion_timer == nil)
end, 10)
assert.is_true(done, "the plugin still has a timer pending")
end

-- For a spec that expects nothing to be sent: settle() ends as soon as no timer is pending, so a send that a later
-- timer makes would pass. Wait 300 ms for it (the old fixed waits gave it that), then settle() again: a timer that
-- fires in the last iteration of the wait has not run its callback yet.
local function wait_for_late_send()
wait(300)
settle()
end

local function start(terminal_config, delay_ms)
claudecode.setup({
auto_start = false,
Expand Down Expand Up @@ -67,12 +94,17 @@ describe("a held selection with the real plugin", function()
vim.cmd("edit " .. vim.fn.fnameescape(path))
end

-- Select alpha (the first word) and release it with Esc: Claude holds it.
local function select_and_release()
-- Select alpha (the first word) and release it with Esc: Claude holds it. The plugin reads the selection when the
-- debounce fires, and arms the demotion when it reads the release. keep_demotion leaves that demotion pending, for a
-- spec that needs it pending (a visual_demotion_delay_ms of 1000).
local function select_and_release(keep_demotion)
press("ve")
wait(150)
settle(true)
press("<Esc>")
wait(300)
settle(keep_demotion)
if keep_demotion then
assert.is_not_nil(selection.state.demotion_timer, "the demotion is not pending")
end
assert.are.equal("alpha", selection.get_latest_selection().text)
end

Expand Down Expand Up @@ -102,13 +134,13 @@ describe("a held selection with the real plugin", function()
it("keeps the selection when the Claude terminal first opens (delay " .. delay_ms .. ")", function()
start({ provider = "native", show_native_term_exit_tip = false }, delay_ms)
open_file("alpha beta gamma")
select_and_release()
select_and_release(delay_ms > 500) -- a demotion that late is still pending when the terminal opens

require("claudecode.terminal").open()
vim.cmd("stopinsert")
wait(300)
settle()
vim.cmd("wincmd p")
wait(600 + delay_ms) -- the debounce, then the demotion
settle() -- the debounce, then the demotion

assert.are.equal("alpha", selection.get_latest_selection().text)
end)
Expand All @@ -122,14 +154,14 @@ describe("a held selection with the real plugin", function()
local term_buf = vim.api.nvim_get_current_buf()
vim.cmd("stopinsert")
vim.cmd("buffer " .. file_buf)
wait(200)
settle()
select_and_release()

vim.cmd("buffer " .. term_buf)
vim.cmd("stopinsert")
wait(300)
settle()
vim.cmd("buffer #")
wait(600)
settle()

assert.are.equal(file_buf, vim.api.nvim_get_current_buf())
assert.are.equal("alpha", selection.get_latest_selection().text)
Expand All @@ -143,12 +175,12 @@ describe("a held selection with the real plugin", function()
local term_buf = vim.api.nvim_get_current_buf()
vim.cmd("stopinsert")
vim.cmd("buffer " .. file_buf)
wait(200)
settle()
select_and_release() -- Claude holds "alpha"

press("$vd") -- a second selection, on the last character, consumed before the debounce
vim.cmd("buffer " .. term_buf) -- and the Claude terminal, where the debounce does nothing
wait(300)
settle()

local held = selection.get_latest_selection()
assert.is_true(held.selection.isEmpty)
Expand Down Expand Up @@ -180,14 +212,14 @@ describe("a held selection with the real plugin", function()
local term_buf = vim.api.nvim_get_current_buf()
vim.cmd("stopinsert")
vim.cmd("buffer " .. file_buf)
wait(200)
settle()
select_and_release()

vim.cmd("buffer " .. term_buf)
vim.cmd("stopinsert")
wait(300)
settle()
vim.api.nvim_buf_set_text(file_buf, 0, 0, 0, 6, {}) -- a plugin deletes "alpha "
wait(300)
settle()

assert.are.equal(term_buf, vim.api.nvim_get_current_buf())
local held = selection.get_latest_selection()
Expand All @@ -205,14 +237,14 @@ describe("a held selection with the real plugin", function()
local term_win = vim.api.nvim_get_current_win()
vim.cmd("stopinsert")
vim.api.nvim_set_current_win(file_win)
wait(200)
settle()
select_and_release()

vim.api.nvim_set_current_win(term_win)
vim.cmd("stopinsert")
wait(300)
settle()
rewrite_on_disk_and_reload(file_buf, "beta gamma")
wait(300)
settle()

assert.are.same({ "beta gamma" }, vim.api.nvim_buf_get_lines(file_buf, 0, -1, false))
assert.are.equal(term_win, vim.api.nvim_get_current_win())
Expand All @@ -232,24 +264,24 @@ describe("a held selection with the real plugin", function()
local term_buf = vim.api.nvim_get_current_buf()
vim.cmd("stopinsert")
vim.cmd("buffer " .. file_buf)
wait(200)
settle()
select_and_release()

vim.cmd("buffer " .. term_buf)
vim.cmd("stopinsert")
wait(300)
settle()
vim.cmd("bunload! " .. file_buf)
wait(200)
settle()
vim.cmd("buffer " .. file_buf)
vim.api.nvim_win_set_cursor(0, { 1, 0 }) -- Neovim put the cursor back where it was
wait(200)
settle()
select_and_release()
vim.cmd("buffer " .. term_buf)
vim.cmd("stopinsert")
wait(300)
settle()

vim.api.nvim_buf_set_text(file_buf, 0, 0, 0, 6, {}) -- a plugin deletes "alpha "
wait(300)
settle()

assert.are.equal(term_buf, vim.api.nvim_get_current_buf())
assert.is_true(selection.get_latest_selection().selection.isEmpty)
Expand All @@ -268,7 +300,8 @@ describe("a held selection with the real plugin", function()

vim.v.errmsg = ""
vim.api.nvim_buf_set_text(file_buf, 0, 0, 0, 6, {}) -- a plugin deletes "alpha "
wait(300)
settle()
wait_for_late_send()

assert.are.equal("", vim.v.errmsg)
assert.are.same({}, sent) -- no selection and no cursor of the terminal reached Claude
Expand All @@ -283,13 +316,14 @@ describe("a held selection with the real plugin", function()
start({ provider = "none" }, 1000)
open_file("alpha beta gamma")
local file_buf = vim.api.nvim_get_current_buf()
select_and_release()
select_and_release(true)
local sent = record_broadcasts()
rename_to_claude_terminal(file_buf)

vim.v.errmsg = ""
vim.api.nvim_buf_set_text(file_buf, 0, 0, 0, 6, {}) -- a plugin deletes "alpha "
wait(1300) -- the demotion delay passes
settle() -- a demotion that the check did not cancel fires here
wait_for_late_send()

assert.are.equal("", vim.v.errmsg)
assert.are.same({}, sent)
Expand All @@ -306,16 +340,17 @@ describe("a held selection with the real plugin", function()
local term_buf = vim.api.nvim_get_current_buf()
vim.cmd("stopinsert")
vim.cmd("buffer " .. file_buf)
wait(200)
settle()
select_and_release()
vim.cmd("buffer " .. term_buf)
vim.cmd("stopinsert")
wait(300)
settle()
local sent = record_broadcasts()

rename_to_claude_terminal(file_buf)
vim.api.nvim_buf_set_text(file_buf, 0, 0, 0, 6, {}) -- a plugin deletes "alpha "
wait(300)
settle()
wait_for_late_send()

assert.are.equal(term_buf, vim.api.nvim_get_current_buf())
assert.are.same({}, sent)
Expand All @@ -341,12 +376,12 @@ describe("a held selection with the real plugin", function()
vim.api.nvim_win_set_cursor(0, { 1, 0 })
select_and_release()
vim.cmd("enew") -- another file: the debounce replaces the selection with that buffer's cursor
wait(300)
settle()
assert.is_true(selection.get_latest_selection().selection.isEmpty)
rewrite_on_disk_and_reload(file_buf, "alpha beta gamma, cycle " .. cycle) -- on_reload, nothing held
wait(200)
settle()
vim.cmd("buffer " .. file_buf)
wait(200)
settle()
end
end)
vim.api.nvim_buf_attach = real_attach
Expand All @@ -364,21 +399,21 @@ describe("a held selection with the real plugin", function()
local term_win = vim.api.nvim_get_current_win()
vim.cmd("stopinsert")
vim.api.nvim_set_current_win(file_win)
wait(200)
settle()
select_and_release()

vim.api.nvim_set_current_win(term_win)
vim.cmd("stopinsert")
wait(300)
settle()
rewrite_on_disk_and_reload(file_buf, "alpha beta gamma, and more")
wait(300)
settle()

assert.are.same({ "alpha beta gamma, and more" }, vim.api.nvim_buf_get_lines(file_buf, 0, -1, false))
assert.are.equal("alpha", selection.get_latest_selection().text)

-- The listener survives the reload: the next change is seen too.
vim.api.nvim_buf_set_text(file_buf, 0, 0, 0, 6, {})
wait(300)
settle()
assert.is_true(selection.get_latest_selection().selection.isEmpty)
end)
end)
Expand Down Expand Up @@ -407,7 +442,7 @@ describe("a held selection with the real plugin", function()
vim.api.nvim_buf_delete(scratch, { force = true })
end
end)
wait(400)
settle()

assert.is_false(vim.api.nvim_buf_is_valid(scratch))
assert.are.equal(file_buf, vim.api.nvim_get_current_buf())
Expand Down
42 changes: 40 additions & 2 deletions tests/integration/selection_visual_edit_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,29 @@ describe("a visual selection released after the buffer changed, with the real pl
end, 5)
end

-- Runs every vim.schedule callback that is queued now. A timer that fires in the last iteration of a wait has not run
-- its vim.schedule_wrap callback when the wait returns.
local function drain()
local drained = false
vim.schedule(function()
drained = true
end)
vim.wait(5000, function()
return drained
end, 5)
end

-- After an operator the plugin reports the cursor once its flush has run, and a busy host can delay that past any fixed
-- wait (get_latest_selection() is nil until then). So wait until it holds a cursor, then let the debounce and the
-- demotion run, so that a selection sent late would show.
-- demotion run for 400 ms and drain the callbacks, so that a selection sent late would show.
local function wait_for_cursor()
local held = vim.wait(5000, function()
local latest = selection.get_latest_selection()
return latest ~= nil and latest.selection.isEmpty
end, 5)
assert.is_true(held, "the plugin holds a cursor after the operator")
wait(200)
wait(400)
Comment thread
ThomasK33 marked this conversation as resolved.
Resolved
drain()
end

local function start()
Expand Down Expand Up @@ -276,6 +289,31 @@ describe("a visual selection released after the buffer changed, with the real pl
assert.is_true(selection.get_latest_selection().selection.isEmpty)
assert_last_sent_is_a_cursor()
end)

-- The two specs above press the operator within the 100 ms debounce, so usually nothing was sent in visual mode and
-- their checks of an early selection (assert_is_the_selection, and a cursor as the last message) have nothing to
-- look at. Here the debounce has sent the selection before d, on every run.
it("sends a cursor for d after the debounce sent the selection in visual mode", function()
start()
open_file({ "alpha beta gamma", "second line" })
wait(50)

press("ve")
local sent_in_visual_mode = vim.wait(5000, function()
return held_text() == "alpha"
end, 5)
assert.is_true(sent_in_visual_mode, "the debounce sent the selection in visual mode")
local before_operator = #sent
assert.is_true(before_operator >= 1)
press("d")
wait_for_cursor()

assert.are.equal(" beta gamma", vim.api.nvim_buf_get_lines(0, 0, 1, false)[1])
assert.are.same({ true }, { sent_no_selection(before_operator) })
assert_is_the_selection(before_operator)
assert.is_true(selection.get_latest_selection().selection.isEmpty)
assert_last_sent_is_a_cursor()
end)
end)

-- A mapping runs its keys with no main-loop turn between them, so the check that the listener schedules for an edit
Expand Down
Loading