fish: Command history improvements (#4672)
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

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
This commit is contained in:
bitraid
2026-02-19 16:41:26 +02:00
committed by GitHub
parent 4a684b6c78
commit 385cccd362
6 changed files with 69 additions and 20 deletions

View File

@@ -35,6 +35,15 @@ CHANGELOG
- Added `--preview-wrap-sign` to set a different wrap indicator for the preview - Added `--preview-wrap-sign` to set a different wrap indicator for the preview
window window
- Added `alt-gutter` color option (#4602) (@hedgieinsocks) - Added `alt-gutter` color option (#4602) (@hedgieinsocks)
- Added `$FZF_WRAP` environment variable to child processes (`char` or `word`
when wrapping is enabled) (#4672) (@bitraid)
- fish: Improved command history (CTRL-R) (#4672) (@bitraid)
- Enabled syntax highlighting in the list on fish 4.3.3+
- Added syntax-highlighted preview window that auto-shows for long or
multi-line commands
- Added `ALT-ENTER` to reformat and insert selected commands
- Improved handling of bulk deletion of selected history entries
(`SHIFT-DELETE`)
- Added fish completion support (#4605) (@lalvarezt) - Added fish completion support (#4605) (@lalvarezt)
- zsh: Handle multi-line history selection (#4595) (@LangLangBart) - zsh: Handle multi-line history selection (#4595) (@LangLangBart)
- Bug fixes - Bug fixes

View File

@@ -1390,6 +1390,8 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1" .BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br .br
.BR FZF_WRAP " The line wrapping mode (char, word) when enabled"
.br
.BR FZF_QUERY " Current query string" .BR FZF_QUERY " Current query string"
.br .br
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)" .BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"

View File

@@ -21,8 +21,8 @@
function fzf_key_bindings function fzf_key_bindings
# Check fish version # Check fish version
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0) if set -l -- fish_ver (string match -r '^(\d+)\.(\d+)' $version 2>/dev/null)
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \) and test "$fish_ver[2]" -lt 3 -o "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1
echo "This script requires fish version 3.1b1 or newer." >&2 echo "This script requires fish version 3.1b1 or newer." >&2
return 1 return 1
else if not type -q fzf else if not type -q fzf
@@ -182,24 +182,46 @@ function fzf_key_bindings
set -l -- total_lines (count $command_line) set -l -- total_lines (count $command_line)
set -l -- fzf_query (string escape -- $command_line[$current_line]) set -l -- fzf_query (string escape -- $command_line[$current_line])
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \ set -lx -- FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \ '--nth=2..,.. --scheme=history --multi --no-multi-line --no-wrap --wrap-sign="\t\t\t↳ " --preview-wrap-sign="↳ "' \
'--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \ '--bind=\'shift-delete:execute-silent(for i in (string split0 -- <{+f}); eval builtin history delete --exact --case-sensitive -- (string escape -n -- $i | string replace -r "^\d*\\\\\\t" ""); end)+reload(eval $FZF_DEFAULT_COMMAND)\'' \
'--bind="alt-enter:become(string join0 -- (string collect -- {+2..} | fish_indent -i))"' \
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \ "--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c) '--accept-nth=2.. --delimiter="\t" --tabstop=4 --read0 --print0 --with-shell='(status fish-path)\\ -c)
# Add dynamic preview options if preview command isn't already set by user
if string match -qvr -- '--preview[= ]' "$FZF_DEFAULT_OPTS"
# Convert the highlighted timestamp using the date command if available
set -l -- date_cmd '{1}'
if type -q date
if date -d @0 '+%s' 2>/dev/null | string match -q 0
# GNU date
set -- date_cmd '(date -d @{1} \\"+%F %a %T\\")'
else if date -r 0 '+%s' 2>/dev/null | string match -q 0
# BSD date
set -- date_cmd '(date -r {1} \\"+%F %a %T\\")'
end
end
# Prepend the options to allow user customizations
set -p -- FZF_DEFAULT_OPTS \
'--bind="focus,resize:bg-transform:if test \\"$FZF_COLUMNS\\" -gt 100 -a \\\\( \\"$FZF_SELECT_COUNT\\" -gt 0 -o \\\\( -z \\"$FZF_WRAP\\" -a (string length -- {}) -gt (math $FZF_COLUMNS - 4) \\\\) -o (string collect -- {2..} | fish_indent | count) -gt 1 \\\\); echo show-preview; else echo hide-preview; end"' \
'--preview="string collect -- (test \\"$FZF_SELECT_COUNT\\" -gt 0; and string collect -- {+2..}) \\"\\n# \\"'$date_cmd' {2..} | fish_indent --ansi"' \
'--preview-window="right,50%,wrap-word,follow,info,hidden"'
end
set -lx FZF_DEFAULT_OPTS_FILE set -lx FZF_DEFAULT_OPTS_FILE
set -lx FZF_DEFAULT_COMMAND
if type -q perl set -lx -- FZF_DEFAULT_COMMAND 'builtin history -z --show-time="%s%t"'
set -a FZF_DEFAULT_OPTS '--tac'
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\'' # Enable syntax highlighting colors on fish v4.3.3 and newer
else if set -l -- v (string match -r -- '^(\d+)\.(\d+)(?:\.(\d+))?' $version)
set FZF_DEFAULT_COMMAND \ and test "$v[2]" -gt 4 -o "$v[2]" -eq 4 -a \
'set -l h (builtin history -z --reverse | string split0);' \ \( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
'for i in (seq (count $h) -1 1);' \ \( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
'end' set -a -- FZF_DEFAULT_OPTS '--ansi'
set -a -- FZF_DEFAULT_COMMAND '--color=always'
end end
# Merge history from other sessions before searching # Merge history from other sessions before searching
@@ -207,11 +229,11 @@ function fzf_key_bindings
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0) if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
if test "$total_lines" -eq 1 if test "$total_lines" -eq 1
commandline -- (string replace -a -- \n\t \n $result) commandline -- $result
else else
set -l a (math $current_line - 1) set -l a (math $current_line - 1)
set -l b (math $current_line + 1) set -l b (math $current_line + 1)
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result) commandline -- $command_line[1..$a] $result
commandline -a -- '' $command_line[$b..-1] commandline -a -- '' $command_line[$b..-1]
end end
end end

View File

@@ -1356,6 +1356,13 @@ func (t *Terminal) environImpl(forPreview bool) []string {
} else if t.paused { } else if t.paused {
inputState = "disabled" inputState = "disabled"
} }
if t.wrap {
if t.wrapWord {
env = append(env, "FZF_WRAP=word")
} else {
env = append(env, "FZF_WRAP=char")
}
}
env = append(env, "FZF_INPUT_STATE="+inputState) env = append(env, "FZF_INPUT_STATE="+inputState)
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count)) env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length())) env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.resultMerger.Length()))

View File

@@ -163,6 +163,7 @@ class Tmux
self.until(true) do |lines| self.until(true) do |lines|
message = "Prepare[#{tries}]" message = "Prepare[#{tries}]"
send_keys ' ', 'C-u', :Enter, message, :Left, :Right send_keys ' ', 'C-u', :Enter, message, :Left, :Right
sleep(0.15)
lines[-1] == message lines[-1] == message
end end
rescue Minitest::Assertion rescue Minitest::Assertion

View File

@@ -138,7 +138,7 @@ module TestShell
tmux.send_keys 'C-r' tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'foo bar' tmux.send_keys 'foo bar'
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh tmux.until { |lines| assert_includes lines[-4], '"foo' } if shell == :bash
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) } tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) } tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
@@ -1020,15 +1020,23 @@ class TestFish < TestBase
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'C-l', 'C-r' tmux.send_keys 'C-l', 'C-r'
offset = -6
block = <<~BLOCK block = <<~BLOCK
echo "foo echo "foo
bar" bar"
echo "bar echo "bar
foo" foo"
BLOCK BLOCK
if shell == :fish
offset = -4
block = <<~FISH
echo "foo␊bar"
echo "bar␊foo"
FISH
end
tmux.until do |lines| tmux.until do |lines|
block.lines.each_with_index do |line, idx| block.lines.each_with_index do |line, idx|
assert_includes lines[-6 + idx], line.chomp assert_includes lines[idx + offset], line.chomp
end end
end end
tmux.send_keys :BTab, :BTab tmux.send_keys :BTab, :BTab