Skip to content

Conversation

@LangLangBart
Copy link
Contributor

@LangLangBart LangLangBart commented Nov 18, 2025

Related discussion #4591

@drelephant

Related PR with implementation in fish: #4280


Add the ability to select multiple zsh history entries

Copy link
Contributor Author

@LangLangBart LangLangBart left a comment

Choose a reason for hiding this comment

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

@junegunn Annotations were added to clarify the terse Zsh syntax and ease the burden of reviewing.

@LangLangBart LangLangBart marked this pull request as ready for review November 18, 2025 07:33
@LangLangBart LangLangBart force-pushed the zsh-hist-multi-select branch 3 times, most recently from 2823e9a to dbfef1b Compare November 19, 2025 11:34
@junegunn
Copy link
Owner

@drelephant Can you test this and see if it works as expected? You can clone this repo, make install, and run source <(bin/fzf --zsh).

@LangLangBart
Copy link
Contributor Author

LangLangBart commented Nov 20, 2025

rebased onto master and triggered the CI again; tests now pass, whereas previously it failed:


@junegunn Is there an easy way to temporarily disable or hide Perl when running a certain test, so I can checkZsh shell integration on OS versions that don’t have Perl installed by default?

EDIT1 unset 'commands[perl]' does the trick

When Perl is not available on the system, fallback to using awk
@junegunn
Copy link
Owner

Sorry, I haven't had the bandwidth to review this yet. I just requested Copilot's review, let's see if it can provide meaningful insights. But feel free to dismiss any comments from it that you consider irrelevant or inaccurate.

Copilot finished reviewing on behalf of junegunn November 24, 2025 10:02
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds multi-line history selection support to zsh's fzf integration, allowing users to select and insert multiple history entries (including multi-line commands) at once, similar to the fish shell implementation in PR #4280.

Key Changes:

  • Modified the fzf-history-widget to support multi-select mode (--multi flag) instead of single selection
  • Added logic to process multiple selected history entries and join them with newlines
  • Implemented handling for both perl-based and awk-based history extraction paths

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
shell/key-bindings.zsh Enhanced fzf-history-widget to enable multi-selection, extract multiple history entries, and handle both perl and awk code paths with proper multi-line command preservation
test/test_shell_integration.rb Added comprehensive test coverage including a helper function to test both perl and awk branches, with tests for multi-selection, single-line mode, and edge cases like multi-line entries with leading numbers

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 172 to 173
# Join by newline after stripping trailing whitespace from each command
BUFFER="${(pj:\n:)${(@)cmds%%[[:space:]]#}}"
Copy link
Owner

Choose a reason for hiding this comment

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

Just wondering. If I'm not mistaken, in the previous version, this code would only strip trailing new lines, but now it strips [[:space:]]. Could you elaborate on the change?

#4595 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

During testing, a command had a trailing newline & space character, and since the
original command only removed trailing newlines, it failed to remove anything. For example:

zsh -o extendedglob -fc $'
ORIGINAL="1 foo\nbar\n "
O_WSPACE="${ORIGINAL%%[[:space:]]#}"
O_NEWLIN="${ORIGINAL%%\n#}"
typeset -p ORIGINAL O_WSPACE O_NEWLIN'
typeset ORIGINAL=$'1 foo\nbar\n '
typeset O_WSPACE=$'1 foo\nbar'
typeset O_NEWLIN=$'1 foo\nbar\n '

My thinking was nobody wants trailing whitespace on a command, right 😬 ?

(Though there is a chance a user may complain, as it affects their workflow – https://www.explainxkcd.com/wiki/index.php/1172:_Workflow.)

Copy link
Owner

Choose a reason for hiding this comment

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

My thinking was nobody wants trailing whitespace on a command, right 😬 ?

Yeah. One concern is that when:

  • User accidentally types in a command with extra spaces.
  • User wants to repeat the command, so they use CTRL-R to select the previous command, and press enter.
  • Now when they press CTRL-R again, they see two duplicate commands in the list, only differ in the trailing spaces.
100   whoami<invisible spaces>
101   whoami

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I was able to reproduce what you described. I am using
'romkatv/zsh4humans', and it has a mechanism1 built in by default that
automatically removes trailing spaces from a command when it is executed, that
is why it took me a bit to reproduce your issue and find out why its not
behaving the same way as you described and only did in 'zsh -f' mode.


Anyway, are you suggesting it’s better to keep the original trailing‑newline
removal?

Because even when reverting back, your original issue still remains: a user sees
two commands.

Footnotes

  1. https://github.com/romkatv/zsh4humans/blob/cd6c4770c802c3a17b4c43e5587adabb9a370a75/fn/-z4h-zle-line-finish#L15-L19

Copy link
Owner

Choose a reason for hiding this comment

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

Anyway, are you suggesting it’s better to keep the original trailing‑newline removal?

Hmm, yeah, I think it's preferable that we don't modify the original command.

Because even when reverting back, your original issue still remains: a user sees two commands.

I didn't test your previous version. I might be missing something, but on zsh, when I repeat the same command multiple times, there's no duplication in the list. Are we on the same page?

$ foo<space><space>
$ foo<space><space>
$ foo<space><space>

# CTRL-R shows only one "foo<space><space>"

$ foo<space>
$ foo

# CTRL-R shows "foo<space><space>", "foo<space>", and "foo"

Copy link
Contributor Author

@LangLangBart LangLangBart Nov 29, 2025

Choose a reason for hiding this comment

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

Are we on the same page?

Yes, we are. When viewing these commands, they appear to be the same, with the trailing space being the only difference.

Hmm, yeah, I think it's preferable that we don't modify the original command.

ok, will adjust that, and adopt test BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"

Copy link
Owner

@junegunn junegunn left a comment

Choose a reason for hiding this comment

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

Thanks for your hard work and thanks for new tests. Is there anything you want to address?

@LangLangBart
Copy link
Contributor Author

Is there anything you want to address?

There is the comment by Copilot.

When entering 1 foobar€ into the query input and having no results, and with
export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query' set, Perl
would not surface anything to the user, while Awk would see 1 as the
history event number and fetch the corresponding command.

Status quo with the latest release (0.67.0), both Perl and Awk
incorrectly retrieve the history value for 1 instead of printing 1 foobar€
to the command-line buffer.

Do you think the I should make the heuristic stricter? At least for Perl it
would then correctly print 1 foobar€ to the command line buffer, but Awk would
still be "fooled".

--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -151,5 +151,6 @@ fzf-history-widget() {
   if [ -n "$selected" ]; then
     # Heuristic to check if the selected value is from history or a custom query
-    if [[ $selected == [[:blank:]]#<->\*#[[:blank:]]* ]]; then
+    if (( extracted_with_perl )) && [[ $selected == <->$'\t'* ]] || \
+      (( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->\*#[[:blank:]]* ]]; then
       # Split at newlines
       for line in ${(ps:\n:)selected}; do

Of course I could then also add a proper test, which only Perl would actually
pass, that is the reason I have simply dismissed this case?

@junegunn
Copy link
Owner

Status quo with the latest release (0.67.0), both Perl and Awk incorrectly retrieve the history value for 1 instead of printing 1 foobar€ to the command-line buffer.

That is unfortunate.

Do you think the I should make the heuristic stricter? At least for Perl it
would then correctly print 1 foobar€ to the command line buffer, but Awk would
still be "fooled".

I think it's understandable we can't "perfect" this. fzf can be configured in numerous ways and it's unrealistic for us to make these scripts compatible with every possible setup.

That said, if the added complexity is not too significant, we can try to cover more cases. I'll leave the decision up to you.

@LangLangBart
Copy link
Contributor Author

LangLangBart commented Nov 29, 2025

I'll leave the decision up to you.

Ok, I think we can make the heuristic a bit stricter for Perl/$history, and technically for Awk/fc as well, since by default the history event number (using fc) and the actual command are separated by two spaces or a * for foreign commands and one space, and this has been the case for a long time1.

I will work on that and let you know later today, so that we can also cover the 1 foobar€ case and have a test with it.

Footnotes

  1. https://github.com/zsh-users/zsh/blame/5539bc3fd59a2577bf1f951430c20e2b1e7b4dce/Src/builtin.c#L1829

@drelephant
Copy link

@drelephant Can you test this and see if it works as expected? You can clone this repo, make install, and run source <(bin/fzf --zsh).

I did that (using ./install instead of make install which came up with errors) and now I can't multi-select using tab after pressing ctrl-r.

I was previously using the code from here in my .zshrc but I took that out. Should I still need that in my .zshrc?

Also, before I took that code out of the .zshrc, I noticed that if I do this:

  1. type something onto the command line, eg "vi"
  2. press ctrl-r
  3. go up to select a previous usage of vi and hit enter

Then the command line will now have "vivi file" (ie it kept what was on the line before entering fzf.

I don't think it did that before updating to the latest commit?

Anyway, I'm on the latest commit now without anything extra in my .zshrc except

export FZF_CTRL_R_OPTS="
  --preview 'echo {}' --preview-window down:3:hidden:wrap
  --bind 'ctrl-p:toggle-preview'
  --multi
  --no-height"

And I can't multi-select using tab key.

@junegunn
Copy link
Owner

junegunn commented Nov 29, 2025

@drelephant

I did that (using ./install instead of make install which came up with errors) and now I can't multi-select using tab after pressing ctrl-r.

./install will download the latest pre-built binary, it does not build a fzf binary from source. We use source <(fzf --zsh) these days so you're using the the shell integration scripts embedded in that binary.

If you can't build the binary from source, you can just source the script files from your zsh.

@LangLangBart
Copy link
Contributor Author

Then the command line will now have "vivi file"

should be fixed in this version; the one from the discussion used the LBUFFER assignment instead of BUFFER

I did that (using ./install instead of make install which came up with errors)

thanks @junegunn

@drelephant If you still have trouble testing, one way would be:

  1. download the file: https://github.com/LangLangBart/fzf/blob/zsh-hist-multi-select/shell/key-bindings.zsh
  2. run source <path to your downloaded file>
  3. press ctrl‑r

I think I am done.

updated the test, checked older zsh versions with the command below:

Version Release Date AWK Perl
4.3.15 2011-12-17
5.0.2 2012-12-21
5.4 2017-08-07
5.8 2020-02-15
5.9 2022-05-14 (latest)
# Requires fzf repo directory and 'zsh-hist-multi-select' branch checked out
# https://hub.docker.com/r/ohmyzsh/zsh/tags
podman run -qit --rm -e ARCH=$(uname -m) -v $PWD:/repo -w /repo ohmyzsh/zsh:5.9 sh -uec '
apt update >/dev/null 2>&1
apt install -y curl jq >/dev/null 2>&1
mkdir /fzf-bin
download_url=$(curl -s https://api.github.com/repos/junegunn/fzf/releases/latest  | \
  jq --arg arch_string "linux_$ARCH" \
    -r '\''.assets[].browser_download_url | select(contains($arch_string))'\'')
curl -sfLo /tmp/fzf.tar.gz "$download_url"
tar -xf /tmp/fzf.tar.gz -C /fzf-bin
{
  echo "export PATH=\"/fzf-bin:\$PATH\""
  echo "source /repo/shell/key-bindings.zsh"
  echo "PS1=\"FZF \$(fzf --version) > \""
  echo "RPS1=\"ZSH \$(zsh --version)\""
  echo "print -s -- \"ls -la\""
  echo "print -s -- \"cat /etc/os-release\""
} > ~/.zshrc
zsh'

@junegunn junegunn merged commit cfc37ca into junegunn:master Nov 30, 2025
5 checks passed
@junegunn
Copy link
Owner

Merged, thanks a ton for the great work!

@drelephant
Copy link

./install will download the latest pre-built binary, it does not build a fzf binary from source. We use source <(fzf --zsh) these days so you're using the the shell integration scripts embedded in that binary.

If you can't build the binary from source, you can just source the script files from your zsh.

I just did a git pull

remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 6 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (6/6), 2.59 KiB | 1.29 MiB/s, done.
From https://github.com/junegunn/fzf
   af2a81d..cfc37ca  master     -> origin/master
Updating af2a81d..cfc37ca
Fast-forward
 shell/key-bindings.zsh         | 39 +++++++++++++++++----
 test/test_shell_integration.rb | 78 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 111 insertions(+), 6 deletions(-)

So I'm on the latest now.

Then source <(fzf --zsh), source ~/.zshrc ; exec zsh

And I couldn't multi-select, but then I did source ~/.fzf/shell/key-bindings.zsh and then it worked.

I can add that line to my ~/.zshrc, but is it odd that source <(fzf --zsh) didn't work properly?

@junegunn
Copy link
Owner

fzf --zsh prints the script embedded in that binary, so updating the external script file doesn't affect its. That's why we told you to build the binary from the source.

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.

3 participants