patch 9.1.1329: cannot get information about command line completion
Problem: cannot get information about command line completion
Solution: add CmdlineLeavePre autocommand and cmdcomplete_info() Vim
script function (Girish Palya)
This commit introduces two features to improve introspection and control
over command-line completion in Vim:
- Add CmdlineLeavePre autocmd event:
A new event triggered just before leaving the command line and before
CmdlineLeave. It allows capturing completion-related state that is
otherwise cleared by the time CmdlineLeave fires.
- Add cmdcomplete_info() Vim script function:
Returns a Dictionary with details about the current command-line
completion state.
These are similar in spirit to InsertLeavePre and complete_info(),
but focused on command-line mode.
**Use case:**
In [[PR #16759](https://github.com/vim/vim/pull/16759)], two examples
demonstrate command-line completion: one for live grep, and another for
fuzzy file finding. However, both examples share two key limitations:
1. **Broken history recall (`<Up>`)**
When selecting a completion item via `<Tab>` or `<C-n>`, the original
pattern used for searching (e.g., a regex or fuzzy string) is
overwritten in the command-line history. This makes it impossible to
recall the original query later.
This is especially problematic for interactive grep workflows, where
it’s useful to recall a previous search and simply select a different
match from the menu.
2. **Lack of default selection on `<CR>`**
Often, it’s helpful to allow `<CR>` (Enter) to accept the first match
in the completion list, even when no item is explicitly selected. This
behavior is particularly useful in fuzzy file finding.
----
Below are the updated examples incorporating these improvements:
**Live grep, fuzzy find file, fuzzy find buffer:**
```vim
command! -nargs=+ -complete=customlist,GrepComplete Grep VisitFile()
def GrepComplete(arglead: string, cmdline: string, cursorpos: number):
list<any>
return arglead->len() > 1 ? systemlist($'grep -REIHns "{arglead}"' ..
' --exclude-dir=.git --exclude=".*" --exclude="tags" --exclude="*.swp"') : []
enddef
def VisitFile()
if (selected_match != null_string)
var qfitem = getqflist({lines: [selected_match]}).items[0]
if qfitem->has_key('bufnr') && qfitem.lnum > 0
var pos = qfitem.vcol > 0 ? 'setcharpos' : 'setpos'
exec $':b +call\ {pos}(".",\ [0,\ {qfitem.lnum},\ {qfitem.col},\ 0]) {qfitem.bufnr}'
setbufvar(qfitem.bufnr, '&buflisted', 1)
endif
endif
enddef
nnoremap <leader>g :Grep<space>
nnoremap <leader>G :Grep <c-r>=expand("<cword>")<cr>
command! -nargs=* -complete=customlist,FuzzyFind Find
execute(selected_match != '' ? $'edit {selected_match}' : '')
var allfiles: list<string>
autocmd CmdlineEnter : allfiles = null_list
def FuzzyFind(arglead: string, _: string, _: number): list<string>
if allfiles == null_list
allfiles = systemlist($'find {get(g:, "fzfind_root", ".")} \! \(
-path "*/.git" -prune -o -name "*.swp" \) -type f -follow')
endif
return arglead == '' ? allfiles : allfiles->matchfuzzy(arglead)
enddef
nnoremap <leader><space> :<c-r>=execute('let
fzfind_root="."')\|''<cr>Find<space><c-@>
nnoremap <leader>fv :<c-r>=execute('let
fzfind_root="$HOME/.vim"')\|''<cr>Find<space><c-@>
nnoremap <leader>fV :<c-r>=execute('let
fzfind_root="$VIMRUNTIME"')\|''<cr>Find<space><c-@>
command! -nargs=* -complete=customlist,FuzzyBuffer Buffer execute('b '
.. selected_match->matchstr('\d\+'))
def FuzzyBuffer(arglead: string, _: string, _: number): list<string>
var bufs = execute('buffers', 'silent!')->split("\n")
var altbuf = bufs->indexof((_, v) => v =~ '^\s*\d\+\s\+#')
if altbuf != -1
[bufs[0], bufs[altbuf]] = [bufs[altbuf], bufs[0]]
endif
return arglead == '' ? bufs : bufs->matchfuzzy(arglead)
enddef
nnoremap <leader><bs> :Buffer <c-@>
var selected_match = null_string
autocmd CmdlineLeavePre : SelectItem()
def SelectItem()
selected_match = ''
if getcmdline() =~ '^\s*\%(Grep\|Find\|Buffer\)\s'
var info = cmdcomplete_info()
if info != {} && info.pum_visible && !info.matches->empty()
selected_match = info.selected != -1 ? info.matches[info.selected] : info.matches[0]
setcmdline(info.cmdline_orig). # Preserve search pattern in history
endif
endif
enddef
```
**Auto-completion snippet:**
```vim
set wim=noselect:lastused,full wop=pum wcm=<C-@> wmnu
autocmd CmdlineChanged : CmdComplete()
def CmdComplete()
var [cmdline, curpos] = [getcmdline(), getcmdpos()]
if getchar(1, {number: true}) == 0 # Typehead is empty (no more pasted input)
&& !pumvisible() && curpos == cmdline->len() + 1
&& cmdline =~ '\%(\w\|[*/:.-]\)$' && cmdline !~ '^\d\+$' # Reduce noise
feedkeys("\<C-@>", "ti")
SkipCmdlineChanged() # Suppress redundant completion attempts
# Remove <C-@> that get inserted when no items are available
timer_start(0, (_) => getcmdline()->substitute('\%x00', '', 'g')->setcmdline())
endif
enddef
cnoremap <expr> <up> SkipCmdlineChanged("\<up>")
cnoremap <expr> <down> SkipCmdlineChanged("\<down>")
autocmd CmdlineEnter : set bo+=error
autocmd CmdlineLeave : set bo-=error
def SkipCmdlineChanged(key = ''): string
set ei+=CmdlineChanged
timer_start(0, (_) => execute('set ei-=CmdlineChanged'))
return key != '' ? ((pumvisible() ? "\<c-e>" : '') .. key) : ''
enddef
```
These customizable snippets can serve as *lightweight* and *native*
alternatives to picker plugins like **FZF** or **Telescope** for common,
everyday workflows. Also, live grep snippet can replace **cscope**
without the overhead of building its database.
closes: #17115
Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/ex_getln.c b/src/ex_getln.c
index c4b0a00..1137708 100644
--- a/src/ex_getln.c
+++ b/src/ex_getln.c
@@ -1915,6 +1915,11 @@
}
}
+ // Trigger CmdlineLeavePre autocommand
+ if (ccline.cmdfirstc != NUL && (c == '\n' || c == '\r' || c == K_KENTER
+ || c == ESC || c == Ctrl_C))
+ trigger_cmd_autocmd(cmdline_type, EVENT_CMDLINELEAVEPRE);
+
// The wildmenu is cleared if the pressed key is not used for
// navigating the wild menu (i.e. the key is not 'wildchar' or
// 'wildcharm' or Ctrl-N or Ctrl-P or Ctrl-A or Ctrl-L).