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
This commit is contained in:
bretello
2025-12-21 05:06:34 +01:00
committed by GitHub
parent fa3d4f2f13
commit dd6e6f1b15
4 changed files with 154 additions and 31 deletions

85
autoload/ale/fzf.vim Normal file
View File

@@ -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

View File

@@ -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,8 +111,10 @@ 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)
if ! (has_key(a:response, 'id') && has_key(s:references_map, a:response.id))
return
endif
let l:options = remove(s:references_map, a:response.id)
" The result can be a Dictionary item, a List of the same, or null.
@@ -119,7 +132,13 @@ function! ale#references#HandleLSPResponse(conn_id, response) abort
if empty(l:item_list)
call ale#util#Execute('echom ''No references found.''')
else
if get(l:options, 'open_in') is# 'quickfix'
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')
@@ -127,7 +146,6 @@ function! ale#references#HandleLSPResponse(conn_id, response) abort
call ale#preview#ShowSelection(l:item_list, l:options)
endif
endif
endif
endfunction
function! s:OnReady(line, column, options, linter, lsp_details) abort
@@ -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

View File

@@ -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.

View File

@@ -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()