mirror of
https://github.com/neovim/neovim.git
synced 2026-08-23 23:51:51 +00:00
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 vim/vim#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: vim/vim#17115
92f68e26ec
Co-authored-by: Girish Palya <girishji@gmail.com>
172 lines
8.3 KiB
Lua
172 lines
8.3 KiB
Lua
return {
|
|
--- @type table<string,boolean>
|
|
--- Keys are events names.
|
|
--- Values are boolean indicating whether the event is window-local.
|
|
events = {
|
|
BufAdd = true, -- after adding a buffer to the buffer list
|
|
BufDelete = true, -- deleting a buffer from the buffer list
|
|
BufEnter = true, -- after entering a buffer
|
|
BufFilePost = true, -- after renaming a buffer
|
|
BufFilePre = true, -- before renaming a buffer
|
|
BufHidden = true, -- just after buffer becomes hidden
|
|
BufLeave = true, -- before leaving a buffer
|
|
BufModifiedSet = true, -- after the 'modified' state of a buffer changes
|
|
BufNew = true, -- after creating any buffer
|
|
BufNewFile = true, -- when creating a buffer for a new file
|
|
BufReadCmd = true, -- read buffer using command
|
|
BufReadPost = true, -- after reading a buffer
|
|
BufReadPre = true, -- before reading a buffer
|
|
BufUnload = true, -- just before unloading a buffer
|
|
BufWinEnter = true, -- after showing a buffer in a window
|
|
BufWinLeave = true, -- just after buffer removed from window
|
|
BufWipeout = true, -- just before really deleting a buffer
|
|
BufWriteCmd = true, -- write buffer using command
|
|
BufWritePost = true, -- after writing a buffer
|
|
BufWritePre = true, -- before writing a buffer
|
|
ChanInfo = false, -- info was received about channel
|
|
ChanOpen = false, -- channel was opened
|
|
CmdUndefined = false, -- command undefined
|
|
CmdlineChanged = false, -- command line was modified
|
|
CmdlineEnter = false, -- after entering cmdline mode
|
|
CmdlineLeave = false, -- before leaving cmdline mode
|
|
CmdlineLeavePre = false, -- just before leaving the command line
|
|
CmdwinEnter = false, -- after entering the cmdline window
|
|
CmdwinLeave = false, -- before leaving the cmdline window
|
|
ColorScheme = false, -- after loading a colorscheme
|
|
ColorSchemePre = false, -- before loading a colorscheme
|
|
CompleteChanged = false, -- after popup menu changed
|
|
CompleteDone = false, -- after finishing insert complete
|
|
CompleteDonePre = false, -- idem, before clearing info
|
|
CursorHold = true, -- cursor in same position for a while
|
|
CursorHoldI = true, -- idem, in Insert mode
|
|
CursorMoved = true, -- cursor was moved
|
|
CursorMovedC = true, -- cursor was moved in Cmdline mode
|
|
CursorMovedI = true, -- cursor was moved in Insert mode
|
|
DiagnosticChanged = false, -- diagnostics in a buffer were modified
|
|
DiffUpdated = false, -- diffs have been updated
|
|
DirChanged = false, -- directory changed
|
|
DirChangedPre = false, -- directory is going to change
|
|
EncodingChanged = false, -- after changing the 'encoding' option
|
|
ExitPre = false, -- before exiting
|
|
FileAppendCmd = true, -- append to a file using command
|
|
FileAppendPost = true, -- after appending to a file
|
|
FileAppendPre = true, -- before appending to a file
|
|
FileChangedRO = true, -- before first change to read-only file
|
|
FileChangedShell = true, -- after shell command that changed file
|
|
FileChangedShellPost = true, -- after (not) reloading changed file
|
|
FileReadCmd = true, -- read from a file using command
|
|
FileReadPost = true, -- after reading a file
|
|
FileReadPre = true, -- before reading a file
|
|
FileType = true, -- new file type detected (user defined)
|
|
FileWriteCmd = true, -- write to a file using command
|
|
FileWritePost = true, -- after writing a file
|
|
FileWritePre = true, -- before writing a file
|
|
FilterReadPost = true, -- after reading from a filter
|
|
FilterReadPre = true, -- before reading from a filter
|
|
FilterWritePost = true, -- after writing to a filter
|
|
FilterWritePre = true, -- before writing to a filter
|
|
FocusGained = false, -- got the focus
|
|
FocusLost = false, -- lost the focus to another app
|
|
FuncUndefined = false, -- if calling a function which doesn't exist
|
|
GUIEnter = false, -- after starting the GUI
|
|
GUIFailed = false, -- after starting the GUI failed
|
|
InsertChange = true, -- when changing Insert/Replace mode
|
|
InsertCharPre = true, -- before inserting a char
|
|
InsertEnter = true, -- when entering Insert mode
|
|
InsertLeave = true, -- just after leaving Insert mode
|
|
InsertLeavePre = true, -- just before leaving Insert mode
|
|
LspAttach = false, -- after an LSP client attaches to a buffer
|
|
LspDetach = false, -- after an LSP client detaches from a buffer
|
|
LspNotify = false, -- after an LSP notice has been sent to the server
|
|
LspProgress = false, -- after a LSP progress update
|
|
LspRequest = false, -- after an LSP request is started, canceled, or completed
|
|
LspTokenUpdate = false, -- after a visible LSP token is updated
|
|
MenuPopup = false, -- just before popup menu is displayed
|
|
ModeChanged = false, -- after changing the mode
|
|
OptionSet = false, -- after setting any option
|
|
QuickFixCmdPost = false, -- after :make, :grep etc.
|
|
QuickFixCmdPre = false, -- before :make, :grep etc.
|
|
QuitPre = false, -- before :quit
|
|
RecordingEnter = true, -- when starting to record a macro
|
|
RecordingLeave = true, -- just before a macro stops recording
|
|
RemoteReply = false, -- upon string reception from a remote vim
|
|
SafeState = false, -- going to wait for a character
|
|
SearchWrapped = true, -- after the search wrapped around
|
|
SessionLoadPost = false, -- after loading a session file
|
|
SessionWritePost = false, -- after writing a session file
|
|
ShellCmdPost = false, -- after ":!cmd"
|
|
ShellFilterPost = true, -- after ":1,2!cmd", ":w !cmd", ":r !cmd".
|
|
Signal = false, -- after nvim process received a signal
|
|
SourceCmd = false, -- sourcing a Vim script using command
|
|
SourcePost = false, -- after sourcing a Vim script
|
|
SourcePre = false, -- before sourcing a Vim script
|
|
SpellFileMissing = false, -- spell file missing
|
|
StdinReadPost = false, -- after reading from stdin
|
|
StdinReadPre = false, -- before reading from stdin
|
|
SwapExists = false, -- found existing swap file
|
|
Syntax = false, -- syntax selected
|
|
TabClosed = false, -- a tab has closed
|
|
TabEnter = false, -- after entering a tab page
|
|
TabLeave = false, -- before leaving a tab page
|
|
TabNew = false, -- when creating a new tab
|
|
TabNewEntered = false, -- after entering a new tab
|
|
TermChanged = false, -- after changing 'term'
|
|
TermClose = false, -- after the process exits
|
|
TermEnter = false, -- after entering Terminal mode
|
|
TermLeave = false, -- after leaving Terminal mode
|
|
TermOpen = false, -- after opening a terminal buffer
|
|
TermRequest = false, -- after an unhandled OSC sequence is emitted
|
|
TermResponse = false, -- after setting "v:termresponse"
|
|
TextChanged = true, -- text was modified
|
|
TextChangedI = true, -- text was modified in Insert mode(no popup)
|
|
TextChangedP = true, -- text was modified in Insert mode(popup)
|
|
TextChangedT = true, -- text was modified in Terminal mode
|
|
TextYankPost = true, -- after a yank or delete was done (y, d, c)
|
|
UIEnter = false, -- after UI attaches
|
|
UILeave = false, -- after UI detaches
|
|
User = false, -- user defined autocommand
|
|
VimEnter = false, -- after starting Vim
|
|
VimLeave = false, -- before exiting Vim
|
|
VimLeavePre = false, -- before exiting Vim and writing ShaDa file
|
|
VimResized = false, -- after Vim window was resized
|
|
VimResume = false, -- after Nvim is resumed
|
|
VimSuspend = false, -- before Nvim is suspended
|
|
WinClosed = true, -- after closing a window
|
|
WinEnter = true, -- after entering a window
|
|
WinLeave = true, -- before leaving a window
|
|
WinNew = false, -- when entering a new window
|
|
WinResized = true, -- after a window was resized
|
|
WinScrolled = true, -- after a window was scrolled or resized
|
|
},
|
|
--- @type table<string,string>
|
|
--- Keys are event aliases.
|
|
--- Values are the names in the `events` table above.
|
|
aliases = {
|
|
BufCreate = 'BufAdd',
|
|
BufRead = 'BufReadPost',
|
|
BufWrite = 'BufWritePre',
|
|
FileEncoding = 'EncodingChanged',
|
|
},
|
|
--- @type table<string,true>
|
|
--- List of Nvim-specific events or aliases for generating syntax file.
|
|
nvim_specific = {
|
|
BufModifiedSet = true,
|
|
DiagnosticChanged = true,
|
|
LspAttach = true,
|
|
LspDetach = true,
|
|
LspNotify = true,
|
|
LspProgress = true,
|
|
LspRequest = true,
|
|
LspTokenUpdate = true,
|
|
RecordingEnter = true,
|
|
RecordingLeave = true,
|
|
Signal = true,
|
|
TabNewEntered = true,
|
|
TermClose = true,
|
|
TermOpen = true,
|
|
TermRequest = true,
|
|
UIEnter = true,
|
|
UILeave = true,
|
|
},
|
|
}
|