summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2025-01-27 15:40:21 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2025-01-27 18:04:57 +0900
commita2aa1a156c29658b62a691f8aa343985043b8c9c (patch)
tree291473b3e429657d064227b9ee5af991e12102fa
parent2f8a72a42a7503276a99cc0234f98f8c56169320 (diff)
downloadfzf-a2aa1a156c29658b62a691f8aa343985043b8c9c.tar.gz
Allow {q} placeholders with range expressions
e.g. {q:1}, {q:2..}
-rw-r--r--ADVANCED.md21
-rw-r--r--CHANGELOG.md24
-rw-r--r--man/man1/fzf.12
-rw-r--r--src/terminal.go29
-rw-r--r--src/terminal_test.go7
-rw-r--r--test/test_preview.rb4
6 files changed, 51 insertions, 36 deletions
diff --git a/ADVANCED.md b/ADVANCED.md
index a045140c..71ee15ae 100644
--- a/ADVANCED.md
+++ b/ADVANCED.md
@@ -517,18 +517,15 @@ remainder of the query is passed to fzf for secondary filtering.
INITIAL_QUERY="${*:-}"
TRANSFORMER='
- words=($FZF_QUERY)
-
- # If $FZF_QUERY contains multiple words, drop the first word,
- # and trigger fzf search with the rest
- if [[ ${#words[@]} -gt 1 ]]; then
- echo "search:${FZF_QUERY#* }"
-
- # Otherwise, if the query does not end with a space,
- # restart ripgrep and reload the list
- elif ! [[ $FZF_QUERY =~ \ $ ]]; then
- pat=${words[0]}
- echo "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case \"$pat\" || true"
+ rg_pat={q:1} # The first word is passed to ripgrep
+ fzf_pat={q:2..} # The rest are passed to fzf
+ rg_pat_org={q:s1} # The first word with trailing whitespaces preserved.
+ # We use this to avoid unnecessary reloading of ripgrep.
+
+ if [[ -n $fzf_pat ]]; then
+ echo "search:$fzf_pat"
+ elif ! [[ $rg_pat_org =~ \ $ ]]; then
+ printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
else
echo search:
fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dda3be87..3bd90d78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,7 @@ CHANGELOG
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
--header-lines-border bottom --no-list-border
```
-- `click-header` event will also set `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use it to implement a clickable header that changes the search scope using the new `transform-nth` action.
+- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
```sh
# Click on the header line to limit search scope
ps -ef | fzf --style full --layout reverse --header-lines 1 \
@@ -26,21 +26,21 @@ CHANGELOG
echo "$FZF_CLICK_HEADER_WORD> "
)'
```
+ - `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
- `kill` completion for bash and zsh were updated to use this feature
+- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
```sh
TRANSFORMER='
- words=($FZF_QUERY)
-
- # If $FZF_QUERY contains multiple words, drop the first word,
- # and trigger fzf search with the rest
- if [[ ${#words[@]} -gt 1 ]]; then
- echo "search:${FZF_QUERY#* }"
-
- # Otherwise, if the query does not end with a space,
- # restart ripgrep and reload the list
- elif ! [[ $FZF_QUERY =~ \ $ ]]; then
- echo "reload:rg --column --color=always --smart-case \"${words[0]}\""
+ rg_pat={q:1} # The first word is passed to ripgrep
+ fzf_pat={q:2..} # The rest are passed to fzf
+ rg_pat_org={q:s1} # The first word with trailing whitespaces preserved.
+ # We use this to avoid unnecessary reloading of ripgrep.
+
+ if [[ -n $fzf_pat ]]; then
+ echo "search:$fzf_pat"
+ elif ! [[ $rg_pat_org =~ \ $ ]]; then
+ printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
else
echo search:
fi
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 6a48b7a6..eefb1fbc 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -740,6 +740,8 @@ Also,
* \fB{q}\fR is replaced to the current query string
.br
+* \fB{q}\fR can contain field index expressions. e.g. \fB{q:1}\fR, \fB{q:2..}\fR, etc.
+.br
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
.br
diff --git a/src/terminal.go b/src/terminal.go
index 5ff9f8de..af6c8a8c 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -39,7 +39,7 @@ cases for example.
\\?(?: # escaped type
{\+?s?f?RANGE(?:,RANGE)*} # token type
- |{q} # query type
+ {q[:s?RANGE]} # query type
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
)
RANGE = (?:
@@ -65,7 +65,7 @@ const maxFocusEvents = 10000
const blockDuration = 1 * time.Second
func init() {
- placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
+ placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
@@ -3621,28 +3621,26 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
return false, match, flags
}
- skipChars := 1
+ trimmed := ""
for _, char := range match[1:] {
switch char {
case '+':
flags.plus = true
- skipChars++
case 's':
flags.preserveSpace = true
- skipChars++
case 'n':
flags.number = true
- skipChars++
case 'f':
flags.file = true
- skipChars++
case 'q':
flags.forceUpdate = true
- // query flag is not skipped
+ trimmed += string(char)
+ default:
+ trimmed += string(char)
}
}
- matchWithoutFlags := "{" + match[skipChars:]
+ matchWithoutFlags := "{" + trimmed
return false, matchWithoutFlags, flags
}
@@ -3756,6 +3754,19 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
return match
case match == "{q}" || match == "{fzf:query}":
return params.executor.QuoteEntry(params.query)
+ case strings.HasPrefix(match, "{q:"):
+ if nth, err := splitNth(match[3 : len(match)-1]); err == nil {
+ elems, prefixLength := awkTokenizer(params.query)
+ tokens := withPrefixLengths(elems, prefixLength)
+ trans := Transform(tokens, nth)
+ result := joinTokens(trans)
+ if !flags.preserveSpace {
+ result = strings.TrimSpace(result)
+ }
+ return params.executor.QuoteEntry(result)
+ }
+
+ return match
case match == "{}":
replace = func(item *Item) string {
switch {
diff --git a/src/terminal_test.go b/src/terminal_test.go
index 44da2bcf..4d55d80e 100644
--- a/src/terminal_test.go
+++ b/src/terminal_test.go
@@ -484,7 +484,12 @@ func TestParsePlaceholder(t *testing.T) {
// III. query type placeholder
// query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes
- `{q}`: `{qq}`,
+ `{q}`: `{qq}`,
+ `{q:1}`: `{qq:1}`,
+ `{q:2..}`: `{qq:2..}`,
+ `{q:..}`: `{qq:..}`,
+ `{q:2..-1}`: `{qq:2..-1}`,
+ `{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
// IV. escaping placeholder
`\{}`: `{}`,
diff --git a/test/test_preview.rb b/test/test_preview.rb
index c3659af0..b92ac8c3 100644
--- a/test/test_preview.rb
+++ b/test/test_preview.rb
@@ -209,9 +209,9 @@ class TestPreview < TestInteractive
end
def test_preview_q_no_match_with_initial_query
- tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}{q}' --query foo), :Enter
+ tmux.send_keys %(: | #{FZF} --preview 'echo foo {q}/{q}/{q:1}/{q:..}/{q:2}/{q:-1}/{q:-2}/{q:x}' --query 'foo bar'), :Enter
tmux.until { |lines| assert_equal 0, lines.match_count }
- tmux.until { |lines| assert_includes lines[1], ' foofoo ' }
+ tmux.until { |lines| assert_includes lines[1], ' foo bar/foo bar/foo/foo bar/bar/bar/foo/{q:x} ' }
end
def test_preview_update_on_select