vim-esearch
Neovim/Vim plugin for easy async search and replace across multiple files.
Features overview
- In-place modifying and writing changes into files.
- Updating search output on the fly while you're typing.
- Searching and viewing files from git history.
- Performance:
- Async neovim/vim8 jobs api are used.
- Fast lua-based rendering.
- Viewport position-based highlights (neovim only).
- Adaptive disabling of certain highlights on a large number of lines.
- Filetype-dependent syntax highlights for better navigation.
- Input prompt interface instead of using the commandline:
- Search patterns can be pasted as is (try url pattern with regex mode enabled by pressing
<c-r><c-r>
within the prompt).
- Search patterns can be pasted as is (try url pattern with regex mode enabled by pressing
- 2 preview modes using both neovim floating windows and plain splits.
- Third party plugins integration:
- vim-visual-multi (multiple cursors plugin) is guarded against editing filenames and line numbers.
- NerdTree, Dirvish, NetRanger, Fern, Defx file browsers can be used to specify search paths.
Install
Add one of the following lines depending on your plugin manager:
call minpac#add('eugen0329/vim-esearch')
call dein#add('eugen0329/vim-esearch')
Plug 'eugen0329/vim-esearch'
Plugin 'eugen0329/vim-esearch'
Optional: install ag or rg for faster searching and extra features.
Quick start
Type <leader>ff
keys (leader is \
unless redefined) to open the input prompt.
Use <c-r><c-r>
, <c-s><c-s>
and <c-t><c-t>
within the prompt to cycle through
regex, case-sensitive and text-objects matching modes or use <c-o>
to open
a menu to set searching paths, filetypes or other configs.
Within the search window use J
and K
to jump between entries or {
and }
to jump between filenames. Use R
to reload the results.
To open a line in a file press <enter>
(open in the current window), o
(open in a split),
s
(split vertically) or t
to open in a new tab. Use the keys with shift
pressed (O
, S
and T
) to open staying in the search window.
Modify or delete results right inside the search window. Press <enter>
in insert mode to add new lines below or above the line with results.
Use im
and am
text-objects to jump to the following match and start operating on it. E.g.
press dam
to delete "a match" with trailing whitespaces under the cursor or jump to the nearest, cim
to delete "inner match" and start
the insert mode. Use any other operator including user-defined to capture matched text. Use .
to repeat the last change.
Use :s/
and :g/.../d
commands without worrying about matched layout (filenames, line numbers). They will be preserved from changes.
Type :write<cr>
to save changes into files. Use undo and :write<cr>
command again to revert.
Press p
to open a preview window. Use multiple p
to zoom and capital P
to enter it.
Default mappings cheatsheet:
Keymap | What it does |
---|---|
<leader>ff |
Open the search pattern input prompt [global] |
<leader>f{textobj} |
Start a new search for a text-object [global] |
<c-r><c-r> / <c-s><c-s> / <c-t><c-t> |
Cycle through regex/case/text-object modes [prompt] |
<c-o> |
Open the menu [prompt] |
<cr> / o / s / t |
Open a search result entry in the current window/vertical split/horizontal split/new tab [window] |
O / S / T |
Same as above, but stay in the window [window] |
K / J |
Jump to the previous/next search entry [window] |
{ / } |
Jump to the previous/next filename [window] |
( / ) |
Jump to the previous/next filename from another directory [window] |
cim / dim / vim |
Jump to the next match and change/delete/select it [window] |
cam / dam / vam |
Same as above, but capture trailing whitespaces as well [window] |
:write<cr> |
Write changes into files [window] |
p / P |
Zoom/enter the preview window [window] |
Configuration
Configurations are scoped within g:esearch
dictionary. Play around with
key-values below if you want to alter the default behavior:
" Use <c-f><c-f> to start the prompt, use <c-f>iw to pre-fill with the current word
" or other text-objects. Try <plug>(esearch-exec) to start a search instantly.
nmap <c-f><c-f> <plug>(esearch)
map <c-f> <plug>(operator-esearch-prefill)
let g:esearch = {}
" Use regex matching with the smart case mode by default and avoid matching text-objects.
let g:esearch.regex = 1
let g:esearch.textobj = 0
let g:esearch.case = 'smart'
" Set the initial pattern content using the highlighted '/' pattern (if
" v:hlsearch is true), the last searched pattern or the clipboard content.
let g:esearch.prefill = ['hlsearch', 'last', 'clipboard']
" Override the default files and directories to determine your project root. Set it
" to blank to always use the current working directory.
let g:esearch.root_markers = ['.git', 'Makefile', 'node_modules']
" Prevent esearch from adding any default keymaps.
let g:esearch.default_mappings = 0
" Start the search only when the enter is hit instead of updating the pattern while you're typing.
let g:esearch.live_update = 0
" Open the search window in a vertical split and reuse it for all further searches.
let g:esearch.name = '[esearch]'
let g:esearch.win_new = {esearch -> esearch#buf#goto_or_open(esearch.name, 'vnew')}
" Redefine the default highlights (see :help highlight and :help esearch-appearance)
highlight esearchHeader cterm=bold gui=bold ctermfg=white ctermbg=white
highlight link esearchStatistics esearchFilename
highlight link esearchFilename Label
highlight esearchMatch ctermbg=27 ctermfg=15 guibg='#005FFF' guifg='#FFFFFF'
API
Automatically update the preview for the entry under the cursor.
NOTE It'll internally wrap CursorMoved
autocommand to collect garbage on reloads, so no augroup
around is required.
autocmd User esearch_win_config
\ let b:autopreview = esearch#async#debounce(b:esearch.preview_open, 100)
\| autocmd CursorMoved <buffer> call b:autopreview.apply({'align': 'right'})
Define <leader>fh
keymap for searching in git history and define a popup with git-show
output. Use P
to maximize and enter the popup for viewing the patch.
" Show the popup with git-show information on CursorMoved is a git revision context is hovered.
let g:GitShow = {ctx -> ctx().rev &&
\ esearch#preview#shell('git show ' . split(ctx().filename, ':')[0], {
\ 'let': {'&filetype': 'git', '&number': 0},
\ 'row': screenpos(0, ctx().begin, 1).row,
\ 'col': screenpos(0, ctx().begin, col([ctx().begin, '$'])).col,
\ 'width': 47, 'height': 3,
\ })
\}
" Debounce the popup updates using 70ms timeout.
autocmd User esearch_win_config
\ let b:git_show = esearch#async#debounce(g:GitShow, 70)
\| autocmd CursorMoved <buffer> call b:git_show.apply(b:esearch.ctx)
nnoremap <leader>fh :call esearch#init({'paths': esearch#xargs#git_log()})<cr>
Other git usage examples.
" Search in modified files only
nnoremap <leader>fm :call esearch#init({'paths': '`git ls-files --modified`'})<cr>
" Search in unmerged commits using range specification
nnoremap <leader>fu :call esearch#init({'paths': esearch#xargs#git_log('development..HEAD')})<cr>
" Search in stashed entries
nnoremap <leader>fs :call esearch#init({'paths': esearch#xargs#git_stash()})<cr>
In place of the built-in git blobs viewer, it's also possible to use custom functions from other plugins to have advanced features. Although, they are generally slower, so if autopreview is used, it's recommended to use the built-ins.
let g:esearch.git_dir = {cwd -> FugitiveExtractGitDir(cwd)}
let g:esearch.git_url = {path, dir -> FugitiveFind(path, dir)}
Use a popup-like floating window to render search results.
let g:esearch = {}
" Try to jump into the opened floating window or open a new one.
let g:esearch.win_new = {esearch ->
\ esearch#buf#goto_or_open(esearch.name, {name ->
\ nvim_open_win(bufadd(name), v:true, {
\ 'relative': 'editor',
\ 'row': &lines / 10,
\ 'col': &columns / 10,
\ 'width': &columns * 8 / 10,
\ 'height': &lines * 8 / 10
\ })
\ })
\}
" Close the floating window when opening an entry.
autocmd User esearch_win_config autocmd BufLeave <buffer> quit
Customize writing behavior by redefining the callback that is invoked after applying changes into files.
" Save applied changes if :write! with '!' was used. Open modified buffers otherwise.
let g:esearch.write_cb = {buf, bang -> bang ? buf.write(bang) : buf.open('$tabnew')}
" Save silently and wipeout buffers if they didn't exist.
let g:esearch.write_cb = {buf, bang -> buf.write(bang) && (!buf.existed && buf.bwipeout())}
" Append buffers data to a location list for reviewing, open it and edit the first entry.
let g:esearch.write_cb = {buf, bang -> setloclist(winnr(), [buf], 'a')}
autocmd User esearch_write_post lopen | wincmd p | lfirst
Use esearch#init({options}})
and esearch#prefill({options}})
functions to start a new search. Specify {options}
dictionary using the same keys as in the global config to customize the
behavior per request. Examples:
" Search for debugger statements instantly (without starting the prompt).
nnoremap <leader>fd :call esearch#init({'pattern': '\b(ipdb\|debugger)\b', 'regex': 1})<cr>
" Search in front-end files using explicitly set paths.
" NOTE It requires `set shell=bash\ -O\ globstar\ -O\ extglob` and GNU bash available
" (type `$ brew install bash` if OSX is used).
nnoremap <leader>fe :call esearch#init({'paths': '**/*.{js,css,html}'})<cr>
" or if one of ag, rg or ack is available
nnoremap <leader>fe :call esearch#init({'filetypes': 'js css html'})<cr>
" Use a callable prefiller to search python functions.
" Rough equivalent of 'def ${VISUAL}$0(' expansion in snippets.
vnoremap <expr><leader>fp esearch#prefill({'prefill': [{VISUAL-> "def ".VISUAL()."(\<left>"}]})
nnoremap <leader>fp :call esearch#init({'prefill': [{VISUAL-> "def ".VISUAL()."(\<left>"}]})<cr>
" esearch#prefill() can be used as an operator. Use '<leader>fl' in visual mode or with
" a text-object like '<leader>fli(' to search for the selected text in lib directories.
noremap <expr><leader>fl esearch#prefill({'paths': $GOPATH.' node_modules/'})
nnoremap <leader>fll :call esearch#init({'paths': $GOPATH.' node_modules/'})<cr>
Add window-local keymaps using g:esearch.win_map
list.
" Keymap | What it does
" ---------+---------------------------------------------------------------------------------------------
" yf | Yank a hovered file absolute path.
" t | Use a custom command to open the file in a tab.
" + | Render [count] more lines after a line with matches. Ex: + adds 1 line, 10+ adds 10.
" - | Render [count] less lines after a line with matches. Ex: - hides 1 line, 10- hides 10.
" gq | Populate QuickFix list using results of the current pattern search.
" gsp | Sort the results by path. NOTE that it's search util-specific.
" gsd | Sort the results by modification date. NOTE that it's search util-specific.
" Each definition contains nvim_set_keymap() args: [{modes}, {lhs}, {rhs}].
let g:esearch.win_map = [
\ ['n', 'yf', ':call setreg(esearch#util#clipboard_reg(), b:esearch.filename())<cr>'],
\ ['n', 't', ':call b:esearch.open("NewTabdrop")<cr>' ],
\ ['n', '+', ':call esearch#init(extend(b:esearch, AddAfter(+v:count1)))<cr>' ],
\ ['n', '-', ':call esearch#init(extend(b:esearch, AddAfter(-v:count1)))<cr>' ],
\ ['n', 'gq', ':call esearch#init(extend(copy(b:esearch), {"out": "qflist"}))<cr>' ],
\ ['n', 'gsp', ':call esearch#init(extend(b:esearch, sort_by_path))<cr>' ],
\ ['n', 'gsd', ':call esearch#init(extend(b:esearch, sort_by_date))<cr>' ],
\]
" Helpers to use in keymaps.
let g:sort_by_path = {'adapters': {'rg': {'options': '--sort path'}}}
let g:sort_by_date = {'adapters': {'rg': {'options': '--sort modified'}}}
" {'backend': 'system'} means synchronous reload using system() call to stay within the
" same context
let g:AddAfter = {n -> {'after': b:esearch.after + n, 'backend': 'system'}}
See :help esearch-api
and :help esearch-api-examples
for more details.
Troubleshooting
- Avoid searching in
log/
,node_modules/
,dist/
and similar folders.
The preferred approach is to use .agignore
for ag, .rgignore
or similar
ignore files. To skip node_modules
try echo node_modules >> ~/.ignore
.
- Git adapter have problems when searching in filenames with non-ASCII names.
Run git config --global core.precomposeunicode true && git config --global core.quotePath false
in your shell to prevent outputting unicode chars like \312
.
- Some regex features like lookaround are not supported.
Use ag, ack or rg (of version >= 0.11) to access PCRE syntax. Git and grep are also support them, but sometimes require to be installed with the corresponding flag.
- Filetype-specific syntax highlights are missing or different from those within opened files.
The plugin uses separate syntax definitions to make the window more lightweight.
If it's misleading for you, please, disable them using let g:esearch.win_contexts_syntax = 0
or open a PR to add or improve the existing syntax files. Highlights can also be cleared automatically if there are too many lines or if there's a long line encountered.
- The search window is slow.
If it's sluggish during updates, try to increase let g:esearch.win_update_throttle_wait = 200
value (100 is the default). If it's still slow after the search has finished, try to use let g:esearch.win_contexts_syntax = 0
or consider to use neovim, as it has position-based highlights comparing to regex-based syntax matches and parses/renders results faster. Also, make sure that echo esearch#has#lua
outputs 1.
See :help esearch-troubleshooting
for more troubleshooting examples.
Acknowledgements
Special thanks to contributors, issue reporters and other plugin authors (vital.vim, arpeggio.vim, incsearch.vim etc.) whose code has helped to develop some aspects of the plugin.
Licence
MIT