Compare commits

...

14 Commits

Author SHA1 Message Date
junegunn
9249ea1739 Deploying to master from @ junegunn/fzf@92bfe68c74 🚀
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
build / build (push) Waiting to run
Test fzf on macOS / build (push) Waiting to run
2026-03-07 15:02:15 +00:00
Junegunn Choi
92bfe68c74 Use a shared work queue instead of static partitioning in matcher
Replace static chunk partitioning (sliceChunks) with a shared atomic
counter that workers pull from. This gives natural load balancing;
workers that finish chunks quickly grab more work instead of idling.

With this change, NumCPU workers suffice (no need for 8x oversubscription),
reducing goroutine overhead while improving throughput by 5-22%.

Now the performance scales linearly to the number of threads:

=== query: 'linux' ===
  [all]   baseline:    17.12ms  current:    14.28ms  (1.20x)  matches: 179966 (12.79%)
  [1T]    baseline:   136.49ms  current:   137.25ms  (0.99x)  matches: 179966 (12.79%)
  [2T]    baseline:    75.74ms  current:    68.75ms  (1.10x)  matches: 179966 (12.79%)
  [4T]    baseline:    41.16ms  current:    34.97ms  (1.18x)  matches: 179966 (12.79%)
  [8T]    baseline:    32.82ms  current:    17.79ms  (1.84x)  matches: 179966 (12.79%)
2026-03-07 18:26:42 +09:00
Junegunn Choi
92dc40ea82 Print ingestion time in --bench output 2026-03-07 18:13:38 +09:00
Junegunn Choi
12a280ba14 Fix lint errors 2026-03-07 18:13:38 +09:00
Junegunn Choi
0c6ead6e98 Replace procFun map with fixed-size array for faster algo dispatch
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
termType is already a small integer enum (0-5), so a [6]algo.Algo
array avoids hash table overhead in the extendedMatch hot loop.
2026-03-07 14:19:05 +09:00
Junegunn Choi
280a011f02 With a non-default --delimiter, --{accept,with}-nth should not remove trailing whitespaces 2026-03-07 13:39:55 +09:00
Junegunn Choi
d324580840 Fix AWK tokenizer not treating a new line character as whitespace 2026-03-07 11:45:02 +09:00
Junegunn Choi
f9830c5a3d Fix test cases not to fail on small screens (contd.)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2026-03-06 19:43:16 +09:00
Junegunn Choi
95bc5b8f0c Fix test cases not to fail on small screens 2026-03-06 19:42:42 +09:00
Junegunn Choi
0b08f0dea0 Fix preview follow/scroll with long wrapped lines
Fixes bugs reported in https://github.com/junegunn/fzf/pull/4703:

* Clamp followOffset return value to avoid going past the end of lines
* Account for t.previewed.filled when determining scrollability
2026-03-06 19:21:22 +09:00
Junegunn Choi
e7300fe300 Fix tab width when --frozen-left is used
https://github.com/junegunn/fzf/pull/4703#issuecomment-4004258816
2026-03-06 18:53:23 +09:00
dependabot[bot]
260d160973 Bump actions/labeler from 5 to 6 (#4700)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 23:58:37 +09:00
Laurent Cheylus
d57ed157ad Remove tmppath pledge on OpenBSD (#4699)
"tmppath" pledge is no longer supported.
See commit c883e836f4

Signed-off-by: Laurent Cheylus <foxy@free.fr>
2026-03-02 22:55:13 +09:00
Junegunn Choi
9226bc605d Fix typos CI failure by excluding .s files 2026-03-02 22:49:54 +09:00
17 changed files with 101 additions and 84 deletions

View File

@@ -12,6 +12,6 @@ jobs:
label: label:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/labeler@v5 - uses: actions/labeler@v6
with: with:
configuration-path: .github/labeler.yml configuration-path: .github/labeler.yml

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2" builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() { __fzf_exec_awk() {

View File

@@ -102,9 +102,9 @@ if [[ -o interactive ]]; then
# the changes. See code comments in "common.sh" for the implementation details. # the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2" builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() { __fzf_exec_awk() {

View File

@@ -25,9 +25,9 @@ if [[ $- =~ i ]]; then
# the changes. See code comments in "common.sh" for the implementation details. # the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2" builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() { __fzf_exec_awk() {

View File

@@ -45,9 +45,9 @@ if [[ -o interactive ]]; then
# the changes. See code comments in "common.sh" for the implementation details. # the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1" builtin printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2" builtin printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() { __fzf_exec_awk() {

View File

@@ -34,9 +34,7 @@ const (
maxBgProcessesPerAction = 3 maxBgProcessesPerAction = 3
// Matcher // Matcher
numPartitionsMultiplier = 8 progressMinDuration = 200 * time.Millisecond
maxPartitions = 32
progressMinDuration = 200 * time.Millisecond
// Capacity of each chunk // Capacity of each chunk
chunkSize int = 1000 chunkSize int = 1000

View File

@@ -195,11 +195,13 @@ func Run(opts *Options) (int, error) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0 streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync && opts.Bench == 0
var reader *Reader var reader *Reader
var ingestionStart time.Time
if !streamingFilter { if !streamingFilter {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
ingestionStart = time.Now()
readyChan := make(chan bool) readyChan := make(chan bool)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan) go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
<-readyChan <-readyChan
@@ -283,6 +285,7 @@ func Run(opts *Options) (int, error) {
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
ingestionTime := time.Since(ingestionStart)
// NOTE: Streaming filter is inherently not compatible with --tail // NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail) snapshot, _, _ := chunkList.Snapshot(opts.Tail)
@@ -316,13 +319,14 @@ func Run(opts *Options) (int, error) {
} }
avg := total / time.Duration(len(times)) avg := total / time.Duration(len(times))
selectivity := float64(matchCount) / float64(totalItems) * 100 selectivity := float64(matchCount) / float64(totalItems) * 100
fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%)\n", fmt.Printf(" %d iterations avg: %.2fms min: %.2fms max: %.2fms total: %.2fs items: %d matches: %d (%.2f%%) ingestion: %.2fms\n",
len(times), len(times),
float64(avg.Microseconds())/1000, float64(avg.Microseconds())/1000,
float64(minD.Microseconds())/1000, float64(minD.Microseconds())/1000,
float64(maxD.Microseconds())/1000, float64(maxD.Microseconds())/1000,
total.Seconds(), total.Seconds(),
totalItems, matchCount, selectivity) totalItems, matchCount, selectivity,
float64(ingestionTime.Microseconds())/1000)
return ExitOk, nil return ExitOk, nil
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"runtime" "runtime"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@@ -57,7 +58,7 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern, func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision revision, threads int) *Matcher {
partitions := min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := runtime.NumCPU()
if threads > 0 { if threads > 0 {
partitions = threads partitions = threads
} }
@@ -148,27 +149,6 @@ func (m *Matcher) Loop() {
} }
} }
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
partitions := m.partitions
perSlice := len(chunks) / partitions
if perSlice == 0 {
partitions = len(chunks)
perSlice = 1
}
slices := make([][]*Chunk, partitions)
for i := 0; i < partitions; i++ {
start := i * perSlice
end := start + perSlice
if i == partitions-1 {
end = len(chunks)
}
slices[i] = chunks[start:end]
}
return slices
}
type partialResult struct { type partialResult struct {
index int index int
matches []Result matches []Result
@@ -192,39 +172,37 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex) maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) numWorkers := min(m.partitions, numChunks)
numSlices := len(slices) var nextChunk atomic.Int32
resultChan := make(chan partialResult, numSlices) resultChan := make(chan partialResult, numWorkers)
countChan := make(chan int, numChunks) countChan := make(chan int, numChunks)
waitGroup := sync.WaitGroup{} waitGroup := sync.WaitGroup{}
for idx, chunks := range slices { for idx := range numWorkers {
waitGroup.Add(1) waitGroup.Add(1)
if m.slab[idx] == nil { if m.slab[idx] == nil {
m.slab[idx] = util.MakeSlab(slab16Size, slab32Size) m.slab[idx] = util.MakeSlab(slab16Size, slab32Size)
} }
go func(idx int, slab *util.Slab, chunks []*Chunk) { go func(idx int, slab *util.Slab) {
defer func() { waitGroup.Done() }() defer waitGroup.Done()
count := 0 var matches []Result
allMatches := make([][]Result, len(chunks)) for {
for idx, chunk := range chunks { ci := int(nextChunk.Add(1)) - 1
matches := request.pattern.Match(chunk, slab) if ci >= numChunks {
allMatches[idx] = matches break
count += len(matches) }
chunkMatches := request.pattern.Match(request.chunks[ci], slab)
matches = append(matches, chunkMatches...)
if cancelled.Get() { if cancelled.Get() {
return return
} }
countChan <- len(matches) countChan <- len(chunkMatches)
}
sliceMatches := make([]Result, 0, count)
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
} }
if m.sort && request.pattern.sortable { if m.sort && request.pattern.sortable {
m.sortBuf[idx] = radixSortResults(sliceMatches, m.tac, m.sortBuf[idx]) m.sortBuf[idx] = radixSortResults(matches, m.tac, m.sortBuf[idx])
} }
resultChan <- partialResult{idx, sliceMatches} resultChan <- partialResult{idx, matches}
}(idx, m.slab[idx], chunks) }(idx, m.slab[idx])
} }
wait := func() bool { wait := func() bool {
@@ -252,8 +230,8 @@ func (m *Matcher) scan(request MatchRequest) MatchResult {
} }
} }
partialResults := make([][]Result, numSlices) partialResults := make([][]Result, numWorkers)
for range slices { for range numWorkers {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }

View File

@@ -61,7 +61,7 @@ type Pattern struct {
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
revision revision revision revision
procFun map[termType]algo.Algo procFun [6]algo.Algo
cache *ChunkCache cache *ChunkCache
denylist map[int32]struct{} denylist map[int32]struct{}
startIndex int32 startIndex int32
@@ -150,7 +150,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
cache: cache, cache: cache,
denylist: denylist, denylist: denylist,
startIndex: startIndex, startIndex: startIndex,
procFun: make(map[termType]algo.Algo)} }
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo) ptr.directAlgo, ptr.directTerm = ptr.buildDirectAlgo(fuzzyAlgo)

View File

@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath") unix.PledgePromises("stdio cpath dpath wpath rpath tty proc exec inet")
} }

View File

@@ -3905,6 +3905,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
frozenRight = line[splitOffsetRight:] frozenRight = line[splitOffsetRight:]
} }
displayWidthSum := 0 displayWidthSum := 0
displayWidthLeft := 0
todo := [3]func(){} todo := [3]func(){}
for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} { for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} {
if len(runes) == 0 { if len(runes) == 0 {
@@ -3930,7 +3931,11 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
// For frozen parts, reserve space for the ellipsis in the middle part // For frozen parts, reserve space for the ellipsis in the middle part
adjustedMaxWidth -= ellipsisWidth adjustedMaxWidth -= ellipsisWidth
} }
displayWidth = t.displayWidthWithLimit(runes, 0, adjustedMaxWidth) var prefixWidth int
if fidx == 2 {
prefixWidth = displayWidthLeft
}
displayWidth = t.displayWidthWithLimit(runes, prefixWidth, adjustedMaxWidth)
if !t.wrap && displayWidth > adjustedMaxWidth { if !t.wrap && displayWidth > adjustedMaxWidth {
maxe = util.Constrain(maxe+min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes)) maxe = util.Constrain(maxe+min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes))
transformOffsets := func(diff int32) { transformOffsets := func(diff int32) {
@@ -3968,6 +3973,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth) displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
} }
displayWidthSum += displayWidth displayWidthSum += displayWidth
if fidx == 0 {
displayWidthLeft = displayWidth
}
if maxWidth > 0 { if maxWidth > 0 {
color := colBase color := colBase
@@ -3975,7 +3983,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
color = color.WithFg(t.theme.Nomatch) color = color.WithFg(t.theme.Nomatch)
} }
todo[fidx] = func() { todo[fidx] = func() {
t.printColoredString(t.window, runes, offs, color) t.printColoredString(t.window, runes, offs, color, prefixWidth)
} }
} else { } else {
break break
@@ -4002,10 +4010,13 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
return finalLineNum return finalLineNum
} }
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) { func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair, initialPrefixWidth ...int) {
var index int32 var index int32
var substr string var substr string
var prefixWidth int var prefixWidth int
if len(initialPrefixWidth) > 0 {
prefixWidth = initialPrefixWidth[0]
}
maxOffset := int32(len(text)) maxOffset := int32(len(text))
var url *url var url *url
for _, offset := range offsets { for _, offset := range offsets {
@@ -4212,7 +4223,7 @@ func (t *Terminal) followOffset() int {
for i := len(body) - 1; i >= 0; i-- { for i := len(body) - 1; i >= 0; i-- {
h := t.previewLineHeight(body[i], maxWidth) h := t.previewLineHeight(body[i], maxWidth)
if visualLines+h > height { if visualLines+h > height {
return headerLines + i + 1 return min(len(lines)-1, headerLines+i+1)
} }
visualLines += h visualLines += h
} }
@@ -4510,7 +4521,7 @@ Loop:
} }
} }
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width() || t.previewed.filled
if fillRet == tui.FillNextLine { if fillRet == tui.FillNextLine {
continue continue
} else if fillRet == tui.FillSuspend { } else if fillRet == tui.FillSuspend {
@@ -4533,7 +4544,7 @@ Loop:
} }
lineNo++ lineNo++
} }
t.previewer.scrollable = t.previewer.scrollable || index < len(lines)-1 t.previewer.scrollable = t.previewer.scrollable || t.previewed.filled || index < len(lines)-1
t.previewed.image = image t.previewed.image = image
t.previewed.wireframe = wireframe t.previewed.wireframe = wireframe
} }

View File

@@ -161,7 +161,7 @@ func awkTokenizer(input string) ([]string, int) {
end := 0 end := 0
for idx := 0; idx < len(input); idx++ { for idx := 0; idx < len(input); idx++ {
r := input[idx] r := input[idx]
white := r == 9 || r == 32 white := r == 9 || r == 32 || r == 10
switch state { switch state {
case awkNil: case awkNil:
if white { if white {
@@ -218,11 +218,12 @@ func Tokenize(text string, delimiter Delimiter) []Token {
return withPrefixLengths(tokens, 0) return withPrefixLengths(tokens, 0)
} }
// StripLastDelimiter removes the trailing delimiter and whitespaces // StripLastDelimiter removes the trailing delimiter
func StripLastDelimiter(str string, delimiter Delimiter) string { func StripLastDelimiter(str string, delimiter Delimiter) string {
if delimiter.str != nil { if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str) return strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil { }
if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(str, -1) locs := delimiter.regex.FindAllStringIndex(str, -1)
if len(locs) > 0 { if len(locs) > 0 {
lastLoc := locs[len(locs)-1] lastLoc := locs[len(locs)-1]
@@ -230,6 +231,7 @@ func StripLastDelimiter(str string, delimiter Delimiter) string {
str = str[:lastLoc[0]] str = str[:lastLoc[0]]
} }
} }
return str
} }
return strings.TrimRightFunc(str, unicode.IsSpace) return strings.TrimRightFunc(str, unicode.IsSpace)
} }

View File

@@ -56,9 +56,9 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) { func TestTokenize(t *testing.T) {
// AWK-style // AWK-style
input := " abc: def: ghi " input := " abc: \n\t def: ghi "
tokens := Tokenize(input, Delimiter{}) tokens := Tokenize(input, Delimiter{})
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 { if tokens[0].text.ToString() != "abc: \n\t " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
@@ -71,9 +71,9 @@ func TestTokenize(t *testing.T) {
// With delimiter regex // With delimiter regex
tokens = Tokenize(input, delimiterRegexp("\\s+")) tokens = Tokenize(input, delimiterRegexp("\\s+"))
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].text.ToString() != "abc: \n\t " || tokens[1].prefixLength != 2 ||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 10 ||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 { tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 16 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }

View File

@@ -1190,6 +1190,16 @@ class TestCore < TestInteractive
tmux.until { |lines| assert lines.any_include?('9999␊10000') } tmux.until { |lines| assert lines.any_include?('9999␊10000') }
end end
def test_freeze_left_tabstop
writelines(%W[1\t2\t3])
# With --freeze-left 1 and --tabstop=2:
# Frozen left: "1" (width 1)
# Middle starts with "\t" at prefix width 1, tabstop 2 → 1 space
# Then "2" at column 2, next "\t" at column 3 → 1 space, then "3"
tmux.send_keys %(cat #{tempname} | #{FZF} --tabstop=2 --freeze-left 1), :Enter
tmux.until { |lines| assert_equal '> 1 2 3', lines[-3] }
end
def test_freeze_left_keep_right def test_freeze_left_keep_right
tmux.send_keys %(seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line), :Enter tmux.send_keys %(seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line), :Enter
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) } tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
@@ -2085,13 +2095,13 @@ class TestCore < TestInteractive
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
wait do wait do
assert_path_exists tempname assert_path_exists tempname
# Last delimiter and the whitespaces are removed # Last delimiter is removed
assert_equal ['bar,bar,foo ,bazfoo'], File.readlines(tempname, chomp: true) assert_equal ['bar,bar,foo ,bazfoo '], File.readlines(tempname, chomp: true)
end end
end end
def test_accept_nth_regex_delimiter def test_accept_nth_regex_delimiter
tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter='[:,]+' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter tmux.send_keys %(echo "foo :,:bar,baz" | #{FZF} --delimiter=' *[:,]+ *' --accept-nth 2,2,1,3,1 --sync --bind start:accept > #{tempname}), :Enter
wait do wait do
assert_path_exists tempname assert_path_exists tempname
# Last delimiter and the whitespaces are removed # Last delimiter and the whitespaces are removed
@@ -2109,7 +2119,7 @@ class TestCore < TestInteractive
end end
def test_accept_nth_template def test_accept_nth_template
tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d, --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter tmux.send_keys %(echo "foo ,bar,baz" | #{FZF} -d " *, *" --accept-nth '[{n}] 1st: {1}, 3rd: {3}, 2nd: {2}' --sync --bind start:accept > #{tempname}), :Enter
wait do wait do
assert_path_exists tempname assert_path_exists tempname
# Last delimiter and the whitespaces are removed # Last delimiter and the whitespaces are removed

View File

@@ -393,6 +393,20 @@ class TestPreview < TestInteractive
end end
end end
def test_preview_follow_wrap_long_line
tmux.send_keys %(seq 1 | #{FZF} --preview "seq 2; yes yes | head -10000 | tr '\n' ' '" --preview-window follow,wrap --bind up:preview-up,down:preview-down), :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert lines.any_include?('3/3 │')
end
tmux.send_keys :Up
tmux.until { |lines| assert lines.any_include?('2/3 │') }
tmux.send_keys :Up
tmux.until { |lines| assert lines.any_include?('1/3 │') }
tmux.send_keys :Down
tmux.until { |lines| assert lines.any_include?('2/3 │') }
end
def test_close def test_close
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count } tmux.until { |lines| assert_equal 100, lines.match_count }
@@ -593,7 +607,7 @@ class TestPreview < TestInteractive
end end
def test_preview_wrap_sign_between_ansi_fragments_overflow def test_preview_wrap_sign_between_ansi_fragments_overflow
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 2,wrap-word), :Enter tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m123 \\x1b[mhi"; echo -e "\\x1b[33m123 \\x1b[mhi"' --preview-window 2,wrap-word,noinfo), :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal 1, lines.match_count assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 12 │') }) assert_equal(2, lines.count { |line| line.include?('│ 12 │') })
@@ -602,7 +616,7 @@ class TestPreview < TestInteractive
end end
def test_preview_wrap_sign_between_ansi_fragments_overflow2 def test_preview_wrap_sign_between_ansi_fragments_overflow2
tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m1234567890 \\x1b[mhello"; echo -e "\\x1b[33m1234567890 \\x1b[mhello"' --preview-window 1,wrap-word), :Enter tmux.send_keys %(seq 1 | #{FZF} --preview 'echo -e "\\x1b[33m123 \\x1b[mhi"; echo -e "\\x1b[33m123 \\x1b[mhi"' --preview-window 1,wrap-word,noinfo), :Enter
tmux.until do |lines| tmux.until do |lines|
assert_equal 1, lines.match_count assert_equal 1, lines.match_count
assert_equal(2, lines.count { |line| line.include?('│ 1 │') }) assert_equal(2, lines.count { |line| line.include?('│ 1 │') })

View File

@@ -7,4 +7,4 @@ tabe = "tabe"
Iterm = "Iterm" Iterm = "Iterm"
[files] [files]
extend-exclude = ["README.md"] extend-exclude = ["README.md", "*.s"]