mirror of
https://github.com/junegunn/fzf.git
synced 2025-12-06 12:54:26 +08:00
feat(zsh): Handle multi-line history selection (#4595)
This commit is contained in:
@@ -128,25 +128,52 @@ fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
local selected extracted_with_perl=0
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
extracted_with_perl=1
|
||||
else
|
||||
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
fi
|
||||
local ret=$?
|
||||
local -a cmds
|
||||
# Avoid leaking auto assigned values when using backreferences '(#b)'
|
||||
local -a mbegin mend match
|
||||
if [ -n "$selected" ]; then
|
||||
if [[ $(__fzf_exec_awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||
zle vi-fetch-history -n $MATCH
|
||||
# Heuristic to check if the selected value is from history or a custom query
|
||||
if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
|
||||
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
|
||||
# Split at newlines
|
||||
for line in ${(ps:\n:)selected}; do
|
||||
if (( extracted_with_perl )); then
|
||||
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
|
||||
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
|
||||
fi
|
||||
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
|
||||
# Avoid $history array: lags behind 'fc' on foreign commands (*)
|
||||
# https://zsh.org/mla/users/2024/msg00692.html
|
||||
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
|
||||
zle .push-line
|
||||
zle vi-fetch-history -n ${match[1]}
|
||||
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
|
||||
BUFFER=""
|
||||
zle .get-line
|
||||
fi
|
||||
done
|
||||
if (( ${#cmds[@]} )); then
|
||||
# Join by newline after stripping trailing newlines from each command
|
||||
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
|
||||
CURSOR=${#BUFFER}
|
||||
fi
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
|
||||
@@ -462,6 +462,84 @@ class TestZsh < TestBase
|
||||
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
|
||||
end
|
||||
|
||||
class TestFish < TestBase
|
||||
|
||||
Reference in New Issue
Block a user