4
4
5
5
local async = require (' blink.cmp.lib.async' )
6
6
local constants = require (' blink.cmp.sources.cmdline.constants' )
7
+ local cmdline_utils = require (' blink.cmp.sources.cmdline.utils' )
7
8
local utils = require (' blink.cmp.sources.lib.utils' )
8
9
local path_lib = require (' blink.cmp.sources.path.lib' )
9
10
10
- --- Split the command line into arguments, handling path escaping and trailing spaces.
11
- --- For path completions, split by paths and normalize each one if needed.
12
- --- For other completions, splits by spaces and preserves trailing empty arguments.
13
- --- @param context table
14
- --- @param is_path_completion boolean
15
- --- @return string , table
16
- local function smart_split (context , is_path_completion )
17
- local line = context .line
18
-
19
- local function contains_vim_expr (line )
20
- -- Checks for common Vim expressions: %, #, %:h, %:p, etc.
21
- return vim .regex ([[ %\%(:[phtrwe~.]\)\?]] ):match_str (line ) ~= nil
22
- end
23
- local function contains_wildcard (line ) return line :find (' [%*%?%[%]]' ) ~= nil end
24
-
25
- if is_path_completion and not contains_vim_expr (line ) and not contains_wildcard (line ) then
26
- -- Split the line into tokens, respecting escaped spaces in paths
27
- local tokens = path_lib :split_unescaped (line :gsub (' ^%s+' , ' ' ))
28
- local cmd = tokens [1 ]
29
- local args = {}
30
-
31
- for i = 2 , # tokens do
32
- local arg = tokens [i ]
33
- -- Escape argument if it contains unescaped spaces
34
- -- Some commands may expect escaped paths (:edit), others may not (:view)
35
- if arg and arg ~= ' ' and not arg :find (' \\ ' ) then arg = path_lib :fnameescape (arg ) end
36
- table.insert (args , arg )
37
- end
38
- return line , { cmd , unpack (args ) }
39
- end
40
-
41
- return line , vim .split (line :gsub (' ^%s+' , ' ' ), ' ' , { plain = true })
42
- end
43
-
44
- -- Find the longest match for a given set of patterns
45
- --- @param str string
46
- --- @param patterns table
47
- --- @return string
48
- local function longest_match (str , patterns )
49
- local best = ' '
50
- for _ , pat in ipairs (patterns ) do
51
- local m = str :match (pat )
52
- if m and # m > # best then best = m end
53
- end
54
- return best
55
- end
56
-
57
- --- Returns completion items for a given pattern and type, with special handling for shell commands on Windows/WSL.
58
- --- @param pattern string The partial command to match for completion
59
- --- @param type string The type of completion
60
- --- @param completion_type ? string Original completion type from vim.fn.getcmdcompltype ()
61
- --- @return table completions
62
- local function get_completions (pattern , type , completion_type )
63
- -- If a shell command is requested on Windows or WSL, update PATH to avoid performance issues.
64
- if completion_type == ' shellcmd' then
65
- local separator , filter_fn
66
-
67
- if vim .fn .has (' win32' ) == 1 then
68
- separator = ' ;'
69
- -- Remove System32 folder on native Windows
70
- filter_fn = function (part ) return not part :lower ():match (' ^[a-z]:\\ windows\\ system32$' ) end
71
- elseif vim .fn .has (' wsl' ) == 1 then
72
- separator = ' :'
73
- -- Remove all Windows filesystem mounts on WSL
74
- filter_fn = function (part ) return not part :lower ():match (' ^/mnt/[a-z]/' ) end
75
- end
76
-
77
- if filter_fn then
78
- local orig_path = vim .env .PATH
79
- local new_path = table.concat (vim .tbl_filter (filter_fn , vim .split (orig_path , separator )), separator )
80
- vim .env .PATH = new_path
81
- local completions = vim .fn .getcompletion (pattern , type , true )
82
- vim .env .PATH = orig_path
83
- return completions
84
- end
85
- end
86
-
87
- return vim .fn .getcompletion (pattern , type , true )
88
- end
89
-
90
11
--- @class blink.cmp.Source
91
12
local cmdline = {
92
13
--- @type table<string , vim.api.keyset.get_option_info ? >
@@ -109,7 +30,7 @@ function cmdline:enabled()
109
30
end
110
31
111
32
--- @return table
112
- function cmdline :get_trigger_characters () return { ' ' , ' .' , ' #' , ' -' , ' =' , ' /' , ' :' , ' !' } end
33
+ function cmdline :get_trigger_characters () return { ' ' , ' .' , ' #' , ' -' , ' =' , ' /' , ' :' , ' !' , ' % ' , ' ~ ' } end
113
34
114
35
--- @param context blink.cmp.Context
115
36
--- @param callback fun ( result ?: blink.cmp.CompletionResponse )
@@ -119,11 +40,15 @@ function cmdline:get_completions(context, callback)
119
40
120
41
local is_path_completion = vim .tbl_contains (constants .completion_types .path , completion_type )
121
42
local is_buffer_completion = vim .tbl_contains (constants .completion_types .buffer , completion_type )
43
+ local is_filename_modifier_completion = cmdline_utils .contains_filename_modifiers (context .line )
44
+ local is_wildcard_completion = cmdline_utils .contains_wildcard (context .line )
122
45
123
- local context_line , arguments = smart_split (context , is_path_completion or is_buffer_completion )
124
- local cmd = arguments [1 ]
46
+ local should_split_path = (is_path_completion or is_buffer_completion )
47
+ and not is_filename_modifier_completion
48
+ and not is_wildcard_completion
49
+ local context_line , arguments = cmdline_utils .smart_split (context .line , should_split_path )
125
50
local before_cursor = context_line :sub (1 , context .cursor [2 ])
126
- local _ , args_before_cursor = smart_split ({ line = before_cursor }, is_path_completion or is_buffer_completion )
51
+ local _ , args_before_cursor = cmdline_utils . smart_split (before_cursor , should_split_path )
127
52
local arg_number = # args_before_cursor
128
53
129
54
local leading_spaces = context .line :match (' ^(%s*)' ) -- leading spaces in the original query
@@ -135,6 +60,10 @@ function cmdline:get_completions(context, callback)
135
60
local keyword = context .get_bounds (keyword_config .range )
136
61
local current_arg_prefix = current_arg :sub (1 , keyword .start_col - # text_before_argument - 1 )
137
62
63
+ local unique_suffixes = {}
64
+ local unique_suffixes_limit = 2000
65
+ local special_char , vim_expr
66
+
138
67
local task = async .task
139
68
.empty ()
140
69
:map (function ()
@@ -181,21 +110,52 @@ function cmdline:get_completions(context, callback)
181
110
-- path completions uniquely expect only the current path
182
111
query = is_path_completion and current_arg_prefix or query
183
112
184
- completions = get_completions (query , compl_type , completion_type )
113
+ completions = cmdline_utils . get_completions (query , compl_type , completion_type )
185
114
if type (completions ) ~= ' table' then completions = {} end
186
115
end
187
116
end
117
+ elseif is_filename_modifier_completion then
118
+ vim_expr = cmdline_utils .extract_quoted_part (current_arg ) or current_arg
119
+ special_char = vim_expr :sub (- 1 )
120
+
121
+ -- Alternate files
122
+ if special_char == ' #' then
123
+ local alt_buf = vim .fn .bufnr (' #' )
124
+ if alt_buf ~= - 1 then
125
+ local buffers = { [' ' ] = vim .fn .expand (' #' ) } -- Keep the '#' prefix as a completion option
126
+ local curr_buf = vim .api .nvim_get_current_buf ()
127
+ for _ , buf in ipairs (vim .fn .getbufinfo ({ bufloaded = 1 , buflisted = 1 })) do
128
+ if buf .bufnr ~= curr_buf and buf .bufnr ~= alt_buf then
129
+ buffers [tostring (buf .bufnr )] = vim .fn .expand (' #' .. buf .bufnr )
130
+ end
131
+ end
132
+ completions = vim .tbl_keys (buffers )
133
+ if # completions < unique_suffixes_limit then
134
+ unique_suffixes = path_lib :compute_unique_suffixes (vim .tbl_values (buffers ))
135
+ end
136
+ end
137
+ -- Current file
138
+ elseif special_char == ' %' then
139
+ completions = { ' ' }
140
+ -- Modifiers
141
+ elseif special_char == ' :' then
142
+ completions = vim .tbl_keys (constants .modifiers )
143
+ elseif vim .tbl_contains ({ ' ~' , ' .' }, special_char ) then
144
+ completions = { special_char }
145
+ end
188
146
189
147
-- Cmdline mode
190
148
else
191
149
local query = (text_before_argument .. current_arg_prefix ):gsub ([[ \\]] , [[ \\\\]] )
192
- completions = get_completions (query , ' cmdline' , completion_type )
150
+ completions = cmdline_utils . get_completions (query , ' cmdline' , completion_type )
193
151
end
194
152
195
153
return completions
196
154
end )
197
155
:schedule ()
198
156
:map (function (completions )
157
+ --- @cast completions string[]
158
+
199
159
-- The getcompletion() api is inconsistent in whether it returns the prefix or not.
200
160
--
201
161
-- I.e. :set shiftwidth=| will return '2'
@@ -209,11 +169,9 @@ function cmdline:get_completions(context, callback)
209
169
-- In all other cases, we want to check for the prefix and remove it from the filter text
210
170
-- and add it to the newText
211
171
212
- --- @cast completions string[]
213
- local unique_prefixes = is_buffer_completion
214
- and # completions < 2000
215
- and path_lib :compute_unique_suffixes (completions )
216
- or {}
172
+ if is_buffer_completion and # completions < unique_suffixes_limit then
173
+ unique_suffixes = path_lib :compute_unique_suffixes (completions )
174
+ end
217
175
218
176
--- @type blink.cmp.CompletionItem[]
219
177
local items = {}
@@ -222,19 +180,36 @@ function cmdline:get_completions(context, callback)
222
180
local label , label_details
223
181
local option_info
224
182
183
+ -- current (%) or alternate (#) filename with optional modifiers (:)
184
+ if is_filename_modifier_completion then
185
+ local expanded = vim .fn .expand (vim_expr .. completion )
186
+ -- expand in command (e.g. :edit %) but don't in expression (e.g =vim.fn.expand("%"))
187
+ new_text = vim_expr :sub (1 , 1 ) == current_arg_prefix :sub (1 , 1 ) and expanded or current_arg_prefix .. completion
188
+
189
+ if special_char == ' #' then
190
+ -- special case: we need to display # along with #n
191
+ if completion == ' ' then filter_text = special_char end
192
+ label_details = { description = unique_suffixes [new_text ] or expanded }
193
+ elseif special_char == ' %' then
194
+ label_details = { description = expanded }
195
+ elseif vim .tbl_contains ({ ' :' , ' ~' , ' .' }, special_char ) then
196
+ label_details = { description = constants .modifiers [completion ] or expanded }
197
+ end
198
+
225
199
-- path completion in commands, e.g. `chdir <path>` and options, e.g. `:set directory=<path>`
226
- if is_path_completion then
200
+ elseif is_path_completion then
201
+ if current_arg == ' ~' then label = completion end
227
202
filter_text = path_lib .basename_with_sep (completion )
228
203
new_text = vim .fn .fnameescape (completion )
229
- if cmd == ' set' then
204
+ if arguments [ 1 ] == ' set' then
230
205
new_text = current_arg_prefix :sub (1 , current_arg_prefix :find (' =' ) or # current_arg_prefix ) .. new_text
231
206
end
232
207
233
208
-- buffer commands
234
209
elseif is_buffer_completion then
235
- label = unique_prefixes [completion ] or completion
236
- if unique_prefixes [completion ] then
237
- label_details = { description = completion :sub (1 , -# unique_prefixes [completion ] - 2 ) }
210
+ label = unique_suffixes [completion ] or completion
211
+ if unique_suffixes [completion ] then
212
+ label_details = { description = completion :sub (1 , -# unique_suffixes [completion ] - 2 ) }
238
213
end
239
214
new_text = vim .fn .fnameescape (completion )
240
215
@@ -281,7 +256,7 @@ function cmdline:get_completions(context, callback)
281
256
282
257
-- exclude range for commands on the first argument
283
258
if arg_number == 1 and completion_type == ' command' then
284
- local prefix = longest_match (current_arg , {
259
+ local prefix = cmdline_utils . longest_match (current_arg , {
285
260
" ^%s*'<%s*,%s*'>%s*" , -- Visual range, e.g., '<,>'
286
261
' ^%s*%d+%s*,%s*%d+%s*' , -- Numeric range, e.g., 3,5
287
262
' ^%s*[%p]+%s*' , -- One or more punctuation characters
0 commit comments