@@ -88,6 +88,14 @@ require('selecta').pick(items, {
88
88
--- @field footer_pos ? " left" | " center" | " right"
89
89
--- @field title_pos ? " left" | string
90
90
91
+ --- @class SelectaMovementConfig
92
+ --- @field next string | string[] Key (s ) for moving to next item
93
+ --- @field previous string | string[] Key (s ) for moving to previous item
94
+ --- @field close string | string[] Key (s ) for closing picker
95
+ --- @field select string | string[] Key (s ) for selecting item
96
+ --- @field alternative_next ? string @deprecated Use next array instead
97
+ --- @field alternative_previous ? string @deprecated Use previous array instead
98
+
91
99
--- @class SelectaOptions
92
100
--- @field title ? string
93
101
--- @field formatter ? fun ( item : SelectaItem ): string
@@ -109,6 +117,7 @@ require('selecta').pick(items, {
109
117
--- @field initially_hidden ? boolean
110
118
--- @field initial_index ? number
111
119
--- @field debug ? boolean
120
+ --- @field movement ? SelectaMovementConfig
112
121
113
122
--- @class SelectaHooks
114
123
--- @field on_render ? fun ( buf : number , items : SelectaItem[] , opts : SelectaOptions ) Called after items are rendered
@@ -177,10 +186,15 @@ M.config = {
177
186
ratio = 0.7 , -- percentage of screen width where right-aligned windows start
178
187
},
179
188
movement = {
180
- next = " <C-n>" ,
181
- previous = " <C-p>" ,
182
- alternative_next = " <DOWN>" ,
183
- alternative_previous = " <UP>" ,
189
+ next = { " <C-n>" , " <DOWN>" }, -- Support multiple keys
190
+ previous = { " <C-p>" , " <UP>" }, -- Support multiple keys
191
+ close = { " <ESC>" }, -- close mapping
192
+ select = { " <CR>" }, -- select mapping
193
+ delete_word = {}, -- delete word mapping
194
+ clear_line = {}, -- clear line mapping
195
+ -- Deprecated mappings (but still working)
196
+ -- alternative_next = "<DOWN>", -- @deprecated: Will be removed in v1.0
197
+ -- alternative_previous = "<UP>", -- @deprecated: Will be removed in v1.0
184
198
},
185
199
multiselect = {
186
200
enabled = false ,
@@ -1055,19 +1069,90 @@ local SPECIAL_KEYS = {
1055
1069
MOUSE = vim .api .nvim_replace_termcodes (" <LeftMouse>" , true , true , true ),
1056
1070
}
1057
1071
1072
+ --- Delete last word from query
1073
+ --- @param state SelectaState
1074
+ local function delete_last_word (state )
1075
+ -- If we're at the start, nothing to delete
1076
+ if state .cursor_pos <= 1 then
1077
+ return
1078
+ end
1079
+
1080
+ -- Find the last non-space character before cursor
1081
+ local last_char_pos = state .cursor_pos - 1
1082
+ while last_char_pos > 1 and state .query [last_char_pos ] == " " do
1083
+ last_char_pos = last_char_pos - 1
1084
+ end
1085
+
1086
+ -- Find the start of the word
1087
+ local word_start = last_char_pos
1088
+ while word_start > 1 and state .query [word_start - 1 ] ~= " " do
1089
+ word_start = word_start - 1
1090
+ end
1091
+
1092
+ -- Remove characters from word_start to last_char_pos
1093
+ for i = word_start , last_char_pos do
1094
+ table.remove (state .query , word_start )
1095
+ end
1096
+
1097
+ -- Update cursor position
1098
+ state .cursor_pos = word_start
1099
+ state .initial_open = false
1100
+ state .cursor_moved = false
1101
+ end
1102
+
1103
+ --- Clear the entire query line
1104
+ --- @param state SelectaState
1105
+ local function clear_query_line (state )
1106
+ state .query = {}
1107
+ state .cursor_pos = 1
1108
+ state .initial_open = false
1109
+ state .cursor_moved = false
1110
+ end
1111
+
1058
1112
-- Pre-compute the movement keys
1113
+ --- @param opts SelectaOptions
1114
+ --- @return table<string , string[]>
1059
1115
local function get_movement_keys (opts )
1060
1116
local movement_config = opts .movement or M .config .movement
1061
- return {
1062
- next = {
1063
- vim .api .nvim_replace_termcodes (movement_config .next or " <C-n>" , true , true , true ),
1064
- vim .api .nvim_replace_termcodes (movement_config .alternative_next or " <C-j>" , true , true , true ),
1065
- },
1066
- previous = {
1067
- vim .api .nvim_replace_termcodes (movement_config .previous or " <C-p>" , true , true , true ),
1068
- vim .api .nvim_replace_termcodes (movement_config .alternative_previous or " <C-k>" , true , true , true ),
1069
- },
1117
+ local keys = {
1118
+ next = {},
1119
+ previous = {},
1120
+ close = {},
1121
+ select = {},
1122
+ delete_word = {},
1123
+ clear_line = {},
1070
1124
}
1125
+
1126
+ -- Handle arrays of keys
1127
+ for action , mapping in pairs (movement_config ) do
1128
+ if action ~= " alternative_next" and action ~= " alternative_previous" then
1129
+ if type (mapping ) == " table" then
1130
+ -- Handle array of keys
1131
+ for _ , key in ipairs (mapping ) do
1132
+ table.insert (keys [action ], vim .api .nvim_replace_termcodes (key , true , true , true ))
1133
+ end
1134
+ elseif type (mapping ) == " string" then
1135
+ -- Handle single key as string
1136
+ table.insert (keys [action ], vim .api .nvim_replace_termcodes (mapping , true , true , true ))
1137
+ end
1138
+ end
1139
+ end
1140
+
1141
+ -- Handle deprecated alternative mappings
1142
+ if movement_config .alternative_next then
1143
+ -- TODO: version 0.6.0 will start sending this message
1144
+ -- vim.notify_once(
1145
+ -- "alternative_next/previous are deprecated and will be removed in v1.0. "
1146
+ -- .. "Use movement.next/previous arrays instead.",
1147
+ -- vim.log.levels.WARN
1148
+ -- )
1149
+ table.insert (keys .next , vim .api .nvim_replace_termcodes (movement_config .alternative_next , true , true , true ))
1150
+ end
1151
+ if movement_config .alternative_previous then
1152
+ table.insert (keys .previous , vim .api .nvim_replace_termcodes (movement_config .alternative_previous , true , true , true ))
1153
+ end
1154
+
1155
+ return keys
1071
1156
end
1072
1157
1073
1158
-- Simplified movement handler
@@ -1226,29 +1311,28 @@ local function handle_char(state, char, opts)
1226
1311
end
1227
1312
1228
1313
local char_key = type (char ) == " number" and vim .fn .nr2char (char ) or char
1314
+ local movement_keys = get_movement_keys (opts )
1229
1315
1316
+ -- Handle custom keymaps first
1230
1317
if opts .keymaps then
1231
1318
for _ , keymap in ipairs (opts .keymaps ) do
1232
1319
if char_key == vim .api .nvim_replace_termcodes (keymap .key , true , true , true ) then
1233
1320
local selected = state .filtered_items [vim .api .nvim_win_get_cursor (state .win )[1 ]]
1234
1321
if selected then
1235
- -- Check for multiselect state
1322
+ -- Handle multiselect state
1236
1323
if opts .multiselect and opts .multiselect .enabled and state .selected_count > 0 then
1237
- -- Collect selected items
1238
1324
local selected_items = {}
1239
1325
for _ , item in ipairs (state .filtered_items ) do
1240
1326
if state .selected [tostring (item .value or item .text )] then
1241
1327
table.insert (selected_items , item )
1242
1328
end
1243
1329
end
1244
- -- Pass all selected items to handler if there are any
1245
1330
local should_close = keymap .handler (selected_items , state , M .close_picker )
1246
1331
if should_close == false then
1247
1332
M .close_picker (state )
1248
1333
return nil
1249
1334
end
1250
1335
else
1251
- -- Original single-item behavior
1252
1336
local should_close = keymap .handler (selected , state , M .close_picker )
1253
1337
if should_close == false then
1254
1338
M .close_picker (state )
@@ -1261,11 +1345,9 @@ local function handle_char(state, char, opts)
1261
1345
end
1262
1346
end
1263
1347
1348
+ -- Handle multiselect keymaps
1264
1349
if opts .multiselect and opts .multiselect .enabled then
1265
1350
local multiselect_keys = opts .multiselect .keymaps or M .config .multiselect .keymaps
1266
-
1267
- M .clear_log () -- Optional: clear the log when starting new session
1268
- -- Handle multiselect keymaps
1269
1351
if char_key == vim .api .nvim_replace_termcodes (multiselect_keys .toggle , true , true , true ) then
1270
1352
if handle_toggle (state , opts , 1 ) then
1271
1353
return nil
@@ -1284,42 +1366,25 @@ local function handle_char(state, char, opts)
1284
1366
return nil
1285
1367
end
1286
1368
end
1287
- -- Handle mouse clicks
1288
- if char_key == SPECIAL_KEYS .MOUSE and vim .v .mouse_win ~= state .win and vim .v .mouse_win ~= state .prompt_win then
1289
- if opts .on_cancel then
1290
- opts .on_cancel ()
1291
- end
1292
- M .close_picker (state )
1293
- return nil
1294
- end
1295
1369
1296
- local movement_keys = get_movement_keys (opts )
1297
- local movement = nil
1298
-
1299
- -- Determine movement direction from configured keys only
1370
+ -- Handle movement and control keys
1300
1371
if vim .tbl_contains (movement_keys .previous , char_key ) then
1301
- movement = - 1
1372
+ state .cursor_moved = true
1373
+ state .initial_open = false
1374
+ handle_movement (state , - 1 , opts )
1375
+ return nil
1302
1376
elseif vim .tbl_contains (movement_keys .next , char_key ) then
1303
- movement = 1
1304
- end
1305
-
1306
- if movement then
1307
1377
state .cursor_moved = true
1308
1378
state .initial_open = false
1309
- handle_movement (state , movement , opts )
1310
- return nil -- prevent further processing
1311
- elseif char_key == SPECIAL_KEYS .LEFT then
1312
- state .cursor_pos = math.max (1 , state .cursor_pos - 1 )
1313
- elseif char_key == SPECIAL_KEYS .RIGHT then
1314
- state .cursor_pos = math.min (# state .query + 1 , state .cursor_pos + 1 )
1315
- elseif char_key == SPECIAL_KEYS .BS then
1316
- if state .cursor_pos > 1 then
1317
- table.remove (state .query , state .cursor_pos - 1 )
1318
- state .cursor_pos = state .cursor_pos - 1
1319
- state .initial_open = false
1320
- state .cursor_moved = false
1379
+ handle_movement (state , 1 , opts )
1380
+ return nil
1381
+ elseif vim .tbl_contains (movement_keys .close , char_key ) then
1382
+ if opts .on_cancel then
1383
+ opts .on_cancel ()
1321
1384
end
1322
- elseif char_key == SPECIAL_KEYS .CR then
1385
+ M .close_picker (state )
1386
+ return nil
1387
+ elseif vim .tbl_contains (movement_keys .select , char_key ) then
1323
1388
if opts .multiselect and opts .multiselect .enabled then
1324
1389
local selected_items = {}
1325
1390
for _ , item in ipairs (state .items ) do
@@ -1330,27 +1395,44 @@ local function handle_char(state, char, opts)
1330
1395
if # selected_items > 0 and opts .multiselect .on_select then
1331
1396
opts .multiselect .on_select (selected_items )
1332
1397
elseif # selected_items == 0 then
1333
- -- If no items selected, treat as single select
1334
1398
local current = state .filtered_items [vim .api .nvim_win_get_cursor (state .win )[1 ]]
1335
1399
if current and opts .on_select then
1336
1400
opts .on_select (current )
1337
1401
end
1338
1402
end
1339
1403
else
1340
- -- Existing single-select behavior
1341
1404
local selected = state .filtered_items [vim .api .nvim_win_get_cursor (state .win )[1 ]]
1342
1405
if selected and opts .on_select then
1343
1406
opts .on_select (selected )
1344
1407
end
1345
1408
end
1346
1409
M .close_picker (state )
1347
1410
return nil
1348
- elseif char_key == SPECIAL_KEYS .ESC then
1411
+ elseif vim .tbl_contains (movement_keys .delete_word , char_key ) then
1412
+ delete_last_word (state )
1413
+ update_display (state , opts )
1414
+ return nil
1415
+ elseif vim .tbl_contains (movement_keys .clear_line , char_key ) then
1416
+ clear_query_line (state )
1417
+ update_display (state , opts )
1418
+ return nil
1419
+ elseif char_key == SPECIAL_KEYS .MOUSE and vim .v .mouse_win ~= state .win and vim .v .mouse_win ~= state .prompt_win then
1349
1420
if opts .on_cancel then
1350
1421
opts .on_cancel ()
1351
1422
end
1352
1423
M .close_picker (state )
1353
1424
return nil
1425
+ elseif char_key == SPECIAL_KEYS .LEFT then
1426
+ state .cursor_pos = math.max (1 , state .cursor_pos - 1 )
1427
+ elseif char_key == SPECIAL_KEYS .RIGHT then
1428
+ state .cursor_pos = math.min (# state .query + 1 , state .cursor_pos + 1 )
1429
+ elseif char_key == SPECIAL_KEYS .BS then
1430
+ if state .cursor_pos > 1 then
1431
+ table.remove (state .query , state .cursor_pos - 1 )
1432
+ state .cursor_pos = state .cursor_pos - 1
1433
+ state .initial_open = false
1434
+ state .cursor_moved = false
1435
+ end
1354
1436
elseif type (char ) == " number" and char >= 32 and char <= 126 then
1355
1437
table.insert (state .query , state .cursor_pos , vim .fn .nr2char (char ))
1356
1438
state .cursor_pos = state .cursor_pos + 1
0 commit comments