mirror of
https://github.com/junegunn/fzf.git
synced 2026-08-28 02:19:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7ae439ff5 | ||
|
|
d46bfd005a | ||
|
|
d0377ed4dc | ||
|
|
5cb7bab702 |
@@ -29,6 +29,9 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplacePlaceholder(t *testing.T) {
|
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")
|
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||||
items1 := [3][]*Item{{item1}, {item1}, nil}
|
items1 := [3][]*Item{{item1}, {item1}, nil}
|
||||||
items2 := [3][]*Item{
|
items2 := [3][]*Item{
|
||||||
@@ -255,6 +258,9 @@ func TestQuoteEntry(t *testing.T) {
|
|||||||
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`, `\`}
|
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`, `\`}
|
||||||
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`, `\`}
|
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`, `\`}
|
||||||
var effectiveStyle 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("")
|
exec := util.NewExecutor("")
|
||||||
|
|
||||||
if util.IsWindows() {
|
if util.IsWindows() {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const (
|
|||||||
offsetPollTries = 10
|
offsetPollTries = 10
|
||||||
queryTimeout = 500 * time.Millisecond
|
queryTimeout = 500 * time.Millisecond
|
||||||
maxInputBuffer = 1024 * 1024
|
maxInputBuffer = 1024 * 1024
|
||||||
|
escapeLookback = 256
|
||||||
maxSelectTries = 100
|
maxSelectTries = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -338,6 +339,45 @@ func getEnv(name string, defaultValue int) int {
|
|||||||
return atoi(env, defaultValue)
|
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) {
|
func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
|
||||||
return r.getBytesInternal(cancellable, r.buffer, false)
|
return r.getBytesInternal(cancellable, r.buffer, false)
|
||||||
}
|
}
|
||||||
@@ -378,6 +418,13 @@ func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblo
|
|||||||
retries = 0
|
retries = 0
|
||||||
}
|
}
|
||||||
buffer = append(buffer, byte(c))
|
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
|
pc = c
|
||||||
|
|
||||||
// This should never happen under normal conditions,
|
// This should never happen under normal conditions,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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