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-   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- 
9011---  @class  blink.cmp.Source 
9112local  cmdline  =  {
9213  --- @type  table<string ,  vim.api.keyset.get_option_info ? >
@@ -109,7 +30,7 @@ function cmdline:enabled()
10930end 
11031
11132--- @return  table 
112- function  cmdline :get_trigger_characters () return  { '  ' ' .' ' #' ' -' ' =' ' /' ' :' ' !'  end 
33+ function  cmdline :get_trigger_characters () return  { '  ' ' .' ' #' ' -' ' =' ' /' ' :' ' !' ,  ' % ' ,  ' ~ '  end 
11334
11435--- @param  context  blink.cmp.Context 
11536--- @param  callback  fun ( result ?:  blink.cmp.CompletionResponse ) 
@@ -119,11 +40,15 @@ function cmdline:get_completions(context, callback)
11940
12041  local  is_path_completion  =  vim .tbl_contains (constants .completion_types .path , completion_type )
12142  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 )
12245
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 )
12550  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 )
12752  local  arg_number  =  # args_before_cursor 
12853
12954  local  leading_spaces  =  context .line :match (' ^(%s*)' --  leading spaces in the original query
@@ -135,6 +60,10 @@ function cmdline:get_completions(context, callback)
13560  local  keyword  =  context .get_bounds (keyword_config .range )
13661  local  current_arg_prefix  =  current_arg :sub (1 , keyword .start_col  -  # text_before_argument  -  1 )
13762
63+   local  unique_suffixes  =  {}
64+   local  unique_suffixes_limit  =  2000 
65+   local  special_char , vim_expr 
66+ 
13867  local  task  =  async .task 
13968    .empty ()
14069    :map (function ()
@@ -181,21 +110,52 @@ function cmdline:get_completions(context, callback)
181110            --  path completions uniquely expect only the current path
182111            query  =  is_path_completion  and  current_arg_prefix  or  query 
183112
184-             completions  =  get_completions (query , compl_type , completion_type )
113+             completions  =  cmdline_utils . get_completions (query , compl_type , completion_type )
185114            if  type (completions ) ~=  ' table'  then  completions  =  {} end 
186115          end 
187116        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 
188146
189147      --  Cmdline mode
190148      else 
191149        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 )
193151      end 
194152
195153      return  completions 
196154    end )
197155    :schedule ()
198156    :map (function (completions )
157+       --- @cast  completions  string[] 
158+ 
199159      --  The getcompletion() api is inconsistent in whether it returns the prefix or not.
200160      -- 
201161      --  I.e. :set shiftwidth=| will return '2'
@@ -209,11 +169,9 @@ function cmdline:get_completions(context, callback)
209169      --  In all other cases, we want to check for the prefix and remove it from the filter text
210170      --  and add it to the newText
211171
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 
217175
218176      --- @type  blink.cmp.CompletionItem[] 
219177      local  items  =  {}
@@ -222,19 +180,36 @@ function cmdline:get_completions(context, callback)
222180        local  label , label_details 
223181        local  option_info 
224182
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+ 
225199        --  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 
227202          filter_text  =  path_lib .basename_with_sep (completion )
228203          new_text  =  vim .fn .fnameescape (completion )
229-           if  cmd  ==  ' set'  then 
204+           if  arguments [ 1 ]  ==  ' set'  then 
230205            new_text  =  current_arg_prefix :sub (1 , current_arg_prefix :find (' =' or  # current_arg_prefix ) ..  new_text 
231206          end 
232207
233208        --  buffer commands
234209        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 ) }
238213          end 
239214          new_text  =  vim .fn .fnameescape (completion )
240215
@@ -281,7 +256,7 @@ function cmdline:get_completions(context, callback)
281256
282257        --  exclude range for commands on the first argument
283258        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 , {
285260            " ^%s*'<%s*,%s*'>%s*" --  Visual range, e.g., '<,>'
286261            ' ^%s*%d+%s*,%s*%d+%s*' --  Numeric range, e.g., 3,5
287262            ' ^%s*[%p]+%s*' --  One or more punctuation characters
0 commit comments