1
- -- Selection tracking for Claude Code Neovim integration
1
+ ---
2
+ -- Manages selection tracking and communication with the Claude server.
3
+ -- This module handles enabling/disabling selection tracking, debouncing updates,
4
+ -- determining the current selection (visual or cursor position), and sending
5
+ -- updates to the Claude server.
6
+ -- @module claudecode.selection
2
7
local M = {}
3
8
4
9
-- Selection state
@@ -9,7 +14,9 @@ M.state = {
9
14
debounce_ms = 300 , -- Default debounce time in milliseconds
10
15
}
11
16
12
- -- Enable selection tracking
17
+ --- Enables selection tracking.
18
+ -- Sets up autocommands to monitor cursor movements, mode changes, and text changes.
19
+ -- @param server table The server object to use for communication.
13
20
function M .enable (server )
14
21
if M .state .tracking_enabled then
15
22
return
@@ -18,53 +25,49 @@ function M.enable(server)
18
25
M .state .tracking_enabled = true
19
26
M .server = server
20
27
21
- -- Set up autocommands for tracking selections
22
28
M ._create_autocommands ()
23
29
end
24
30
25
- -- Disable selection tracking
31
+ --- Disables selection tracking.
32
+ -- Clears autocommands, resets internal state, and stops any active debounce timers.
26
33
function M .disable ()
27
34
if not M .state .tracking_enabled then
28
35
return
29
36
end
30
37
31
38
M .state .tracking_enabled = false
32
39
33
- -- Remove autocommands
34
40
M ._clear_autocommands ()
35
41
36
- -- Clear state
37
42
M .state .latest_selection = nil
38
43
M .server = nil
39
44
40
- -- Clear debounce timer if active
41
45
if M .state .debounce_timer then
42
46
vim .loop .timer_stop (M .state .debounce_timer )
43
47
M .state .debounce_timer = nil
44
48
end
45
49
end
46
50
47
- -- Create autocommands for tracking selections
51
+ --- Creates autocommands for tracking selections.
52
+ -- Sets up listeners for CursorMoved, CursorMovedI, ModeChanged, and TextChanged events.
53
+ -- @local
48
54
function M ._create_autocommands ()
49
55
local group = vim .api .nvim_create_augroup (" ClaudeCodeSelection" , { clear = true })
50
56
51
- -- Track selection changes in various modes
52
57
vim .api .nvim_create_autocmd ({ " CursorMoved" , " CursorMovedI" }, {
53
58
group = group ,
54
59
callback = function ()
55
60
M .on_cursor_moved ()
56
61
end ,
57
62
})
58
63
59
- -- Track mode changes
60
64
vim .api .nvim_create_autocmd (" ModeChanged" , {
61
65
group = group ,
62
66
callback = function ()
63
67
M .on_mode_changed ()
64
68
end ,
65
69
})
66
70
67
- -- Track buffer content changes
68
71
vim .api .nvim_create_autocmd (" TextChanged" , {
69
72
group = group ,
70
73
callback = function ()
@@ -73,74 +76,75 @@ function M._create_autocommands()
73
76
})
74
77
end
75
78
76
- -- Clear autocommands
79
+ --- Clears the autocommands related to selection tracking.
80
+ -- @local
77
81
function M ._clear_autocommands ()
78
82
vim .api .nvim_clear_autocmds ({ group = " ClaudeCodeSelection" })
79
83
end
80
84
81
- -- Handle cursor movement events
85
+ --- Handles cursor movement events.
86
+ -- Triggers a debounced update of the selection.
82
87
function M .on_cursor_moved ()
83
88
-- Debounce the update to avoid sending too many updates
84
89
M .debounce_update ()
85
90
end
86
91
87
- -- Handle mode change events
92
+ --- Handles mode change events.
93
+ -- Triggers an immediate update of the selection.
88
94
function M .on_mode_changed ()
89
95
-- Update selection immediately on mode change
90
96
M .update_selection ()
91
97
end
92
98
93
- -- Handle text change events
99
+ --- Handles text change events.
100
+ -- Triggers a debounced update of the selection.
94
101
function M .on_text_changed ()
95
- -- Debounce the update
96
102
M .debounce_update ()
97
103
end
98
104
99
- -- Debounce selection updates
105
+ --- Debounces selection updates.
106
+ -- Ensures that `update_selection` is not called too frequently by deferring
107
+ -- its execution.
100
108
function M .debounce_update ()
101
- -- Cancel existing timer if active
102
109
if M .state .debounce_timer then
103
110
vim .loop .timer_stop (M .state .debounce_timer )
104
111
end
105
112
106
- -- Create new timer for debounced update
107
113
M .state .debounce_timer = vim .defer_fn (function ()
108
114
M .update_selection ()
109
115
M .state .debounce_timer = nil
110
116
end , M .state .debounce_ms )
111
117
end
112
118
113
- -- Update the current selection
119
+ --- Updates the current selection state.
120
+ -- Determines the current selection based on the editor mode (visual or normal)
121
+ -- and sends an update to the server if the selection has changed.
114
122
function M .update_selection ()
115
123
if not M .state .tracking_enabled then
116
124
return
117
125
end
118
126
119
127
local current_mode = vim .api .nvim_get_mode ().mode
120
128
121
- -- Get selection based on mode
122
129
local current_selection
123
130
if current_mode == " v" or current_mode == " V" or current_mode == " \022 " then
124
- -- Visual mode selection
125
131
current_selection = M .get_visual_selection ()
126
132
else
127
- -- Normal mode - no selection, just track cursor position
128
133
current_selection = M .get_cursor_position ()
129
134
end
130
135
131
- -- Check if selection has changed
132
136
if M .has_selection_changed (current_selection ) then
133
- -- Store latest selection
134
137
M .state .latest_selection = current_selection
135
138
136
- -- Send selection update if connected to Claude
137
139
if M .server then
138
140
M .send_selection_update (current_selection )
139
141
end
140
142
end
141
143
end
142
144
143
- -- Get the current visual selection
145
+ --- Gets the current visual selection details.
146
+ -- @return table|nil A table containing selection text, file path, URL, and
147
+ -- start/end positions, or nil if no visual selection exists.
144
148
function M .get_visual_selection ()
145
149
local start_pos = vim .fn .getpos (" '<" )
146
150
local end_pos = vim .fn .getpos (" '>" )
@@ -153,23 +157,20 @@ function M.get_visual_selection()
153
157
local current_buf = vim .api .nvim_get_current_buf ()
154
158
local file_path = vim .api .nvim_buf_get_name (current_buf )
155
159
156
- -- Get selection text
157
160
local lines = vim .api .nvim_buf_get_lines (
158
161
current_buf ,
159
162
start_pos [2 ] - 1 , -- 0-indexed line
160
163
end_pos [2 ], -- end line is exclusive
161
164
false
162
165
)
163
166
164
- -- Adjust for column positions
165
167
if # lines == 1 then
166
168
lines [1 ] = string.sub (lines [1 ], start_pos [3 ], end_pos [3 ])
167
169
else
168
170
lines [1 ] = string.sub (lines [1 ], start_pos [3 ])
169
171
lines [# lines ] = string.sub (lines [# lines ], 1 , end_pos [3 ])
170
172
end
171
173
172
- -- Combine lines
173
174
local text = table.concat (lines , " \n " )
174
175
175
176
return {
@@ -184,7 +185,9 @@ function M.get_visual_selection()
184
185
}
185
186
end
186
187
187
- -- Get the current cursor position (no selection)
188
+ --- Gets the current cursor position when no visual selection is active.
189
+ -- @return table A table containing an empty text, file path, URL, and cursor
190
+ -- position as start/end, with isEmpty set to true.
188
191
function M .get_cursor_position ()
189
192
local cursor_pos = vim .api .nvim_win_get_cursor (0 )
190
193
local current_buf = vim .api .nvim_get_current_buf ()
@@ -202,67 +205,71 @@ function M.get_cursor_position()
202
205
}
203
206
end
204
207
205
- -- Check if selection has changed
208
+ --- Checks if the selection has changed compared to the latest stored selection.
209
+ -- @param new_selection table|nil The new selection object to compare.
210
+ -- @return boolean true if the selection has changed, false otherwise.
206
211
function M .has_selection_changed (new_selection )
207
- if not M .state .latest_selection then
208
- return true
212
+ local old_selection = M .state .latest_selection
213
+
214
+ if not new_selection then
215
+ -- If old selection was also nil, no change. Otherwise (old selection existed), it's a change.
216
+ return old_selection ~= nil
209
217
end
210
218
211
- local current = M .state .latest_selection
219
+ if not old_selection then
220
+ return true
221
+ end
212
222
213
- -- Compare file paths
214
- if current .filePath ~= new_selection .filePath then
223
+ if old_selection .filePath ~= new_selection .filePath then
215
224
return true
216
225
end
217
226
218
- -- Compare text content
219
- if current .text ~= new_selection .text then
227
+ if old_selection .text ~= new_selection .text then
220
228
return true
221
229
end
222
230
223
- -- Compare selection positions
224
231
if
225
- current .selection .start .line ~= new_selection .selection .start .line
226
- or current .selection .start .character ~= new_selection .selection .start .character
227
- or current .selection [" end" ].line ~= new_selection .selection [" end" ].line
228
- or current .selection [" end" ].character ~= new_selection .selection [" end" ].character
232
+ old_selection .selection .start .line ~= new_selection .selection .start .line
233
+ or old_selection .selection .start .character ~= new_selection .selection .start .character
234
+ or old_selection .selection [" end" ].line ~= new_selection .selection [" end" ].line
235
+ or old_selection .selection [" end" ].character ~= new_selection .selection [" end" ].character
229
236
then
230
237
return true
231
238
end
232
239
233
240
return false
234
241
end
235
242
236
- -- Send selection update to Claude
243
+ --- Sends the selection update to the Claude server.
244
+ -- @param selection table The selection object to send.
237
245
function M .send_selection_update (selection )
238
- -- Send via WebSocket
239
246
M .server .broadcast (" selection_changed" , selection )
240
247
end
241
248
242
- -- Get the latest selection
249
+ --- Gets the latest recorded selection.
250
+ -- @return table|nil The latest selection object, or nil if none recorded.
243
251
function M .get_latest_selection ()
244
252
return M .state .latest_selection
245
253
end
246
254
247
- -- Send current selection to Claude (user command)
255
+ --- Sends the current selection to Claude.
256
+ -- This function is typically invoked by a user command. It forces an immediate
257
+ -- update and sends the latest selection.
248
258
function M .send_current_selection ()
249
259
if not M .state .tracking_enabled or not M .server then
250
260
vim .api .nvim_err_writeln (" Claude Code is not running" )
251
261
return
252
262
end
253
263
254
- -- Force an immediate selection update
255
264
M .update_selection ()
256
265
257
- -- Get the latest selection
258
266
local selection = M .state .latest_selection
259
267
260
268
if not selection then
261
269
vim .api .nvim_err_writeln (" No selection available" )
262
270
return
263
271
end
264
272
265
- -- Send it to Claude
266
273
M .send_selection_update (selection )
267
274
268
275
vim .api .nvim_echo ({ { " Selection sent to Claude" , " Normal" } }, false , {})
0 commit comments