6
6
-- @module claudecode.selection
7
7
local M = {}
8
8
9
+ local config_module = require (" claudecode.config" )
10
+ local terminal = require (" claudecode.terminal" )
11
+
9
12
-- Selection state
10
13
M .state = {
11
14
latest_selection = nil ,
12
15
tracking_enabled = false ,
13
16
debounce_timer = nil ,
14
17
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
15
23
}
16
24
17
25
--- Enables selection tracking.
@@ -25,6 +33,11 @@ function M.enable(server)
25
33
M .state .tracking_enabled = true
26
34
M .server = server
27
35
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
+
28
41
M ._create_autocommands ()
29
42
end
30
43
@@ -124,26 +137,170 @@ function M.update_selection()
124
137
return
125
138
end
126
139
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
128
167
129
- local current_selection
130
- if current_mode == " v" or current_mode == " V" or current_mode == " \022 " then
131
168
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
133
231
current_selection = M .get_cursor_position ()
134
232
end
135
233
136
234
local changed = M .has_selection_changed (current_selection )
137
235
138
236
if changed then
139
237
M .state .latest_selection = current_selection
140
-
141
238
if M .server then
142
239
M .send_selection_update (current_selection )
143
240
end
144
241
end
145
242
end
146
243
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
+
147
304
--- Validates if we're in a valid visual selection mode
148
305
-- @return boolean, string|nil - true if valid, false and error message if not
149
306
local function validate_visual_mode ()
0 commit comments