mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-03 21:52:00 -07:00
Allow {q} placeholders with range expressions
e.g. {q:1}, {q:2..}
This commit is contained in:
19
ADVANCED.md
19
ADVANCED.md
@@ -517,18 +517,15 @@ remainder of the query is passed to fzf for secondary filtering.
|
|||||||
|
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
TRANSFORMER='
|
TRANSFORMER='
|
||||||
words=($FZF_QUERY)
|
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 $FZF_QUERY contains multiple words, drop the first word,
|
if [[ -n $fzf_pat ]]; then
|
||||||
# and trigger fzf search with the rest
|
echo "search:$fzf_pat"
|
||||||
if [[ ${#words[@]} -gt 1 ]]; then
|
elif ! [[ $rg_pat_org =~ \ $ ]]; then
|
||||||
echo "search:${FZF_QUERY#* }"
|
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||||
|
|
||||||
# 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"
|
|
||||||
else
|
else
|
||||||
echo search:
|
echo search:
|
||||||
fi
|
fi
|
||||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -14,7 +14,7 @@ CHANGELOG
|
|||||||
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
|
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
|
||||||
--header-lines-border bottom --no-list-border
|
--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
|
```sh
|
||||||
# Click on the header line to limit search scope
|
# Click on the header line to limit search scope
|
||||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||||
@@ -26,21 +26,21 @@ CHANGELOG
|
|||||||
echo "$FZF_CLICK_HEADER_WORD> "
|
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
|
- `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.
|
- 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
|
```sh
|
||||||
TRANSFORMER='
|
TRANSFORMER='
|
||||||
words=($FZF_QUERY)
|
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 $FZF_QUERY contains multiple words, drop the first word,
|
if [[ -n $fzf_pat ]]; then
|
||||||
# and trigger fzf search with the rest
|
echo "search:$fzf_pat"
|
||||||
if [[ ${#words[@]} -gt 1 ]]; then
|
elif ! [[ $rg_pat_org =~ \ $ ]]; then
|
||||||
echo "search:${FZF_QUERY#* }"
|
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||||
|
|
||||||
# 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]}\""
|
|
||||||
else
|
else
|
||||||
echo search:
|
echo search:
|
||||||
fi
|
fi
|
||||||
|
@@ -740,6 +740,8 @@ Also,
|
|||||||
|
|
||||||
* \fB{q}\fR is replaced to the current query string
|
* \fB{q}\fR is replaced to the current query string
|
||||||
.br
|
.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.
|
* \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.
|
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
||||||
.br
|
.br
|
||||||
|
@@ -39,7 +39,7 @@ cases for example.
|
|||||||
|
|
||||||
\\?(?: # escaped type
|
\\?(?: # escaped type
|
||||||
{\+?s?f?RANGE(?:,RANGE)*} # token 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)
|
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
|
||||||
)
|
)
|
||||||
RANGE = (?:
|
RANGE = (?:
|
||||||
@@ -65,7 +65,7 @@ const maxFocusEvents = 10000
|
|||||||
const blockDuration = 1 * time.Second
|
const blockDuration = 1 * time.Second
|
||||||
|
|
||||||
func init() {
|
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*$`)
|
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||||
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||||
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||||
@@ -3621,28 +3621,26 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
|||||||
return false, match, flags
|
return false, match, flags
|
||||||
}
|
}
|
||||||
|
|
||||||
skipChars := 1
|
trimmed := ""
|
||||||
for _, char := range match[1:] {
|
for _, char := range match[1:] {
|
||||||
switch char {
|
switch char {
|
||||||
case '+':
|
case '+':
|
||||||
flags.plus = true
|
flags.plus = true
|
||||||
skipChars++
|
|
||||||
case 's':
|
case 's':
|
||||||
flags.preserveSpace = true
|
flags.preserveSpace = true
|
||||||
skipChars++
|
|
||||||
case 'n':
|
case 'n':
|
||||||
flags.number = true
|
flags.number = true
|
||||||
skipChars++
|
|
||||||
case 'f':
|
case 'f':
|
||||||
flags.file = true
|
flags.file = true
|
||||||
skipChars++
|
|
||||||
case 'q':
|
case 'q':
|
||||||
flags.forceUpdate = true
|
flags.forceUpdate = true
|
||||||
// query flag is not skipped
|
trimmed += string(char)
|
||||||
|
default:
|
||||||
|
trimmed += string(char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
matchWithoutFlags := "{" + match[skipChars:]
|
matchWithoutFlags := "{" + trimmed
|
||||||
|
|
||||||
return false, matchWithoutFlags, flags
|
return false, matchWithoutFlags, flags
|
||||||
}
|
}
|
||||||
@@ -3756,6 +3754,19 @@ func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
|
|||||||
return match
|
return match
|
||||||
case match == "{q}" || match == "{fzf:query}":
|
case match == "{q}" || match == "{fzf:query}":
|
||||||
return params.executor.QuoteEntry(params.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 == "{}":
|
case match == "{}":
|
||||||
replace = func(item *Item) string {
|
replace = func(item *Item) string {
|
||||||
switch {
|
switch {
|
||||||
|
@@ -485,6 +485,11 @@ func TestParsePlaceholder(t *testing.T) {
|
|||||||
// query flag is not removed after parsing, so it gets doubled
|
// query flag is not removed after parsing, so it gets doubled
|
||||||
// while the double q is invalid, it is useful here for testing purposes
|
// 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
|
// IV. escaping placeholder
|
||||||
`\{}`: `{}`,
|
`\{}`: `{}`,
|
||||||
|
@@ -209,9 +209,9 @@ class TestPreview < TestInteractive
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_preview_q_no_match_with_initial_query
|
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_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
|
end
|
||||||
|
|
||||||
def test_preview_update_on_select
|
def test_preview_update_on_select
|
||||||
|
Reference in New Issue
Block a user