@@ -6,6 +6,54 @@ local async = require('blink.cmp.lib.async')
6
6
local constants = require (' blink.cmp.sources.cmdline.constants' )
7
7
local path_lib = require (' blink.cmp.sources.path.lib' )
8
8
9
+ --- Split the command line into arguments, handling path escaping and trailing spaces.
10
+ --- For path completions, split by paths and normalize each one if needed.
11
+ --- For other completions, splits by spaces and preserves trailing empty arguments.
12
+ --- @param context table
13
+ --- @param is_path_completion boolean
14
+ --- @return string , table
15
+ local function smart_split (context , is_path_completion )
16
+ local line = context .line
17
+
18
+ if is_path_completion then
19
+ -- Split the line into tokens, respecting escaped spaces in paths
20
+ local tokens = path_lib :split_unescaped (line :gsub (' ^%s+' , ' ' ))
21
+ local cmd = tokens [1 ]
22
+ local args = {}
23
+
24
+ for i = 2 , # tokens do
25
+ local arg = tokens [i ]
26
+ -- Escape argument if it contains unescaped spaces
27
+ -- Some commands may expect escaped paths (:edit), others may not (:view)
28
+ if arg and arg ~= ' ' and not arg :find (' \\ ' ) then arg = path_lib :fnameescape (arg ) end
29
+ table.insert (args , arg )
30
+ end
31
+ return line , { cmd , unpack (args ) }
32
+ end
33
+
34
+ return line , vim .split (line :gsub (' ^%s+' , ' ' ), ' ' , { plain = true })
35
+ end
36
+
37
+ -- Find the longest match for a given set of patterns
38
+ --- @param str string
39
+ --- @param patterns table
40
+ --- @return string
41
+ local function longest_match (str , patterns )
42
+ local best = ' '
43
+ for _ , pat in ipairs (patterns ) do
44
+ local m = str :match (pat )
45
+ if m and # m > # best then best = m end
46
+ end
47
+ return best
48
+ end
49
+
50
+ --- @param name string
51
+ --- @return boolean ?
52
+ local function is_boolean_option (name )
53
+ local ok , opt = pcall (function () return vim .opt [name ]:get () end )
54
+ if ok then return type (opt ) == ' boolean' end
55
+ end
56
+
9
57
--- @class blink.cmp.Source
10
58
local cmdline = {}
11
59
@@ -15,32 +63,30 @@ function cmdline.new()
15
63
self .offset = - 1
16
64
self .ctype = ' '
17
65
self .items = {}
18
- return self
66
+ return self --[[ @as blink.cmp.Source ]]
19
67
end
20
68
69
+ --- @return boolean
21
70
function cmdline :enabled ()
22
71
return vim .api .nvim_get_mode ().mode == ' c' and vim .tbl_contains ({ ' :' , ' @' }, vim .fn .getcmdtype ())
23
72
end
24
73
25
- --- @param name string
26
- --- @return boolean ?
27
- function cmdline :is_boolean_option (name )
28
- local ok , opt = pcall (function () return vim .opt [name ]:get () end )
29
- if ok then return type (opt ) == ' boolean' end
30
- end
31
-
74
+ --- @return table
32
75
function cmdline :get_trigger_characters () return { ' ' , ' .' , ' #' , ' -' , ' =' , ' /' , ' :' , ' !' } end
33
76
77
+ --- @param context blink.cmp.Context
78
+ --- @param callback fun ( result ?: blink.cmp.CompletionResponse )
79
+ --- @return fun ()
34
80
function cmdline :get_completions (context , callback )
35
81
local completion_type = vim .fn .getcmdcompltype ()
36
82
37
83
local is_path_completion = vim .tbl_contains (constants .completion_types .path , completion_type )
38
84
local is_buffer_completion = vim .tbl_contains (constants .completion_types .buffer , completion_type )
39
85
40
- local context_line , arguments = self : smart_split (context , is_path_completion or is_buffer_completion )
86
+ local context_line , arguments = smart_split (context , is_path_completion or is_buffer_completion )
41
87
local cmd = arguments [1 ]
42
88
local before_cursor = context_line :sub (1 , context .cursor [2 ])
43
- local _ , args_before_cursor = self : smart_split ({ line = before_cursor }, is_path_completion or is_buffer_completion )
89
+ local _ , args_before_cursor = smart_split ({ line = before_cursor }, is_path_completion or is_buffer_completion )
44
90
local arg_number = # args_before_cursor
45
91
46
92
local leading_spaces = context .line :match (' ^(%s*)' ) -- leading spaces in the original query
@@ -126,27 +172,17 @@ function cmdline:get_completions(context, callback)
126
172
-- In all other cases, we want to check for the prefix and remove it from the filter text
127
173
-- and add it to the newText
128
174
129
- -- Helper function: find the longest match for a given set of patterns
130
- local function longest_match (str , patterns )
131
- local best = ' '
132
- for _ , pat in ipairs (patterns ) do
133
- local m = str :match (pat )
134
- if m and # m > # best then best = m end
135
- end
136
- return best
137
- end
138
-
139
175
--- @cast completions string[]
140
- local is_first_arg = arg_number == 1
141
- local is_lua_expr = completion_type == ' lua'
142
176
local unique_prefixes = is_buffer_completion
143
177
and # completions < 2000
144
178
and path_lib :compute_unique_suffixes (completions )
145
179
or {}
146
180
181
+ --- @type blink.cmp.CompletionItem[]
147
182
local items = {}
148
183
for _ , completion in ipairs (completions ) do
149
- local filter_text , new_text , label , label_details
184
+ local filter_text , new_text = completion , completion
185
+ local label , label_details
150
186
151
187
-- path completion in commands, e.g. `chdir <path>` and options, e.g. `:set directory=<path>`
152
188
if is_path_completion then
@@ -162,36 +198,22 @@ function cmdline:get_completions(context, callback)
162
198
if # unique_prefixes [completion ] then
163
199
label_details = { description = completion :sub (1 , -# unique_prefixes [completion ] - 2 ) }
164
200
end
165
- filter_text = completion
166
201
new_text = vim .fn .fnameescape (completion )
167
202
168
- -- lua expr, e.g. `:=<expr>`
169
- elseif is_lua_expr then
170
- filter_text = completion
171
- new_text = current_arg_prefix .. completion
172
-
173
203
-- env variables
174
204
elseif completion_type == ' environment' then
175
205
filter_text = ' $' .. completion
176
206
new_text = ' $' .. completion
177
207
178
- -- for other completions, check if the prefix is already present
179
- elseif not is_first_arg then
180
- local has_prefix = string.find (completion , current_arg_prefix , 1 , true ) == 1
181
-
182
- filter_text = has_prefix and completion :sub (# current_arg_prefix + 1 ) or completion
183
- new_text = has_prefix and completion or current_arg_prefix .. completion
184
-
185
- -- fallback
186
- else
187
- filter_text = completion
188
- new_text = completion
208
+ -- for other completions, prepend the prefix
209
+ elseif vim .tbl_contains ({ ' lua' , ' ' }, completion_type ) then
210
+ new_text = current_arg_prefix .. completion
189
211
end
190
212
191
213
local start_pos = # text_before_argument + # leading_spaces
192
214
193
- -- exclude range on the first argument
194
- if is_first_arg and not is_lua_expr then
215
+ -- exclude range for commands on the first argument
216
+ if arg_number == 1 and completion_type == ' command ' then
195
217
local prefix = longest_match (current_arg , {
196
218
" ^%s*'<%s*,%s*'>%s*" , -- Visual range, e.g., '<,>'
197
219
' ^%s*%d+%s*,%s*%d+%s*' , -- Numeric range, e.g., 3,5
@@ -200,6 +222,7 @@ function cmdline:get_completions(context, callback)
200
222
start_pos = start_pos + # prefix
201
223
end
202
224
225
+ --- @type blink.cmp.CompletionItem
203
226
local item = {
204
227
label = label or filter_text ,
205
228
filterText = filter_text ,
@@ -224,57 +247,31 @@ function cmdline:get_completions(context, callback)
224
247
}
225
248
items [# items + 1 ] = item
226
249
227
- if completion_type == ' option' and cmdline : is_boolean_option (filter_text ) then
250
+ if completion_type == ' option' and is_boolean_option (filter_text ) then
228
251
filter_text = ' no' .. filter_text
229
252
items [# items + 1 ] = vim .tbl_deep_extend (' force' , {}, item , {
230
253
label = filter_text ,
231
254
filterText = filter_text ,
232
255
sortText = filter_text ,
233
256
textEdit = { newText = ' no' .. new_text },
234
- })
257
+ }) --[[ @as blink.cmp.CompletionItem ]]
235
258
end
236
259
end
237
260
238
261
callback ({
239
262
is_incomplete_backward = completion_type ~= ' help' ,
240
263
is_incomplete_forward = false ,
241
264
items = items ,
265
+ --- @diagnostic disable-next-line : missing-return
242
266
})
243
267
end )
244
268
:catch (function (err )
245
269
vim .notify (' Error while fetching completions: ' .. err , vim .log .levels .ERROR , { title = ' blink.cmp' })
270
+ --- @diagnostic disable-next-line : missing-return
246
271
callback ({ is_incomplete_backward = false , is_incomplete_forward = false , items = {} })
247
272
end )
248
273
249
274
return function () task :cancel () end
250
275
end
251
276
252
- --- Split the command line into arguments, handling path escaping and trailing spaces.
253
- --- For path completions, split by paths and normalize each one if needed.
254
- --- For other completions, splits by spaces and preserves trailing empty arguments.
255
- --- @param context table
256
- --- @param is_path_completion boolean
257
- --- @return string , table
258
- function cmdline :smart_split (context , is_path_completion )
259
- local line = context .line
260
-
261
- if is_path_completion then
262
- -- Split the line into tokens, respecting escaped spaces in paths
263
- local tokens = path_lib :split_unescaped (line :gsub (' ^%s+' , ' ' ))
264
- local cmd = tokens [1 ]
265
- local args = {}
266
-
267
- for i = 2 , # tokens do
268
- local arg = tokens [i ]
269
- -- Escape argument if it contains unescaped spaces
270
- -- Some commands may expect escaped paths (:edit), others may not (:view)
271
- if arg and arg ~= ' ' and not arg :find (' \\ ' ) then arg = path_lib :fnameescape (arg ) end
272
- table.insert (args , arg )
273
- end
274
- return line , { cmd , unpack (args ) }
275
- end
276
-
277
- return line , vim .split (line :gsub (' ^%s+' , ' ' ), ' ' , { plain = true })
278
- end
279
-
280
277
return cmdline
0 commit comments