mirror of
https://github.com/junegunn/fzf.git
synced 2026-08-31 20:07:04 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3900dd17e4 | ||
|
|
5e73c2ddd3 | ||
|
|
18e5009e0f |
@@ -3,9 +3,6 @@ CHANGELOG
|
||||
|
||||
0.74.4
|
||||
------
|
||||
- Fixed an escape sequence split across reads being parsed as a fragment, which leaked the rest into the query (#4899)
|
||||
- e.g. A terminal answering the startup `DECRQM` query late left `?2004;2$y`, CTRL-UP left `5A`, and SGR mouse input left `0;1;1M`
|
||||
- Fixed `--tiebreak=pathname` not detecting the last path separator when the line contains a non-ASCII character before it (#4902)
|
||||
- Vim plugin
|
||||
- fzf no longer blocks the editor, so live previews keep working while fzf is open
|
||||
- `fzf#run` returns an empty list when it runs fzf asynchronously. Use `sink`, `sinklist`, or `exit` to get the result
|
||||
|
||||
+3
-3
@@ -82,10 +82,10 @@ func buildResultFromBounds(item *Item, score int, minBegin, minEnd, maxEnd int,
|
||||
val = item.TrimLength()
|
||||
case byPathname:
|
||||
if validOffsetFound {
|
||||
// Rune index, to be comparable with minBegin
|
||||
lastDelim := -1
|
||||
for i := numChars - 1; i >= 0; i-- {
|
||||
if r := item.text.Get(i); r == '/' || r == '\\' {
|
||||
s := item.text.ToString()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '/' || s[i] == '\\' {
|
||||
lastDelim = i
|
||||
break
|
||||
}
|
||||
|
||||
@@ -272,28 +272,3 @@ func TestRadixSortResults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathnameTiebreak(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byPathname}
|
||||
|
||||
score := 100
|
||||
test := func(input string, offset Offset, expected uint16) {
|
||||
for _, chars := range []util.Chars{util.ToChars([]byte(input)), util.RunesToChars([]rune(input))} {
|
||||
item := buildResult(withIndex(&Item{text: chars}, 1), []Offset{offset}, score)
|
||||
if item.points[3] != math.MaxUint16-uint16(score) || item.points[2] != expected {
|
||||
t.Error(input, item.points, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match in the file name
|
||||
test("x/foo/foo.txt", Offset{6, 9}, 1)
|
||||
// Match in the directory path
|
||||
test("x/foo/aa.txt", Offset{2, 5}, math.MaxUint16)
|
||||
|
||||
// Offsets are rune indexes, so a multi-byte character before the last
|
||||
// delimiter must not shift the delimiter position
|
||||
test("一x/foo/foo.txt", Offset{7, 10}, 1)
|
||||
test("一x/foo/aa.txt", Offset{3, 6}, math.MaxUint16)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
// Pin $SHELL so the quoting style doesn't depend on the test runner's shell
|
||||
t.Setenv("SHELL", "cmd")
|
||||
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := [3][]*Item{{item1}, {item1}, nil}
|
||||
items2 := [3][]*Item{
|
||||
@@ -258,9 +255,6 @@ func TestQuoteEntry(t *testing.T) {
|
||||
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`, `\`}
|
||||
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`, `\`}
|
||||
var effectiveStyle quotes
|
||||
|
||||
// Pin $SHELL so the quoting style doesn't depend on the test runner's shell
|
||||
t.Setenv("SHELL", "cmd")
|
||||
exec := util.NewExecutor("")
|
||||
|
||||
if util.IsWindows() {
|
||||
|
||||
@@ -26,7 +26,6 @@ const (
|
||||
offsetPollTries = 10
|
||||
queryTimeout = 500 * time.Millisecond
|
||||
maxInputBuffer = 1024 * 1024
|
||||
escapeLookback = 256
|
||||
maxSelectTries = 100
|
||||
)
|
||||
|
||||
@@ -339,45 +338,6 @@ func getEnv(name string, defaultValue int) int {
|
||||
return atoi(env, defaultValue)
|
||||
}
|
||||
|
||||
// Bytes of a CSI sequence: parameter and intermediate bytes continue it, a
|
||||
// final byte ends it. Order is not enforced. Strictness would only make fzf
|
||||
// give up on a sequence it could have framed.
|
||||
//
|
||||
// https://vt100.net/emu/dec_ansi_parser
|
||||
func csiContinues(b byte) bool { return b >= 0x20 && b <= 0x3f }
|
||||
func csiFinal(b byte) bool { return b >= 0x40 && b <= 0x7e }
|
||||
|
||||
// incompleteEscape reports whether the buffer ends in an escape sequence that
|
||||
// has not been terminated yet. The read loop keeps waiting in that case, so the
|
||||
// parser is never handed a fragment to guess at.
|
||||
func incompleteEscape(buffer []byte) bool {
|
||||
// Only the tail can hold a sequence still arriving. This runs once per byte
|
||||
// read, so scanning all of a large paste would make the read quadratic.
|
||||
tail := buffer
|
||||
if len(tail) > escapeLookback {
|
||||
tail = tail[len(tail)-escapeLookback:]
|
||||
}
|
||||
start := bytes.LastIndexByte(tail, Esc.Byte())
|
||||
if start < 0 || len(tail)-start < 2 {
|
||||
return false
|
||||
}
|
||||
switch tail[start+1] {
|
||||
case '[':
|
||||
for _, b := range tail[start+2:] {
|
||||
if csiFinal(b) {
|
||||
return false
|
||||
}
|
||||
if !csiContinues(b) {
|
||||
return false // malformed, do not wait for a terminator
|
||||
}
|
||||
}
|
||||
return true
|
||||
case 'O':
|
||||
return len(tail)-start < 3
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
|
||||
return r.getBytesInternal(cancellable, r.buffer, false)
|
||||
}
|
||||
@@ -418,13 +378,6 @@ func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblo
|
||||
retries = 0
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
// Keep waiting while a sequence is still arriving. Dropping the budget
|
||||
// after every byte left fzf parsing whatever the read happened to end on.
|
||||
// Past the introducer this is not the ESC key, so the wait costs no
|
||||
// Escape latency and ESCDELAY=0 must not reduce it to nothing.
|
||||
if retries == 0 && incompleteEscape(buffer) {
|
||||
retries = max(r.escDelay, defaultEscDelay) / escPollInterval
|
||||
}
|
||||
pc = c
|
||||
|
||||
// This should never happen under normal conditions,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIncompleteEscape(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
buffer string
|
||||
want bool
|
||||
}{
|
||||
// Complete sequences: nothing to wait for
|
||||
{"\x1b[A", false},
|
||||
{"\x1bOA", false},
|
||||
{"\x1b[1;5A", false},
|
||||
{"\x1b[200~", false},
|
||||
{"\x1b[<0;1;1M", false},
|
||||
{"\x1b[12;34R", false},
|
||||
{"\x1b[?2004;2$y", false},
|
||||
{"\x1b[?1;2c", false},
|
||||
|
||||
// Fragments: keep waiting
|
||||
{"\x1b[", true},
|
||||
{"\x1b[?", true},
|
||||
{"\x1b[1;", true},
|
||||
{"\x1b[?2004;2$", true},
|
||||
{"\x1bO", true},
|
||||
{"\x1b[<0;1;", true},
|
||||
|
||||
// Only the trailing sequence matters
|
||||
{"ab\x1b[?2004;2$", true},
|
||||
{"\x1b[A\x1b[", true},
|
||||
{"\x1b[A\x1b[B", false},
|
||||
|
||||
// Long buffers: only the tail is scanned, so an introducer further
|
||||
// back than escapeLookback is not waited for
|
||||
{strings.Repeat("a", 100000), false},
|
||||
{"\x1b[" + strings.Repeat("a", 100000), false},
|
||||
{strings.Repeat("a", 100000) + "\x1b[1;", true},
|
||||
|
||||
// Not a sequence fzf waits on
|
||||
{"", false},
|
||||
{"abc", false},
|
||||
{"\x1b", false}, // lone ESC, handled by the existing escDelay branch
|
||||
{"\x1ba", false}, // ALT-a
|
||||
{"\x1b[\x01", false}, // malformed, do not stall on it
|
||||
} {
|
||||
if got := incompleteEscape([]byte(c.buffer)); got != c.want {
|
||||
t.Errorf("incompleteEscape(%q) = %v, want %v", c.buffer, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user