From dd6e6f1b15b58c857cf4990a5d98ea6cd49d4c51 Mon Sep 17 00:00:00 2001 From: bretello Date: Sun, 21 Dec 2025 05:06:34 +0100 Subject: [PATCH] ALEFindReferences: add -fzf flag to show output in fzf (#5018) * references: add ALEFindReferences -fzf option Allows using -fzf to show previews using fzf.vim. Includes: - add support for opening in bufers, splits, tabs and for adding matches quickfix - add support for -relative - add fzf preview `--highlight-line` option - add fzf.vim autoload module * tests: fix references tests for fzf support update --- autoload/ale/fzf.vim | 85 +++++++++++++++++++++++++++++++++ autoload/ale/references.vim | 65 ++++++++++++++++--------- doc/ale.txt | 16 +++++++ test/test_find_references.vader | 19 ++++---- 4 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 autoload/ale/fzf.vim diff --git a/autoload/ale/fzf.vim b/autoload/ale/fzf.vim new file mode 100644 index 00000000..368d6eab --- /dev/null +++ b/autoload/ale/fzf.vim @@ -0,0 +1,85 @@ +" Author: bretello https://github.com/bretello +" Description: Functions for integrating with fzf + +" Handle references found with ALEFindReferences using fzf +function! ale#fzf#ShowReferences(item_list, options) abort + let l:name = 'LSP References' + let l:capname = 'References' + let l:items = copy(a:item_list) + let l:cwd = getcwd() " no-custom-checks + let l:sep = has('win32') ? '\' : '/' + + function! s:relative_paths(line) closure abort + return substitute(a:line, '^' . l:cwd . l:sep, '', '') + endfunction + + if get(a:options, 'use_relative_paths') + let l:items = map(filter(l:items, 'len(v:val)'), 's:relative_paths(v:val)') + endif + + let l:start_query = '' + let l:fzf_options = { + \ 'source': items, + \ 'options': ['--prompt', l:name.'> ', '--query', l:start_query, + \ '--multi', '--bind', 'alt-a:select-all,alt-d:deselect-all', + \ '--delimiter', ':', '--preview-window', '+{2}/2'] + \} + + call add(l:fzf_options['options'], '--highlight-line') " this only works for more recent fzf versions (TODO: handle version check?) + + " wrap with #with_preview and #fzfwrap before adding the sinklist, + " otherwise --expect options are not added + let l:opts_with_preview = fzf#vim#with_preview(l:fzf_options) + let l:bang = 0 " TODO: handle bang + let l:wrapped = fzf#wrap(l:name, l:opts_with_preview, l:bang) + + call remove(l:wrapped, 'sink*') " remove the default sinklist to add in our custom sinklist + + function! l:wrapped.sinklist(lines) closure abort + if len(a:lines) <2 + return + endif + + let l:cmd = a:lines[0] + + function! s:references_to_qf(line) closure abort + " mimics ag_to_qf in junegunn/fzf.vim + let l:parts = matchlist(a:line, '\(.\{-}\)\s*:\s*\(\d\+\)\%(\s*:\s*\(\d\+\)\)\?\%(\s*:\(.*\)\)\?') + let l:filename = &autochdir ? fnamemodify(l:parts[1], ':p') : l:parts[1] + + return {'filename': l:filename, 'lnum': l:parts[2], 'col': l:parts[3], 'text': l:parts[4]} + endfunction + + let l:references = map(filter(a:lines[1:], 'len(v:val)'), 's:references_to_qf(v:val)') + + if empty(l:references) + return + endif + + if get(a:options, 'open_in') is# 'quickfix' + call setqflist([], 'r') + call setqflist(l:references, 'a') + + call ale#util#Execute('cc 1') + endif + + function! s:action(key, file) abort + " copied from fzf.vim + let l:default_action = { + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + + let fzf_actions = get(g:, 'fzf_action', l:default_action) + let l:Cmd = get(fzf_actions, a:key, 'edit') + + let l:cursor_cmd = escape('call cursor(' . a:file['lnum'] . ',' . a:file['col'] . ')', ' ') + let l:fullcmd = l:Cmd . ' +' . l:cursor_cmd . ' ' . fnameescape(a:file['filename']) + silent keepjumps keepalt execute fullcmd + endfunction + + return map(l:references, 's:action(cmd, v:val)') + endfunction + + call fzf#run(l:wrapped) +endfunction diff --git a/autoload/ale/references.vim b/autoload/ale/references.vim index 7ffa5381..6cf3ff5e 100644 --- a/autoload/ale/references.vim +++ b/autoload/ale/references.vim @@ -1,5 +1,6 @@ let g:ale_default_navigation = get(g:, 'ale_default_navigation', 'buffer') let g:ale_references_show_contents = get(g:, 'ale_references_show_contents', 1) +let g:ale_references_use_fzf = get(g:, 'ale_references_use_fzf', 0) let s:references_map = {} @@ -82,6 +83,16 @@ function! ale#references#FormatLSPResponseItem(response_item, options) abort endtry endif + if get(a:options, 'use_fzf') == 1 + let l:filename = ale#util#ToResource(a:response_item.uri) + let l:nline = a:response_item.range.start.line + 1 + let l:ncol = a:response_item.range.start.character + 1 + + " grep-style output (filename:line:col:text) so that fzf can properly + " show matches and previews using ':' as delimiter + return l:filename . ':' . l:nline . ':' . l:ncol . ':' . l:line_text + endif + if get(a:options, 'open_in') is# 'quickfix' return { \ 'filename': l:filename, @@ -100,32 +111,39 @@ function! ale#references#FormatLSPResponseItem(response_item, options) abort endfunction function! ale#references#HandleLSPResponse(conn_id, response) abort - if has_key(a:response, 'id') - \&& has_key(s:references_map, a:response.id) - let l:options = remove(s:references_map, a:response.id) + if ! (has_key(a:response, 'id') && has_key(s:references_map, a:response.id)) + return + endif - " The result can be a Dictionary item, a List of the same, or null. - let l:result = get(a:response, 'result', []) - let l:item_list = [] + let l:options = remove(s:references_map, a:response.id) - if type(l:result) is v:t_list - for l:response_item in l:result - call add(l:item_list, - \ ale#references#FormatLSPResponseItem(l:response_item, l:options) - \) - endfor - endif + " The result can be a Dictionary item, a List of the same, or null. + let l:result = get(a:response, 'result', []) + let l:item_list = [] - if empty(l:item_list) - call ale#util#Execute('echom ''No references found.''') - else - if get(l:options, 'open_in') is# 'quickfix' - call setqflist([], 'r') - call setqflist(l:item_list, 'a') - call ale#util#Execute('cc 1') - else - call ale#preview#ShowSelection(l:item_list, l:options) + if type(l:result) is v:t_list + for l:response_item in l:result + call add(l:item_list, + \ ale#references#FormatLSPResponseItem(l:response_item, l:options) + \) + endfor + endif + + if empty(l:item_list) + call ale#util#Execute('echom ''No references found.''') + else + if get(l:options, 'use_fzf') == 1 + if !exists('*fzf#run') + throw 'fzf#run function not found. You also need Vim plugin from the main fzf repository (i.e. junegunn/fzf *and* junegunn/fzf.vim)' endif + + call ale#fzf#ShowReferences(l:item_list, l:options) + elseif get(l:options, 'open_in') is# 'quickfix' + call setqflist([], 'r') + call setqflist(l:item_list, 'a') + call ale#util#Execute('cc 1') + else + call ale#preview#ShowSelection(l:item_list, l:options) endif endif endfunction @@ -165,6 +183,7 @@ function! s:OnReady(line, column, options, linter, lsp_details) abort \ 'use_relative_paths': has_key(a:options, 'use_relative_paths') ? a:options.use_relative_paths : 0, \ 'open_in': get(a:options, 'open_in', 'current-buffer'), \ 'show_contents': a:options.show_contents, + \ 'use_fzf': get(a:options, 'use_fzf', g:ale_references_use_fzf), \} endfunction @@ -185,6 +204,8 @@ function! ale#references#Find(...) abort let l:options.open_in = 'quickfix' elseif l:option is? '-contents' let l:options.show_contents = 1 + elseif l:option is? '-fzf' + let l:options.use_fzf = 1 endif endfor endif diff --git a/doc/ale.txt b/doc/ale.txt index da9bd7fe..a2cd542b 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -2280,6 +2280,16 @@ g:ale_references_show_contents If set to `true` or `1`, matches found by `:ALEFindReferences` will be shown with a preview of the matching line. + *ale-options.references_use_fzf* + *g:ale_references_use_fzf* +references_use_fzf +g:ale_references_use_fzf + Type: |Boolean| or |Number| + Default: `false` + + If set to `true` or `1`, matches found by `:ALEFindReferences` will be + always shown using |fzf-vim| (https://github.com/junegunn/fzf.vim). + *ale-options.rename_tsserver_find_in_comments* *g:ale_rename_tsserver_find_in_comments* rename_tsserver_find_in_comments @@ -4085,6 +4095,7 @@ documented in additional help files. `:ALEFindReferences -vsplit` - Open the location in a vertical split. `:ALEFindReferences -quickfix` - Put the locations into quickfix list. `:ALEFindReferences -contents` - Show line contents for matches. + `:ALEFindReferences -fzf` - Show matches/previews using |fzf-vim|. The default method used for navigating to a new location can be changed by modifying |g:ale_default_navigation|. @@ -4092,6 +4103,11 @@ documented in additional help files. The default behaviour on whether to show line content for matches can be changed by modifying |g:ale_references_show_contents|. + The default behaviour on whether to use `fzf` to show matches/file previews + can be changed by modifying |g:ale_references_use_fzf|. `-fzf` can be combined + with `-tab`, `-split`, `-vsplit`, `-quickfix` and `-relative`, while line + contents/file previews are always shown. + You can add `-relative` to the command to view results with relatives paths, instead of absolute paths. This option has no effect if `-quickfix` is used. diff --git a/test/test_find_references.vader b/test/test_find_references.vader index f1e67794..db191eab 100644 --- a/test/test_find_references.vader +++ b/test/test_find_references.vader @@ -121,6 +121,7 @@ Execute(Results should be shown for tsserver responses): \ 'ignorethis': 'x', \ 'open_in': 'tab', \ 'use_relative_paths': 1, + \ 'use_fzf': 0, \ } \ } \) @@ -283,7 +284,7 @@ Execute(tsserver reference requests should be sent): \ [0, 'ts@references', {'file': expand('%:p'), 'line': 2, 'offset': 5}] \ ], \ g:message_list - AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 0, 'use_fzf': 0, }}, ale#references#GetMap() Execute('-relative' argument should enable 'use_relative_paths' in HandleTSServerResponse): runtime ale_linters/typescript/tsserver.vim @@ -293,7 +294,7 @@ Execute('-relative' argument should enable 'use_relative_paths' in HandleTSServe call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 1}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 1, 'use_fzf': 0}}, ale#references#GetMap() Execute(`-tab` should display results in tabs): runtime ale_linters/typescript/tsserver.vim @@ -303,7 +304,7 @@ Execute(`-tab` should display results in tabs): call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'tab', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'tab', 'use_relative_paths': 0, 'use_fzf': 0}}, ale#references#GetMap() Execute(The default navigation type should be used): runtime ale_linters/typescript/tsserver.vim @@ -314,7 +315,7 @@ Execute(The default navigation type should be used): call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'tab', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'tab', 'use_relative_paths': 0, 'use_fzf': 0}}, ale#references#GetMap() Execute(`-split` should display results in splits): runtime ale_linters/typescript/tsserver.vim @@ -324,7 +325,7 @@ Execute(`-split` should display results in splits): call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'split', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'split', 'use_relative_paths': 0, 'use_fzf': 0}}, ale#references#GetMap() Execute(`-vsplit` should display results in vsplits): runtime ale_linters/typescript/tsserver.vim @@ -334,7 +335,7 @@ Execute(`-vsplit` should display results in vsplits): call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'vsplit', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'vsplit', 'use_relative_paths': 0, 'use_fzf': 0}}, ale#references#GetMap() Execute(`-quickfix` should display results in quickfix): runtime ale_linters/typescript/tsserver.vim @@ -344,7 +345,7 @@ Execute(`-quickfix` should display results in quickfix): call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'quickfix', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'quickfix', 'use_relative_paths': 0, 'use_fzf': 0}}, ale#references#GetMap() Given python(Some Python file): foo @@ -627,7 +628,7 @@ Execute(LSP reference requests should be sent): \ ], \ g:message_list - AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 0}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 0, 'use_fzf': 0}}, ale#references#GetMap() Execute('-relative' argument should enable 'use_relative_paths' in HandleLSPResponse): runtime ale_linters/python/pylsp.vim @@ -638,4 +639,4 @@ Execute('-relative' argument should enable 'use_relative_paths' in HandleLSPResp call g:InitCallback() - AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 1}}, ale#references#GetMap() + AssertEqual {'42': {'show_contents': 1, 'open_in': 'current-buffer', 'use_relative_paths': 1, 'use_fzf': 0}}, ale#references#GetMap()