Files
fzf/test/test_shell_integration.rb
bitraid 385cccd362
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
fish: Command history improvements (#4672)
This change provides the following improvements:

- Changes the view of the command history list, so that the script no longer depends on perl for performance.
- Enables syntax color highlighting on fish v4.3.3 and newer.
- Provides a preview window with the selected commands, and expanded view of the highlighted command if available.
- Improves the delete functionality, by successfully handling very large numbers of selected commands.
- Inserts commands in their formatted form with `<Alt-Enter>`.

---

* fish: Change history list view

The view of the command history is changed, so that no manipulation is
performed on the output of history command: The history item count
number is replaced by the Unix time of the command, multi-line display
is disabled and line wrapping is enabled by default. On fish v4.3.3
and newer, the use of ANSI color codes for syntax highlighting is now
possible, while the script no longer depends on perl for performance.

Fixes #4661

* fish: Reformat selected history commands with ALT-ENTER

* Add $FZF_WRAP environment variable

The variable is set when line wrapping is enabled. It has the value
`word` if word wrapping mode is set, otherwise it has the value `char`.

* fish: Add command history preview

The preview shows the highlighted command and any selected commands,
after first being formatted and colorized by fish_indent. The timestamp
of the highlighted command in the preview is converted to strftime
format "%F %a %T" if the date command is available.

The preview is hidden on start, and is displayed if more than 100
columns are available and one of the following conditions are met:
- The highlighted item doesn't completely fit in list view (line
wrapping is enabled for the preview and is now disabled for the list).
- The highlighted item contains newlines (multi-line commands or
strings).
- The highlighted item contains chained commands in a single line, that
can be broken down by the formatter for cleaner view.
- One or more commands are marked as selected.

* fish: Handle deletion of large number of selected history entries

* fish: Change wrapping options for the preview-window
2026-02-19 23:41:26 +09:00

1056 lines
32 KiB
Ruby

# frozen_string_literal: true
require_relative 'lib/common'
# Testing shell integration
module TestShell
attr_reader :tmux
def setup
@tmux = Tmux.new(shell)
tmux.prepare
end
def teardown
@tmux.kill
end
def set_var(name, val)
tmux.prepare
tmux.send_keys "export #{name}='#{val}'", :Enter
tmux.prepare
end
def unset_var(name)
tmux.prepare
tmux.send_keys "unset #{name}", :Enter
tmux.prepare
end
def trigger
'**'
end
def test_ctrl_t
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
tmux.prepare
tmux.send_keys 'C-t'
tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| assert lines.any_include?(' (3)') }
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('1 2 3') }
tmux.send_keys 'C-c'
end
def test_ctrl_t_unicode
writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
tmux.prepare
tmux.send_keys 'echo ', 'C-t'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys 'fzf-unicode'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '2'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
end
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| assert lines[-1]&.end_with?(expected) }
end
def test_alt_c_command
set_var('FZF_ALT_C_COMMAND', 'echo /tmp')
tmux.prepare
tmux.send_keys 'cd /', :Enter
tmux.prepare
tmux.send_keys :Escape, :c
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| assert_equal '/tmp', lines[-1] }
end
def test_ctrl_r
tmux.prepare
tmux.send_keys 'echo 1st', :Enter
tmux.prepare
tmux.send_keys 'echo 2nd', :Enter
tmux.prepare
tmux.send_keys 'echo 3d', :Enter
tmux.prepare
3.times do
tmux.send_keys 'echo 3rd', :Enter
tmux.prepare
end
tmux.send_keys 'echo 4th', :Enter
tmux.prepare
tmux.send_keys 'C-r'
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'e3d'
# Duplicates removed: 3d (1) + 3rd (1) => 2 matches
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') }
tmux.send_keys 'C-r'
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal '3rd', lines[-1] }
end
def test_ctrl_r_multiline
# NOTE: Current bash implementation shows an extra new line if there's
# only entry in the history
tmux.send_keys ':', :Enter
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
tmux.prepare
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar'
tmux.until { |lines| assert_includes lines[-4], '"foo' } if shell == :bash
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
end
def test_ctrl_r_abort
skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4
%w[foo ' "].each do |query|
tmux.prepare
tmux.send_keys :Enter, query
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
tmux.send_keys 'C-g'
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
end
end
end
module CompletionTest
def test_file_completion
FileUtils.mkdir_p('/tmp/fzf-test')
FileUtils.mkdir_p('/tmp/fzf test')
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
['no~such~user', '/tmp/fzf test/foobar'].each do |f|
FileUtils.touch(File.expand_path(f))
end
tmux.prepare
if shell == :fish
tmux.send_keys 'cat /tmp/fzf-test/10', 'C-t'
else
tmux.send_keys "cat /tmp/fzf-test/10#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys ' !d'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Enter
tmux.until(true) do |lines|
assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1]
end
# ~USERNAME**<TAB>
user = `whoami`.chomp
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys "cat ~#{user}", 'C-t'
else
tmux.send_keys "cat ~#{user}#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys "/#{user}"
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
tmux.send_keys :Enter
tmux.until(true) do |lines|
assert_match %r{cat .*/#{user}}, lines[-1]
end
# ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys 'cat ~such', 'C-t'
else
tmux.send_keys "cat ~such#{trigger}", :Tab
end
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
tmux.send_keys :Enter
tmux.until(true) do |lines|
if shell == :fish
# Fish's string escape quotes filenames with ~ to prevent tilde expansion
assert_equal 'cat no\\~such\\~user', lines[-1]
else
assert_equal 'cat no~such~user', lines[-1]
end
end
# /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys 'cat /tmp/fzf\\ test/', 'C-t'
else
tmux.send_keys "cat /tmp/fzf\\ test/#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'foobar$'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> /tmp/fzf test/foobar')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
# Should include hidden files
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
tmux.send_keys 'C-u'
if shell == :fish
tmux.send_keys 'cat /tmp/fzf-test/hidden', 'C-t'
else
tmux.send_keys "cat /tmp/fzf-test/hidden#{trigger}", :Tab
end
tmux.until(true) do |lines|
assert_equal 100, lines.match_count
assert lines.any_include?('/tmp/fzf-test/.hidden-')
end
tmux.send_keys :Enter
ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
FileUtils.rm_rf(File.expand_path(f))
end
end
def test_file_completion_root
if shell == :fish
tmux.send_keys 'ls /', 'C-t'
else
tmux.send_keys "ls /#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Enter
end
def test_dir_completion
FileUtils.mkdir_p('/tmp/fzf-test-dir')
(1..100).each do |idx|
FileUtils.mkdir_p("/tmp/fzf-test-dir/d#{idx}")
end
FileUtils.touch('/tmp/fzf-test-dir/d55/xxx')
tmux.prepare
if shell == :fish
tmux.send_keys 'cd /tmp/fzf-test-dir/', 'C-t'
else
tmux.send_keys "cd /tmp/fzf-test-dir/#{trigger}", :Tab
end
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
# Tab selects items in C-t's --multi mode, so skip for fish
tmux.send_keys :Tab, :Tab unless shell == :fish # Tab does not work here
tmux.send_keys '55/$'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 55/$'
assert_includes lines, '> /tmp/fzf-test-dir/d55/'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
# C-t appends a trailing space after the result
tmux.send_keys :BSpace if shell == :fish
tmux.send_keys :xx
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
# Should not match regular files (bash-only)
if instance_of?(TestBash)
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/xx', lines[-1] }
end
# Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55', lines[-1] }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test-dir/d55/', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-dir')
end
def test_process_completion
skip('fish background job format differs') if shell == :fish
begin
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
pid = lines[-1]&.split&.last
tmux.prepare
tmux.send_keys 'C-L'
tmux.send_keys "kill #{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'sleep12345'
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
ensure
if pid
begin
Process.kill('KILL', pid.to_i)
rescue StandardError
nil
end
end
end
end
def test_custom_completion
skip('fish does not use _fzf_compgen_path; path completion is via ctrl-t') if shell == :fish
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
tmux.prepare
tmux.send_keys "ls /tmp/#{trigger}", :Tab
tmux.until { |lines| assert_equal 11, lines.match_count }
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { |lines| assert_equal 3, lines.select_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] }
end
def test_unset_completion
skip('fish has native completion for set and unset variables') if shell == :fish
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
# Using tmux
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
tmux.send_keys 'C-c'
# FZF_TMUX=1
new_shell
tmux.focus
tmux.send_keys "unset FZFFOOBR#{trigger}", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
end
def test_completion_in_command_sequence
if shell == :fish
FileUtils.mkdir_p('/tmp/fzf-test-seq')
FileUtils.touch('/tmp/fzf-test-seq/fzffoobar')
tmux.prepare
# Fish uses Shift-Tab for fzf completion (no trigger system)
command = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobr'
expected = 'echo foo; QUX=THUD ls /tmp/fzf-test-seq/fzffoobar'
tmux.send_keys command, :BTab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal expected, lines[-1] }
else
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
tmux.prepare
triggers = ['**', '~~', '++', 'ff', '/']
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
triggers.each do |trigger|
set_var('FZF_COMPLETION_TRIGGER', trigger)
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
expected = 'echo foo; QUX=THUD unset FZFFOOBAR'
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal expected, lines[-1] }
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-seq') if shell == :fish
end
def test_file_completion_unicode
FileUtils.mkdir_p('/tmp/fzf-test')
# Shell-agnostic file creation
File.write('/tmp/fzf-test/fzf-unicode 테스트1', "test3\n")
File.write('/tmp/fzf-test/fzf-unicode 테스트2', "test4\n")
tmux.send_keys 'cd /tmp/fzf-test', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'cat fzf-unicode', 'C-t'
else
tmux.send_keys "cat fzf-unicode#{trigger}", :Tab
end
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '1'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :BSpace
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys '2'
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
end
def test_custom_completion_api
skip('bash-specific _comprun/declare syntax') if shell == :fish
begin
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
%w[f g].each do |command|
tmux.prepare
tmux.send_keys "#{command} b#{trigger}", :Tab
tmux.until do |lines|
assert_equal 2, lines.item_count
assert_equal 1, lines.match_count
assert lines.any_include?("prompt-#{command}")
assert lines.any_include?("preview-#{command}-bar")
end
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
tmux.send_keys 'C-u'
end
ensure
tmux.prepare
tmux.send_keys 'unset -f _fzf_comprun', :Enter
end
end
def test_ssh_completion
skip('fish uses native ssh completion') if shell == :fish
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
tmux.send_keys "ssh jg@localhost#{trigger}", :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
tmux.send_keys " -i /tmp/fzf-test-ssh#{trigger}", :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 5
assert_equal 0, lines.select_count
end
tmux.send_keys :Tab, :Tab, :Tab
tmux.until do |lines|
assert_equal 3, lines.select_count
end
tmux.send_keys :Enter
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
tmux.send_keys "localhost#{trigger}", :Tab
tmux.until do |lines|
assert_operator lines.match_count, :>=, 1
end
end
def test_option_equals_long_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long')
FileUtils.touch('/tmp/fzf-test-opt-eq-long/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command --opt=SECURI', 'C-t'
else
tmux.send_keys "some-command --opt=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURI'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command --opt=SECURITY.md', lines[-1] }
when :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --opt=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long')
end
def test_option_equals_long_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-long-ddash')
FileUtils.touch('/tmp/fzf-test-opt-eq-long-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-long-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- --opt=SECURI', 'C-t'
else
tmux.send_keys "some-command -- --opt=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURI'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -- --opt=SECURITY.md', lines[-1] }
when :fish, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --opt=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-long-ddash')
end
def test_option_equals_short_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short')
FileUtils.touch('/tmp/fzf-test-opt-eq-short/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -o=SECURI', 'C-t'
else
tmux.send_keys "some-command -o=SECURI#{trigger}", :Tab
end
case shell
when :bash, :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -o=SECURITY.md', lines[-1] }
when :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -o=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short')
end
def test_option_equals_short_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-eq-short-ddash')
FileUtils.touch('/tmp/fzf-test-opt-eq-short-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-eq-short-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- -o=SECURI', 'C-t'
else
tmux.send_keys "some-command -- -o=SECURI#{trigger}", :Tab
end
case shell
when :bash
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> SECURITY.md'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -- -o=SECURITY.md', lines[-1] }
when :fish, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -o=SECURI'
end
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-eq-short-ddash')
end
def test_option_no_equals_long_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command --optSECURI', 'C-t'
else
tmux.send_keys "some-command --optSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --optSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long')
end
def test_option_no_equals_long_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-long-ddash')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-long-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-long-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- --optSECURI', 'C-t'
else
tmux.send_keys "some-command -- --optSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> --optSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-long-ddash')
end
def test_option_no_equals_short_option
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -oSECURI', 'C-t'
else
tmux.send_keys "some-command -oSECURI#{trigger}", :Tab
end
case shell
when :bash, :zsh
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -oSECURI'
end
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('> SECURITY.md')
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'some-command -oSECURITY.md', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short')
end
def test_option_no_equals_short_option_after_double_dash
FileUtils.mkdir_p('/tmp/fzf-test-opt-no-eq-short-ddash')
FileUtils.touch('/tmp/fzf-test-opt-no-eq-short-ddash/SECURITY.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-opt-no-eq-short-ddash', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'some-command -- -oSECURI', 'C-t'
else
tmux.send_keys "some-command -- -oSECURI#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 0, lines.match_count
assert_includes lines, '> -oSECURI'
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-opt-no-eq-short-ddash')
end
def test_filename_with_newline
FileUtils.mkdir_p('/tmp/fzf-test-newline')
FileUtils.touch("/tmp/fzf-test-newline/xyz\nwith\nnewlines")
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-newline', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'cat xyz', 'C-t'
else
tmux.send_keys "cat xyz#{trigger}", :Tab
end
case shell
when :fish
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> xyz'
end
tmux.send_keys :Enter
# fish escapes newlines in filenames
tmux.until(true) { |lines| assert_equal 'cat xyz\\nwith\\nnewlines', lines[-1] }
when :bash, :zsh
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> xyz'
end
tmux.send_keys :Enter
# bash and zsh replace newlines with spaces in filenames
tmux.until(true) { |lines| assert_equal 'cat xyz with newlines', lines[-1] }
end
ensure
FileUtils.rm_rf('/tmp/fzf-test-newline')
end
def test_path_with_special_chars
FileUtils.mkdir_p('/tmp/fzf-test-[special]')
FileUtils.touch('/tmp/fzf-test-[special]/xyz123')
tmux.prepare
if shell == :fish
tmux.send_keys 'ls /tmp/fzf-test-\[special\]/xyz', 'C-t'
else
tmux.send_keys "ls /tmp/fzf-test-\\[special\\]/xyz#{trigger}", :Tab
end
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls /tmp/fzf-test-\\[special\\]/xyz123', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-[special]')
end
def test_query_with_dollar_anchor
FileUtils.mkdir_p('/tmp/fzf-test-dollar-anchor')
FileUtils.touch('/tmp/fzf-test-dollar-anchor/file.txt')
FileUtils.touch('/tmp/fzf-test-dollar-anchor/filetxt.md')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-dollar-anchor', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls txt$', 'C-t'
else
tmux.send_keys "ls txt$#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> txt$'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls file.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-dollar-anchor')
end
def test_single_flag_completion
FileUtils.mkdir_p('/tmp/fzf-test-single-flag')
FileUtils.touch('/tmp/fzf-test-single-flag/-testfile.txt')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-single-flag', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls -', 'C-t'
else
tmux.send_keys "ls -#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> -'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls -testfile.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-single-flag')
end
def test_double_flag_completion
FileUtils.mkdir_p('/tmp/fzf-test-double-flag')
FileUtils.touch('/tmp/fzf-test-double-flag/--testfile.txt')
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test-double-flag', :Enter
tmux.prepare
if shell == :fish
tmux.send_keys 'ls --', 'C-t'
else
tmux.send_keys "ls --#{trigger}", :Tab
end
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> --'
end
tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'ls --testfile.txt', lines[-1] }
ensure
FileUtils.rm_rf('/tmp/fzf-test-double-flag')
end
end
class TestBash < TestBase
include TestShell
include CompletionTest
def shell
:bash
end
def new_shell
tmux.prepare
tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
tmux.prepare
end
def test_dynamic_completion_loader
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
tmux.paste '_completion_loader() { complete -o default fake; }'
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys 'C-c'
tmux.prepare
tmux.send_keys 'fake /tmp/foo'
tmux.send_keys :Tab, 'C-u'
tmux.prepare
tmux.send_keys "fake /tmp/foo#{trigger}", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
end
end
class TestZsh < TestBase
include TestShell
include CompletionTest
def shell
:zsh
end
def new_shell
tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
tmux.prepare
end
def test_complete_quoted_command
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
['unset', '\unset', "'unset'"].each do |command|
tmux.prepare
tmux.send_keys "#{command} FZFFOOBR#{trigger}", :Tab
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
tmux.send_keys 'C-c'
end
end
# Helper function to run test with Perl and again with Awk
def self.test_perl_and_awk(name, &block)
define_method(:"test_#{name}") do
instance_eval(&block)
end
define_method(:"test_#{name}_awk") do
tmux.send_keys "unset 'commands[perl]'", :Enter
tmux.prepare
# Verify perl is actually unset (0 = not found)
tmux.send_keys 'echo ${+commands[perl]}', :Enter
tmux.until { |lines| assert_equal '0', lines[-1] }
tmux.prepare
instance_eval(&block)
end
end
def prepare_ctrl_r_test
tmux.send_keys ':', :Enter
tmux.send_keys 'echo match-collision', :Enter
tmux.prepare
tmux.send_keys 'echo "line 1', :Enter, '2 line 2"', :Enter
tmux.prepare
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'echo "trailing_space "', :Enter
tmux.prepare
tmux.send_keys 'cat <<EOF | wc -c', :Enter, 'qux thud', :Enter, 'EOF', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
end
test_perl_and_awk 'ctrl_r_accept_or_print_query' do
set_var('FZF_CTRL_R_OPTS', '--bind enter:accept-or-print-query')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '1 foobar'
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal '1 foobar', lines[-1] }
end
test_perl_and_awk 'ctrl_r_multiline_index_collision' do
# Leading number in multi-line history content is not confused with index
prepare_ctrl_r_test
tmux.send_keys "'line 1"
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['echo "line 1', '2 line 2"'], lines[-2..]
end
end
test_perl_and_awk 'ctrl_r_multi_selection' do
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF', 'echo "trailing_space "', 'echo "bar', 'foo"'], lines[-6..]
end
end
test_perl_and_awk 'ctrl_r_no_multi_selection' do
set_var('FZF_CTRL_R_OPTS', '--no-multi')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| refute_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF'], lines[-3..]
end
end
# NOTE: 'Perl/$history' won't see foreign cmds immediately, unlike 'awk/fc'.
# Perl passes only because another cmd runs between mocking and triggering C-r
# https://github.com/junegunn/fzf/issues/4061
# https://zsh.org/mla/users/2024/msg00692.html
test_perl_and_awk 'ctrl_r_foreign_commands' do
histfile = "#{tempname}-foreign-hist"
tmux.send_keys "HISTFILE=#{histfile}", :Enter
tmux.prepare
# SHARE_HISTORY picks up foreign commands; marked with * in fc
tmux.send_keys 'setopt SHARE_HISTORY', :Enter
tmux.prepare
tmux.send_keys 'fzf_cmd_local', :Enter
tmux.prepare
# Mock foreign command (for testing only; don't edit your HISTFILE this way)
tmux.send_keys "echo ': 0:0;fzf_cmd_foreign' >> $HISTFILE", :Enter
tmux.prepare
# Verify fc shows foreign command with asterisk
tmux.send_keys 'fc -rl -1', :Enter
tmux.until { |lines| assert(lines.any? { |l| l.match?(/^\s*\d+\* fzf_cmd_foreign/) }) }
tmux.prepare
# Test ctrl-r correctly extracts the foreign command
tmux.send_keys 'C-r'
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '^fzf_cmd_'
tmux.until { |lines| assert_equal 2, lines.match_count }
tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(2)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal %w[fzf_cmd_foreign fzf_cmd_local], lines[-2..]
end
ensure
FileUtils.rm_f(histfile)
end
end
class TestFish < TestBase
include TestShell
include CompletionTest
def shell
:fish
end
def trigger
'++'
end
def new_shell
tmux.send_keys 'env FZF_TMUX=1 XDG_CONFIG_HOME=/tmp/fzf-fish fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| assert_empty lines }
end
def set_var(name, val)
tmux.prepare
tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare
end
def test_ctrl_r_multi
tmux.send_keys ':', :Enter
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
offset = -6
block = <<~BLOCK
echo "foo
bar"
echo "bar
foo"
BLOCK
if shell == :fish
offset = -4
block = <<~FISH
echo "foo␊bar"
echo "bar␊foo"
FISH
end
tmux.until do |lines|
block.lines.each_with_index do |line, idx|
assert_includes lines[idx + offset], line.chomp
end
end
tmux.send_keys :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(2)' }
tmux.send_keys :Enter
block = <<~BLOCK
echo "bar
foo"
echo "foo
bar"
BLOCK
tmux.until do |lines|
assert_equal block.lines.map(&:chomp), lines
end
end
end