Skip to content

Add ctrl-o to show the entry without closing fzf - #1621

Open
junegunn wants to merge 1 commit into
masterfrom
show-key
Open

Add ctrl-o to show the entry without closing fzf#1621
junegunn wants to merge 1 commit into
masterfrom
show-key

Conversation

@junegunn

Copy link
Copy Markdown
Owner

Files, GFiles, GFiles?, Rg/Ag, Buffers, History, Locate, Lines, BLines, Tags, BTags, Marks and Changes. Writes the entry to an fzf#vim#ipc fifo, so it needs the asynchronous fzf#run from junegunn/fzf@5cb7bab7 (junegunn/fzf#4897, 0.74.4), which the g:loaded_fzf check detects.

Commands declare '_show' for the entry kind and '_hint' for footer keys, so the footer and the binding cannot disagree. s:can_show withholds both when the run would block or the window to open in is not visible.

g:fzf_vim.show_key configures the key, empty to turn it off, comma separated to bind several ('ctrl-o,double-click'). Named for its 'Show' footer label, since Enter already occupies 'Open' there. Only the first key is labelled, and a clash on any of them drops all of them, so the footer cannot list a key that does nothing. Binding Enter drops its 'Open' hint too, since fzf accepts on Enter rather than through g:fzf_action.

s:show_entry parses each entry the way that command's sink does and runs win_execute() in the window fzf started from. That keeps the focus in fzf and works from a popup, which win_gotoid() cannot leave. It opens with 'keepalt keepjumps hide', so stepping through entries leaves the alternate file and the jumplist unchanged.

fzf#run is asynchronous, so runs can overlap, and a second run would stop the first's ipc channel and overwrite the script locals its sink reads back afterwards. Channels are now keyed by fifo path and per-run state is bound into the callback instead, which is why Buffers, Colors, Jumps, Maps, Helptags and complete change here.

@junegunn
junegunn requested a balanced review from Copilot August 26, 2026 10:26
@junegunn junegunn self-assigned this Aug 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Key collisions can make the footer inaccurate, and the documented required fzf version is not released.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds configurable entry inspection without closing fzf, supported by asynchronous IPC and per-run state.

Changes:

  • Adds the show_key binding and command-specific entry handling.
  • Supports concurrent IPC channels and callback state.
  • Documents the feature and configuration.
File summaries
File Description
README.md Documents entry showing and configuration.
doc/fzf-vim.txt Adds Vim help for the feature.
autoload/fzf/vim/ipc.vim Supports concurrent FIFO channels.
autoload/fzf/vim.vim Implements showing, hints, and per-run state.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread autoload/fzf/vim.vim Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Buffer-local Show actions can target the wrong buffer after the originating window changes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

README.md:111

  • This supported-command list omits :RG and :BMarks, although fzf#vim#grep2() and the shared marks implementation both opt into Show. Include them so users of these documented commands do not incorrectly conclude that CTRL-O is unavailable.
  `:Rg`, `:Ag`, `:Grep`, `:Lines`, `:BLines`, `:Tags`, `:BTags`, `:Marks`,
  `:Changes`). Requires fzf 0.74.4 or later, and is not offered in fullscreen

doc/fzf-vim.txt:164

  • This supported-command list omits :RG and :BMarks, although fzf#vim#grep2() and the shared marks implementation both opt into Show. Include them so the help accurately describes availability for those commands.
   entries in a single run (Files, GFiles, Buffers, History, Locate, Rg, Ag,
   Grep, Lines, BLines, Tags, BTags, Marks, Changes). Requires fzf 0.74.4 or
   later, and is not offered in fullscreen mode, with a layout that leaves no
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread autoload/fzf/vim.vim Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Key collisions, relative tilde paths, concurrent color previews, and the unpublished dependency version remain unresolved.

Review details

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

autoload/fzf/vim.vim:642

  • Expanding every leading ~ as a home path breaks valid relative entries from :Files, :GFiles, and grep. For example, a tracked file named ~/notes is emitted relative to the run directory but this resolves it to $HOME/notes, so Show opens the wrong file (if it exists) or does nothing. Preserve directory-relative paths for those commands and apply tilde expansion specifically to :History entries.
  " filereadable does not expand a leading tilde, which :History reports
  if a:path =~# '^\~'
    return fnamemodify(a:path, ':p')

autoload/fzf/vim.vim:1049

  • Per-run callback arguments prevent the old script-local overwrite, but a canceled older :Colors run still restores its original scheme whenever the global scheme differs—even when that difference is the preview from a newer active run. Overlapping runs can therefore still clobber each other's preview. Track each run's last applied scheme and restore only if that run still owns the current preview, or otherwise coordinate/serialize Colors runs.
function! s:colors_exit(fifo, original, code)
  if !empty(a:original) && a:code > 0 && a:original !=# get(g:, 'colors_name', '')
    execute 'colo' a:original
  endif

autoload/fzf/vim.vim:830

  • s:split_keys() normalizes configured show keys to fzf's matching rules, but has_key(actions, key) compares g:fzf_action keys verbatim. A configuration such as {'CTRL-O': 'split'} therefore bypasses this collision check even though fzf treats it as the same key; --expect wins and closes fzf while the footer still advertises Show. Normalize every action key before comparing it.
    if has_key(actions, key) || index(paste, key) >= 0 || index(a:claimed, key) >= 0
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Key collisions, newline-containing paths, directory entries, and nested color restoration remain incorrect.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

autoload/fzf/vim.vim:769

  • This rejects directories, but :Locate can return directory entries and its normal sink can open them. Consequently the documented Show key silently does nothing on those results. Accept isdirectory(path) as another valid editable target.
    if !filereadable(path)
      return

autoload/fzf/vim.vim:456

  • enter and return remain distinct after this normalization even though fzf parses both as the same key. For example, show_key = 'return' with paste_key = 'enter' passes s:can_show(), so the footer advertises Show while the existing --expect action wins. Canonicalize this alias here before all collision checks.
  return map(keys, 'strchars(v:val) == 1 ? v:val
        \ : v:val =~? "^alt-.$" ? "alt-".v:val[4:] : tolower(v:val)')
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread autoload/fzf/vim.vim Outdated
Comment thread autoload/fzf/vim.vim Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Entry encoding corrupts valid control-byte paths, and buffer-number parsing fails for names containing tabs.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 6
  • Review effort level: Balanced

Comment thread autoload/fzf/vim.vim
Comment thread autoload/fzf/vim.vim
Comment thread README.md Outdated
Comment thread doc/fzf-vim.txt Outdated
Comment thread README.md Outdated
Comment thread doc/fzf-vim.txt Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Key aliases and some valid tag and directory entries currently produce incorrect Show behavior.

Review details

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

autoload/fzf/vim.vim:456

  • The collision check compares key spellings rather than fzf key events, so aliases can still make the advertised binding ineffective. For example, the default paste key is alt-enter; setting show_key to its fzf alias alt-return passes this normalization, but fzf maps both to the same event and --expect wins before the Show binding, closing fzf instead. Canonicalize fzf aliases (including enter/return/ctrl-m and alt-enter/alt-return/ctrl-alt-m) before comparing keys and deciding which footer hints to display.
  return map(keys, 'strchars(v:val) == 1 ? v:val
        \ : v:val =~? "^alt-.$" ? "alt-".v:val[4:] : tolower(v:val)')

autoload/fzf/vim.vim:723

  • This unconditionally re-edits the tag file even when the target window already displays it, unlike the normal tag sink, whose s:action_for() skips that edit. Re-editing can reload/reset the buffer before applying the tag address; use s:edit_cmds() here so same-file tags only execute their address while other files retain the existing guarded edit behavior.
    let cmds = ['keepalt keepjumps hide edit '.s:escape(path),
          \ 'keepjumps '.excmd, 'normal! ^zvzz']

autoload/fzf/vim.vim:772

  • :Locate can return directories, and its normal sink opens them with :edit, but this regular-file-only guard makes Show silently do nothing for every directory result. Accept directories here as well so Show preserves the command's existing entry behavior.
    if !filereadable(path)
      return
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@junegunn

Copy link
Copy Markdown
Owner Author

All three from the last pass were real, and all three were small. Taken.

  • 05b2324 Key aliases. fzf reads return as enter and alt-return as
    alt-enter, so show_key=alt-return passed the check against the default paste key
    alt-enter and --expect closed fzf while the footer advertised Show. Keys are now
    canonicalized before comparison, which also let the footer check drop its separate
    return arm. One correction: ctrl-m is not an alias. It has no case in the switch
    and goes through the generic ctrl- branch to its own event, so I left it out.
  • a92f1cc Tag in the file already shown. The tag branch edited unconditionally,
    unlike every other kind, so a tag in the window's own buffer reloaded it and reset
    the undo history. It uses s:edit_cmds() now, so a same-file tag only runs its
    address. Cross-file tags still take the guarded edit.
  • 5a9f08f Directory entries. :Locate returns them and its sink opens them, but
    the readable-file guard made Show do nothing. It accepts a directory now.

Each was reproduced before the change and re-checked after: alt-return is withheld
like alt-enter, a same-file tag lands on its address with the buffer untouched, a
cross-file tag still opens beta.py:4, and a directory entry opens instead of doing
nothing.

@junegunn
junegunn requested a balanced review from Copilot August 26, 2026 15:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@junegunn
junegunn requested a balanced review from Copilot August 31, 2026 09:50

This comment was marked as duplicate.

@junegunn
junegunn force-pushed the show-key branch 3 times, most recently from 5c41dcb to 9ef02da Compare August 31, 2026 10:24
@junegunn
junegunn force-pushed the show-key branch 3 times, most recently from dedd5ac to e9015ff Compare August 31, 2026 10:43
Files, GFiles, GFiles?, Rg/Ag, Buffers, History, Locate, Lines, BLines,
Tags, BTags, Marks, Changes, Commits and BCommits. Writes the entry to an
fzf#vim#ipc fifo, so it needs the asynchronous fzf#run from
junegunn/fzf@5cb7bab7 (junegunn/fzf#4897, 0.74.4), which the g:loaded_fzf
check detects.

Commands declare '_show' for the entry kind and '_hint' for footer keys, so
the footer and the binding cannot disagree. s:can_show withholds both when
the run would block, when the window to open in is not visible, or when
g:fzf_action, the paste key or the command itself already claims the key.

g:fzf_vim.show_key configures the key, empty to turn it off, comma-separated
to bind several ('ctrl-o,double-click'). Named for its 'Show' footer label,
since Enter already occupies 'Open' there. Only the first key is labelled,
and a clash on any of them drops all of them, so the footer cannot list a
key that does nothing. Binding Enter drops its 'Open' hint too, since fzf
accepts on Enter rather than through g:fzf_action.

s:show_entry parses each entry the way that command's sink does and runs
win_execute() in the window fzf started from. That keeps the focus in fzf
and works from a popup, which win_gotoid() cannot leave. It opens with
'keepalt keepjumps hide', so stepping through entries leaves the alternate
file and the jumplist unchanged. BLines, BTags and a lowercase mark carry a
position but no buffer, so the callback carries the buffer the run started
on as well as the window.

fzf#run is asynchronous, so runs can overlap, and a second run would stop
the first's ipc channel and overwrite the script locals its sink reads back
afterwards. Channels are now keyed by fifo path and per-run state is bound
into the callback instead, which is why Buffers, Colors, Jumps, Maps,
Helptags and complete change here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants