fzf.vim: fuzzy matching with a fall-back source - vim

I would like to implement a vim command to select buffers with the following behaviour:
When called, it present the user with a list of loaded buffers and other recently used buffers from the current directory (This is different from the :History command provided by fzf.vim in that we list only the recently used buffers from the current directory, fzf.vim lists all the recent buffers). The user can search for the file name they would like to load
If none of the options matches user's search term, expand the scope of the search by listing all the files in the current directory and letting the user fuzzy search through them.
This is what I have so far (This assumes that junegunn/fzf.vim is already installed):
nnoremap <silent> <space><space> :call <SID>recent_files()<CR>
function! s:recent_files_sink(items)
if len(a:items) == 2
execute "edit" a:items[1]
return
endif
call fzf#vim#files("", {'options': ['--query', a:items[0]]})
endfunction
" deduped is a list of items without duplicates, this
" function inserts elements from items into deduped
function! s:add_unique(deduped, items)
let dict = {}
for item in a:deduped
let dict[item] = ''
endfor
for f in a:items
if has_key(dict, f) | continue | endif
let dict[f] = ''
call add(a:deduped, f)
endfor
return a:deduped
endfunction
function! s:recent_files()
let regex = '^' . fnamemodify(getcwd(), ":p")
let buffers = filter(map(
\ getbufinfo({'buflisted':1}), {_, b -> fnamemodify(b.name, ":p")}),
\ {_, f -> filereadable(f)}
\ )
let recent = filter(
\ map(copy(v:oldfiles), {_, f -> fnamemodify(f, ":p")}),
\ {_, f -> filereadable(f) && f =~# regex})
let combined = s:add_unique(buffers, recent)
call fzf#run(fzf#wrap({
\ 'source': map(combined, {_, f -> fnamemodify(f, ":~:.")}),
\ 'sink*': function('s:recent_files_sink'),
\ 'options': '--print-query --exit-0 --prompt "Recent> "'
\ }))
endfunction
SpaceSpace invokes s:recent_files() which lists loaded buffers and recently used files from viminfo. The interesting bit here is the sink* option in the call to fzf#run (4th line from the bottom). The sink is another function. If a filename was selected, the sink function loads it for editing, otherwise, it calls fzf#vim#files, which lists the contents of the directory.
This is pretty close to what I want but there are a couple of problems:
When no matches are found in recent files, the user must press Return to trigger the fall-back. (One can easily argue that this is the correct behaviour)
When the fall-back fzf window is loaded, it starts in the normal mode and not the insert mode
The user must enter the search query again in the new fzf window (solved)
I'm looking for suggestions on how to solve these problems, particularly 2 and 3. I'm also open to solutions that don't meet the specifications exactly but provide a good user experience.
EDIT: I came up with another approach to achieve this using this as the source for fzf
cat recent_files.txt fifo.txt
where recent_files.txt is a list of recent files and fifo.txt is an empty fifo created using mkfifo. A mapping can be added to buffer which triggers a command to write to the fifo. This way, the user can use that mapping to include a list of all files in case they don't find a match in recent files. This approach becomes problematic in cases where user finds the file in recents and presses enter. Since fzf is till waiting to read from fifo, it does not exit https://github.com/junegunn/fzf/issues/2288

I was finally able to come to a solution that is pretty close using fzf's reload functionality. (Thanks to junegunn)
This is how it goes:
nnoremap <silent> <space><space> :call <SID>recent_files()<CR>
" Initialize fzf with a list of loaded buffers and recent files from
" the current directory. If <space> is pressed, we load a list of all
" the files in the current directory
function! s:recent_files()
let regex = '^' . fnamemodify(getcwd(), ":p")
let buffers = filter(map(
\ getbufinfo({'buflisted':1}), {_, b -> fnamemodify(b.name, ":p")}),
\ {_, f -> filereadable(f)}
\ )
let recent = filter(
\ map(copy(v:oldfiles), {_, f -> fnamemodify(f, ":p")}),
\ {_, f -> filereadable(f) && f =~# regex})
let combined = <SID>add_unique(buffers, recent)
"-------------------------------------------
" This is the key piece that makes it work
"-------------------------------------------
let options = [
\ '--bind', 'space:reload:git ls-files',
\ ]
call fzf#run(fzf#wrap({
\ 'source': map(combined, {_, f -> fnamemodify(f, ":~:.")}),
\ 'options': options
\ }))
endfunction
" deduped is a list of items without duplicates, this
" function inserts elements from items into deduped
function! s:add_unique(deduped, items)
let dict = {}
for item in a:deduped
let dict[item] = ''
endfor
for f in a:items
if has_key(dict, f) | continue | endif
let dict[f] = ''
call add(a:deduped, f)
endfor
return a:deduped
endfunction
I start FZF by using <space><space>. This FZF window contains only the most recent files from the current directory. If I then press <space>, the FZF window is updated with a new list of files obtained from git ls-files.

Related

Vim - set custom editors to open `.md` & '.pdf' files

According to this official guide it is possible to determine your own editors that are called per the filename suffix when in VIM we use command g + x (while cursor is over a file name or URI).
This partially works for me. I used this block of code in the ~/.vimrc file:
" make sure that viewer is selected according to the suffix.
let g:netrw_browsex_viewer="-"
" functions for file extension '.md'.
function! NFH_md(filename)
typora filename
endfunction
" functions for file extension '.pdf'.
function! NFH_pdf(filename)
zathura filename
endfunction
Now I use Vim to open a source file e.g. main.c and navigate to these two line comments:
// EXAMPLE: ../../001--documentation/motorola--SREC_format.pdf
// EXAMPLE: ./markdown.md
If I hover with the cursor over the 1st one and press g + x I get this error:
Not an editor command: zathura filename
If I hover with the cursor over the 2nd one and press g + x I get this error:
Not an editor command: typora filename
So it looks like functions are executed according to a file suffix (this is good), but why isnt a file opened? I am probably missing the knowledge on how to properly pass an argument to the funmctions? What should I do to sucessfuly open the files with editors zathura and typora (both are installed on my system and can be run from the terminal)?
After #phd suggestions I tried this:
" make sure that viewer is selected according to the suffix.
let g:netrw_browsex_viewer="-"
" functions for file extension '.md'.
function! NFH_md(filename)
:! typora filename
endfunction
" functions for file extension '.md'.
function! NFH_pdf(filename)
:! zathura filename
endfunction
Which now opens the programs but it searches for the filename and can't find it. So function doesn't get the argument properly...
Errors are now:
error: Unknown file type: 'cannot open `/home/ziga/Dropbox/workspace/racunalnistvo/projects--pistam/2021-01-06--programmer_migration/002--sw/006/filename' (No such file or directory)'
and:
ENOENT: no such file or directory, open '/home/ziga/.../002--sw--006/filename
Also I read here that ! is not asynchronous and !start is a Windows only. I need Linux solution which is asynchronous.
The answer is:
" make sure that viewer is selected according to the suffix.
let g:netrw_browsex_viewer="-"
" functions for file extension '.md'.
function! NFH_md(f)
execute '!typora' a:f
endfunction
" functions for file extension '.pdf'.
function! NFH_pdf(f)
execute '!zathura' a:f
endfunction
If execute is not used then a:f is not expanded.
Note:
This is not asynchronous solution!
For an asynchronous solution we can install vim plugin asyncrun and change the above code to this:
" make sure that viewer is selected according to the suffix.
let g:netrw_browsex_viewer="-"
" functions for file extension '.md'.
function! NFH_md(f)
call asyncrun#run("", "cwd", "typora " . a:f)
endfunction
" functions for file extension '.pdf'.
function! NFH_pdf(f)
call asyncrun#run("", "cwd" , "evince " . a:f)
endfunction
" functions for file extension '.html'.
function! NFH_html(f)
call asyncrun#run("", "cwd", "firefox --new-window " . a:f)
endfunction
TODO:
With this there are very little annoying problems left. For example
URI's ending in .com, .org, gov, php... aren't opened as it is
virtually impossible to create a handler function for every possible
top domain! There is another problem, because URI's to a PDF files
that end with #page=100 or #some_chapter aren't opened. Similar is
with markdown source files where '#some_chapter' is ignored.
Here is a quick reference works and what does not (copy in vim,
hoover over the URI / file and press g+x):
// ✔ EXAMPLE (g + x): ./markdown.md
// ✘ EXAMPLE (g + x): ./markdown.md#test
// ✔ EXAMPLE (g + x): ../../001--documentation/motorola--SREC_format.pdf
// ✘ EXAMPLE (g + x): ../../001--documentation/stm32--boot_mode_and_bootloader.pdf#page=2
// ✔ EXAMPLE (g + x): ../../001--documentation/C_--_HTML_documentation/en/index.html
// ✔ EXAMPLE (g + x): https://learnvimscriptthehardway.stevelosh.com/chapters/23.html
// ✘ EXAMPLE (g + x): https://www.google.com
// ✔ EXAMPLE (g + x): https://www.google.com/index.html
// ✘ EXAMPLE (g + x): https://wiki.archlinux.org/index.php/HiDPI#GNOME
// ✘ EXAMPLE (g + x): file:///home/ziga/Dropbox/workspace/racunalnistvo/projects/2021-01-06--programmer_migration/001--documentation/stm32--usart_protocol_used_to_talk_with_bootloader.pdf
// ✘ EXAMPLE (g + x): file:///home/ziga/Dropbox/workspace/racunalnistvo/projects/2021-01-06--programmer_migration/001--documentation/stm32--usart_protocol_used_to_talk_with_bootloader.pdf#page=10
If anyone finds a way to fix these problems or can write a vim plugin
capable of opening these URIs it would be wonderful!

Error while calling sort on a list of quickfix items even though the list items are copied to a local variable

Below error is observed while calling TestQFItem even though list items are copied to a local variable using let itemsCopied = deepcopy(items):
Error detected while processing function TestQFItem:
line: 3
E:21 cannot make changes, modifiable is off:^Isort(itemscopied,function("CompareQuickFixItems"))
Even though the list items are copied to a local variable using deepcopy why vim is throwing the error "E:21 cannot make changes, modifiable is off"?
function! CompareQuickFixItems(arg1, arg2)
if a:arg1['lnum'] == a:arg2['lnum']
return 0
elseif a:arg1['lnum'] < a:arg2['lnum']
return -1
else
return 1
endif
endfunction
function! TestQFItem()
let items = getqflist()
let itemsCopied = deepcopy(items)
call sort(itemsCopied, function("CompareQuickFixItems"))
call setqflist(itemsCopied, "r")
endfunction

How to make vim’s vimgrep command keep indentations?

I’m trying to use vim’s quick fix (or local) list to get some information extracted from a file. For example, I want to get all the method names of a python module (the idea was borrowed from pycharm). What I want to get in vim’s “local list” is just something like the following:
class Foo:
def one():
def two():
def three():
def bar():
def bazz():
To achieve that, I do approximately the following steps:
:" OK, the current buffer is being used.
:let file_name = expand('%:p')
:" The heart of the process is one of vim’s grep-like command.
:execute 'lvimgrep /\v^\s*(class|def)/ '.file_name
:" I open the results with the “lopen” command because “llist”
:" doesn’t allow me to use concealing.
:lopen
:" Since I’m working with one file, I don’t need information
:" about file name, line number etc.
:setlocal conceallevel=3
:syntax match NonText /\v^.+col \d+([:]|[|])/ transparent conceal
:" Please note, I‘m still able to jump to a line
:" with the “ll” command.
But unfortunately I get:
class Foo:
def one():
def two():
def three():
def bar():
def bazz():
All the indents are swallowed! The result is quite useless… I can’t differentiate which of the functions belong to a class, which of them are stand-alone.
Please note, the concealing doesn’t have a meaningful influence on the result. If I took away the two last commands (conceal-related), nothing significant would change, only the file name and line/column numbers would be shown but the text in the lines would be still without indents anyway.
So, my questions are:
Is it possible to make lvimgrep (or an analogue) keep the lines untouched in order to save indentation? Is there a magic command or option to do that? Or should I program my own implementation of lvimgrep?
P.S. I’d like to use vim’s regular expressions. But if it’s impossible, I could switch to the external “grep” command (I’m a linux guy) and use the BRE or ERE syntax as well.
No, currently, it is impossible to make lvimgrep (or even similar commands) keep leading whitespace characters in the quickfix (location) list entries, since space and tab characters are unconditionally skipped from the beginning, if the text length is greater than 3.
The only way to achieve the desired behavior (at least, using *vimgrep commands) is to modify the source code. For example, you might add an option as demonstrated in the following patch:
diff --git a/runtime/optwin.vim b/runtime/optwin.vim
index 7d3a8804d..caac55cf2 100644
--- a/runtime/optwin.vim
+++ b/runtime/optwin.vim
## -1299,6 +1299,7 ## call <SID>OptionG("ve", &ve)
call append("$", "eventignore\tlist of autocommand events which are to be ignored")
call <SID>OptionG("ei", &ei)
call append("$", "loadplugins\tload plugin scripts when starting up")
+call append("$", "locws\tenables whitespace characters for entries in the location window")
call <SID>BinOptionG("lpl", &lpl)
call append("$", "exrc\tenable reading .vimrc/.exrc/.gvimrc in the current directory")
call <SID>BinOptionG("ex", &ex)
diff --git a/src/option.c b/src/option.c
index aabfc7f53..4ba280806 100644
--- a/src/option.c
+++ b/src/option.c
## -1791,6 +1791,9 ## static struct vimoption options[] =
{"loadplugins", "lpl", P_BOOL|P_VI_DEF,
(char_u *)&p_lpl, PV_NONE,
{(char_u *)TRUE, (char_u *)0L} SCTX_INIT},
+ {"locws", NULL, P_BOOL|P_VI_DEF,
+ (char_u *)&p_locws, PV_NONE,
+ {(char_u *)FALSE, (char_u *)0L} SCTX_INIT},
{"luadll", NULL, P_STRING|P_EXPAND|P_VI_DEF|P_SECURE,
#if defined(DYNAMIC_LUA)
(char_u *)&p_luadll, PV_NONE,
diff --git a/src/option.h b/src/option.h
index c1a25b342..5e17c459e 100644
--- a/src/option.h
+++ b/src/option.h
## -602,6 +602,7 ## EXTERN char_u *p_lcs; // 'listchars'
EXTERN int p_lz; // 'lazyredraw'
EXTERN int p_lpl; // 'loadplugins'
+EXTERN int p_locws; // 'locws'
#if defined(DYNAMIC_LUA)
EXTERN char_u *p_luadll; // 'luadll'
#endif
diff --git a/src/quickfix.c b/src/quickfix.c
index 136c472e1..8e206ddd7 100644
--- a/src/quickfix.c
+++ b/src/quickfix.c
## -4417,8 +4417,9 ## qf_update_buffer(qf_info_T *qi, qfline_T *old_last)
static int
qf_buf_add_line(buf_T *buf, linenr_T lnum, qfline_T *qfp, char_u *dirname)
{
- int len;
- buf_T *errbuf;
+ int len;
+ buf_T *errbuf;
+ long lval;
if (qfp->qf_module != NULL)
{
## -4472,10 +4473,12 ## qf_buf_add_line(buf_T *buf, linenr_T lnum, qfline_T *qfp, char_u *dirname)
IObuff[len++] = '|';
IObuff[len++] = ' ';
- // Remove newlines and leading whitespace from the text.
+ // Remove newlines and leading whitespace from the text,
+ // if the user not enabled whitespaces explicitly via locws option.
// For an unrecognized line keep the indent, the compiler may
// mark a word with ^^^^.
- qf_fmt_text(len > 3 ? skipwhite(qfp->qf_text) : qfp->qf_text,
+ get_option_value((char_u *)"locws", &lval, NULL, 0);
+ qf_fmt_text(len > 3 ? (lval ? qfp->qf_text : skipwhite(qfp->qf_text)) : qfp->qf_text,
IObuff + len, IOSIZE - len);
if (ml_append_buf(buf, lnum, IObuff,
With locws option, you could enable whitespace characters in the quickfix/location entries as follows:
:set locws
Alternative Option
As an alternative, you could just list out the results via :# an :global
:g/\v^\s*(class|def)/#
This will print out the relevant lines with their associated line numbers.
A slightly fancier mapping:
nnoremap <leader>f :keeppatterns g/\v^\s*(class|def)/#<cr>:
With this mapping you can just type the line number and press enter to jump to a line after executing the mapping.
For more help see:
:h :g
:h :#
:h :keeppatterns
:h :range
Using Quickfix List
In order to use the quickfix list you will need to "mangle" your indent text with another character, e.g. >.
command! PyLocations call <SID>py_locations()
function! s:py_locations()
let lst = []
let bufnr = bufnr('%')
let pat = repeat(' ', shiftwidth())
let Fn = {l -> substitute(matchstr(l, '^\s*'), pat, '▶', 'g') . matchstr(l, '\S.*')}
keeppatterns g/\v^\s*(class|def)>/call add(lst, {'bufnr': bufnr, 'lnum': line('.'), 'text': call(Fn, [getline('.')])})
call setqflist(lst, ' ')
cwindow
endfunction
I’ve done it! But it took more than “half an hour” as I supposed early.
During the research, I found that vim’s local list (and I‘m sure quick fix list too) keeps the indentation of a line when it’s unable to recognize the line as a valid “goto information”, when the line format doesn’t correspond to errorformat. (See :help quickfix-valid) So, to get a nice looking list it must be rendered manually. But in order to have the possibility of jumping to the items of a search result, a quickfix or local list must be created as well.
I’ve split the task into two functions: the fist one retrieves the data, the second one shows it.
function! s:grep_buffer(pattern)
let file_name = expand("%")
let b:grepped = [] |" It will store the search results.
lexpr! [] |" The local list will give the possibility of jumping.
for line_number in range(1, line('$'))
let line_content = getline(line_number)
if line_content =~ '\V'.a:pattern
call add(b:grepped, line_content)
laddexpr file_name.':'.line_number.':'.line_content
endif
endfor
endfunction
function! s:show_result()
if exists('b:grepped')
let grepped = b:grepped |" After creation a new window it’ll be lost.
vnew
call append(0, grepped)
setlocal buftype=nofile |" Don’t make vim save the content.
setlocal noswapfile
setlocal nomodifiable
nn <silent> <buffer> <CR> :exe line(".").'ll'<CR>
wincmd l |" Now the old window is on the right.
hide
endif
endfunction
Of course, a convenient key mapping must be designed. (There is a trailing space in the second line.)
command! -nargs=1 GrepBuffer call <SID>grep_buffer(<f-args>)
nn <leader>g :GrepBuffer
nn <silent> <leader>s :call <SID>show_result()<CR>
It’s super convenient! When I want to overview the search results again, I call the show_result function which replaces the current window with the search results. I can use all the usual navigation tools to move the cursor through the search results. And all I need to jump to an interesting place is just to hit the enter key!
Thanks for all! The problem is solved, vim is the greatest editor.

vim fzf get selected word as --query parameter

I'm actually searching to make fuzzy search with :FZF in vim, but with parameter like this :FZF -q /tmp/boulou
But, replace the /tmp/boulou by the words selected in the visual mode and bind it to a vmap.
Have you got an idea to help me ?
Thanks, kind regards
Accordingly to xolox answer, you can create mapping like this:
function! s:getVisualSelection()
let [line_start, column_start] = getpos("'<")[1:2]
let [line_end, column_end] = getpos("'>")[1:2]
let lines = getline(line_start, line_end)
if len(lines) == 0
return ""
endif
let lines[-1] = lines[-1][:column_end - (&selection == "inclusive" ? 1 : 2)]
let lines[0] = lines[0][column_start - 1:]
return join(lines, "\n")
endfunction
vnoremap <silent><leader>f <Esc>:FZF -q <C-R>=<SID>getVisualSelection()<CR><CR>
Keep in mind, it will work only for word selection. It will not work for multilines or selection which contains tab characters. You just need to call a function instead of a command but since I do not have fzf installed you have to do this by yourself.
In case you need to search the word under the cursor (in normal mode):
nnoremap <silent><leader>f :FZF -q <C-R>=expand("<cword>")<CR><CR>

Get filename and line number that called vim autoload function

I'm trying to populate the quickfix list using an autoload function, i.e.:
function! myplugin#myfunc(msg)
" this doesn't work from *inside* an autoload function
let filename = fnamemodify(resolve(expand('<sfile>:p')))
" not sure if it's possible to get the line number from where
" a function was called
let linenum = '?#'
" create qf dict object
" set filename, line number, bufnr, text, etc
" add dict to qflist
" setqflist(qfdictlist)
endfunction!
The problem I've run into is I can't figure out a way to get the filename and line number from the code that called the autoload function. Any suggestions?
Out of the box. This is not possible.
However, depending on the exact scenario, here are a few leads.
I've attempted to write a function that decodes the callstack from v:throwpoint when an exception is caught. It's still experimental. See https://github.com/LucHermitte/lh-vim-lib/blob/master/autoload/lh/exception.vim
From my test unit framework, I know precisely which test-file/line is failing. To do so, I had to parse the UT file in order to inject the line number of the caller into the :Assert* commands.
As you see, none of these solutions is very good. But there are none other right now. The callstack isn't available, except from v:throwpoint in an exception context. The only other solution is to have callers inject their references (~__FILE__ + ~__LINE__) when calling. And the only way to automate this is to compile the caller script into another script that automatically injects the missing information.
By right now, understand there had been a proposal on vim-dev mailing list this last month in order to permit to have access to the call stack, but alas, only during debugging sessions: https://github.com/vim/vim/pull/433
If this is accepted, may be it could be extended later to offer a viml function that'll export this information.
EDIT: Your question inspired me to write a simplistic logging facility for vim:
" Function: lh#log#new(where, kind) {{{3
" - where: "vert"/""/...
" - kind: "qf"/"loc" for loclist
" NOTE: In order to obtain the name of the calling function, an exception is
" thrown and the backtrace is analysed.
" In order to work, this trick requires:
" - a reasonable callstack size (past a point, vim shortens the names returned
" by v:throwpoint
" - named functions ; i.e. functions defined on dictionaries (and not attached
" to them) will have their names mangled (actually it'll be a number) and
" lh#exception#callstack() won't be able to decode them.
" i.e.
" function s:foo() dict abort
" logger.log("here I am");
" endfunction
" let dict.foo = function('s:foo')
" will work correctly fill the quicklist/loclist, but
" function dict.foo() abort
" logger.log("here I am");
" endfunction
" won't
" TODO: add verbose levels
function! lh#log#new(where, kind) abort
let log = { 'winnr': bufwinnr('%'), 'kind': a:kind, 'where': a:where}
" open loc/qf window {{{4
function! s:open() abort dict
try
let buf = bufnr('%')
exe 'silent! '.(self.where). ' '.(self.kind == 'loc' ? 'l' : 'c').'open'
finally
call lh#buffer#find(buf)
endtry
endfunction
" add {{{4
function! s:add_loc(msg) abort dict
call setloclist(self.winnr, [a:msg], 'a')
endfunction
function! s:add_qf(msg) abort dict
call setqflist([a:msg], 'a')
endfunction
" clear {{{4
function! s:clear_loc() abort dict
call setloclist(self.winnr, [])
endfunction
function! s:clear_qf() abort dict
call setqflist([])
endfunction
" log {{{4
function! s:log(msg) abort dict
let data = { 'text': a:msg }
try
throw "dummy"
catch /.*/
let bt = lh#exception#callstack(v:throwpoint)
if len(bt) > 1
let data.filename = bt[1].script
let data.lnum = bt[1].pos
endif
endtry
call self._add(data)
endfunction
" reset {{{4
function! s:reset() dict abort
call self.clear()
call self.open()
return self
endfunction
" register methods {{{4
let log.open = function('s:open')
let log._add = function('s:add_'.a:kind)
let log.clear = function('s:clear_'.a:kind)
let log.log = function('s:log')
let log.reset = function('s:reset')
" open the window {{{4
call log.reset()
return log
endfunction
Which use this other function of mine that decodes the callstack.
Well, if you can't get the file and line from inside the autoload function, you have to pass that into your function.
Your autoloaded function is invoked somehow, either by a custom mapping, command, or on an :autocmd event. From there, you can resolve the current file (expand('%') and line number ('line('.')) and pass that in.
But why do you need that?! For normal editing tasks, I can't imagine why. If you're writting a custom Vim debugging plugin, okay, that might be useful. But debugging via Vimscript is difficult (as you've found out), the callstack isn't exposed. Better stick with the built-in :debug and :breakadd; I've so far found them sufficient.

Resources