44
55local async = require (' blink.cmp.lib.async' )
66local constants = require (' blink.cmp.sources.cmdline.constants' )
7+ local cmdline_utils = require (' blink.cmp.sources.cmdline.utils' )
78local utils = require (' blink.cmp.sources.lib.utils' )
89local path_lib = require (' blink.cmp.sources.path.lib' )
910
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-
8911--- @class blink.cmp.Source
9012local cmdline = {
9113 --- @type table<string , vim.api.keyset.get_option_info ? >
@@ -108,7 +30,7 @@ function cmdline:enabled()
10830end
10931
11032--- @return table
111- function cmdline :get_trigger_characters () return { ' ' , ' .' , ' #' , ' -' , ' =' , ' /' , ' :' , ' !' } end
33+ function cmdline :get_trigger_characters () return { ' ' , ' .' , ' #' , ' -' , ' =' , ' /' , ' :' , ' !' , ' % ' , ' ~ ' } end
11234
11335--- @param context blink.cmp.Context
11436--- @param callback fun ( result ?: blink.cmp.CompletionResponse )
@@ -118,11 +40,12 @@ function cmdline:get_completions(context, callback)
11840
11941 local is_path_completion = vim .tbl_contains (constants .completion_types .path , completion_type )
12042 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 )
12144
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 )
12447 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 )
12649 local arg_number = # args_before_cursor
12750
12851 local leading_spaces = context .line :match (' ^(%s*)' ) -- leading spaces in the original query
@@ -134,6 +57,10 @@ function cmdline:get_completions(context, callback)
13457 local keyword = context .get_bounds (keyword_config .range )
13558 local current_arg_prefix = current_arg :sub (1 , keyword .start_col - # text_before_argument - 1 )
13659
60+ local unique_suffixes = {}
61+ local unique_suffixes_limit = 2000
62+ local special_char , vim_expr
63+
13764 local task = async .task
13865 .empty ()
13966 :map (function ()
@@ -180,21 +107,52 @@ function cmdline:get_completions(context, callback)
180107 -- path completions uniquely expect only the current path
181108 query = is_path_completion and current_arg_prefix or query
182109
183- completions = get_completions (query , compl_type , completion_type )
110+ completions = cmdline_utils . get_completions (query , compl_type , completion_type )
184111 if type (completions ) ~= ' table' then completions = {} end
185112 end
186113 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
187143
188144 -- Cmdline mode
189145 else
190146 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 )
192148 end
193149
194150 return completions
195151 end )
196152 :schedule ()
197153 :map (function (completions )
154+ --- @cast completions string[]
155+
198156 -- The getcompletion() api is inconsistent in whether it returns the prefix or not.
199157 --
200158 -- I.e. :set shiftwidth=| will return '2'
@@ -208,11 +166,9 @@ function cmdline:get_completions(context, callback)
208166 -- In all other cases, we want to check for the prefix and remove it from the filter text
209167 -- and add it to the newText
210168
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
216172
217173 --- @type blink.cmp.CompletionItem[]
218174 local items = {}
@@ -221,19 +177,35 @@ function cmdline:get_completions(context, callback)
221177 local label , label_details
222178 local option_info
223179
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+
224196 -- 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
226198 filter_text = path_lib .basename_with_sep (completion )
227199 new_text = vim .fn .fnameescape (completion )
228- if cmd == ' set' then
200+ if arguments [ 1 ] == ' set' then
229201 new_text = current_arg_prefix :sub (1 , current_arg_prefix :find (' =' ) or # current_arg_prefix ) .. new_text
230202 end
231203
232204 -- buffer commands
233205 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 ) }
237209 end
238210 new_text = vim .fn .fnameescape (completion )
239211
@@ -274,7 +246,7 @@ function cmdline:get_completions(context, callback)
274246
275247 -- exclude range for commands on the first argument
276248 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 , {
278250 " ^%s*'<%s*,%s*'>%s*" , -- Visual range, e.g., '<,>'
279251 ' ^%s*%d+%s*,%s*%d+%s*' , -- Numeric range, e.g., 3,5
280252 ' ^%s*[%p]+%s*' , -- One or more punctuation characters
0 commit comments