mirror of
https://github.com/junegunn/fzf.vim.git
synced 2026-09-28 14:42:22 +08:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0097439b74 | ||
|
|
b4a045f8b4 | ||
|
|
21aacd5304 | ||
|
|
bbe5cacb28 | ||
|
|
14f808b543 | ||
|
|
319f5ac437 | ||
|
|
b268241f4a | ||
|
|
3e34f1dc6d | ||
|
|
81afa1b9d9 | ||
|
|
74cab39bbf | ||
|
|
0ce4cca7bd | ||
|
|
747cd594a2 | ||
|
|
b1c03ea0ef | ||
|
|
ba458b129e | ||
|
|
8da2aa53ab | ||
|
|
8039b2747c | ||
|
|
adcdb91d70 | ||
|
|
32baf162c0 | ||
|
|
5ad2fa7bed | ||
|
|
777e783382 | ||
|
|
63fb5c974f | ||
|
|
2b6e8b2132 | ||
|
|
81ae642d5b | ||
|
|
35e8ef8e20 | ||
|
|
a4979fdcd4 | ||
|
|
9705771895 | ||
|
|
c228c70ee3 | ||
|
|
a5717367fc | ||
|
|
88b0ae41c8 | ||
|
|
d538da05b6 | ||
|
|
a630ef91e1 | ||
|
|
8a0068127a |
@@ -201,6 +201,27 @@ let g:fzf_vim.show_key = 'ctrl-o'
|
||||
let g:fzf_vim.show_key = 'ctrl-o,double-click'
|
||||
```
|
||||
|
||||
#### Key hints
|
||||
|
||||
Most commands show a footer listing the keys they bind, so you can see what
|
||||
`Show`, `Open`, `HSplit` and the rest are without leaving fzf. It is fzf's
|
||||
footer, so `--no-footer` hides it:
|
||||
|
||||
```vim
|
||||
let g:fzf_vim.options = '--no-footer'
|
||||
```
|
||||
|
||||
#### Global fzf options
|
||||
|
||||
You can set fzf options for all commands with `g:fzf_vim.options`.
|
||||
`g:fzf_vim.{command}_options` takes precedence over it.
|
||||
|
||||
For example, add `--no-footer` to hide the key hints.
|
||||
|
||||
```vim
|
||||
let g:fzf_vim.options = '--no-footer'
|
||||
```
|
||||
|
||||
#### Command-level options
|
||||
|
||||
```vim
|
||||
|
||||
+129
-26
@@ -400,6 +400,9 @@ function! s:fzf(name, opts, extra)
|
||||
let extra = copy(extra)
|
||||
let eopts = has_key(extra, 'options') ? remove(extra, 'options') : ''
|
||||
let merged = extend(copy(a:opts), extra)
|
||||
" Global fzf options
|
||||
call s:merge_opts(merged, s:conf('options', []))
|
||||
|
||||
call s:merge_opts(merged, eopts)
|
||||
|
||||
" Command-level fzf options
|
||||
@@ -495,7 +498,10 @@ function! fzf#vim#paste(items) abort
|
||||
return s:warn('Cannot paste into a nomodifiable buffer')
|
||||
endif
|
||||
let line = getline('.')
|
||||
let idx = empty(line) ? 0 : col('.')
|
||||
" col('.') is the first byte of the character under the cursor. Skip the
|
||||
" whole character so that a multibyte character is not split in half.
|
||||
let cursor = col('.') - 1
|
||||
let idx = empty(line) ? 0 : cursor + strlen(matchstr(line, '.', cursor))
|
||||
let head = strpart(line, 0, idx)
|
||||
let tail = strpart(line, idx)
|
||||
let pad = (!empty(head) && head !~ '\s$') ? ' ' : ''
|
||||
@@ -528,6 +534,49 @@ function! s:execute_silent(cmd)
|
||||
silent keepjumps keepalt execute a:cmd
|
||||
endfunction
|
||||
|
||||
" A tag address can be any Ex command, and a tags file can come from an
|
||||
" untrusted source, so run it in a sandbox as a builtin tag jump does. ':sandbox'
|
||||
" as a modifier only covers the command up to the first '|', so hand the address
|
||||
" to ':execute', which keeps all of it inside the sandbox
|
||||
function! s:execute_tag_address(excmd)
|
||||
silent keepjumps keepalt sandbox execute a:excmd
|
||||
endfunction
|
||||
|
||||
" The same, as a command for a win_execute() list
|
||||
function! s:sandboxed(cmd)
|
||||
return 'sandbox execute '.string(a:cmd)
|
||||
endfunction
|
||||
|
||||
" Address of a tag, given the rest of the line after the file name, or, with
|
||||
" 'whole' off, the address field alone as :BTags reads it. Only the forms a tags
|
||||
" file is meant to hold, a line number and a search pattern, chained with ';' for
|
||||
" '--excmd=combine' and cut at the ';"' terminator as Vim's find_extra() does.
|
||||
" Returns an empty string for anything else, which is then not run.
|
||||
"
|
||||
" A pattern holding a tab reaches :BTags cut short. Where earlier parts parsed,
|
||||
" keep those, since a line number beats half a pattern. A fragment on its own is
|
||||
" an address only where the whole line is in hand, as in a helptags file, whose
|
||||
" '/*:Ag*' carries no terminator. ctags always terminates, so on the field alone
|
||||
" a fragment is the cut, and it would match some other line. Running to the end
|
||||
" of the line it takes any '|' or ';' with it, so it cannot chain a command.
|
||||
let s:tag_address_part = '\%(\d\+\|/\%(\\.\|[^/\\]\)*/\|?\%(\\.\|[^?\\]\)*?\)'
|
||||
let s:tag_address_open = '\%(/\%(\\.\|[^/\\\t]\)*\|?\%(\\.\|[^?\\\t]\)*\)'
|
||||
function! s:tag_address(rest, whole)
|
||||
" :BTags aligns its columns, so its field arrives padded, and a tags file with
|
||||
" CRLF endings leaves a carriage return, which s:strip() keeps
|
||||
let rest = substitute(a:rest, '^[ \t\r]*\|[ \t\r]*$', '', 'g')
|
||||
let chain = '^'.s:tag_address_part.'\%(;'.s:tag_address_part.'\)*'
|
||||
let address = matchstr(rest, chain.'\ze\%(;"\|$\)')
|
||||
if !empty(address)
|
||||
return address
|
||||
endif
|
||||
let address = matchstr(rest, chain.'\ze;[/?]')
|
||||
if !empty(address)
|
||||
return address
|
||||
endif
|
||||
return a:whole ? matchstr(rest, '^'.s:tag_address_open.'\ze\%(\t\|$\)') : ''
|
||||
endfunction
|
||||
|
||||
" [key, [filename, [stay_on_edit: 0]]]
|
||||
function! s:action_for(key, ...)
|
||||
let Cmd = get(get(g:, 'fzf_action', s:default_action), a:key, '')
|
||||
@@ -675,15 +724,15 @@ function! s:show_entry(winid, bufnr, dir, kind, entry) abort
|
||||
" 'acd' would move the cwd on the first open, and s:ag_to_qf resolves a
|
||||
" relative match against it. Tag addresses are searched with the settings
|
||||
" s:tags_sink uses.
|
||||
let [magic, wrapscan, acd] = [&magic, &wrapscan, &acd]
|
||||
let [magic, wrapscan, acd, ic] = [&magic, &wrapscan, &acd, &ignorecase]
|
||||
let &acd = 0
|
||||
if a:kind ==# 'tags' || a:kind ==# 'btags'
|
||||
let [&magic, &wrapscan] = [0, 1]
|
||||
let [&magic, &wrapscan, &ignorecase] = [0, 1, 0]
|
||||
endif
|
||||
try
|
||||
call s:goto_entry(a:winid, a:bufnr, a:dir, a:kind, entry)
|
||||
finally
|
||||
let [&magic, &wrapscan, &acd] = [magic, wrapscan, acd]
|
||||
let [&magic, &wrapscan, &acd, &ignorecase] = [magic, wrapscan, acd, ic]
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
@@ -723,23 +772,32 @@ function! s:goto_entry(winid, bufnr, dir, kind, entry) abort
|
||||
if len(parts) < 3
|
||||
return
|
||||
endif
|
||||
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
|
||||
let excmd = s:tag_address(join(parts[2:-2], '')[:-2], 1)
|
||||
if empty(excmd)
|
||||
return s:warn('Unsupported tag address: '.s:strip(parts[0]))
|
||||
endif
|
||||
let relpath = parts[1][:-2]
|
||||
let path = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/')
|
||||
\ ? relpath : join([fnamemodify(parts[-1], ':h'), relpath], '/')
|
||||
let path = expand(path, 1)
|
||||
if empty(excmd) || !filereadable(path)
|
||||
" fnamemodify(), not expand(), which runs backticks in the name a tags file
|
||||
" gives, as s:in_dir says
|
||||
let path = fnamemodify(path, ':p')
|
||||
if !filereadable(path)
|
||||
return
|
||||
endif
|
||||
let cmds = s:edit_cmds(a:winid, path)
|
||||
\ + ['keepjumps '.excmd, 'normal! ^zvzz']
|
||||
\ + [s:sandboxed('keepjumps '.excmd), 'normal! ^zvzz']
|
||||
elseif a:kind ==# 'btags'
|
||||
let parts = split(entry, "\t")
|
||||
if len(parts) < 3 || !bufexists(a:bufnr)
|
||||
return
|
||||
endif
|
||||
let excmd = s:tag_address(parts[2], 0)
|
||||
if empty(excmd)
|
||||
return s:warn('Unsupported tag address: '.s:strip(parts[0]))
|
||||
endif
|
||||
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
|
||||
\ 'keepjumps '.parts[2], 'normal! zvzz']
|
||||
\ s:sandboxed('keepjumps '.excmd), 'normal! zvzz']
|
||||
elseif a:kind ==# 'marks'
|
||||
" A lowercase mark is local to the buffer the run started on
|
||||
let mark = matchstr(entry, '^\s*\zs\S')
|
||||
@@ -1594,6 +1652,17 @@ endfunction
|
||||
" ------------------------------------------------------------------
|
||||
" BTags
|
||||
" ------------------------------------------------------------------
|
||||
" ctags leaves a tab inside a pattern as it found it, and splitting the line on
|
||||
" tabs would cut the address short, leaving a fragment that matches some other
|
||||
" line. '\t' matches the same text and keeps the field the line's third. Only the
|
||||
" ';"' terminator says where the address ends, so a line without one is left as
|
||||
" it is rather than guessed at
|
||||
function! s:tabs_in_address(line)
|
||||
return substitute(a:line, '^\%([^\t]*\t\)\{2}\zs'.s:tag_address_part
|
||||
\ .'\%(;'.s:tag_address_part.'\)*\ze;"\t',
|
||||
\ '\=substitute(submatch(0), "\t", ''\\t'', "g")', '')
|
||||
endfunction
|
||||
|
||||
function! s:btags_source(tag_cmds)
|
||||
if !filereadable(expand('%'))
|
||||
throw 'Save the file first'
|
||||
@@ -1610,7 +1679,8 @@ function! s:btags_source(tag_cmds)
|
||||
elseif empty(lines)
|
||||
throw 'No tags found'
|
||||
endif
|
||||
return map(s:align_lists(map(lines, 'split(v:val, "\t")')), 'join(v:val, "\t")')
|
||||
return map(s:align_lists(map(map(lines, 's:tabs_in_address(v:val)'),
|
||||
\ 'split(v:val, "\t")')), 'join(v:val, "\t")')
|
||||
endfunction
|
||||
|
||||
" Position to return to with CTRL-T. Captured before fzf opens, because the
|
||||
@@ -1646,21 +1716,39 @@ function! s:btags_sink(from, lines)
|
||||
let tagname = ''
|
||||
call s:action_for(a:lines[0])
|
||||
let qfl = []
|
||||
for line in a:lines[1:]
|
||||
let parts = split(line, "\t")
|
||||
call s:execute_silent(parts[2])
|
||||
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
||||
if empty(tagname)
|
||||
let tagname = s:strip(parts[0])
|
||||
endif
|
||||
endfor
|
||||
try
|
||||
" Searched with the settings s:tags_sink and the CTRL-O callback use, and a
|
||||
" failing address must not drop the rest of the selection
|
||||
let [magic, &magic, wrapscan, &wrapscan, ic, &ignorecase] =
|
||||
\ [&magic, 0, &wrapscan, 1, &ignorecase, 0]
|
||||
for line in a:lines[1:]
|
||||
try
|
||||
let parts = split(line, "\t")
|
||||
let excmd = len(parts) > 2 ? s:tag_address(parts[2], 0) : ''
|
||||
if empty(excmd)
|
||||
throw 'Unsupported tag address: '.s:strip(get(parts, 0, line))
|
||||
endif
|
||||
call s:execute_tag_address(excmd)
|
||||
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
||||
if empty(tagname)
|
||||
let tagname = s:strip(parts[0])
|
||||
endif
|
||||
catch /^Vim:Interrupt$/
|
||||
break
|
||||
catch
|
||||
call s:warn(v:exception)
|
||||
endtry
|
||||
endfor
|
||||
finally
|
||||
let [&magic, &wrapscan, &ignorecase] = [magic, wrapscan, ic]
|
||||
endtry
|
||||
|
||||
if len(qfl) > 1
|
||||
" Go back to the original position
|
||||
normal! g`'
|
||||
" Because 'listproc' will use 'cfirst' to go to the first item in the list
|
||||
call s:fill_quickfix('btags', qfl)
|
||||
else
|
||||
elseif len(qfl) == 1
|
||||
normal! zvzz
|
||||
endif
|
||||
if !empty(tagname)
|
||||
@@ -1714,21 +1802,27 @@ function! s:tags_sink(from, lines)
|
||||
let [key; list] = a:lines
|
||||
|
||||
try
|
||||
let [magic, &magic, wrapscan, &wrapscan, acd, &acd] = [&magic, 0, &wrapscan, 1, &acd, 0]
|
||||
let [magic, &magic, wrapscan, &wrapscan, acd, &acd, ic, &ignorecase] =
|
||||
\ [&magic, 0, &wrapscan, 1, &acd, 0, &ignorecase, 0]
|
||||
for line in list
|
||||
try
|
||||
let parts = split(line, '\t\zs')
|
||||
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
|
||||
let excmd = s:tag_address(join(parts[2:-2], '')[:-2], 1)
|
||||
if empty(excmd)
|
||||
throw 'Unsupported tag address: '.s:strip(get(parts, 0, line))
|
||||
endif
|
||||
let base = fnamemodify(parts[-1], ':h')
|
||||
let relpath = parts[1][:-2]
|
||||
let abspath = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/') ? relpath : join([base, relpath], '/')
|
||||
|
||||
" fnamemodify(), not expand(), which runs backticks in the name
|
||||
let abspath = fnamemodify(abspath, ':p')
|
||||
if len(list) == 1
|
||||
call s:action_for(key, expand(abspath, 1))
|
||||
call s:action_for(key, abspath)
|
||||
else
|
||||
call s:open(expand(abspath, 1))
|
||||
call s:open(abspath)
|
||||
endif
|
||||
call s:execute_silent(excmd)
|
||||
call s:execute_tag_address(excmd)
|
||||
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
||||
if empty(tagname)
|
||||
let tagname = s:strip(parts[0])
|
||||
@@ -1740,7 +1834,7 @@ function! s:tags_sink(from, lines)
|
||||
endtry
|
||||
endfor
|
||||
finally
|
||||
let [&magic, &wrapscan, &acd] = [magic, wrapscan, acd]
|
||||
let [&magic, &wrapscan, &acd, &ignorecase] = [magic, wrapscan, acd, ic]
|
||||
endtry
|
||||
|
||||
if len(qfl) > 1
|
||||
@@ -1754,7 +1848,16 @@ function! s:tags_sink(from, lines)
|
||||
call s:action_for(key, qfl[0].filename, 1)
|
||||
|
||||
call s:fill_quickfix('tags', qfl)
|
||||
else
|
||||
elseif len(qfl) == 1
|
||||
" Entries opened in the loop take the action only when one was selected, so
|
||||
" the last one standing of a multi-selection still owes the key its action.
|
||||
" The loop opened it in the starting window, which goes back as it was first
|
||||
if len(list) > 1
|
||||
call s:execute_silent('b '.buf)
|
||||
call winrestview(view)
|
||||
call s:action_for(key, qfl[0].filename)
|
||||
call cursor(qfl[0].lnum, 1)
|
||||
endif
|
||||
normal! ^zvzz
|
||||
endif
|
||||
if !empty(tagname)
|
||||
|
||||
+13
-1
@@ -30,9 +30,21 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ex mode starts on the last line, so an entry with no address would report the
|
||||
# end of the file as the tag. Every other path refuses such an entry
|
||||
if [ -z "${EXCMD}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The address comes from a tags file, which can be untrusted, and a preview runs
|
||||
# as soon as an entry is highlighted. Sandboxed as a builtin tag jump is, so that
|
||||
# it cannot run a shell command or touch a file. ':sandbox' on its own only covers
|
||||
# the command up to the first '|', so hand the whole address to ':execute' as a
|
||||
# string literal, doubling the quotes in it
|
||||
EXCMD_LITERAL=${EXCMD//\'/\'\'}
|
||||
CENTER="$("${VIMNAME}" -R -i NONE -u NONE -e -m -s "${FILE}" \
|
||||
-c "set nomagic" \
|
||||
-c "silent ${EXCMD}" \
|
||||
-c "silent sandbox execute '${EXCMD_LITERAL}'" \
|
||||
-c 'let l=line(".") | new | put =l | print | qa!')" || exit
|
||||
|
||||
START_LINE="$(( CENTER - FZF_PREVIEW_LINES / 2 ))"
|
||||
|
||||
@@ -13,6 +13,10 @@ FZF-VIM - TABLE OF CONTENTS *fzf-vim* *fzf-vim-to
|
||||
Configuration options of the base plugin |fzf-vim-configuration-options-of-the-base-plugin|
|
||||
Configuration options for fzf.vim |fzf-vim-configuration-options-for-fzf-vim|
|
||||
Preview window |fzf-vim-preview-window|
|
||||
Paste key |fzf-vim-paste-key|
|
||||
Show key |fzf-vim-show-key|
|
||||
Key hints |fzf-vim-key-hints|
|
||||
Global fzf options |fzf-vim-global-fzf-options|
|
||||
Command-level options |fzf-vim-command-level-options|
|
||||
Command-level fzf options |fzf-vim-command-level-fzf-options|
|
||||
List type to handle multiple selections |fzf-vim-list-type-to-handle-multiple-selections|
|
||||
@@ -269,6 +273,28 @@ another key or turn it off to keep a binding of your own.
|
||||
let g:fzf_vim.show_key = 'ctrl-o,double-click'
|
||||
<
|
||||
|
||||
Key hints~
|
||||
*fzf-vim-key-hints*
|
||||
|
||||
Most commands show a footer listing the keys they bind, so you can see what
|
||||
Show, Open, HSplit and the rest are without leaving fzf. It is fzf's footer,
|
||||
so `--no-footer` hides it:
|
||||
>
|
||||
let g:fzf_vim.options = '--no-footer'
|
||||
<
|
||||
|
||||
Global fzf options~
|
||||
*fzf-vim-global-fzf-options*
|
||||
*g:fzf_vim.options*
|
||||
|
||||
You can set fzf options for all commands with `g:fzf_vim.options`.
|
||||
`g:fzf_vim.{command}_options` takes precedence over it.
|
||||
|
||||
For example, add `--no-footer` to hide the key hints.
|
||||
>
|
||||
let g:fzf_vim.options = '--no-footer'
|
||||
<
|
||||
|
||||
Command-level options~
|
||||
*fzf-vim-command-level-options*
|
||||
|
||||
|
||||
Reference in New Issue
Block a user