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