mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-12 05:13:49 -05:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0076ec2e8d | ||
|
|
82c9671f79 | ||
|
|
d364a1122e | ||
|
|
fb570e94e7 | ||
|
|
6e3c830cd2 | ||
|
|
d7db7fc132 | ||
|
|
ff1550bb38 | ||
|
|
976001e474 | ||
|
|
531dd6fb4f | ||
|
|
ba035f2a76 | ||
|
|
d34675d3c9 | ||
|
|
ce95adc66c |
@@ -1,2 +1,2 @@
|
||||
golang 1.20.13
|
||||
golang 1.20.14
|
||||
ruby 3.4.1
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,6 +1,24 @@
|
||||
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 ABC| 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
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -493,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
@@ -503,7 +503,7 @@ LICENSE *fzf-license*
|
||||
|
||||
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:
|
||||
|
||||
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.63.0
|
||||
version=0.64.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.63.0"
|
||||
$version="0.64.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.63"
|
||||
var version = "0.64"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
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
|
||||
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
|
||||
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
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
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
|
||||
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
|
||||
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
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -1609,6 +1609,10 @@ e.g.
|
||||
# Beware not to introduce an infinite loop
|
||||
seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR
|
||||
.RE
|
||||
\fImulti\fR
|
||||
.RS
|
||||
Triggered when the multi\-selection has changed.
|
||||
.RE
|
||||
|
||||
\fIone\fR
|
||||
.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\-query(...)\fR (change query string to the given string)
|
||||
\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)
|
||||
\fBclear\-query\fR (clear query string)
|
||||
\fBdelete\-char\fR \fIdel\fR
|
||||
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
|
||||
\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)
|
||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||
\fBenable\-search\fR (enable search functionality)
|
||||
@@ -1938,6 +1942,17 @@ e.g.
|
||||
echo "change\-header:Invalid selection"'
|
||||
\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
|
||||
|
||||
Transform actions are synchronous, meaning fzf becomes unresponsive while the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2013-2024 Junegunn Choi
|
||||
" Copyright (c) 2013-2025 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -303,7 +303,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,103 @@ var normalized = map[rune]rune{
|
||||
'ử': '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
|
||||
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
|
||||
@@ -41,6 +41,13 @@ func (c *Chunk) IsFull() bool {
|
||||
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 {
|
||||
return cl.chunks[len(cl.chunks)-1]
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
@@ -236,7 +237,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
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
|
||||
@@ -22,14 +22,16 @@ type Merger struct {
|
||||
pass bool
|
||||
revision revision
|
||||
minIndex int32
|
||||
maxIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
var minIndex int32
|
||||
var minIndex, maxIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||
}
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
@@ -38,7 +40,8 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -47,7 +50,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *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{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
@@ -59,7 +62,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// 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")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -70,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// 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")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -80,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
||||
@@ -1008,6 +1008,8 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
|
||||
add(tui.JumpCancel)
|
||||
case "click-header":
|
||||
add(tui.ClickHeader)
|
||||
case "multi":
|
||||
add(tui.Multi)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
@@ -1561,7 +1563,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actCancel)
|
||||
case "clear-query":
|
||||
appendAction(actClearQuery)
|
||||
case "clear-selection":
|
||||
case "clear-multi", "clear-selection":
|
||||
appendAction(actClearSelection)
|
||||
case "forward-char":
|
||||
appendAction(actForwardChar)
|
||||
|
||||
105
src/terminal.go
105
src/terminal.go
@@ -450,31 +450,35 @@ func (a byTimeOrder) Less(i, j int) bool {
|
||||
return a[i].at.Before(a[j].at)
|
||||
}
|
||||
|
||||
// EventTypes are listed in the order of their priority.
|
||||
const (
|
||||
reqPrompt util.EventType = iota
|
||||
reqResize util.EventType = iota
|
||||
reqReinit
|
||||
reqFullRedraw
|
||||
reqRedraw
|
||||
|
||||
reqJump
|
||||
reqPrompt
|
||||
reqInfo
|
||||
reqHeader
|
||||
reqFooter
|
||||
reqList
|
||||
reqJump
|
||||
reqActivate
|
||||
reqReinit
|
||||
reqFullRedraw
|
||||
reqResize
|
||||
reqRedraw
|
||||
reqRedrawInputLabel
|
||||
reqRedrawHeaderLabel
|
||||
reqRedrawFooterLabel
|
||||
reqRedrawListLabel
|
||||
reqRedrawBorderLabel
|
||||
reqRedrawPreviewLabel
|
||||
reqClose
|
||||
reqPrintQuery
|
||||
|
||||
reqPreviewReady
|
||||
reqPreviewEnqueue
|
||||
reqPreviewDisplay
|
||||
reqPreviewRefresh
|
||||
reqPreviewDelayed
|
||||
|
||||
reqActivate
|
||||
reqClose
|
||||
reqPrintQuery
|
||||
reqBecome
|
||||
reqQuit
|
||||
reqFatal
|
||||
@@ -1624,14 +1628,12 @@ func (t *Terminal) changeHeader(header string) bool {
|
||||
return needFullRedraw
|
||||
}
|
||||
|
||||
func (t *Terminal) changeFooter(footer string) bool {
|
||||
func (t *Terminal) changeFooter(footer string) {
|
||||
var lines []string
|
||||
if len(footer) > 0 {
|
||||
lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
|
||||
}
|
||||
needFullRedraw := len(t.footer) != len(lines)
|
||||
t.footer = lines
|
||||
return needFullRedraw
|
||||
}
|
||||
|
||||
// UpdateHeader updates the header
|
||||
@@ -1678,12 +1680,12 @@ func (t *Terminal) UpdateList(merger *Merger) {
|
||||
// Trimmed by --tail: filter selection by index
|
||||
filtered := make(map[int32]selectedItem)
|
||||
minIndex := merger.minIndex
|
||||
maxIndex := minIndex + int32(merger.Length())
|
||||
maxIndex := merger.maxIndex
|
||||
for k, v := range t.selected {
|
||||
var included bool
|
||||
if maxIndex > minIndex {
|
||||
included = k >= minIndex && k < maxIndex
|
||||
} else { // int32 overflow [==> <==]
|
||||
} else if maxIndex < minIndex { // int32 overflow [==> <==]
|
||||
included = k >= minIndex || k < maxIndex
|
||||
}
|
||||
if included {
|
||||
@@ -2889,19 +2891,29 @@ func (t *Terminal) resizeIfNeeded() bool {
|
||||
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
|
||||
allHeaderLines := t.visibleHeaderLines()
|
||||
primaryHeaderLines := allHeaderLines
|
||||
if t.hasHeaderLinesWindow() {
|
||||
needHeaderWindow := t.hasHeaderWindow()
|
||||
needHeaderLinesWindow := t.hasHeaderLinesWindow()
|
||||
if needHeaderLinesWindow {
|
||||
primaryHeaderLines -= t.headerLines
|
||||
}
|
||||
// FIXME: Full redraw is triggered if there are too many lines in the header
|
||||
// so that the header window cannot display all of them.
|
||||
needHeaderLinesWindow := t.hasHeaderLinesWindow()
|
||||
if (t.headerBorderShape.Visible() || needHeaderLinesWindow) &&
|
||||
(t.headerWindow == nil && primaryHeaderLines > 0 || t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
|
||||
needHeaderLinesWindow && (t.headerLinesWindow == nil || t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) ||
|
||||
!needHeaderLinesWindow && t.headerLinesWindow != nil {
|
||||
if (needHeaderWindow && t.headerWindow == nil) ||
|
||||
(!needHeaderWindow && t.headerWindow != nil) ||
|
||||
(needHeaderWindow && t.headerWindow != nil && primaryHeaderLines != t.headerWindow.Height()) ||
|
||||
(needHeaderLinesWindow && t.headerLinesWindow == nil) ||
|
||||
(!needHeaderLinesWindow && t.headerLinesWindow != nil) ||
|
||||
(needHeaderLinesWindow && t.headerLinesWindow != nil && t.headerLines != t.headerLinesWindow.Height()) {
|
||||
t.printAll()
|
||||
return true
|
||||
}
|
||||
@@ -5116,6 +5128,8 @@ func (t *Terminal) Loop() error {
|
||||
t.uiMutex.Lock()
|
||||
t.mutex.Lock()
|
||||
info := false
|
||||
header := false
|
||||
footer := false
|
||||
for _, key := range keys {
|
||||
req := util.EventType(key)
|
||||
value := (*events)[req]
|
||||
@@ -5153,13 +5167,9 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
t.printList()
|
||||
case reqHeader:
|
||||
if !t.resizeIfNeeded() {
|
||||
t.printHeader()
|
||||
}
|
||||
header = true
|
||||
case reqFooter:
|
||||
if !t.resizeIfNeeded() {
|
||||
t.printFooter()
|
||||
}
|
||||
footer = true
|
||||
case reqActivate:
|
||||
t.suppress = false
|
||||
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)
|
||||
case reqRedrawPreviewLabel:
|
||||
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.activePreviewOpts.Border(), true)
|
||||
case reqReinit:
|
||||
t.tui.Resume(t.fullscreen, true)
|
||||
t.fullRedraw()
|
||||
case reqResize, reqFullRedraw, reqRedraw:
|
||||
case reqReinit, reqResize, reqFullRedraw, reqRedraw:
|
||||
if req == reqReinit {
|
||||
t.tui.Resume(t.fullscreen, true)
|
||||
}
|
||||
if req == reqResize {
|
||||
t.termSize = t.tui.Size()
|
||||
}
|
||||
@@ -5243,8 +5253,16 @@ func (t *Terminal) Loop() error {
|
||||
return
|
||||
}
|
||||
}
|
||||
if info && !t.resizeIfNeeded() {
|
||||
t.printInfo()
|
||||
if (info || header || footer) && !t.resizeIfNeeded() {
|
||||
if info {
|
||||
t.printInfo()
|
||||
}
|
||||
if header {
|
||||
t.printHeader()
|
||||
}
|
||||
if footer {
|
||||
t.printFooter()
|
||||
}
|
||||
}
|
||||
t.flush()
|
||||
t.mutex.Unlock()
|
||||
@@ -5384,6 +5402,7 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
previousInput := t.input
|
||||
previousCx := t.cx
|
||||
previousVersion := t.version
|
||||
t.lastKey = event.KeyName()
|
||||
updatePreviewWindow := func(forcePreview bool) {
|
||||
t.resizeWindows(forcePreview, false)
|
||||
@@ -5670,24 +5689,17 @@ func (t *Terminal) Loop() error {
|
||||
t.cx = len(t.input)
|
||||
case actChangeHeader, actTransformHeader, actBgTransformHeader:
|
||||
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.headerWindow != nil {
|
||||
// Need to resize header window
|
||||
req(reqRedraw)
|
||||
} else {
|
||||
req(reqHeader, reqList, reqPrompt, reqInfo)
|
||||
}
|
||||
} else {
|
||||
req(reqHeader)
|
||||
req(reqList, reqPrompt, reqInfo)
|
||||
}
|
||||
req(reqHeader)
|
||||
})
|
||||
case actChangeFooter, actTransformFooter, actBgTransformFooter:
|
||||
capture(false, func(footer string) {
|
||||
if t.changeFooter(footer) {
|
||||
req(reqRedraw)
|
||||
} else {
|
||||
req(reqFooter)
|
||||
}
|
||||
t.changeFooter(footer)
|
||||
req(reqFooter)
|
||||
})
|
||||
case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
if onMultis, prs := t.keymap[tui.Multi.AsEvent()]; t.version != previousVersion && prs && !doActions(onMultis) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
jumpEvent := tui.JumpCancel
|
||||
if event.Type == tui.Rune {
|
||||
|
||||
@@ -110,11 +110,12 @@ func _() {
|
||||
_ = x[Jump-99]
|
||||
_ = x[JumpCancel-100]
|
||||
_ = 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 {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
||||
@@ -132,6 +132,7 @@ const (
|
||||
Jump
|
||||
JumpCancel
|
||||
ClickHeader
|
||||
Multi
|
||||
)
|
||||
|
||||
func (t EventType) AsEvent() Event {
|
||||
|
||||
@@ -1930,7 +1930,10 @@ class TestCore < TestInteractive
|
||||
|
||||
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.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.until { |lines| assert lines.any_include?('bar') }
|
||||
end
|
||||
@@ -1981,4 +1984,55 @@ class TestCore < TestInteractive
|
||||
refute lines.any_include?('[1]')
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user