Skip to content

Conversation

@girishji
Copy link
Contributor

@girishji girishji commented Jul 20, 2025

This PR introduces a new wildtrigger() function.

See :h wildtrigger() (diffs in builtin.txt for how to enable command-line autocompletion).

wildtrigger() behaves like pressing the wildchar, but provides a more refined and controlled completion experience:

  • Suppresses beeps when no matches are found.
  • Avoids displaying irrelevant completions (like full command lists) when the prefix is insufficient or doesn't match.
  • Skips completion if the typeahead buffer has pending input or if a wildmenu is already active.
  • Does not print "..." before completion.

This is an improvement on the feedkeys() based autocompletion script given in #16759.


Added:

Beyond reducing the amount of typing, autocompletion system can be used to build fuzzy pickers and interactive command interfaces. Below are two examples:

Fuzzy file picker: Transforms the native :find command into a fzf-like experience.

set findfunc=FuzzyFind

func FuzzyFind(cmdarg, _)
  if s:allfiles == []
    let s:allfiles = systemlist('find . \! \( -path "*/.git" -prune -o -name "*.sw?" \) -type f -follow')
  endif
  return a:cmdarg == '' ? s:allfiles : matchfuzzy(s:allfiles, a:cmdarg)
endfunc

let s:allfiles = []
autocmd CmdlineEnter : let s:allfiles = []

Live grep: Dynamically search and display matching lines as you type.

command! -nargs=+ -complete=customlist,GrepComplete Grep call VisitFile()

func GrepComplete(arglead, cmdline, cursorpos)
  let l:cmd = $'grep -REIHns "{a:arglead}" --exclude-dir=.git --exclude=".*"'
  let s:selected = ''
  return len(a:arglead) > 1 ? systemlist(l:cmd) : [] " Trigger after 2 chars
endfunc

func VisitFile()
  if (s:selected != '')
    let l:item = getqflist(#{lines: [s:selected]}).items[0]
    if l:item->has_key('bufnr')
      let l:pos = l:item.vcol > 0 ? 'setcharpos' : 'setpos'
      exec $':b +call\ {l:pos}(".",\ [0,\ {l:item.lnum},\ {l:item.col},\ 0]) {l:item.bufnr}'
      call setbufvar(l:item.bufnr, '&buflisted', 1)
    endif
  endif
endfunc

Auto-select the first item in the completion list, and (in case of grep) add the typed pattern into the command-line history:

autocmd CmdlineLeavePre :
      \ if get(cmdcomplete_info(), 'matches', []) != [] |
      \   let s:info = cmdcomplete_info() |
      \   if getcmdline() =~ '^\s*fin\%[d]\s' && s:info.selected == -1 |
      \     call setcmdline($'find {s:info.matches[0]}') |
      \   endif |
      \   if getcmdline() =~ '^\s*Grep\s' |
      \     let s:selected = s:info.selected != -1 ? s:info.matches[s:info.selected] : s:info.matches[0] |
      \     call setcmdline(s:info.cmdline_orig) |
      \   endif |
      \ endif

NOTE: It's usually not necessary to make these commands asynchronous using job_start() and polling for input with getchar(), as they perform well enough for typical use cases.

asciicast

@chrisbra
Copy link
Member

@habamax can you test this please? Is this what you need?

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a more
refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists) when the
  prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a wildmenu
  is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script given
in vim#16759.

M  runtime/doc/builtin.txt
M  runtime/doc/cmdline.txt
M  runtime/doc/options.txt
M  runtime/doc/tags
M  runtime/doc/usr_41.txt
M  src/cmdexpand.c
M  src/evalfunc.c
M  src/ex_getln.c
M  src/keymap.h
M  src/proto/ex_getln.pro
M  src/testdir/test_cmdline.vim
M  src/vim.h
@habamax
Copy link
Contributor

habamax commented Jul 20, 2025

@habamax can you test this please? Is this what you need?

Have just tried it and it looks like it simplifies the setup quite a bit:

image

@habamax
Copy link
Contributor

habamax commented Jul 20, 2025

@dkearns wildtrigger() needs to be added to known vimscript functions once this is merged.

Co-authored-by: zeertzjq <zeertzjq@outlook.com>
@chrisbra
Copy link
Member

thanks

@chrisbra chrisbra closed this in b486ed8 Jul 21, 2025
zeertzjq added a commit to zeertzjq/neovim that referenced this pull request Jul 22, 2025
Problem:  cannot easily trigger wildcard expansion
Solution: Introduce wildtrigger() function
          (Girish Palya)

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a
more refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists)
  when the prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a
  wildmenu is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script
given in vim/vim#16759.

closes: vim/vim#17806

vim/vim@b486ed8

While at it, also make Ctrl-Z trigger search completion.

Co-authored-by: Girish Palya <girishji@gmail.com>
zeertzjq added a commit to zeertzjq/neovim that referenced this pull request Jul 22, 2025
Problem:  cannot easily trigger wildcard expansion
Solution: Introduce wildtrigger() function
          (Girish Palya)

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a
more refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists)
  when the prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a
  wildmenu is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script
given in vim/vim#16759.

closes: vim/vim#17806

vim/vim@b486ed8

While at it, also make Ctrl-Z trigger search completion.

Co-authored-by: Girish Palya <girishji@gmail.com>
zeertzjq added a commit to zeertzjq/neovim that referenced this pull request Jul 22, 2025
Problem:  cannot easily trigger wildcard expansion
Solution: Introduce wildtrigger() function
          (Girish Palya)

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a
more refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists)
  when the prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a
  wildmenu is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script
given in vim/vim#16759.

closes: vim/vim#17806

vim/vim@b486ed8

While at it, also make Ctrl-Z trigger search completion.

Co-authored-by: Girish Palya <girishji@gmail.com>
@girishji
Copy link
Contributor Author

Thanks

@girishji girishji deleted the wildtrigger branch July 22, 2025 03:56
@habamax
Copy link
Contributor

habamax commented Jul 22, 2025

@girishji do you think there should be room for improvements wrt up, down, ctrl-p and ctrl-n or we still need to use SkipCmdlineChanged for that?

# command line completion
set wildmode=noselect:lastused,full
set wildmenu wildoptions=pum,fuzzy pumheight=20

def SkipCmdlineChanged(key = ''): string
    set eventignore+=CmdlineChanged
    timer_start(0, (_) => execute('set eventignore-=CmdlineChanged'))
    return key == '' ? '' : ((wildmenumode() ? "\<C-E>" : '') .. key)
enddef

cnoremap <expr> <up> SkipCmdlineChanged("\<up>")
cnoremap <expr> <down> SkipCmdlineChanged("\<down>")
cnoremap <expr> <C-n> SkipCmdlineChanged("\<C-n>")
cnoremap <expr> <C-p> SkipCmdlineChanged("\<C-p>")

augroup cmdcomplete
    au!
    autocmd CmdlineChanged : wildtrigger()
augroup END

@girishji
Copy link
Contributor Author

girishji commented Jul 22, 2025

No need for that function. See :h wildtrigger() for up/down mapping. No need to map other keys. wildtrigger() is tailored for autocompletion, unlike wildchar. Let me know if there are issues.

@girishji
Copy link
Contributor Author

girishji commented Jul 22, 2025

Also, please test for any jitteriness (e.g., unnecessary redraws) on the command-line. One known limitation that I do not intend to fix is that the popup menu is always cleared before being redrawn, causing a brief "blink" even when its position remains unchanged and only a few items have changed.

@habamax
Copy link
Contributor

habamax commented Jul 22, 2025

Oh, I missed that part, great!

One known limitation that I do not intend to fix is that the popup menu is always cleared before being redrawn, causing a brief "blink" even when its position remains unchanged and only a few items have changed.

Unnecessary redraws are there but it is not that of a big deal.

zeertzjq added a commit to neovim/neovim that referenced this pull request Jul 22, 2025
Problem:  cannot easily trigger wildcard expansion
Solution: Introduce wildtrigger() function
          (Girish Palya)

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a
more refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists)
  when the prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a
  wildmenu is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script
given in vim/vim#16759.

closes: vim/vim#17806

vim/vim@b486ed8

While at it, also make Ctrl-Z trigger search completion.

Co-authored-by: Girish Palya <girishji@gmail.com>
@habamax
Copy link
Contributor

habamax commented Aug 18, 2025

@girishji do you know if there is a way to detect if command-line was canceled with <ESC>?

The use case is with auto-selecting the first item in the popup menu for some of the commands, in your example, it would be find:

...
      if getcmdline() =~ '^\s*fin\%[d]\s' && s:info.selected == -1 |
      \     call setcmdline($'find {s:info.matches[0]}') |
      \   endif |
...

Here if you press ESC to cancel, history would still be populated with :find whatever_first_file_in_popup. Nitpicking, I would like not to have it in the history, but can't find a way to do so.

@girishji
Copy link
Contributor Author

What triggered CmdlineLeavePre is not exposed. But you can set the history (cmdline) explicitly to cmdline_orig.

autocmd CmdlineLeavePre :
      \ if getcmdline() =~ '^\s*\%(Grep\|Find\)\s' && get(cmdcomplete_info(), 'matches', []) != [] |
      \   let s:info = cmdcomplete_info() |
      \   let s:selected = s:info.selected != -1 ? s:info.matches[s:info.selected] : s:info.matches[0] |
      \   call setcmdline(s:info.cmdline_orig) |
      \ endif
command! -nargs=* -complete=customlist,<SID>FuzzyFind Find exec $'edit! {s:selected}'

func s:FuzzyFind(cmdarg, cmdline, cursorpos)
  if s:allfiles == []
    let s:allfiles = systemlist($'find {get(g:, "fzfind_root", ".")} \! \( -path "*/.git" -prune -o -name "*.sw?" \) -type f -follow')
  endif
  return a:cmdarg == '' ? s:allfiles : matchfuzzy(s:allfiles, a:cmdarg)
endfunc

let s:allfiles = []
autocmd CmdlineEnter : let s:allfiles = []

@habamax
Copy link
Contributor

habamax commented Aug 18, 2025

What triggered CmdlineLeavePre is not exposed. But you can set the history (cmdline) explicitly to cmdline_orig

Yes, this would work for custom commands, but I also have similar things for existing commands too (find, buffer, etc):

def CmdCompleteSelectFirst()
    var info = cmdcomplete_info()
    if empty(get(info, 'cmdline_orig', ''))
        return
    endif
    var cmd = info.cmdline_orig->split()
    if getcmdcompltype() == 'command' && cmd->len() == 1
        return
    endif

    var commands = [
        'fin%[d]', 'b%[uffer]', 'bd%[elete]', 'colo%[rscheme]',
        'Recent', 'Bookmark', 'Project', 'Help',
        'LoadSession', 'InsertTemplate', 'Colorscheme'
    ]
    if !commands->reduce((acc, val) => acc || match(info.cmdline_orig, $'\v\C^\s*{val}\s') != -1, false)
        return
    endif

    if !empty(get(info, 'matches', []))
        if info.selected == -1 && info.pum_visible # && (imaginary not CTRL-C or ESC was pressed check)
            setcmdline($'{cmd[0]} {info.matches[0]}')
        endif
    endif
    if cmd->len() == 1
        return
    endif
enddef
...
augroup cmdcomplete
    au!
    autocmd CmdlineChanged : wildtrigger()
    autocmd CmdlineEnter : CmdCompleteResetCache()
    autocmd CmdlineLeavePre : CmdCompleteSelectFirst()
augroup END

And it lets me have the same experience with :b somebuf<CR> or find somefile<CR>. The only issue is that I am not able to distinguish cancel/accept for the commandline to set it to the original if it was canceled.

https://asciinema.org/a/iSgzDSCK5dJXNmd422BlwzJ44

But as you confirmed, we don't have anything like that. I would need to think if I can get some kind of workaround or just live with it.

PS, I wonder if v:event could be populated for CmdlineLeavePre.

@girishji
Copy link
Contributor Author

girishji commented Aug 19, 2025

PS, I wonder if v:event could be populated for CmdlineLeavePre.

It's possible. Maybe a better solution is to have a boolean option selectfirst or wildselectfirst that will automatically select the first item if none selected. This works for all commands. You can get rid of all that configuration.

@habamax
Copy link
Contributor

habamax commented Aug 19, 2025

For some of the commands I would like to have it, for some not.

But maybe you will have an implementation where we would be able to iron out edge cases.

@girishji
Copy link
Contributor Author

For some of the commands I would like to have it, for some not.

But maybe you will have an implementation where we would be able to iron out edge cases.

Can you explain? For what commands would you not have it.

@habamax
Copy link
Contributor

habamax commented Aug 19, 2025

For commands themselves I think. Like if I do :q it shouldn’t accept first command from popup.

And I need to refresh memory why I accept first menu items only for some of the commands.

@girishji
Copy link
Contributor Author

girishji commented Aug 19, 2025

Makes sense. There are 2 optons:

  • use v:event
  • create a list or regex option for which first item should be automatically selected

@habamax
Copy link
Contributor

habamax commented Aug 19, 2025

And I need to refresh memory why I accept first menu items only for some of the commands

When I do :e somefile I would like to edit exactly somefile, not the first unselected file from the popup.

@girishji
Copy link
Contributor Author

Fair enough. Using v:event is feasible, but the amount of configuration required for something so simple is worth considering. The second option seems easier to configure, but it’s unclear how users would specify commands—whether as a list or via regex—especially since commands can have shortcuts (e.g., b for buffer).

@habamax
Copy link
Contributor

habamax commented Aug 19, 2025

I am currently comparing list of commands with fullcommand

https://github.com/habamax/.vim/blob/18b7e4721e9a751d9d872426011095b8cdc77042/plugin/complete.vim#L39

@girishji
Copy link
Contributor Author

I looked into this. Implementing an option that accepts a list is fairly straightforward. However, that would leave us with two different ways to achieve the same result. A better approach might be to add an entry to the dictionary returned by cmdcomplete_info(), indicating which key triggered the exit. Alternatively, we could add a boolean flag to signal whether <CR> was pressed.

@habamax
Copy link
Contributor

habamax commented Aug 19, 2025

yes, that is another way to get it, something that will give us accept/cancel value.

Although I think that v:event is better suited as it describes what happened to cmdline -- was CR pressed there or it was cancelled no matter if we care about completion at all.

But as I said, any way to get it would be fine for me :)

chrisbra pushed a commit that referenced this pull request Aug 23, 2025
Problem:  unclear what key causes CmdlineLeave autocommand
Solution: Set |v:char| to the key (Girish Palya).

related: #17806
closes: #18063

Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
sahinf pushed a commit to sahinf/vim that referenced this pull request Sep 7, 2025
Problem:  unclear what key causes CmdlineLeave autocommand
Solution: Set |v:char| to the key (Girish Palya).

related: vim#17806
closes: vim#18063

Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
zeertzjq added a commit to zeertzjq/neovim that referenced this pull request Sep 8, 2025
Problem:  unclear what key causes CmdlineLeave autocommand
Solution: Set |v:char| to the key (Girish Palya).

related: vim/vim#17806
closes: vim/vim#18063

vim/vim@ba9551d

Co-authored-by: Girish Palya <girishji@gmail.com>
zeertzjq added a commit to zeertzjq/neovim that referenced this pull request Sep 8, 2025
Problem:  unclear what key causes CmdlineLeave autocommand
Solution: Set |v:char| to the key (Girish Palya).

related: vim/vim#17806
closes: vim/vim#18063

vim/vim@ba9551d

Co-authored-by: Girish Palya <girishji@gmail.com>
zeertzjq added a commit to neovim/neovim that referenced this pull request Sep 8, 2025
…35677)

Problem:  unclear what key causes CmdlineLeave autocommand
Solution: Set |v:char| to the key (Girish Palya).

related: vim/vim#17806
closes: vim/vim#18063

vim/vim@ba9551d

Co-authored-by: Girish Palya <girishji@gmail.com>
dundargoc pushed a commit to dundargoc/neovim that referenced this pull request Sep 27, 2025
…35022)

Problem:  cannot easily trigger wildcard expansion
Solution: Introduce wildtrigger() function
          (Girish Palya)

This PR introduces a new `wildtrigger()` function.

See `:h wildtrigger()`

`wildtrigger()` behaves like pressing the `wildchar,` but provides a
more refined and controlled completion experience:

- Suppresses beeps when no matches are found.
- Avoids displaying irrelevant completions (like full command lists)
  when the prefix is insufficient or doesn't match.
- Skips completion if the typeahead buffer has pending input or if a
  wildmenu is already active.
- Does not print "..." before completion.

This is an improvement on the `feedkeys()` based autocompletion script
given in vim/vim#16759.

closes: vim/vim#17806

vim/vim@b486ed8

While at it, also make Ctrl-Z trigger search completion.

Co-authored-by: Girish Palya <girishji@gmail.com>
dundargoc pushed a commit to dundargoc/neovim that referenced this pull request Sep 27, 2025
…eovim#35677)

Problem:  unclear what key causes CmdlineLeave autocommand
Solution: Set |v:char| to the key (Girish Palya).

related: vim/vim#17806
closes: vim/vim#18063

vim/vim@ba9551d

Co-authored-by: Girish Palya <girishji@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants