mirror of
https://github.com/junegunn/fzf.git
synced 2026-02-20 08:38:37 +08:00
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
252 lines
5.4 KiB
Ruby
252 lines
5.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'bundler/setup'
|
|
require 'minitest/autorun'
|
|
require 'fileutils'
|
|
require 'English'
|
|
require 'shellwords'
|
|
require 'erb'
|
|
require 'tempfile'
|
|
require 'net/http'
|
|
require 'json'
|
|
|
|
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
|
FISH_TEMPLATE = File.read(File.expand_path('common.fish', __dir__))
|
|
UNSETS = %w[
|
|
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
|
FZF_TMUX FZF_TMUX_OPTS
|
|
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
|
FZF_ALT_C_COMMAND
|
|
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
|
FZF_API_KEY
|
|
].freeze
|
|
DEFAULT_TIMEOUT = 10
|
|
|
|
FILE = File.expand_path(__FILE__)
|
|
BASE = File.expand_path('../..', __dir__)
|
|
Dir.chdir(BASE)
|
|
FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
|
|
|
|
def wait(timeout = DEFAULT_TIMEOUT)
|
|
since = Time.now
|
|
begin
|
|
yield or raise Minitest::Assertion, 'Assertion failure'
|
|
rescue Minitest::Assertion
|
|
raise if Time.now - since > timeout
|
|
|
|
sleep(0.05)
|
|
retry
|
|
end
|
|
end
|
|
|
|
class Shell
|
|
class << self
|
|
def bash
|
|
@bash ||=
|
|
begin
|
|
bashrc = '/tmp/fzf.bash'
|
|
File.open(bashrc, 'w') do |f|
|
|
f.puts ERB.new(TEMPLATE).result(binding)
|
|
end
|
|
|
|
"bash --rcfile #{bashrc}"
|
|
end
|
|
end
|
|
|
|
def zsh
|
|
@zsh ||=
|
|
begin
|
|
zdotdir = '/tmp/fzf-zsh'
|
|
FileUtils.rm_rf(zdotdir)
|
|
FileUtils.mkdir_p(zdotdir)
|
|
File.open("#{zdotdir}/.zshrc", 'w') do |f|
|
|
f.puts ERB.new(TEMPLATE).result(binding)
|
|
end
|
|
"ZDOTDIR=#{zdotdir} zsh"
|
|
end
|
|
end
|
|
|
|
def fish
|
|
@fish ||=
|
|
begin
|
|
confdir = '/tmp/fzf-fish'
|
|
FileUtils.rm_rf(confdir)
|
|
FileUtils.mkdir_p("#{confdir}/fish/conf.d")
|
|
File.open("#{confdir}/fish/conf.d/fzf.fish", 'w') do |f|
|
|
f.puts ERB.new(FISH_TEMPLATE).result(binding)
|
|
end
|
|
"rm -f ~/.local/share/fish/fzf_test_history; XDG_CONFIG_HOME=#{confdir} fish"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class Tmux
|
|
attr_reader :win
|
|
|
|
def initialize(shell = :bash)
|
|
@win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
|
|
go(%W[set-window-option -t #{@win} pane-base-index 0])
|
|
return unless shell == :fish
|
|
|
|
send_keys 'function fish_prompt; end; clear', :Enter
|
|
self.until(&:empty?)
|
|
end
|
|
|
|
def kill
|
|
go(%W[kill-window -t #{win}])
|
|
end
|
|
|
|
def focus
|
|
go(%W[select-window -t #{win}])
|
|
end
|
|
|
|
def send_keys(*args)
|
|
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
|
end
|
|
|
|
def paste(str)
|
|
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
|
end
|
|
|
|
def capture
|
|
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
|
end
|
|
|
|
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
|
lines = nil
|
|
begin
|
|
wait(timeout) do
|
|
lines = capture
|
|
class << lines
|
|
def counts
|
|
lazy
|
|
.map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) }
|
|
.reject(&:empty?)
|
|
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
|
|
end
|
|
|
|
def match_count
|
|
counts[0]
|
|
end
|
|
|
|
def item_count
|
|
counts[1]
|
|
end
|
|
|
|
def select_count
|
|
counts[2]
|
|
end
|
|
|
|
def any_include?(val)
|
|
method = val.is_a?(Regexp) ? :match : :include?
|
|
find { |line| line.send(method, val) }
|
|
end
|
|
end
|
|
yield(lines).tap do |ok|
|
|
send_keys 'C-l' if refresh && !ok
|
|
end
|
|
end
|
|
rescue Minitest::Assertion
|
|
puts $ERROR_INFO.backtrace
|
|
puts '>' * 80
|
|
puts lines
|
|
puts '<' * 80
|
|
raise
|
|
end
|
|
lines
|
|
end
|
|
|
|
def prepare
|
|
tries = 0
|
|
begin
|
|
self.until(true) do |lines|
|
|
message = "Prepare[#{tries}]"
|
|
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
|
sleep(0.15)
|
|
lines[-1] == message
|
|
end
|
|
rescue Minitest::Assertion
|
|
(tries += 1) < 5 ? retry : raise
|
|
end
|
|
send_keys 'C-u', 'C-l'
|
|
end
|
|
|
|
private
|
|
|
|
def go(args)
|
|
IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) }
|
|
end
|
|
end
|
|
|
|
class TestBase < Minitest::Test
|
|
TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
|
|
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
|
|
|
|
def writelines(lines)
|
|
File.write(TEMPNAME, lines.join("\n"))
|
|
end
|
|
|
|
def tempname
|
|
TEMPNAME
|
|
end
|
|
|
|
def fzf_output
|
|
@thread.join.value.chomp.tap { @thread = nil }
|
|
end
|
|
|
|
def fzf_output_lines
|
|
fzf_output.lines(chomp: true)
|
|
end
|
|
|
|
def setup
|
|
File.mkfifo(FIFONAME)
|
|
end
|
|
|
|
def teardown
|
|
FileUtils.rm_f([TEMPNAME, FIFONAME])
|
|
end
|
|
|
|
alias assert_equal_org assert_equal
|
|
def assert_equal(expected, actual)
|
|
# Ignore info separator
|
|
actual = actual&.sub(/\s*─+$/, '') if actual.is_a?(String) && actual&.match?(%r{\d+/\d+})
|
|
assert_equal_org(expected, actual)
|
|
end
|
|
|
|
# Run fzf with its output piped to a fifo
|
|
def fzf(*opts)
|
|
raise 'fzf_output not taken' if @thread
|
|
|
|
@thread = Thread.new { File.read(FIFONAME) }
|
|
fzf!(*opts) + " > #{FIFONAME.shellescape}"
|
|
end
|
|
|
|
def fzf!(*opts)
|
|
opts = opts.filter_map do |o|
|
|
case o
|
|
when Symbol
|
|
o = o.to_s
|
|
o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}"
|
|
when String, Numeric
|
|
o.to_s
|
|
end
|
|
end
|
|
"#{FZF} #{opts.join(' ')}"
|
|
end
|
|
end
|
|
|
|
class TestInteractive < TestBase
|
|
attr_reader :tmux
|
|
|
|
def setup
|
|
super
|
|
@tmux = Tmux.new
|
|
end
|
|
|
|
def teardown
|
|
super
|
|
@tmux.kill
|
|
end
|
|
end
|