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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

- `focus_after_send = true` no longer fails silently with `terminal.provider = "none"`/`"external"`: those providers run Claude outside Neovim, so focus cannot move there. A one-time warning is now emitted at setup pointing to the new `User ClaudeCodeSendComplete` autocmd, which you can hook to focus your own terminal. (`focus_after_send` still only auto-focuses the in-editor providers.) ([#228](https://github.com/coder/claudecode.nvim/issues/228))
- Rejecting a Claude diff with `:q` (or `:close` / `<C-w>c` / closing the tab) now resolves it as rejected, matching the documented behavior. The proposed buffer is a scratch buffer that `:q` only hides, so the existing `BufDelete`/`BufUnload`/`BufWipeout` autocmds never fired; a `WinClosed` autocmd now handles window-close rejection. ([#238](https://github.com/coder/claudecode.nvim/issues/238))
- Push quickly-made visual selections to Claude reliably. Selections made and released faster than the selection-tracking debounce were never broadcast, and any selection was wiped shortly after leaving visual mode when Claude runs in an external terminal (the `/ide` flow) — so single-line selections in particular often never reached Claude. Selections are now flushed synchronously on visual-mode exit (from the `'<`/`'>` marks) and persist until the cursor actually moves; a single-line linewise `V` made right after a charwise selection is also no longer mis-extracted to a single character. ([#246](https://github.com/coder/claudecode.nvim/issues/246))
- Diffs opened via `openDiff` no longer linger forever when they are resolved outside this Neovim or their Claude session goes away. Pending diffs are now automatically closed when the client that opened them disconnects or the integration is stopped, and `closeAllDiffTabs` now also resolves/cleans the diff module's tracked state instead of only closing windows. ([#248](https://github.com/coder/claudecode.nvim/issues/248))
- Show diffs when the Claude Code terminal is the only window (no other splits). Previously `openDiff` failed with "No suitable editor window found"; now a split is created to host the diff, matching the behavior of the `openFile` tool. ([#231](https://github.com/coder/claudecode.nvim/issues/231))
- Work around a Neovim core bug (< 0.12.2) that fragmented large bracketed pastes into the terminal across `vim.paste` phases, making Cmd+V appear to truncate content. Added a scoped, version-gated `vim.paste` shim controlled by `terminal.fix_streamed_paste` (`"auto"` by default; no-op on Neovim >= 0.12.2). ([#161](https://github.com/coder/claudecode.nvim/issues/161))
Expand Down
255 changes: 238 additions & 17 deletions lua/claudecode/selection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ local terminal = require("claudecode.terminal")

local uv = vim.uv or vim.loop

---Returns true if the given mode string denotes a visual mode (charwise, linewise, blockwise).
---Select mode (`s`/`S`/`<C-s>`) is deliberately NOT treated as visual: like the rest of the
---module it tracks visual selections only (Select mode is an atypical workflow here).
---@param mode string|nil
---@return boolean
local function is_visual_mode(mode)
return mode == "v" or mode == "V" or mode == "\22"
end

---Returns true if the cursor is still at the position captured by the most recent flush
---for this buffer (i.e. the held visual selection has not been navigated away from yet).
---@param bufnr number
---@return boolean
local function cursor_unmoved_since_flush(bufnr)
local caf = M.state.cursor_at_flush
if not caf or caf.bufnr ~= bufnr then
return false
end
local pos = vim.api.nvim_win_get_cursor(0)
return pos[1] == caf.pos[1] and pos[2] == caf.pos[2]
end

M.state = {
latest_selection = nil,
tracking_enabled = false,
Expand All @@ -16,6 +38,16 @@ M.state = {
last_active_visual_selection = nil,
demotion_timer = nil,
visual_demotion_delay_ms = 50,

-- Cursor position captured when a visual selection is flushed on visual-mode exit.
-- Demotion only fires once the cursor actually moves away from this position, so a
-- held selection persists (see issue #246 and M.flush_visual_selection).
cursor_at_flush = nil,

-- { bufnr, tick } captured when visual mode is entered. Used to detect that an operator
-- consumed/mutated the selection (d/c/>/x...) so the flush does not broadcast stale,
-- post-edit marks as a phantom selection (see M.flush_visual_selection).
visual_entry = nil,
}

---Enables selection tracking.
Expand Down Expand Up @@ -47,6 +79,8 @@ function M.disable()

M.state.latest_selection = nil
M.state.last_active_visual_selection = nil
M.state.cursor_at_flush = nil
M.state.visual_entry = nil
M.server = nil

M._cancel_debounce_timer()
Expand Down Expand Up @@ -124,8 +158,26 @@ function M.on_cursor_moved()
end

---Handles mode change events.
---Triggers an immediate update of the selection.
---When leaving visual mode, the selection is flushed synchronously: at that instant
---`nvim_get_mode()` already reports the new (non-visual) mode, so the debounced
---`update_selection()` path can no longer capture the visual selection. Flushing here
---(from the still-valid `'<`/`'>` marks) ensures fast selections that are made and
---released in under `debounce_ms` are not lost (issue #246).
---Entering visual mode records the buffer's changedtick so the flush can tell an
---abandoned selection apart from one consumed by a mutating operator (d/c/>/x...).
function M.on_mode_changed()
local event = vim.v.event
if event then
local leaving_visual = is_visual_mode(event.old_mode) and not is_visual_mode(event.new_mode)
local entering_visual = is_visual_mode(event.new_mode) and not is_visual_mode(event.old_mode)
if entering_visual then
local buf = vim.api.nvim_get_current_buf()
M.state.visual_entry = { bufnr = buf, tick = vim.api.nvim_buf_get_changedtick(buf) }
elseif leaving_visual then
M.flush_visual_selection()
end
end

M.debounce_update()
end

Expand Down Expand Up @@ -226,10 +278,16 @@ function M.update_selection()
local last_visual = M.state.last_active_visual_selection

if M.state.demotion_timer then
-- A demotion is already pending. For this specific update_selection call (e.g. cursor moved),
-- current_selection reflects the immediate cursor position.
-- M.state.latest_selection (the one that might be sent) is still the visual one until timer resolves.
current_selection = M.get_cursor_position()
-- A demotion is already pending. While the cursor is still on the flushed position the
-- held visual selection must be preserved (an idle re-entry here must not wipe it before
-- the cursor actually moves -- matters when visual_demotion_delay_ms >= debounce_ms).
-- Once the cursor has moved, reflect the immediate cursor position; M.state.latest_selection
-- stays the visual one until the timer resolves.
if cursor_unmoved_since_flush(current_buf) then
current_selection = M.state.latest_selection
else
current_selection = M.get_cursor_position()
end
elseif
last_visual
and last_visual.bufnr == current_buf
Expand Down Expand Up @@ -264,11 +322,14 @@ function M.update_selection()
end)
)
else
-- Genuinely in normal mode, no recent visual exit, no pending demotion.
-- Genuinely in normal mode, no recent visual exit, no pending demotion. The
-- selection_changed protocol reflects the active editor, so report this buffer's
-- cursor. Any held visual selection (for this buffer, or one navigated away from
-- without moving the cursor) is no longer current -- clear its tracked state so it
-- does not leak across buffer switches.
current_selection = M.get_cursor_position()
if last_visual and last_visual.bufnr == current_buf then
M.state.last_active_visual_selection = nil -- Clear it as it's no longer relevant for demotion
end
M.state.last_active_visual_selection = nil
M.state.cursor_at_flush = nil
end
end

Expand Down Expand Up @@ -334,6 +395,16 @@ function M.handle_selection_demotion(original_bufnr_when_scheduled)

-- Condition 3: Still in Original Buffer & Not Visual & Not Claude Term -> Demote
if current_buf == original_bufnr_when_scheduled then
-- Only demote once the cursor has actually moved since the selection was flushed.
-- This lets a held selection persist (matching the official VS Code extension, and
-- fixing the external-Claude case where there is no in-Neovim Claude terminal to
-- switch to), while still clearing a stale selection as soon as the user navigates
-- away from it. last_active_visual_selection is intentionally left intact when the
-- cursor is unmoved so a later cursor move re-arms demotion. See issue #246.
if cursor_unmoved_since_flush(current_buf) then
return
end

local new_sel_for_demotion = M.get_cursor_position()
-- Check if this new cursor position is actually different from the (visual) latest_selection
if M.has_selection_changed(new_sel_for_demotion) then
Expand All @@ -346,13 +417,16 @@ function M.handle_selection_demotion(original_bufnr_when_scheduled)
end
-- User switched to different buffer

-- Always clear last_active_visual_selection for the original buffer as its pending demotion is resolved.
-- The pending demotion for the original buffer is resolved: clear its tracked state.
if
M.state.last_active_visual_selection
and M.state.last_active_visual_selection.bufnr == original_bufnr_when_scheduled
then
M.state.last_active_visual_selection = nil
end
if M.state.cursor_at_flush and M.state.cursor_at_flush.bufnr == original_bufnr_when_scheduled then
M.state.cursor_at_flush = nil
end
end

---Validates if we're in a valid visual selection mode
Expand All @@ -372,17 +446,17 @@ local function validate_visual_mode()
return true, nil
end

---Determines the effective visual mode character
---Determines the effective visual mode character.
---Prefers the LIVE mode; `vim.fn.visualmode()` (the LAST COMPLETED visual mode) is only
---used as a fallback when not currently in a visual mode. Trusting `visualmode()` while
---live in a different visual mode misclassifies the selection -- e.g. a fresh linewise
---`V` made right after a charwise selection would be extracted charwise, broadcasting a
---single character (or an empty selection on an empty line) instead of the whole line.
---See issue #246.
---@return string|nil - the visual mode character or nil if invalid
local function get_effective_visual_mode()
local current_nvim_mode = vim.api.nvim_get_mode().mode
local visual_fn_mode_char = vim.fn.visualmode()

if visual_fn_mode_char and visual_fn_mode_char ~= "" then
return visual_fn_mode_char
end

-- Fallback to current mode
if current_nvim_mode == "V" then
return "V"
elseif current_nvim_mode == "v" then
Expand All @@ -391,6 +465,12 @@ local function get_effective_visual_mode()
return "\22"
end

-- Not currently in a visual mode: fall back to the last completed visual mode.
local visual_fn_mode_char = vim.fn.visualmode()
if visual_fn_mode_char and visual_fn_mode_char ~= "" then
return visual_fn_mode_char
end

return nil
end

Expand Down Expand Up @@ -511,6 +591,83 @@ function M.get_visual_selection()
if visual_mode == "V" then
final_text = extract_linewise_text(lines_content, start_coords)
elseif visual_mode == "v" or visual_mode == "\22" then
-- Blockwise ("\22") is approximated as the contiguous charwise span: selection_changed
-- carries a single start/end range and cannot represent a rectangular block. Proper
-- per-column block extraction is a follow-up.
final_text = extract_characterwise_text(lines_content, start_coords, end_coords)
if not final_text then
return nil
end
else
return nil
end

local lsp_positions = calculate_lsp_positions(start_coords, end_coords, visual_mode, lines_content)

return {
text = final_text or "",
filePath = file_path,
fileUrl = "file://" .. file_path,
selection = {
start = lsp_positions.start,
["end"] = lsp_positions["end"],
isEmpty = (not final_text or #final_text == 0),
},
}
end

---Gets the just-completed visual selection from the `'<` and `'>` marks.
---Unlike `get_visual_selection()`, this does NOT require the editor to currently be in
---visual mode, so it can be called from a `ModeChanged` (visual -> normal) handler where
---`nvim_get_mode()` already reports normal mode but the marks are still valid. The marks
---are always in chronological order (`'<` before `'>`).
---Only valid immediately on visual exit: it pairs the buffer-local `'<`/`'>` marks with the
---GLOBAL `vim.fn.visualmode()`, so both must describe the same just-completed selection
---(do not call it after an unrelated visual selection in another buffer).
---@return table|nil selection A selection table matching get_visual_selection()'s shape, or nil.
function M.get_visual_selection_from_marks()
local visual_mode = vim.fn.visualmode()
if not visual_mode or visual_mode == "" then
return nil
end

local start_pos = vim.fn.getpos("'<")
local end_pos = vim.fn.getpos("'>")
if start_pos[2] == 0 or end_pos[2] == 0 then
return nil -- no recorded visual selection
end

local current_buf = vim.api.nvim_get_current_buf()
local file_path = vim.api.nvim_buf_get_name(current_buf)

local start_coords = { lnum = start_pos[2], col = start_pos[3] }
local end_coords = { lnum = end_pos[2], col = end_pos[3] }

local lines_content = vim.api.nvim_buf_get_lines(
current_buf,
start_coords.lnum - 1, -- Convert to 0-indexed
end_coords.lnum, -- nvim_buf_get_lines end is exclusive
false
)

if #lines_content == 0 then
return nil
end

-- For linewise selections (and `$`), the `'>` column can be MAXCOL (2147483647). Clamp it
-- to the last line's length + 1 so it never overflows string.sub / LSP character math.
local last_line = lines_content[#lines_content] or ""
if end_coords.col > #last_line + 1 then
end_coords.col = #last_line + 1
end
Comment on lines +657 to +662

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 When the MAXCOL clamp at lines 657-662 fires for non-linewise modes (e.g. <C-V>$ blockwise extending to end of line), it sets end_coords.col = #last_line + 1, which calculate_lsp_positions then uses verbatim as lsp_end_char for non-V modes — one past the LSP-exclusive-end position. Text extraction is correct (Lua string.sub clamps gracefully) and LSP clients typically truncate over-length characters, so practical impact is minimal — but the fix is trivial: gate the clamp on visual_mode == "V", or use min(end_coords.col, #last_line) for non-linewise modes in calculate_lsp_positions.

Extended reasoning...

What the bug is

In M.get_visual_selection_from_marks (lua/claudecode/selection.lua:657-662), the MAXCOL clamp is applied unconditionally, before the visual-mode dispatch:

local last_line = lines_content[#lines_content] or ""
if end_coords.col > #last_line + 1 then
  end_coords.col = #last_line + 1
end

For linewise V, the clamp is harmless because calculate_lsp_positions (lines 541-548) ignores end_coords.col and computes lsp_end_char from #lines_content[#lines_content]. But for charwise v and blockwise \22, line 551 uses end_coords.col directly as lsp_end_char. After clamping a MAXCOL '> to #last_line + 1, lsp_end_char = #last_line + 1 — one past the correct LSP-exclusive-end (#last_line).

Does it actually trigger?

The author's own comment is the key evidence — line 657 reads: "For linewise selections (and $), the '> column can be MAXCOL (2147483647)". The clamp was added specifically because the author believed $ can also produce a MAXCOL '> outside linewise mode. The concrete trigger is <C-V>$ (blockwise extending to end of line), where Neovim records a sentinel column in '>.

Some verifiers refuted this on the grounds that '> for non-linewise modes always holds the actual byte column, never MAXCOL — making the clamp condition (end_coords.col > #last_line + 1) unreachable for non-V. If that were true, the clamp would be pure dead code for non-V modes, with no behavioral consequence either way. But the author wrote this clamp deliberately, with a comment that names $ as a trigger — they clearly believe the path is live. Either way, the recommended fix is a one-line tightening that costs nothing if the path is in fact dead, and corrects the off-by-one if it isn't.

Step-by-step proof (assuming MAXCOL '> for <C-V>$ on hello)

  1. vim.fn.visualmode() returns "\22" (CTRL-V).
  2. getpos("'>") returns column 2147483647 (MAXCOL).
  3. end_coords.col = 2147483647.
  4. #last_line + 1 = 5 + 1 = 6; clamp fires: end_coords.col = 6.
  5. Dispatch enters the "v" or "\22" branch.
  6. calculate_lsp_positions line 551: lsp_end_char = end_coords.col = 6.
  7. Correct value: for selecting all of "hello" (5 chars), LSP-exclusive end is 5.

Impact

Low. Text extraction returns "hello" correctly because Lua's string.sub("hello", 1, 6) clamps. The LSP Position spec says clients normalize character past line length down to the line length, so most consumers silently round to the right value. The affected case (blockwise + $) is also already documented in this PR as an approximation ("selection_changed carries a single range and cannot represent a rectangular block"). Worth fixing because the fix is trivial and the off-by-one is in code the author wrote intentionally for this case.

How to fix

Either gate the clamp on visual_mode == "V" (its documented use case), e.g.:

if visual_mode == "V" and end_coords.col > #last_line + 1 then
  end_coords.col = #last_line + 1
end

Or, in calculate_lsp_positions line 551, clamp at the LSP step:

lsp_end_char = math.min(end_coords.col, #lines_content[#lines_content] or 0)

The second form is more robust (the LSP boundary is the natural place to enforce LSP-exclusive-end semantics).


local final_text
if visual_mode == "V" then
final_text = extract_linewise_text(lines_content, start_coords)
elseif visual_mode == "v" or visual_mode == "\22" then
-- Blockwise ("\22") is approximated as the contiguous charwise span (matching
-- get_visual_selection), since selection_changed carries a single start/end range and
-- cannot represent a rectangular block. Proper per-column block extraction is a follow-up.
final_text = extract_characterwise_text(lines_content, start_coords, end_coords)
if not final_text then
return nil
Expand All @@ -533,6 +690,70 @@ function M.get_visual_selection()
}
end

---Flushes the just-completed visual selection synchronously when leaving visual mode.
---Captures the selection from the `'<`/`'>` marks, records it as the active visual
---selection, cancels any pending demotion, and broadcasts it if it changed. This closes
---the debounce race where a selection made and released faster than `debounce_ms` was
---never broadcast at all (issue #246). The Claude terminal buffer is ignored, mirroring
---`update_selection()`. Deduplicates against the last sent selection so a selection
---already broadcast by the in-visual debounce is not sent twice on exit. If the buffer
---was mutated while in visual mode (a consuming operator like d/c/>/x), the marks no
---longer describe the user's selection, so the flush is skipped to avoid broadcasting
---stale, post-edit text as a phantom selection.
function M.flush_visual_selection()
if not M.state.tracking_enabled then
return
end

local current_buf = vim.api.nvim_get_current_buf()
local buf_name = vim.api.nvim_buf_get_name(current_buf)

if buf_name and buf_name:match("^term://") and buf_name:lower():find("claude", 1, true) then
return
end
if terminal then
local claude_term_bufnr = terminal.get_active_terminal_bufnr()
if claude_term_bufnr and current_buf == claude_term_bufnr then
return
end
end

-- Skip when the buffer was edited while in visual mode: the changedtick advancing since
-- visual entry means an operator (d/c/x...) consumed the selection, so the '<,'> marks
-- point at post-edit text the user never selected. This is intentionally conservative --
-- it also skips in-place transforms that leave a valid selection (gU/gu/~/J/=), because
-- they cannot be told apart from consuming operators by the marks alone (both leave a
-- non-empty, in-bounds region). Such fast transforms simply are not flushed; the prior
-- behavior never broadcast them either (they lost the debounce race), so this is a
-- residual non-broadcast, not a regression.
local entry = M.state.visual_entry
if entry and entry.bufnr == current_buf and vim.api.nvim_buf_get_changedtick(current_buf) ~= entry.tick then
return
end
Comment on lines +728 to +732

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Stale cursor_at_flush from a previous successful flush survives the changedtick early-return at line 730-732 (and is also not cleared on visual entry). If the cursor coincidentally lands on the prior flush position after a mutating operator (e.g. vd on a single character at the same column, or other cases where the post-mutation cursor matches), cursor_unmoved_since_flush returns true against stale state on the next handle_selection_demotion, suppressing demotion — so latest_selection keeps pointing at deleted/mutated text until the user moves the cursor. One-line fix: clear M.state.cursor_at_flush on the early-return paths in flush_visual_selection (or in the entering_visual branch of on_mode_changed).

Extended reasoning...

What the bug is

M.flush_visual_selection (lua/claudecode/selection.lua:703-755) has an early-return guard at line 730-732 that skips the flush when the buffer's changedtick advanced since visual entry (a consuming operator like d/c/x mutated the selection). The guard is intentionally conservative — its comment explains it treats this as 'a residual non-broadcast, not a regression'. But it forgets to invalidate M.state.cursor_at_flush left behind by a previous successful flush, and the visual-entry branch of on_mode_changed (line 168-176) does not clear it either.

The code path that triggers it

cursor_at_flush is set on every successful flush at line 758. It is read by cursor_unmoved_since_flush (line 24-31), which handle_selection_demotion uses at line 404 to suppress demotion while the cursor still sits on the previous flush position. Two paths exist that should invalidate it but do not:

  1. The changedtick early-return at line 730-732 (flush_visual_selection aborts because an operator consumed the selection).
  2. The entering_visual branch in on_mode_changed (line 173-175) — re-entering visual mode without first moving the cursor leaves the prior flush position intact.

Why existing code does not prevent it

handle_selection_demotion's Condition 3 (line 397-415) intentionally leaves cursor_at_flush in place when the cursor is unmoved, per the comment at line 398-403, so a later cursor move can re-arm demotion. That design is correct for the happy path (held selection, then real cursor move → demote). What is missing is invalidation on the abort paths, where the held selection has been consumed by a mutating operator but cursor_at_flush still references it.

Step-by-step proof (single-char vd reproducer)

Buffer line 1 = alpha. Cursor starts at {1, 4} (on 'a' at end via prior viw or $ motion).

  1. User does viw + Esc selecting alpha. flush_visual_selection succeeds: broadcasts alpha, sets cursor_at_flush = {bufnr=1, pos={1,4}}, last_active_visual_selection = sel_alpha, latest_selection = sel_alpha.
  2. update_selection debounce fires. demotion_timer is armed for buffer 1.
  3. Timer fires within 50ms with cursor still at {1,4}. handle_selection_demotion enters Condition 3 at line 397, cursor_unmoved_since_flush(1) returns true (pos {1,4} == flush {1,4}), returns early at line 405 without clearing cursor_at_flush or last_active_visual_selection (intentional, per design).
  4. User does vd at {1,4}: n→v fires on_mode_changed, sets visual_entry = {bufnr=1, tick=T} at line 174. cursor_at_flush is not touched.
  5. d consumes the visual range, deleting 'a'. Buffer becomes alph. changedtick advances to T+1. v→n fires on_mode_changedflush_visual_selection.
  6. Line 730 sees T+1 != T, early-returns at line 732. cursor_at_flush is not cleared — still {bufnr=1, pos={1,4}} from step 1.
  7. Cursor now at {1,4} (single-char delete: cursor stays at the same column on the next char, or here at the new end-of-line).
  8. Debounce fires update_selection. Mode is normal, demotion_timer is nil. last_active_visual_selection exists and matches buffer 1 → elif branch at line 287-321 arms a new demotion timer.
  9. Timer fires. handle_selection_demotion(1): Condition 3, cursor_unmoved_since_flush(1) returns true (current {1,4} == stale flush {1,4}). Returns early at line 405. Demotion suppressed.

Result: latest_selection keeps pointing at the now-deleted alpha. get_latest_selection and any ClaudeCodeSend calls return text that no longer exists in the buffer. Self-heals on the very next cursor move (the new cursor_at_flush check fails, demotion fires, bottom of handle_selection_demotion clears both pieces of state).

Impact

Narrow and self-resolving: the trigger requires the post-mutation cursor to coincidentally land at the prior flush column, and a single keystroke (any motion) recovers state. No spurious broadcast happens during the stuck window — has_selection_changed compares against latest_selection itself, so no message is sent. The user-visible effect is that Claude's view of the selection lags behind reality until the user moves the cursor. Worth flagging as a nit because the cursor-guarded demotion mechanism is new in this PR and the gap is a one-line oversight.

Fix

Clear M.state.cursor_at_flush on the early-return paths in flush_visual_selection (the term-buffer branches at line 716-727 and the changedtick branch at line 730-732), or clear it in the entering_visual branch of on_mode_changed at line 173-175. Either approach prevents stale state from outliving the selection it was tracking. Clearing on entering_visual is the most defensive — it invalidates the prior flush position whenever a new visual selection begins, regardless of how the prior one was resolved.


local selection = M.get_visual_selection_from_marks()
if not selection or selection.selection.isEmpty then
return
end

-- Record the cursor position at flush time so demotion only fires after a real move.
M.state.cursor_at_flush = { bufnr = current_buf, pos = vim.api.nvim_win_get_cursor(0) }
M.state.last_active_visual_selection = {
bufnr = current_buf,
selection_data = vim.deepcopy(selection),
timestamp = vim.loop.now(),
}

M._cancel_demotion_timer()

if M.has_selection_changed(selection) then
M.state.latest_selection = selection
if M.server then
M.send_selection_update(selection)
end
end
end

---Gets the current cursor position when no visual selection is active.
---@return table A table containing an empty text, file path, URL, and cursor
---position as start/end, with isEmpty set to true.
Expand Down
Loading
Loading