66-- @module claudecode.selection
77local M = {}
88
9+ local config_module = require (" claudecode.config" )
10+ local terminal = require (" claudecode.terminal" )
11+
912-- Selection state
1013M .state = {
1114 latest_selection = nil ,
1215 tracking_enabled = false ,
1316 debounce_timer = nil ,
1417 debounce_ms = 300 , -- Default debounce time in milliseconds
18+
19+ -- New state for delayed visual demotion
20+ last_active_visual_selection = nil , -- Stores { bufnr, selection_data, timestamp }
21+ demotion_timer = nil , -- Timer object for visual demotion delay
22+ visual_demotion_delay_ms = 50 , -- Default, will be overridden by config in M.enable
1523}
1624
1725--- Enables selection tracking.
@@ -25,6 +33,11 @@ function M.enable(server)
2533 M .state .tracking_enabled = true
2634 M .server = server
2735
36+ -- Get the full configuration to access visual_demotion_delay_ms
37+ local user_config = vim .g .claudecode_user_config or {}
38+ local full_config = config_module .apply (user_config )
39+ M .state .visual_demotion_delay_ms = full_config .visual_demotion_delay_ms
40+
2841 M ._create_autocommands ()
2942end
3043
@@ -124,26 +137,170 @@ function M.update_selection()
124137 return
125138 end
126139
127- local current_mode = vim .api .nvim_get_mode ().mode
140+ local current_buf = vim .api .nvim_get_current_buf () -- Get current buffer early
141+
142+ -- If the current buffer is the Claude terminal, do not update selection
143+ if terminal then
144+ local claude_term_bufnr = terminal .get_active_terminal_bufnr ()
145+ if claude_term_bufnr and current_buf == claude_term_bufnr then
146+ -- Cancel any pending demotion if we switch to the Claude terminal
147+ if M .state .demotion_timer then
148+ M .state .demotion_timer :stop ()
149+ M .state .demotion_timer :close ()
150+ M .state .demotion_timer = nil
151+ end
152+ return
153+ end
154+ end
155+
156+ local current_mode_info = vim .api .nvim_get_mode ()
157+ local current_mode = current_mode_info .mode
158+ local current_selection -- This will be the candidate for M.state.latest_selection
159+
160+ if current_mode == " v" or current_mode == " V" or current_mode == " \022 " then -- Visual modes
161+ -- If a new visual selection is made, cancel any pending demotion
162+ if M .state .demotion_timer then
163+ M .state .demotion_timer :stop ()
164+ M .state .demotion_timer :close ()
165+ M .state .demotion_timer = nil
166+ end
128167
129- local current_selection
130- if current_mode == " v" or current_mode == " V" or current_mode == " \022 " then
131168 current_selection = M .get_visual_selection ()
132- else
169+
170+ if current_selection then
171+ M .state .last_active_visual_selection = {
172+ bufnr = current_buf ,
173+ selection_data = vim .deepcopy (current_selection ), -- Store a copy
174+ timestamp = vim .loop .now (),
175+ }
176+ else
177+ -- No valid visual selection (e.g., get_visual_selection returned nil)
178+ -- Clear last_active_visual if it was for this buffer
179+ if M .state .last_active_visual_selection and M .state .last_active_visual_selection .bufnr == current_buf then
180+ M .state .last_active_visual_selection = nil
181+ end
182+ end
183+ else -- Not in visual mode
184+ local last_visual = M .state .last_active_visual_selection
185+
186+ if M .state .demotion_timer then
187+ -- A demotion is already pending. For this specific update_selection call (e.g. cursor moved),
188+ -- current_selection reflects the immediate cursor position.
189+ -- M.state.latest_selection (the one that might be sent) is still the visual one until timer resolves.
190+ current_selection = M .get_cursor_position ()
191+ elseif
192+ last_visual
193+ and last_visual .bufnr == current_buf
194+ and last_visual .selection_data
195+ and not last_visual .selection_data .selection .isEmpty
196+ then
197+ -- We just exited visual mode in this buffer, and no demotion timer is running for it.
198+ -- Keep M.state.latest_selection as is (it's the visual one from the previous update).
199+ -- The 'current_selection' for comparison should also be this visual one.
200+ current_selection = M .state .latest_selection -- This should hold the visual selection
201+
202+ if M .state .demotion_timer then -- Should not happen due to elseif, but as safeguard
203+ M .state .demotion_timer :stop ()
204+ M .state .demotion_timer :close ()
205+ end
206+ M .state .demotion_timer = vim .loop .new_timer ()
207+ M .state .demotion_timer :start (
208+ M .state .visual_demotion_delay_ms ,
209+ 0 , -- 0 repeat = one-shot
210+ vim .schedule_wrap (function ()
211+ if M .state .demotion_timer then -- Check if it wasn't cancelled right before firing
212+ M .state .demotion_timer :stop () -- Ensure it's stopped
213+ M .state .demotion_timer :close ()
214+ M .state .demotion_timer = nil
215+ end
216+ M .handle_selection_demotion (current_buf ) -- Pass buffer at time of scheduling
217+ end )
218+ )
219+ else
220+ -- Genuinely in normal mode, no recent visual exit, no pending demotion.
221+ current_selection = M .get_cursor_position ()
222+ if last_visual and last_visual .bufnr == current_buf then
223+ M .state .last_active_visual_selection = nil -- Clear it as it's no longer relevant for demotion
224+ end
225+ end
226+ end
227+
228+ -- If current_selection could not be determined (e.g. get_visual_selection was nil and no other path set it)
229+ -- default to cursor position to avoid errors.
230+ if not current_selection then
133231 current_selection = M .get_cursor_position ()
134232 end
135233
136234 local changed = M .has_selection_changed (current_selection )
137235
138236 if changed then
139237 M .state .latest_selection = current_selection
140-
141238 if M .server then
142239 M .send_selection_update (current_selection )
143240 end
144241 end
145242end
146243
244+ --- Handles the demotion of a visual selection after a delay.
245+ -- Called by the demotion_timer.
246+ -- @param original_bufnr_when_scheduled number The buffer number that was active when demotion was scheduled.
247+ function M .handle_selection_demotion (original_bufnr_when_scheduled )
248+ -- Timer object is already stopped and cleared by its own callback wrapper or cancellation points.
249+ -- M.state.demotion_timer should be nil here if it fired normally or was cancelled.
250+
251+ local current_buf = vim .api .nvim_get_current_buf ()
252+ local claude_term_bufnr = terminal .get_active_terminal_bufnr ()
253+
254+ -- Condition 1: Switched to Claude Terminal
255+ if claude_term_bufnr and current_buf == claude_term_bufnr then
256+ -- Visual selection is preserved (M.state.latest_selection is still the visual one).
257+ -- The "pending" status of last_active_visual_selection is resolved.
258+ if
259+ M .state .last_active_visual_selection
260+ and M .state .last_active_visual_selection .bufnr == original_bufnr_when_scheduled
261+ then
262+ M .state .last_active_visual_selection = nil
263+ end
264+ return
265+ end
266+
267+ local current_mode_info = vim .api .nvim_get_mode ()
268+ -- Condition 2: Back in Visual Mode in the Original Buffer
269+ if
270+ current_buf == original_bufnr_when_scheduled
271+ and (current_mode_info .mode == " v" or current_mode_info .mode == " V" or current_mode_info .mode == " \022 " )
272+ then
273+ -- A new visual selection will take precedence. M.state.latest_selection will be updated by main flow.
274+ if
275+ M .state .last_active_visual_selection
276+ and M .state .last_active_visual_selection .bufnr == original_bufnr_when_scheduled
277+ then
278+ M .state .last_active_visual_selection = nil
279+ end
280+ return
281+ end
282+
283+ -- Condition 3: Still in Original Buffer & Not Visual & Not Claude Term -> Demote
284+ if current_buf == original_bufnr_when_scheduled then
285+ local new_sel_for_demotion = M .get_cursor_position () -- Demote to current cursor position
286+ -- Check if this new cursor position is actually different from the (visual) latest_selection
287+ if M .has_selection_changed (new_sel_for_demotion ) then
288+ M .state .latest_selection = new_sel_for_demotion
289+ if M .server then
290+ M .send_selection_update (M .state .latest_selection )
291+ end
292+ end
293+ end
294+
295+ -- Always clear last_active_visual_selection for the original buffer as its pending demotion is resolved.
296+ if
297+ M .state .last_active_visual_selection
298+ and M .state .last_active_visual_selection .bufnr == original_bufnr_when_scheduled
299+ then
300+ M .state .last_active_visual_selection = nil
301+ end
302+ end
303+
147304--- Validates if we're in a valid visual selection mode
148305-- @return boolean, string|nil - true if valid, false and error message if not
149306local function validate_visual_mode ()
0 commit comments