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 )
81-       vim .env .PATH  =  orig_path 
82-       return  completions 
83-     end 
84-   end 
85- 
86-   return  vim .fn .getcompletion (pattern , type )
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