m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-12 13:23:48 -05:00

Compare commits

...

12 Commits

Author SHA1 Message Date
Junegunn Choi
0076ec2e8d 0.64.0 2025-07-06 22:11:36 +09:00
Junegunn Choi
82c9671f79 Fix selection lost on revision bump 2025-07-06 22:02:12 +09:00
Junegunn Choi
d364a1122e Fix regression where header is not updated 2025-07-06 20:24:23 +09:00
Junegunn Choi
fb570e94e7 Update: make generate 2025-07-06 20:03:13 +09:00
Junegunn Choi
6e3c830cd2 Add 'multi' event triggered on multi-selection changes 2025-07-06 10:05:25 +09:00
junegunn
d7db7fc132 Deploying to master from @ junegunn/fzf@ff1550bb38 🚀 2025-07-06 00:02:27 +00:00
Junegunn Choi
ff1550bb38 Normalize halfwidth and fullwidth characers for matching 2025-07-03 20:57:19 +09:00
Junegunn Choi
976001e474 Explain the need to escape placeholders in transform actions 2025-07-02 22:26:56 +09:00
Junegunn Choi
531dd6fb4f Update copyright year 2025-07-02 22:10:05 +09:00
Junegunn Choi
ba035f2a76 Run preview command when preview window appears after CTRL-Z
80b8846318
2025-07-02 21:40:02 +09:00
Junegunn Choi
d34675d3c9 Fix panic caused by incorrect update ordering
Fix #4442

Make sure to prepare windows before rendering elements.

Thanks to @nugged for the report.
2025-07-02 21:28:11 +09:00
junegunn
ce95adc66c Deploying to master from @ junegunn/fzf@397fe8e395 🚀 2025-06-29 00:02:28 +00:00
24 changed files with 292 additions and 77 deletions

View File

@@ -1,2 +1,2 @@
golang 1.20.13 golang 1.20.14
ruby 3.4.1 ruby 3.4.1

View File

@@ -1,6 +1,24 @@
CHANGELOG CHANGELOG
========= =========
0.64.0
------
- Added `multi` event that is triggered when the multi-selection has changed.
```sh
fzf --multi \
--bind 'ctrl-a:select-all,ctrl-d:deselect-all' \
--bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'
```
- [Halfwidth and fullwidth alphanumeric and punctuation characters](https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)) are now internally normalized to their ASCII equivalents to allow matching with ASCII queries.
```sh
echo | fzf -q abc
```
- Renamed `clear-selection` action to `clear-multi` for consistency.
- `clear-selection` remains supported as an alias for backward compatibility.
- Bug fixes
- Fixed a bug that could cause fzf to abort due to incorrect update ordering.
- Fixed a bug where some multi-selections were lost when using `exclude` or `change-nth`.
0.63.0 0.63.0
------ ------
_Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_ _Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -493,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi

File diff suppressed because one or more lines are too long

View File

@@ -503,7 +503,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.63.0 version=0.64.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.63.0" $version="0.64.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.63" var version = "0.64"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Jun 2025" "fzf 0.63.0" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Jul 2025" "fzf 0.64.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf\-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jun 2025" "fzf 0.63.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jul 2025" "fzf 0.64.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -1609,6 +1609,10 @@ e.g.
# Beware not to introduce an infinite loop # Beware not to introduce an infinite loop
seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR
.RE .RE
\fImulti\fR
.RS
Triggered when the multi\-selection has changed.
.RE
\fIone\fR \fIone\fR
.RS .RS
@@ -1711,13 +1715,13 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-prompt(...)\fR (change prompt to the given string) \fBchange\-prompt(...)\fR (change prompt to the given string)
\fBchange\-query(...)\fR (change query string to the given string) \fBchange\-query(...)\fR (change query string to the given string)
\fBclear\-screen\fR \fIctrl\-l\fR \fBclear\-screen\fR \fIctrl\-l\fR
\fBclear\-selection\fR (clear multi\-selection) \fBclear\-multi\fR (clear multi\-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise) \fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear\-query\fR (clear query string) \fBclear\-query\fR (clear query string)
\fBdelete\-char\fR \fIdel\fR \fBdelete\-char\fR \fIdel\fR
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty) \fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
\fBdeselect\fR \fBdeselect\fR
\fBdeselect\-all\fR (deselect all matches) \fBdeselect\-all\fR (deselect all matches; to also clear non-matched selections, use \fBclear\-multi\fR)
\fBdisable\-search\fR (disable search functionality) \fBdisable\-search\fR (disable search functionality)
\fBdown\fR \fIctrl\-j ctrl\-n down\fR \fBdown\fR \fIctrl\-j ctrl\-n down\fR
\fBenable\-search\fR (enable search functionality) \fBenable\-search\fR (enable search functionality)
@@ -1938,6 +1942,17 @@ e.g.
echo "change\-header:Invalid selection"' echo "change\-header:Invalid selection"'
\fR \fR
A common mistake when writing a \fBtransform\fR action is not escaping
placeholder expressions when passing them back to fzf. In the following
example, if you don't escape \fB{}\fR, fzf will immediately replace it with the
single-quoted string of the current item. This causes single quotes to appear
in the header and footer, and the script will break if any item contains
double-quote characters.
\fBfzf \-\-bind 'focus:transform:[[ $FZF_ACTION =~ up ]] &&
echo "change\-header()+transform\-footer:echo \\{}" ||
echo "change\-footer()+transform\-header:echo \\{}"'\fR
.SS TRANSFORM IN THE BACKGROUND .SS TRANSFORM IN THE BACKGROUND
Transform actions are synchronous, meaning fzf becomes unresponsive while the Transform actions are synchronous, meaning fzf becomes unresponsive while the

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013-2024 Junegunn Choi " Copyright (c) 2013-2025 Junegunn Choi
" "
" MIT License " MIT License
" "

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -303,7 +303,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
} }
func normalizeRune(r rune) rune { func normalizeRune(r rune) rune {
if r < 0x00C0 || r > 0x2184 { if r < 0x00C0 || r > 0xFF61 {
return r return r
} }

View File

@@ -473,6 +473,103 @@ var normalized = map[rune]rune{
'ử': 'u', 'ử': 'u',
'ữ': 'u', 'ữ': 'u',
'ự': 'u', 'ự': 'u',
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
0xFF01: '!', // Fullwidth exclamation
0xFF02: '"', // Fullwidth quotation mark
0xFF03: '#', // Fullwidth number sign
0xFF04: '$', // Fullwidth dollar sign
0xFF05: '%', // Fullwidth percent
0xFF06: '&', // Fullwidth ampersand
0xFF07: '\'', // Fullwidth apostrophe
0xFF08: '(', // Fullwidth left parenthesis
0xFF09: ')', // Fullwidth right parenthesis
0xFF0A: '*', // Fullwidth asterisk
0xFF0B: '+', // Fullwidth plus
0xFF0C: ',', // Fullwidth comma
0xFF0D: '-', // Fullwidth hyphen-minus
0xFF0E: '.', // Fullwidth period
0xFF0F: '/', // Fullwidth slash
0xFF10: '0',
0xFF11: '1',
0xFF12: '2',
0xFF13: '3',
0xFF14: '4',
0xFF15: '5',
0xFF16: '6',
0xFF17: '7',
0xFF18: '8',
0xFF19: '9',
0xFF1A: ':', // Fullwidth colon
0xFF1B: ';', // Fullwidth semicolon
0xFF1C: '<', // Fullwidth less-than
0xFF1D: '=', // Fullwidth equal
0xFF1E: '>', // Fullwidth greater-than
0xFF1F: '?', // Fullwidth question mark
0xFF20: '@', // Fullwidth at sign
0xFF21: 'A',
0xFF22: 'B',
0xFF23: 'C',
0xFF24: 'D',
0xFF25: 'E',
0xFF26: 'F',
0xFF27: 'G',
0xFF28: 'H',
0xFF29: 'I',
0xFF2A: 'J',
0xFF2B: 'K',
0xFF2C: 'L',
0xFF2D: 'M',
0xFF2E: 'N',
0xFF2F: 'O',
0xFF30: 'P',
0xFF31: 'Q',
0xFF32: 'R',
0xFF33: 'S',
0xFF34: 'T',
0xFF35: 'U',
0xFF36: 'V',
0xFF37: 'W',
0xFF38: 'X',
0xFF39: 'Y',
0xFF3A: 'Z',
0xFF3B: '[', // Fullwidth left bracket
0xFF3C: '\\', // Fullwidth backslash
0xFF3D: ']', // Fullwidth right bracket
0xFF3E: '^', // Fullwidth circumflex
0xFF3F: '_', // Fullwidth underscore
0xFF40: '`', // Fullwidth grave accent
0xFF41: 'a',
0xFF42: 'b',
0xFF43: 'c',
0xFF44: 'd',
0xFF45: 'e',
0xFF46: 'f',
0xFF47: 'g',
0xFF48: 'h',
0xFF49: 'i',
0xFF4A: 'j',
0xFF4B: 'k',
0xFF4C: 'l',
0xFF4D: 'm',
0xFF4E: 'n',
0xFF4F: 'o',
0xFF50: 'p',
0xFF51: 'q',
0xFF52: 'r',
0xFF53: 's',
0xFF54: 't',
0xFF55: 'u',
0xFF56: 'v',
0xFF57: 'w',
0xFF58: 'x',
0xFF59: 'y',
0xFF5A: 'z',
0xFF5B: '{', // Fullwidth left brace
0xFF5C: '|', // Fullwidth vertical bar
0xFF5D: '}', // Fullwidth right brace
0xFF5E: '~', // Fullwidth tilde
0xFF61: '.', // Halfwidth ideographic full stop
} }
// NormalizeRunes normalizes latin script letters // NormalizeRunes normalizes latin script letters
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
ret := make([]rune, len(runes)) ret := make([]rune, len(runes))
copy(ret, runes) copy(ret, runes)
for idx, r := range runes { for idx, r := range runes {
if r < 0x00C0 || r > 0x2184 { if r < 0x00C0 || r > 0xFF61 {
continue continue
} }
n := normalized[r] n := normalized[r]

View File

@@ -41,6 +41,13 @@ func (c *Chunk) IsFull() bool {
return c.count == chunkSize return c.count == chunkSize
} }
func (c *Chunk) lastIndex(minValue int32) int32 {
if c.count == 0 {
return minValue
}
return c.items[c.count-1].Index() + 1 // Exclusive
}
func (cl *ChunkList) lastChunk() *Chunk { func (cl *ChunkList) lastChunk() *Chunk {
return cl.chunks[len(cl.chunks)-1] return cl.chunks[len(cl.chunks)-1]
} }

View File

@@ -165,6 +165,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
minIndex := request.chunks[0].items[0].Index() minIndex := request.chunks[0].items[0].Index()
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) slices := m.sliceChunks(request.chunks)
@@ -236,7 +237,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search

View File

@@ -4,7 +4,7 @@ import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
func EmptyMerger(revision revision) *Merger { func EmptyMerger(revision revision) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision, 0) return NewMerger(nil, [][]Result{}, false, false, revision, 0, 0)
} }
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
@@ -22,14 +22,16 @@ type Merger struct {
pass bool pass bool
revision revision revision revision
minIndex int32 minIndex int32
maxIndex int32
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex int32 var minIndex, maxIndex int32
if len(*chunks) > 0 { if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index() minIndex = (*chunks)[0].items[0].Index()
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
} }
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
@@ -38,7 +40,8 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
count: 0, count: 0,
pass: true, pass: true,
revision: revision, revision: revision,
minIndex: minIndex} minIndex: minIndex,
maxIndex: maxIndex}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@@ -47,7 +50,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
@@ -59,7 +62,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
final: false, final: false,
count: 0, count: 0,
revision: revision, revision: revision,
minIndex: minIndex} minIndex: minIndex,
maxIndex: maxIndex}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)

View File

@@ -58,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(nil, lists, false, false, revision{}, 0) mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -70,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(nil, lists, true, false, revision{}, 0) mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@@ -80,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(nil, lists, true, false, revision{}, 0) mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 0)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

View File

@@ -1008,6 +1008,8 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
add(tui.JumpCancel) add(tui.JumpCancel)
case "click-header": case "click-header":
add(tui.ClickHeader) add(tui.ClickHeader)
case "multi":
add(tui.Multi)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key chords[tui.CtrlAltKey('m')] = key
case "alt-space": case "alt-space":
@@ -1561,7 +1563,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actCancel) appendAction(actCancel)
case "clear-query": case "clear-query":
appendAction(actClearQuery) appendAction(actClearQuery)
case "clear-selection": case "clear-multi", "clear-selection":
appendAction(actClearSelection) appendAction(actClearSelection)
case "forward-char": case "forward-char":
appendAction(actForwardChar) appendAction(actForwardChar)

View File

@@ -450,31 +450,35 @@ func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at) return a[i].at.Before(a[j].at)
} }
// EventTypes are listed in the order of their priority.
const ( const (
reqPrompt util.EventType = iota reqResize util.EventType = iota
reqReinit
reqFullRedraw
reqRedraw
reqJump
reqPrompt
reqInfo reqInfo
reqHeader reqHeader
reqFooter reqFooter
reqList reqList
reqJump
reqActivate
reqReinit
reqFullRedraw
reqResize
reqRedraw
reqRedrawInputLabel reqRedrawInputLabel
reqRedrawHeaderLabel reqRedrawHeaderLabel
reqRedrawFooterLabel reqRedrawFooterLabel
reqRedrawListLabel reqRedrawListLabel
reqRedrawBorderLabel reqRedrawBorderLabel
reqRedrawPreviewLabel reqRedrawPreviewLabel
reqClose
reqPrintQuery
reqPreviewReady reqPreviewReady
reqPreviewEnqueue reqPreviewEnqueue
reqPreviewDisplay reqPreviewDisplay
reqPreviewRefresh reqPreviewRefresh
reqPreviewDelayed reqPreviewDelayed
reqActivate
reqClose
reqPrintQuery
reqBecome reqBecome
reqQuit reqQuit
reqFatal reqFatal
@@ -1624,14 +1628,12 @@ func (t *Terminal) changeHeader(header string) bool {
return needFullRedraw return needFullRedraw
} }
func (t *Terminal) changeFooter(footer string) bool { func (t *Terminal) changeFooter(footer string) {
var lines []string var lines []string
if len(footer) > 0 { if len(footer) > 0 {
lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n") lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
} }
needFullRedraw := len(t.footer) != len(lines)
t.footer = lines t.footer = lines
return needFullRedraw
} }
// UpdateHeader updates the header // UpdateHeader updates the header
@@ -1678,12 +1680,12 @@ func (t *Terminal) UpdateList(merger *Merger) {
// Trimmed by --tail: filter selection by index // Trimmed by --tail: filter selection by index
filtered := make(map[int32]selectedItem) filtered := make(map[int32]selectedItem)
minIndex := merger.minIndex minIndex := merger.minIndex
maxIndex := minIndex + int32(merger.Length()) maxIndex := merger.maxIndex
for k, v := range t.selected { for k, v := range t.selected {
var included bool var included bool
if maxIndex > minIndex { if maxIndex > minIndex {
included = k >= minIndex && k < maxIndex included = k >= minIndex && k < maxIndex
} else { // int32 overflow [==> <==] } else if maxIndex < minIndex { // int32 overflow [==> <==]
included = k >= minIndex || k < maxIndex included = k >= minIndex || k < maxIndex
} }
if included { if included {
@@ -2889,19 +2891,29 @@ func (t *Terminal) resizeIfNeeded() bool {
return true return true
} }
// Check footer window
if len(t.footer) > 0 && (t.footerWindow == nil || t.footerWindow.Height() != len(t.footer)) ||
len(t.footer) == 0 && t.footerWindow != nil {
t.printAll()
return true
}
// Check if the header borders are used and header has changed // Check if the header borders are used and header has changed
allHeaderLines := t.visibleHeaderLines() allHeaderLines := t.visibleHeaderLines()
primaryHeaderLines := allHeaderLines primaryHeaderLines := allHeaderLines
if t.hasHeaderLinesWindow() { needHeaderWindow := t.hasHeaderWindow()
needHeaderLinesWindow := t.hasHeaderLinesWindow()
if needHeaderLinesWindow {
primaryHeaderLines -= t.headerLines primaryHeaderLines -= t.headerLines
} }
// FIXME: Full redraw is triggered if there are too many lines in the header // FIXME: Full redraw is triggered if there are too many lines in the header
// so that the header window cannot display all of them. // so that the header window cannot display all of them.
needHeaderLinesWindow := t.hasHeaderLinesWindow() if (needHeaderWindow && t.headerWindow == nil) ||
if (t.headerBorderShape.Visible() || needHeaderLinesWindow) && (!needHeaderWindow && t.headerWindow != nil) ||
(t.headerWindow == nil && primaryHeaderLines > 0 || t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) || (needHeaderWindow && t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
needHeaderLinesWindow && (t.headerLinesWindow == nil || t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) || (needHeaderLinesWindow && t.headerLinesWindow == nil) ||
!needHeaderLinesWindow && t.headerLinesWindow != nil { (!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
(needHeaderLinesWindow && t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) {
t.printAll() t.printAll()
return true return true
} }
@@ -5116,6 +5128,8 @@ func (t *Terminal) Loop() error {
t.uiMutex.Lock() t.uiMutex.Lock()
t.mutex.Lock() t.mutex.Lock()
info := false info := false
header := false
footer := false
for _, key := range keys { for _, key := range keys {
req := util.EventType(key) req := util.EventType(key)
value := (*events)[req] value := (*events)[req]
@@ -5153,13 +5167,9 @@ func (t *Terminal) Loop() error {
} }
t.printList() t.printList()
case reqHeader: case reqHeader:
if !t.resizeIfNeeded() { header = true
t.printHeader()
}
case reqFooter: case reqFooter:
if !t.resizeIfNeeded() { footer = true
t.printFooter()
}
case reqActivate: case reqActivate:
t.suppress = false t.suppress = false
if t.hasPreviewer() { if t.hasPreviewer() {
@@ -5177,10 +5187,10 @@ func (t *Terminal) Loop() error {
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true) t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
case reqRedrawPreviewLabel: case reqRedrawPreviewLabel:
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), true) t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), true)
case reqReinit: case reqReinit, reqResize, reqFullRedraw, reqRedraw:
if req == reqReinit {
t.tui.Resume(t.fullscreen, true) t.tui.Resume(t.fullscreen, true)
t.fullRedraw() }
case reqResize, reqFullRedraw, reqRedraw:
if req == reqResize { if req == reqResize {
t.termSize = t.tui.Size() t.termSize = t.tui.Size()
} }
@@ -5243,9 +5253,17 @@ func (t *Terminal) Loop() error {
return return
} }
} }
if info && !t.resizeIfNeeded() { if (info || header || footer) && !t.resizeIfNeeded() {
if info {
t.printInfo() t.printInfo()
} }
if header {
t.printHeader()
}
if footer {
t.printFooter()
}
}
t.flush() t.flush()
t.mutex.Unlock() t.mutex.Unlock()
t.uiMutex.Unlock() t.uiMutex.Unlock()
@@ -5384,6 +5402,7 @@ func (t *Terminal) Loop() error {
} }
previousInput := t.input previousInput := t.input
previousCx := t.cx previousCx := t.cx
previousVersion := t.version
t.lastKey = event.KeyName() t.lastKey = event.KeyName()
updatePreviewWindow := func(forcePreview bool) { updatePreviewWindow := func(forcePreview bool) {
t.resizeWindows(forcePreview, false) t.resizeWindows(forcePreview, false)
@@ -5670,24 +5689,17 @@ func (t *Terminal) Loop() error {
t.cx = len(t.input) t.cx = len(t.input)
case actChangeHeader, actTransformHeader, actBgTransformHeader: case actChangeHeader, actTransformHeader, actBgTransformHeader:
capture(false, func(header string) { capture(false, func(header string) {
// When a dedicated header window is not used, we may need to
// update other elements as well.
if t.changeHeader(header) { if t.changeHeader(header) {
if t.headerWindow != nil { req(reqList, reqPrompt, reqInfo)
// Need to resize header window
req(reqRedraw)
} else {
req(reqHeader, reqList, reqPrompt, reqInfo)
} }
} else {
req(reqHeader) req(reqHeader)
}
}) })
case actChangeFooter, actTransformFooter, actBgTransformFooter: case actChangeFooter, actTransformFooter, actBgTransformFooter:
capture(false, func(footer string) { capture(false, func(footer string) {
if t.changeFooter(footer) { t.changeFooter(footer)
req(reqRedraw)
} else {
req(reqFooter) req(reqFooter)
}
}) })
case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel: case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
capture(true, func(label string) { capture(true, func(label string) {
@@ -6648,6 +6660,9 @@ func (t *Terminal) Loop() error {
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) { if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) {
continue continue
} }
if onMultis, prs := t.keymap[tui.Multi.AsEvent()]; t.version != previousVersion && prs && !doActions(onMultis) {
continue
}
} else { } else {
jumpEvent := tui.JumpCancel jumpEvent := tui.JumpCancel
if event.Type == tui.Rune { if event.Type == tui.Rune {

View File

@@ -110,11 +110,12 @@ func _() {
_ = x[Jump-99] _ = x[Jump-99]
_ = x[JumpCancel-100] _ = x[JumpCancel-100]
_ = x[ClickHeader-101] _ = x[ClickHeader-101]
_ = x[Multi-102]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderMulti"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684} var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684, 689}
func (i EventType) String() string { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -132,6 +132,7 @@ const (
Jump Jump
JumpCancel JumpCancel
ClickHeader ClickHeader
Multi
) )
func (t EventType) AsEvent() Event { func (t EventType) AsEvent() Event {

View File

@@ -1930,7 +1930,10 @@ class TestCore < TestInteractive
def test_change_header_on_header_window def test_change_header_on_header_window
tmux.send_keys %(seq 100 | #{FZF} --list-border --input-border --bind 'start:change-header(foo),space:change-header(bar)'), :Enter tmux.send_keys %(seq 100 | #{FZF} --list-border --input-border --bind 'start:change-header(foo),space:change-header(bar)'), :Enter
tmux.until { |lines| assert lines.any_include?('foo') } tmux.until do |lines|
assert lines.any_include?('100/100')
assert lines.any_include?('foo')
end
tmux.send_keys :Space tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('bar') } tmux.until { |lines| assert lines.any_include?('bar') }
end end
@@ -1981,4 +1984,55 @@ class TestCore < TestInteractive
refute lines.any_include?('[1]') refute lines.any_include?('[1]')
end end
end end
def test_render_order
tmux.send_keys %(seq 100 | #{FZF} --bind='focus:preview(echo boom)+change-footer(bam)'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.until { assert it.any_include?('boom') }
tmux.until { assert it.any_include?('bam') }
end
def test_multi_event
tmux.send_keys %(seq 100 | #{FZF} --multi --bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.send_keys :Tab
tmux.until { assert_equal 1, it.select_count }
tmux.until { assert it.any_include?('Selected 1 item(s)') }
tmux.send_keys :Tab
tmux.until { assert_equal 0, it.select_count }
tmux.until { refute it.any_include?('Selected') }
end
def test_preserve_selection_on_revision_bump
tmux.send_keys %(seq 100 | #{FZF} --multi --sync --query "'1" --bind 'a:select-all+change-header(pressed a),b:change-header(pressed b)+change-nth(1),c:exclude'), :Enter
tmux.until do
assert_equal 20, it.match_count
assert_equal 0, it.select_count
end
tmux.send_keys :a
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
assert it.any_include?('pressed a')
end
tmux.send_keys :b
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
refute it.any_include?('pressed a')
assert it.any_include?('pressed b')
end
tmux.send_keys :a
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
assert it.any_include?('pressed a')
refute it.any_include?('pressed b')
end
tmux.send_keys :c
tmux.until do
assert_equal 19, it.match_count
assert_equal 19, it.select_count
end
end
end end