This commit is contained in:
Junegunn Choi 2015-01-12 03:01:24 +09:00
parent 9dbf6b02d2
commit 7a2bc2cada
24 changed files with 478 additions and 405 deletions

View File

@ -10,6 +10,7 @@ import "strings"
* In short: They try to do as little work as possible.
*/
// FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
@ -36,7 +37,7 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
if sidx < 0 {
sidx = index
}
if pidx += 1; pidx == len(pattern) {
if pidx++; pidx == len(pattern) {
eidx = index + 1
break
}
@ -44,14 +45,14 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
}
if sidx >= 0 && eidx >= 0 {
pidx -= 1
pidx--
for index := eidx - 1; index >= sidx; index-- {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
}
if char == pattern[pidx] {
if pidx -= 1; pidx < 0 {
if pidx--; pidx < 0 {
sidx = index
break
}
@ -62,6 +63,8 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
return -1, -1
}
// ExactMatchStrings performs exact-match using strings package.
// Currently not used.
func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) {
var str string
if caseSensitive {
@ -77,15 +80,13 @@ func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int,
return -1, -1
}
/*
* This is a basic string searching algorithm that handles case sensitivity.
* Although naive, it still performs better than the combination of
* strings.ToLower + strings.Index for typical fzf use cases where input
* strings and patterns are not very long.
*
* We might try to implement better algorithms in the future:
* http://en.wikipedia.org/wiki/String_searching_algorithm
*/
// ExactMatchNaive is a basic string searching algorithm that handles case
// sensitivity. Although naive, it still performs better than the combination
// of strings.ToLower + strings.Index for typical fzf use cases where input
// strings and patterns are not very long.
//
// We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
numRunes := len(runes)
@ -101,7 +102,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in
char += 32
}
if pattern[pidx] == char {
pidx += 1
pidx++
if pidx == plen {
return index - plen + 1, index + 1
}
@ -113,6 +114,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in
return -1, -1
}
// PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
if len(runes) < len(pattern) {
@ -131,6 +133,7 @@ func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
return 0, len(pattern)
}
// SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(strings.TrimRight(*input, " "))
trimmedLen := len(runes)

View File

@ -2,23 +2,28 @@ package fzf
import "sync"
// AtomicBool is a boxed-class that provides synchronized access to the
// underlying boolean value
type AtomicBool struct {
mutex sync.Mutex
state bool
}
// NewAtomicBool returns a new AtomicBool
func NewAtomicBool(initialState bool) *AtomicBool {
return &AtomicBool{
mutex: sync.Mutex{},
state: initialState}
}
// Get returns the current boolean value synchronously
func (a *AtomicBool) Get() bool {
a.mutex.Lock()
defer a.mutex.Unlock()
return a.state
}
// Set updates the boolean value synchronously
func (a *AtomicBool) Set(newState bool) bool {
a.mutex.Lock()
defer a.mutex.Unlock()

View File

@ -2,16 +2,21 @@ package fzf
import "sync"
// QueryCache associates strings to lists of items
type QueryCache map[string][]*Item
// ChunkCache associates Chunk and query string to lists of items
type ChunkCache struct {
mutex sync.Mutex
cache map[*Chunk]*QueryCache
}
// NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)}
}
// Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
if len(key) == 0 || !chunk.IsFull() {
return
@ -28,6 +33,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
(*qc)[key] = list
}
// Find is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) {
if len(key) == 0 || !chunk.IsFull() {
return nil, false

View File

@ -4,7 +4,7 @@ import "testing"
func TestChunkCache(t *testing.T) {
cache := NewChunkCache()
chunk2 := make(Chunk, CHUNK_SIZE)
chunk2 := make(Chunk, ChunkSize)
chunk1p := &Chunk{}
chunk2p := &chunk2
items1 := []*Item{&Item{}}

View File

@ -2,12 +2,17 @@ package fzf
import "sync"
const CHUNK_SIZE int = 100
// Capacity of each chunk
const ChunkSize int = 100
// Chunk is a list of Item pointers whose size has the upper limit of ChunkSize
type Chunk []*Item // >>> []Item
// Transformer is a closure type that builds Item object from a pointer to a
// string and an integer
type Transformer func(*string, int) *Item
// ChunkList is a list of Chunks
type ChunkList struct {
chunks []*Chunk
count int
@ -15,6 +20,7 @@ type ChunkList struct {
trans Transformer
}
// NewChunkList returns a new ChunkList
func NewChunkList(trans Transformer) *ChunkList {
return &ChunkList{
chunks: []*Chunk{},
@ -27,34 +33,38 @@ func (c *Chunk) push(trans Transformer, data *string, index int) {
*c = append(*c, trans(data, index))
}
// IsFull returns true if the Chunk is full
func (c *Chunk) IsFull() bool {
return len(*c) == CHUNK_SIZE
return len(*c) == ChunkSize
}
func (cl *ChunkList) lastChunk() *Chunk {
return cl.chunks[len(cl.chunks)-1]
}
// CountItems returns the total number of Items
func CountItems(cs []*Chunk) int {
if len(cs) == 0 {
return 0
}
return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1]))
return ChunkSize*(len(cs)-1) + len(*(cs[len(cs)-1]))
}
// Push adds the item to the list
func (cl *ChunkList) Push(data string) {
cl.mutex.Lock()
defer cl.mutex.Unlock()
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
newChunk := Chunk(make([]*Item, 0, CHUNK_SIZE))
newChunk := Chunk(make([]*Item, 0, ChunkSize))
cl.chunks = append(cl.chunks, &newChunk)
}
cl.lastChunk().push(cl.trans, &data, cl.count)
cl.count += 1
cl.count++
}
// Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock()
defer cl.mutex.Unlock()

View File

@ -45,7 +45,7 @@ func TestChunkList(t *testing.T) {
}
// Add more data
for i := 0; i < CHUNK_SIZE*2; i++ {
for i := 0; i < ChunkSize*2; i++ {
cl.Push(fmt.Sprintf("item %d", i))
}
@ -57,7 +57,7 @@ func TestChunkList(t *testing.T) {
// New snapshot
snapshot, count = cl.Snapshot()
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != CHUNK_SIZE*2+2 {
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != ChunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk")
}
if len(*snapshot[2]) != 2 {

View File

@ -1,12 +1,17 @@
package fzf
const VERSION = "0.9.0"
// Current version
const Version = "0.9.0"
// EventType is the type for fzf events
type EventType int
// fzf events
const (
EVT_READ_NEW EventType = iota
EVT_READ_FIN
EVT_SEARCH_NEW
EVT_SEARCH_PROGRESS
EVT_SEARCH_FIN
EVT_CLOSE
EvtReadNew EventType = iota
EvtReadFin
EvtSearchNew
EvtSearchProgress
EvtSearchFin
EvtClose
)

View File

@ -32,28 +32,29 @@ import (
"time"
)
const COORDINATOR_DELAY_MAX time.Duration = 100 * time.Millisecond
const COORDINATOR_DELAY_STEP time.Duration = 10 * time.Millisecond
const coordinatorDelayMax time.Duration = 100 * time.Millisecond
const coordinatorDelayStep time.Duration = 10 * time.Millisecond
func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
/*
Reader -> EVT_READ_FIN
Reader -> EVT_READ_NEW -> Matcher (restart)
Terminal -> EVT_SEARCH_NEW -> Matcher (restart)
Matcher -> EVT_SEARCH_PROGRESS -> Terminal (update info)
Matcher -> EVT_SEARCH_FIN -> Terminal (update list)
Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list)
*/
// Run starts fzf
func Run(options *Options) {
initProcs()
opts := ParseOptions()
if opts.Version {
fmt.Println(VERSION)
fmt.Println(Version)
os.Exit(0)
}
@ -108,12 +109,12 @@ func Run(options *Options) {
pattern := patternBuilder([]rune(patternString))
looping := true
eventBox.Unwatch(EVT_READ_NEW)
eventBox.Unwatch(EvtReadNew)
for looping {
eventBox.Wait(func(events *Events) {
for evt, _ := range *events {
for evt := range *events {
switch evt {
case EVT_READ_FIN:
case EvtReadFin:
looping = false
return
}
@ -133,7 +134,7 @@ func Run(options *Options) {
fmt.Println(patternString)
}
for i := 0; i < merger.Length(); i++ {
merger.Get(i).Print()
fmt.Println(merger.Get(i).AsString())
}
os.Exit(0)
}
@ -149,33 +150,33 @@ func Run(options *Options) {
// Event coordination
reading := true
ticks := 0
eventBox.Watch(EVT_READ_NEW)
eventBox.Watch(EvtReadNew)
for {
delay := true
ticks += 1
ticks++
eventBox.Wait(func(events *Events) {
defer events.Clear()
for evt, value := range *events {
switch evt {
case EVT_READ_NEW, EVT_READ_FIN:
reading = reading && evt == EVT_READ_NEW
case EvtReadNew, EvtReadFin:
reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false)
case EVT_SEARCH_NEW:
case EvtSearchNew:
snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true)
delay = false
case EVT_SEARCH_PROGRESS:
case EvtSearchProgress:
switch val := value.(type) {
case float32:
terminal.UpdateProgress(val)
}
case EVT_SEARCH_FIN:
case EvtSearchFin:
switch val := value.(type) {
case *Merger:
terminal.UpdateList(val)
@ -185,8 +186,8 @@ func Run(options *Options) {
})
if delay && reading {
dur := DurWithin(
time.Duration(ticks)*COORDINATOR_DELAY_STEP,
0, COORDINATOR_DELAY_MAX)
time.Duration(ticks)*coordinatorDelayStep,
0, coordinatorDelayMax)
time.Sleep(dur)
}
}

View File

@ -20,66 +20,68 @@ import (
"unicode/utf8"
)
// Types of user action
const (
RUNE = iota
Rune = iota
CTRL_A
CTRL_B
CTRL_C
CTRL_D
CTRL_E
CTRL_F
CTRL_G
CTRL_H
TAB
CTRL_J
CTRL_K
CTRL_L
CTRL_M
CTRL_N
CTRL_O
CTRL_P
CTRL_Q
CTRL_R
CTRL_S
CTRL_T
CTRL_U
CTRL_V
CTRL_W
CTRL_X
CTRL_Y
CTRL_Z
CtrlA
CtrlB
CtrlC
CtrlD
CtrlE
CtrlF
CtrlG
CtrlH
Tab
CtrlJ
CtrlK
CtrlL
CtrlM
CtrlN
CtrlO
CtrlP
CtrlQ
CtrlR
CtrlS
CtrlT
CtrlU
CtrlV
CtrlW
CtrlX
CtrlY
CtrlZ
ESC
INVALID
MOUSE
Invalid
Mouse
BTAB
BTab
DEL
PGUP
PGDN
Del
PgUp
PgDn
ALT_B
ALT_F
ALT_D
ALT_BS
AltB
AltF
AltD
AltBS
)
// Pallete
const (
ColNormal = iota
ColPrompt
ColMatch
ColCurrent
ColCurrentMatch
ColSpinner
ColInfo
ColCursor
ColSelected
)
const (
COL_NORMAL = iota
COL_PROMPT
COL_MATCH
COL_CURRENT
COL_CURRENT_MATCH
COL_SPINNER
COL_INFO
COL_CURSOR
COL_SELECTED
)
const (
DOUBLE_CLICK_DURATION = 500 * time.Millisecond
doubleClickDuration = 500 * time.Millisecond
)
type Event struct {
@ -112,8 +114,8 @@ func init() {
}
func attrColored(pair int, bold bool) C.int {
var attr C.int = 0
if pair > COL_NORMAL {
var attr C.int
if pair > ColNormal {
attr = C.COLOR_PAIR(C.int(pair))
}
if bold {
@ -123,15 +125,15 @@ func attrColored(pair int, bold bool) C.int {
}
func attrMono(pair int, bold bool) C.int {
var attr C.int = 0
var attr C.int
switch pair {
case COL_CURRENT:
case ColCurrent:
if bold {
attr = C.A_REVERSE
}
case COL_MATCH:
case ColMatch:
attr = C.A_UNDERLINE
case COL_CURRENT_MATCH:
case ColCurrentMatch:
attr = C.A_UNDERLINE | C.A_REVERSE
}
if bold {
@ -198,23 +200,23 @@ func Init(color bool, color256 bool, black bool, mouse bool) {
bg = -1
}
if color256 {
C.init_pair(COL_PROMPT, 110, bg)
C.init_pair(COL_MATCH, 108, bg)
C.init_pair(COL_CURRENT, 254, 236)
C.init_pair(COL_CURRENT_MATCH, 151, 236)
C.init_pair(COL_SPINNER, 148, bg)
C.init_pair(COL_INFO, 144, bg)
C.init_pair(COL_CURSOR, 161, 236)
C.init_pair(COL_SELECTED, 168, 236)
C.init_pair(ColPrompt, 110, bg)
C.init_pair(ColMatch, 108, bg)
C.init_pair(ColCurrent, 254, 236)
C.init_pair(ColCurrentMatch, 151, 236)
C.init_pair(ColSpinner, 148, bg)
C.init_pair(ColInfo, 144, bg)
C.init_pair(ColCursor, 161, 236)
C.init_pair(ColSelected, 168, 236)
} else {
C.init_pair(COL_PROMPT, C.COLOR_BLUE, bg)
C.init_pair(COL_MATCH, C.COLOR_GREEN, bg)
C.init_pair(COL_CURRENT, C.COLOR_YELLOW, C.COLOR_BLACK)
C.init_pair(COL_CURRENT_MATCH, C.COLOR_GREEN, C.COLOR_BLACK)
C.init_pair(COL_SPINNER, C.COLOR_GREEN, bg)
C.init_pair(COL_INFO, C.COLOR_WHITE, bg)
C.init_pair(COL_CURSOR, C.COLOR_RED, C.COLOR_BLACK)
C.init_pair(COL_SELECTED, C.COLOR_MAGENTA, C.COLOR_BLACK)
C.init_pair(ColPrompt, C.COLOR_BLUE, bg)
C.init_pair(ColMatch, C.COLOR_GREEN, bg)
C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK)
C.init_pair(ColSpinner, C.COLOR_GREEN, bg)
C.init_pair(ColInfo, C.COLOR_WHITE, bg)
C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK)
C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK)
}
_color = attrColored
} else {
@ -245,7 +247,7 @@ func GetBytes() []byte {
// 27 (91 79) 77 type x y
func mouseSequence(sz *int) Event {
if len(_buf) < 6 {
return Event{INVALID, 0, nil}
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[3] {
@ -258,7 +260,7 @@ func mouseSequence(sz *int) Event {
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < DOUBLE_CLICK_DURATION {
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
@ -266,18 +268,18 @@ func mouseSequence(sz *int) Event {
_prevDownTime = now
} else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < DOUBLE_CLICK_DURATION {
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{MOUSE, 0, &MouseEvent{y, x, 0, down, double, mod}}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2
return Event{MOUSE, 0, &MouseEvent{0, 0, s, false, false, mod}}
return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}}
}
return Event{INVALID, 0, nil}
return Event{Invalid, 0, nil}
}
func escSequence(sz *int) Event {
@ -287,81 +289,81 @@ func escSequence(sz *int) Event {
*sz = 2
switch _buf[1] {
case 98:
return Event{ALT_B, 0, nil}
return Event{AltB, 0, nil}
case 100:
return Event{ALT_D, 0, nil}
return Event{AltD, 0, nil}
case 102:
return Event{ALT_F, 0, nil}
return Event{AltF, 0, nil}
case 127:
return Event{ALT_BS, 0, nil}
return Event{AltBS, 0, nil}
case 91, 79:
if len(_buf) < 3 {
return Event{INVALID, 0, nil}
return Event{Invalid, 0, nil}
}
*sz = 3
switch _buf[2] {
case 68:
return Event{CTRL_B, 0, nil}
return Event{CtrlB, 0, nil}
case 67:
return Event{CTRL_F, 0, nil}
return Event{CtrlF, 0, nil}
case 66:
return Event{CTRL_J, 0, nil}
return Event{CtrlJ, 0, nil}
case 65:
return Event{CTRL_K, 0, nil}
return Event{CtrlK, 0, nil}
case 90:
return Event{BTAB, 0, nil}
return Event{BTab, 0, nil}
case 72:
return Event{CTRL_A, 0, nil}
return Event{CtrlA, 0, nil}
case 70:
return Event{CTRL_E, 0, nil}
return Event{CtrlE, 0, nil}
case 77:
return mouseSequence(sz)
case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 {
return Event{INVALID, 0, nil}
return Event{Invalid, 0, nil}
}
*sz = 4
switch _buf[2] {
case 50:
return Event{INVALID, 0, nil} // INS
return Event{Invalid, 0, nil} // INS
case 51:
return Event{DEL, 0, nil}
return Event{Del, 0, nil}
case 52:
return Event{CTRL_E, 0, nil}
return Event{CtrlE, 0, nil}
case 53:
return Event{PGUP, 0, nil}
return Event{PgUp, 0, nil}
case 54:
return Event{PGDN, 0, nil}
return Event{PgDn, 0, nil}
case 49:
switch _buf[3] {
case 126:
return Event{CTRL_A, 0, nil}
return Event{CtrlA, 0, nil}
case 59:
if len(_buf) != 6 {
return Event{INVALID, 0, nil}
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[4] {
case 50:
switch _buf[5] {
case 68:
return Event{CTRL_A, 0, nil}
return Event{CtrlA, 0, nil}
case 67:
return Event{CTRL_E, 0, nil}
return Event{CtrlE, 0, nil}
}
case 53:
switch _buf[5] {
case 68:
return Event{ALT_B, 0, nil}
return Event{AltB, 0, nil}
case 67:
return Event{ALT_F, 0, nil}
return Event{AltF, 0, nil}
}
} // _buf[4]
} // _buf[3]
} // _buf[2]
} // _buf[2]
} // _buf[1]
return Event{INVALID, 0, nil}
return Event{Invalid, 0, nil}
}
func GetChar() Event {
@ -378,21 +380,21 @@ func GetChar() Event {
}()
switch _buf[0] {
case CTRL_C, CTRL_G, CTRL_Q:
return Event{CTRL_C, 0, nil}
case CtrlC, CtrlG, CtrlQ:
return Event{CtrlC, 0, nil}
case 127:
return Event{CTRL_H, 0, nil}
return Event{CtrlH, 0, nil}
case ESC:
return escSequence(&sz)
}
// CTRL-A ~ CTRL-Z
if _buf[0] <= CTRL_Z {
if _buf[0] <= CtrlZ {
return Event{int(_buf[0]), 0, nil}
}
r, rsz := utf8.DecodeRune(_buf)
sz = rsz
return Event{RUNE, r, nil}
return Event{Rune, r, nil}
}
func Move(y int, x int) {

View File

@ -2,16 +2,17 @@ package fzf
import "sync"
type EventType int
// Events is a type that associates EventType to any data
type Events map[EventType]interface{}
// EventBox is used for coordinating events
type EventBox struct {
events Events
cond *sync.Cond
ignore map[EventType]bool
}
// NewEventBox returns a new EventBox
func NewEventBox() *EventBox {
return &EventBox{
events: make(Events),
@ -19,6 +20,7 @@ func NewEventBox() *EventBox {
ignore: make(map[EventType]bool)}
}
// Wait blocks the goroutine until signaled
func (b *EventBox) Wait(callback func(*Events)) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
@ -30,6 +32,7 @@ func (b *EventBox) Wait(callback func(*Events)) {
callback(&b.events)
}
// Set turns on the event type on the box
func (b *EventBox) Set(event EventType, value interface{}) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
@ -39,6 +42,7 @@ func (b *EventBox) Set(event EventType, value interface{}) {
}
}
// Clear clears the events
// Unsynchronized; should be called within Wait routine
func (events *Events) Clear() {
for event := range *events {
@ -46,6 +50,7 @@ func (events *Events) Clear() {
}
}
// Peak peaks at the event box if the given event is set
func (b *EventBox) Peak(event EventType) bool {
b.cond.L.Lock()
defer b.cond.L.Unlock()
@ -53,6 +58,7 @@ func (b *EventBox) Peak(event EventType) bool {
return ok
}
// Watch deletes the events from the ignore list
func (b *EventBox) Watch(events ...EventType) {
b.cond.L.Lock()
defer b.cond.L.Unlock()
@ -61,6 +67,7 @@ func (b *EventBox) Watch(events ...EventType) {
}
}
// Unwatch adds the events to the ignore list
func (b *EventBox) Unwatch(events ...EventType) {
b.cond.L.Lock()
defer b.cond.L.Unlock()

View File

@ -9,16 +9,16 @@ func TestEventBox(t *testing.T) {
ch := make(chan bool)
go func() {
eb.Set(EVT_READ_NEW, 10)
eb.Set(EvtReadNew, 10)
ch <- true
<-ch
eb.Set(EVT_SEARCH_NEW, 10)
eb.Set(EVT_SEARCH_NEW, 15)
eb.Set(EVT_SEARCH_NEW, 20)
eb.Set(EVT_SEARCH_PROGRESS, 30)
eb.Set(EvtSearchNew, 10)
eb.Set(EvtSearchNew, 15)
eb.Set(EvtSearchNew, 20)
eb.Set(EvtSearchProgress, 30)
ch <- true
<-ch
eb.Set(EVT_SEARCH_FIN, 40)
eb.Set(EvtSearchFin, 40)
ch <- true
<-ch
}()
@ -39,7 +39,7 @@ func TestEventBox(t *testing.T) {
events.Clear()
})
ch <- true
count += 1
count++
}
if count != 3 {

View File

@ -1,9 +1,9 @@
package fzf
import "fmt"
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
// Item represents each input line
type Item struct {
text *string
origText *string
@ -13,12 +13,14 @@ type Item struct {
rank Rank
}
// Rank is used to sort the search result
type Rank struct {
matchlen uint16
strlen uint16
index uint32
}
// Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) {
return i.rank
@ -45,14 +47,15 @@ func (i *Item) Rank(cache bool) Rank {
return rank
}
func (i *Item) Print() {
// AsString returns the original string
func (i *Item) AsString() string {
if i.origText != nil {
fmt.Println(*i.origText)
} else {
fmt.Println(*i.text)
return *i.origText
}
return *i.text
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
@ -69,6 +72,7 @@ func (a ByOrder) Less(i, j int) bool {
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Item
func (a ByRelevance) Len() int {

View File

@ -8,11 +8,13 @@ import (
"time"
)
// MatchRequest represents a search request
type MatchRequest struct {
chunks []*Chunk
pattern *Pattern
}
// Matcher is responsible for performing search
type Matcher struct {
patternBuilder func([]rune) *Pattern
sort bool
@ -23,20 +25,15 @@ type Matcher struct {
}
const (
REQ_RETRY EventType = iota
REQ_RESET
reqRetry EventType = iota
reqReset
)
const (
STAT_CANCELLED int = iota
STAT_QCH
STAT_CHUNKS
)
const (
PROGRESS_MIN_DURATION = 200 * time.Millisecond
progressMinDuration = 200 * time.Millisecond
)
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, eventBox *EventBox) *Matcher {
return &Matcher{
@ -48,6 +45,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
mergerCache: make(map[string]*Merger)}
}
// Loop puts Matcher in action
func (m *Matcher) Loop() {
prevCount := 0
@ -91,7 +89,7 @@ func (m *Matcher) Loop() {
if !cancelled {
m.mergerCache[patternString] = merger
m.eventBox.Set(EVT_SEARCH_FIN, merger)
m.eventBox.Set(EvtSearchFin, merger)
}
}
}
@ -172,7 +170,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
count := 0
matchCount := 0
for matchesInChunk := range countChan {
count += 1
count++
matchCount += matchesInChunk
if limit > 0 && matchCount > limit {
@ -183,12 +181,12 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
break
}
if !empty && m.reqBox.Peak(REQ_RESET) {
if !empty && m.reqBox.Peak(reqReset) {
return nil, wait()
}
if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION {
m.eventBox.Set(EVT_SEARCH_PROGRESS, float32(count)/float32(numChunks))
if time.Now().Sub(startedAt) > progressMinDuration {
m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
}
}
@ -200,14 +198,15 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
return NewMerger(partialResults, !empty && m.sort), false
}
// Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
pattern := m.patternBuilder(patternRunes)
var event EventType
if cancel {
event = REQ_RESET
event = reqReset
} else {
event = REQ_RETRY
event = reqRetry
}
m.reqBox.Set(event, MatchRequest{chunks, pattern})
}

View File

@ -2,8 +2,11 @@ package fzf
import "fmt"
var EmptyMerger *Merger = NewMerger([][]*Item{}, false)
// Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false)
// Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list
type Merger struct {
lists [][]*Item
merged []*Item
@ -12,6 +15,7 @@ type Merger struct {
count int
}
// NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool) *Merger {
mg := Merger{
lists: lists,
@ -26,10 +30,12 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger {
return &mg
}
// Length returns the number of items
func (mg *Merger) Length() int {
return mg.count
}
// Get returns the pointer to the Item object indexed by the given integer
func (mg *Merger) Get(idx int) *Item {
if len(mg.lists) == 1 {
return mg.lists[0][idx]
@ -69,7 +75,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
if minIdx >= 0 {
chosen := mg.lists[minIdx]
mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]])
mg.cursors[minIdx] += 1
mg.cursors[minIdx]++
} else {
panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count))
}

View File

@ -8,7 +8,7 @@ import (
"strings"
)
const USAGE = `usage: fzf [options]
const usage = `usage: fzf [options]
Search
-x, --extended Extended-search mode
@ -47,22 +47,27 @@ const USAGE = `usage: fzf [options]
`
// Mode denotes the current search mode
type Mode int
// Search modes
const (
MODE_FUZZY Mode = iota
MODE_EXTENDED
MODE_EXTENDED_EXACT
ModeFuzzy Mode = iota
ModeExtended
ModeExtendedExact
)
// Case denotes case-sensitivity of search
type Case int
// Case-sensitivities
const (
CASE_SMART Case = iota
CASE_IGNORE
CASE_RESPECT
CaseSmart Case = iota
CaseIgnore
CaseRespect
)
// Options stores the values of command-line options
type Options struct {
Mode Mode
Case Case
@ -85,10 +90,10 @@ type Options struct {
Version bool
}
func DefaultOptions() *Options {
func defaultOptions() *Options {
return &Options{
Mode: MODE_FUZZY,
Case: CASE_SMART,
Mode: ModeFuzzy,
Case: CaseSmart,
Nth: make([]Range, 0),
WithNth: make([]Range, 0),
Delimiter: nil,
@ -109,7 +114,7 @@ func DefaultOptions() *Options {
}
func help(ok int) {
os.Stderr.WriteString(USAGE)
os.Stderr.WriteString(usage)
os.Exit(ok)
}
@ -123,9 +128,8 @@ func optString(arg string, prefix string) (bool, string) {
matches := rx.FindStringSubmatch(arg)
if len(matches) > 1 {
return true, matches[1]
} else {
return false, ""
}
return false, ""
}
func nextString(args []string, i *int, message string) string {
@ -183,11 +187,11 @@ func parseOptions(opts *Options, allArgs []string) {
case "-h", "--help":
help(0)
case "-x", "--extended":
opts.Mode = MODE_EXTENDED
opts.Mode = ModeExtended
case "-e", "--extended-exact":
opts.Mode = MODE_EXTENDED_EXACT
opts.Mode = ModeExtendedExact
case "+x", "--no-extended", "+e", "--no-extended-exact":
opts.Mode = MODE_FUZZY
opts.Mode = ModeFuzzy
case "-q", "--query":
opts.Query = nextString(allArgs, &i, "query string required")
case "-f", "--filter":
@ -204,9 +208,9 @@ func parseOptions(opts *Options, allArgs []string) {
case "+s", "--no-sort":
opts.Sort = 0
case "-i":
opts.Case = CASE_IGNORE
opts.Case = CaseIgnore
case "+i":
opts.Case = CASE_RESPECT
opts.Case = CaseRespect
case "-m", "--multi":
opts.Multi = true
case "+m", "--no-multi":
@ -263,8 +267,9 @@ func parseOptions(opts *Options, allArgs []string) {
}
}
// ParseOptions parses command-line options
func ParseOptions() *Options {
opts := DefaultOptions()
opts := defaultOptions()
// Options from Env var
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))

View File

@ -15,20 +15,20 @@ func TestSplitNth(t *testing.T) {
{
ranges := splitNth("..")
if len(ranges) != 1 ||
ranges[0].begin != RANGE_ELLIPSIS ||
ranges[0].end != RANGE_ELLIPSIS {
ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis {
t.Errorf("%s", ranges)
}
}
{
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2")
if len(ranges) != 8 ||
ranges[0].begin != RANGE_ELLIPSIS || ranges[0].end != 3 ||
ranges[1].begin != 1 || ranges[1].end != RANGE_ELLIPSIS ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != 1 || ranges[1].end != rangeEllipsis ||
ranges[2].begin != 2 || ranges[2].end != 3 ||
ranges[3].begin != 4 || ranges[3].end != -1 ||
ranges[4].begin != -3 || ranges[4].end != -2 ||
ranges[5].begin != RANGE_ELLIPSIS || ranges[5].end != RANGE_ELLIPSIS ||
ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
ranges[6].begin != 2 || ranges[6].end != 2 ||
ranges[7].begin != -2 || ranges[7].end != -2 {
t.Errorf("%s", ranges)

View File

@ -6,7 +6,7 @@ import (
"strings"
)
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// fuzzy
// 'exact
@ -17,31 +17,32 @@ const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// !^not-exact-prefix
// !not-exact-suffix$
type TermType int
type termType int
const (
TERM_FUZZY TermType = iota
TERM_EXACT
TERM_PREFIX
TERM_SUFFIX
termFuzzy termType = iota
termExact
termPrefix
termSuffix
)
type Term struct {
typ TermType
type term struct {
typ termType
inv bool
text []rune
origText []rune
}
// Pattern represents search pattern
type Pattern struct {
mode Mode
caseSensitive bool
text []rune
terms []Term
terms []term
hasInvTerm bool
delimiter *regexp.Regexp
nth []Range
procFun map[TermType]func(bool, *string, []rune) (int, int)
procFun map[termType]func(bool, *string, []rune) (int, int)
}
var (
@ -62,12 +63,13 @@ func clearPatternCache() {
_patternCache = make(map[string]*Pattern)
}
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern {
var asString string
switch mode {
case MODE_EXTENDED, MODE_EXTENDED_EXACT:
case ModeExtended, ModeExtendedExact:
asString = strings.Trim(string(runes), " ")
default:
asString = string(runes)
@ -79,19 +81,19 @@ func BuildPattern(mode Mode, caseMode Case,
}
caseSensitive, hasInvTerm := true, false
terms := []Term{}
terms := []term{}
switch caseMode {
case CASE_SMART:
if !strings.ContainsAny(asString, UPPERCASE) {
case CaseSmart:
if !strings.ContainsAny(asString, uppercaseLetters) {
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
case CASE_IGNORE:
case CaseIgnore:
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
switch mode {
case MODE_EXTENDED, MODE_EXTENDED_EXACT:
case ModeExtended, ModeExtendedExact:
terms = parseTerms(mode, string(runes))
for _, term := range terms {
if term.inv {
@ -108,25 +110,25 @@ func BuildPattern(mode Mode, caseMode Case,
hasInvTerm: hasInvTerm,
nth: nth,
delimiter: delimiter,
procFun: make(map[TermType]func(bool, *string, []rune) (int, int))}
procFun: make(map[termType]func(bool, *string, []rune) (int, int))}
ptr.procFun[TERM_FUZZY] = FuzzyMatch
ptr.procFun[TERM_EXACT] = ExactMatchNaive
ptr.procFun[TERM_PREFIX] = PrefixMatch
ptr.procFun[TERM_SUFFIX] = SuffixMatch
ptr.procFun[termFuzzy] = FuzzyMatch
ptr.procFun[termExact] = ExactMatchNaive
ptr.procFun[termPrefix] = PrefixMatch
ptr.procFun[termSuffix] = SuffixMatch
_patternCache[asString] = ptr
return ptr
}
func parseTerms(mode Mode, str string) []Term {
func parseTerms(mode Mode, str string) []term {
tokens := _splitRegex.Split(str, -1)
terms := []Term{}
terms := []term{}
for _, token := range tokens {
typ, inv, text := TERM_FUZZY, false, token
typ, inv, text := termFuzzy, false, token
origText := []rune(text)
if mode == MODE_EXTENDED_EXACT {
typ = TERM_EXACT
if mode == ModeExtendedExact {
typ = termExact
}
if strings.HasPrefix(text, "!") {
@ -135,20 +137,20 @@ func parseTerms(mode Mode, str string) []Term {
}
if strings.HasPrefix(text, "'") {
if mode == MODE_EXTENDED {
typ = TERM_EXACT
if mode == ModeExtended {
typ = termExact
text = text[1:]
}
} else if strings.HasPrefix(text, "^") {
typ = TERM_PREFIX
typ = termPrefix
text = text[1:]
} else if strings.HasSuffix(text, "$") {
typ = TERM_SUFFIX
typ = termSuffix
text = text[:len(text)-1]
}
if len(text) > 0 {
terms = append(terms, Term{
terms = append(terms, term{
typ: typ,
inv: inv,
text: []rune(text),
@ -158,20 +160,22 @@ func parseTerms(mode Mode, str string) []Term {
return terms
}
// IsEmpty returns true if the pattern is effectively empty
func (p *Pattern) IsEmpty() bool {
if p.mode == MODE_FUZZY {
if p.mode == ModeFuzzy {
return len(p.text) == 0
} else {
return len(p.terms) == 0
}
return len(p.terms) == 0
}
// AsString returns the search query in string type
func (p *Pattern) AsString() string {
return string(p.text)
}
// CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string {
if p.mode == MODE_FUZZY {
if p.mode == ModeFuzzy {
return p.AsString()
}
cacheableTerms := []string{}
@ -184,6 +188,7 @@ func (p *Pattern) CacheKey() string {
return strings.Join(cacheableTerms, " ")
}
// Match returns the list of matches Items in the given Chunk
func (p *Pattern) Match(chunk *Chunk) []*Item {
space := chunk
@ -213,7 +218,7 @@ Loop:
}
var matches []*Item
if p.mode == MODE_FUZZY {
if p.mode == ModeFuzzy {
matches = p.fuzzyMatch(space)
} else {
matches = p.extendedMatch(space)

View File

@ -3,17 +3,17 @@ package fzf
import "testing"
func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(MODE_EXTENDED,
terms := parseTerms(ModeExtended,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
terms[0].typ != TERM_FUZZY || terms[0].inv ||
terms[1].typ != TERM_EXACT || terms[1].inv ||
terms[2].typ != TERM_PREFIX || terms[2].inv ||
terms[3].typ != TERM_SUFFIX || terms[3].inv ||
terms[4].typ != TERM_FUZZY || !terms[4].inv ||
terms[5].typ != TERM_EXACT || !terms[5].inv ||
terms[6].typ != TERM_PREFIX || !terms[6].inv ||
terms[7].typ != TERM_SUFFIX || !terms[7].inv {
terms[0].typ != termFuzzy || terms[0].inv ||
terms[1].typ != termExact || terms[1].inv ||
terms[2].typ != termPrefix || terms[2].inv ||
terms[3].typ != termSuffix || terms[3].inv ||
terms[4].typ != termFuzzy || !terms[4].inv ||
terms[5].typ != termExact || !terms[5].inv ||
terms[6].typ != termPrefix || !terms[6].inv ||
terms[7].typ != termSuffix || !terms[7].inv {
t.Errorf("%s", terms)
}
for idx, term := range terms {
@ -27,23 +27,23 @@ func TestParseTermsExtended(t *testing.T) {
}
func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(MODE_EXTENDED_EXACT,
terms := parseTerms(ModeExtendedExact,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
terms[0].typ != TERM_EXACT || terms[0].inv || len(terms[0].text) != 3 ||
terms[1].typ != TERM_EXACT || terms[1].inv || len(terms[1].text) != 4 ||
terms[2].typ != TERM_PREFIX || terms[2].inv || len(terms[2].text) != 3 ||
terms[3].typ != TERM_SUFFIX || terms[3].inv || len(terms[3].text) != 3 ||
terms[4].typ != TERM_EXACT || !terms[4].inv || len(terms[4].text) != 3 ||
terms[5].typ != TERM_EXACT || !terms[5].inv || len(terms[5].text) != 4 ||
terms[6].typ != TERM_PREFIX || !terms[6].inv || len(terms[6].text) != 3 ||
terms[7].typ != TERM_SUFFIX || !terms[7].inv || len(terms[7].text) != 3 {
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
terms[1].typ != termExact || terms[1].inv || len(terms[1].text) != 4 ||
terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 ||
terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 ||
terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 ||
terms[5].typ != termExact || !terms[5].inv || len(terms[5].text) != 4 ||
terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 ||
terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 {
t.Errorf("%s", terms)
}
}
func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(MODE_EXTENDED, "' $ ^ !' !^ !$")
terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$")
if len(terms) != 0 {
t.Errorf("%s", terms)
}
@ -52,7 +52,7 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(MODE_EXTENDED, CASE_SMART,
pattern := BuildPattern(ModeExtended, CaseSmart,
[]Range{}, nil, []rune("'abc"))
str := "aabbcc abc"
sidx, eidx := ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text)
@ -64,17 +64,17 @@ func TestExact(t *testing.T) {
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pat1 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("abc"))
pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("abc"))
clearPatternCache()
pat2 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("Abc"))
pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("Abc"))
clearPatternCache()
pat3 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("abc"))
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("Abc"))
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("abc"))
pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("Abc"))
pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@ -90,7 +90,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
strptr := func(str string) *string {
return &str
}
pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, []Range{}, nil, []rune("jg"))
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg"))
tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}})

View File

@ -10,31 +10,33 @@ import (
"os/exec"
)
const DEFAULT_COMMAND = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null`
const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null`
// Reader reads from command or standard input
type Reader struct {
pusher func(string)
eventBox *EventBox
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource() {
if int(C.isatty(C.int(os.Stdin.Fd()))) != 0 {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
cmd = DEFAULT_COMMAND
cmd = defaultCommand
}
r.readFromCommand(cmd)
} else {
r.readFromStdin()
}
r.eventBox.Set(EVT_READ_FIN, nil)
r.eventBox.Set(EvtReadFin, nil)
}
func (r *Reader) feed(src io.Reader) {
if scanner := bufio.NewScanner(src); scanner != nil {
for scanner.Scan() {
r.pusher(scanner.Text())
r.eventBox.Set(EVT_READ_NEW, nil)
r.eventBox.Set(EvtReadNew, nil)
}
}
}

View File

@ -10,8 +10,8 @@ func TestReadFromCommand(t *testing.T) {
eventBox: eb}
// Check EventBox
if eb.Peak(EVT_READ_NEW) {
t.Error("EVT_READ_NEW should not be set yet")
if eb.Peak(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
// Normal command
@ -21,21 +21,21 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if !eb.Peak(EVT_READ_NEW) {
t.Error("EVT_READ_NEW should be set yet")
if !eb.Peak(EvtReadNew) {
t.Error("EvtReadNew should be set yet")
}
// Wait should return immediately
eb.Wait(func(events *Events) {
if _, found := (*events)[EVT_READ_NEW]; !found {
if _, found := (*events)[EvtReadNew]; !found {
t.Errorf("%s", events)
}
events.Clear()
})
// EventBox is cleared
if eb.Peak(EVT_READ_NEW) {
t.Error("EVT_READ_NEW should not be set yet")
if eb.Peak(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
// Failing command
@ -46,7 +46,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if eb.Peak(EVT_READ_NEW) {
t.Error("Command failed. EVT_READ_NEW should be set")
if eb.Peak(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set")
}
}

View File

@ -11,6 +11,7 @@ import (
"time"
)
// Terminal represents terminal input/output
type Terminal struct {
prompt string
reverse bool
@ -34,23 +35,24 @@ type Terminal struct {
suppress bool
}
var _spinner []string = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
const (
REQ_PROMPT EventType = iota
REQ_INFO
REQ_LIST
REQ_REFRESH
REQ_REDRAW
REQ_CLOSE
REQ_QUIT
reqPrompt EventType = iota
reqInfo
reqList
reqRefresh
reqRedraw
reqClose
reqQuit
)
const (
INITIAL_DELAY = 100 * time.Millisecond
SPINNER_DURATION = 200 * time.Millisecond
initialDelay = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond
)
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *EventBox) *Terminal {
input := []rune(opts.Query)
return &Terminal{
@ -75,23 +77,26 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal {
}}
}
// Input returns current query string
func (t *Terminal) Input() []rune {
t.mutex.Lock()
defer t.mutex.Unlock()
return copySlice(t.input)
}
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.mutex.Unlock()
t.reqBox.Set(REQ_INFO, nil)
t.reqBox.Set(reqInfo, nil)
if final {
t.reqBox.Set(REQ_REFRESH, nil)
t.reqBox.Set(reqRefresh, nil)
}
}
// UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock()
newProgress := int(progress * 100)
@ -100,25 +105,25 @@ func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Unlock()
if changed {
t.reqBox.Set(REQ_INFO, nil)
t.reqBox.Set(reqInfo, nil)
}
}
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) {
t.mutex.Lock()
t.progress = 100
t.merger = merger
t.mutex.Unlock()
t.reqBox.Set(REQ_INFO, nil)
t.reqBox.Set(REQ_LIST, nil)
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
}
func (t *Terminal) listIndex(y int) int {
if t.tac {
return t.merger.Length() - y - 1
} else {
return y
}
return y
}
func (t *Terminal) output() {
@ -127,7 +132,7 @@ func (t *Terminal) output() {
}
if len(t.selected) == 0 {
if t.merger.Length() > t.cy {
t.merger.Get(t.listIndex(t.cy)).Print()
fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString())
}
} else {
for ptr, orig := range t.selected {
@ -167,16 +172,16 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
C.CPrint(C.COL_PROMPT, true, t.prompt)
C.CPrint(C.COL_NORMAL, true, string(t.input))
C.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input))
}
func (t *Terminal) printInfo() {
t.move(1, 0, true)
if t.reading {
duration := int64(SPINNER_DURATION)
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.COL_SPINNER, true, _spinner[idx])
C.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
@ -187,7 +192,7 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
C.CPrint(C.COL_INFO, false, output)
C.CPrint(C.ColInfo, false, output)
}
func (t *Terminal) printList() {
@ -206,21 +211,21 @@ func (t *Terminal) printList() {
func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.text]
if current {
C.CPrint(C.COL_CURSOR, true, ">")
C.CPrint(C.ColCursor, true, ">")
if selected {
C.CPrint(C.COL_CURRENT, true, ">")
C.CPrint(C.ColCurrent, true, ">")
} else {
C.CPrint(C.COL_CURRENT, true, " ")
C.CPrint(C.ColCurrent, true, " ")
}
t.printHighlighted(item, true, C.COL_CURRENT, C.COL_CURRENT_MATCH)
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch)
} else {
C.CPrint(C.COL_CURSOR, true, " ")
C.CPrint(C.ColCursor, true, " ")
if selected {
C.CPrint(C.COL_SELECTED, true, ">")
C.CPrint(C.ColSelected, true, ">")
} else {
C.Print(" ")
}
t.printHighlighted(item, false, 0, C.COL_MATCH)
t.printHighlighted(item, false, 0, C.ColMatch)
}
}
@ -232,25 +237,25 @@ func trimRight(runes []rune, width int) ([]rune, int) {
sz := len(runes)
currentWidth -= runewidth.RuneWidth(runes[sz-1])
runes = runes[:sz-1]
trimmed += 1
trimmed++
}
return runes, trimmed
}
func trimLeft(runes []rune, width int) ([]rune, int32) {
currentWidth := displayWidth(runes)
var trimmed int32 = 0
var trimmed int32
for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:]
trimmed += 1
trimmed++
}
return runes, trimmed
}
func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
var maxe int32 = 0
var maxe int32
for _, offset := range item.offsets {
if offset[1] > maxe {
maxe = offset[1]
@ -293,7 +298,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
}
sort.Sort(ByOrder(offsets))
var index int32 = 0
var index int32
for _, offset := range offsets {
b := Max32(index, offset[0])
e := Max32(index, offset[1])
@ -364,6 +369,7 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...)
}
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
{ // Late initialization
t.mutex.Lock()
@ -374,9 +380,9 @@ func (t *Terminal) Loop() {
t.printInfo()
t.mutex.Unlock()
go func() {
timer := time.NewTimer(INITIAL_DELAY)
timer := time.NewTimer(initialDelay)
<-timer.C
t.reqBox.Set(REQ_REFRESH, nil)
t.reqBox.Set(reqRefresh, nil)
}()
}
@ -387,22 +393,22 @@ func (t *Terminal) Loop() {
t.mutex.Lock()
for req := range *events {
switch req {
case REQ_PROMPT:
case reqPrompt:
t.printPrompt()
case REQ_INFO:
case reqInfo:
t.printInfo()
case REQ_LIST:
case reqList:
t.printList()
case REQ_REFRESH:
case reqRefresh:
t.suppress = false
case REQ_REDRAW:
case reqRedraw:
C.Clear()
t.printAll()
case REQ_CLOSE:
case reqClose:
C.Close()
t.output()
os.Exit(0)
case REQ_QUIT:
case reqQuit:
C.Close()
os.Exit(1)
}
@ -420,11 +426,11 @@ func (t *Terminal) Loop() {
t.mutex.Lock()
previousInput := t.input
events := []EventType{REQ_PROMPT}
events := []EventType{reqPrompt}
req := func(evts ...EventType) {
for _, event := range evts {
events = append(events, event)
if event == REQ_CLOSE || event == REQ_QUIT {
if event == reqClose || event == reqQuit {
looping = false
}
}
@ -438,99 +444,99 @@ func (t *Terminal) Loop() {
} else {
delete(t.selected, item.text)
}
req(REQ_INFO)
req(reqInfo)
}
}
switch event.Type {
case C.INVALID:
case C.Invalid:
t.mutex.Unlock()
continue
case C.CTRL_A:
case C.CtrlA:
t.cx = 0
case C.CTRL_B:
case C.CtrlB:
if t.cx > 0 {
t.cx -= 1
t.cx--
}
case C.CTRL_C, C.CTRL_G, C.CTRL_Q, C.ESC:
req(REQ_QUIT)
case C.CTRL_D:
case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC:
req(reqQuit)
case C.CtrlD:
if !t.delChar() && t.cx == 0 {
req(REQ_QUIT)
req(reqQuit)
}
case C.CTRL_E:
case C.CtrlE:
t.cx = len(t.input)
case C.CTRL_F:
case C.CtrlF:
if t.cx < len(t.input) {
t.cx += 1
t.cx++
}
case C.CTRL_H:
case C.CtrlH:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx -= 1
t.cx--
}
case C.TAB:
case C.Tab:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(REQ_LIST)
req(reqList)
}
case C.BTAB:
case C.BTab:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(REQ_LIST)
req(reqList)
}
case C.CTRL_J, C.CTRL_N:
case C.CtrlJ, C.CtrlN:
t.vmove(-1)
req(REQ_LIST)
case C.CTRL_K, C.CTRL_P:
req(reqList)
case C.CtrlK, C.CtrlP:
t.vmove(1)
req(REQ_LIST)
case C.CTRL_M:
req(REQ_CLOSE)
case C.CTRL_L:
req(REQ_REDRAW)
case C.CTRL_U:
req(reqList)
case C.CtrlM:
req(reqClose)
case C.CtrlL:
req(reqRedraw)
case C.CtrlU:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case C.CTRL_W:
case C.CtrlW:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case C.ALT_BS:
case C.AltBS:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case C.CTRL_Y:
case C.CtrlY:
t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...)
t.cx += len(t.yanked)
case C.DEL:
case C.Del:
t.delChar()
case C.PGUP:
case C.PgUp:
t.vmove(maxItems() - 1)
req(REQ_LIST)
case C.PGDN:
req(reqList)
case C.PgDn:
t.vmove(-(maxItems() - 1))
req(REQ_LIST)
case C.ALT_B:
req(reqList)
case C.AltB:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case C.ALT_F:
case C.AltF:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case C.ALT_D:
case C.AltD:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case C.RUNE:
case C.Rune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx += 1
case C.MOUSE:
t.cx++
case C.Mouse:
me := event.MouseEvent
mx, my := Min(len(t.input), Max(0, me.X-len(t.prompt))), me.Y
if !t.reverse {
@ -543,13 +549,13 @@ func (t *Terminal) Loop() {
toggle()
}
t.vmove(me.S)
req(REQ_LIST)
req(reqList)
}
} else if me.Double {
// Double-click
if my >= 2 {
if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() {
req(REQ_CLOSE)
req(reqClose)
}
}
} else if me.Down {
@ -561,7 +567,7 @@ func (t *Terminal) Loop() {
if t.vset(t.offset+my-2) && t.multi && me.Mod {
toggle()
}
req(REQ_LIST)
req(reqList)
}
}
}
@ -569,7 +575,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
t.eventBox.Set(EVT_SEARCH_NEW, nil)
t.eventBox.Set(EvtSearchNew, nil)
}
for _, event := range events {
t.reqBox.Set(event, nil)

View File

@ -6,40 +6,42 @@ import (
"strings"
)
const RANGE_ELLIPSIS = 0
const rangeEllipsis = 0
// Range represents nth-expression
type Range struct {
begin int
end int
}
// Transformed holds the result of tokenization and transformation
type Transformed struct {
whole *string
parts []Token
}
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
text *string
prefixLength int
}
// ParseRange parses nth-expression and returns the corresponding Range object
func ParseRange(str *string) (Range, bool) {
if (*str) == ".." {
return Range{RANGE_ELLIPSIS, RANGE_ELLIPSIS}, true
return Range{rangeEllipsis, rangeEllipsis}, true
} else if strings.HasPrefix(*str, "..") {
end, err := strconv.Atoi((*str)[2:])
if err != nil || end == 0 {
return Range{}, false
} else {
return Range{RANGE_ELLIPSIS, end}, true
}
return Range{rangeEllipsis, end}, true
} else if strings.HasSuffix(*str, "..") {
begin, err := strconv.Atoi((*str)[:len(*str)-2])
if err != nil || begin == 0 {
return Range{}, false
} else {
return Range{begin, RANGE_ELLIPSIS}, true
}
return Range{begin, rangeEllipsis}, true
} else if strings.Contains(*str, "..") {
ns := strings.Split(*str, "..")
if len(ns) != 2 {
@ -75,9 +77,9 @@ func withPrefixLengths(tokens []string, begin int) []Token {
}
const (
AWK_NIL = iota
AWK_BLACK
AWK_WHITE
awkNil = iota
awkBlack
awkWhite
)
func awkTokenizer(input *string) ([]string, int) {
@ -85,28 +87,28 @@ func awkTokenizer(input *string) ([]string, int) {
ret := []string{}
str := []rune{}
prefixLength := 0
state := AWK_NIL
state := awkNil
for _, r := range []rune(*input) {
white := r == 9 || r == 32
switch state {
case AWK_NIL:
case awkNil:
if white {
prefixLength++
} else {
state = AWK_BLACK
state = awkBlack
str = append(str, r)
}
case AWK_BLACK:
case awkBlack:
str = append(str, r)
if white {
state = AWK_WHITE
state = awkWhite
}
case AWK_WHITE:
case awkWhite:
if white {
str = append(str, r)
} else {
ret = append(ret, string(str))
state = AWK_BLACK
state = awkBlack
str = []rune{r}
}
}
@ -117,15 +119,15 @@ func awkTokenizer(input *string) ([]string, int) {
return ret, prefixLength
}
// Tokenize tokenizes the given string with the delimiter
func Tokenize(str *string, delimiter *regexp.Regexp) []Token {
if delimiter == nil {
// AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(str)
return withPrefixLengths(tokens, prefixLength)
} else {
tokens := delimiter.FindAllString(*str, -1)
return withPrefixLengths(tokens, 0)
}
tokens := delimiter.FindAllString(*str, -1)
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens []Token) string {
@ -136,6 +138,7 @@ func joinTokens(tokens []Token) string {
return ret
}
// Transform is used to transform the input when --with-nth option is given
func Transform(tokens []Token, withNth []Range) *Transformed {
transTokens := make([]Token, len(withNth))
numTokens := len(tokens)
@ -145,7 +148,7 @@ func Transform(tokens []Token, withNth []Range) *Transformed {
minIdx := 0
if r.begin == r.end {
idx := r.begin
if idx == RANGE_ELLIPSIS {
if idx == rangeEllipsis {
part += joinTokens(tokens)
} else {
if idx < 0 {
@ -158,12 +161,12 @@ func Transform(tokens []Token, withNth []Range) *Transformed {
}
} else {
var begin, end int
if r.begin == RANGE_ELLIPSIS { // ..N
if r.begin == rangeEllipsis { // ..N
begin, end = 1, r.end
if end < 0 {
end += numTokens + 1
}
} else if r.end == RANGE_ELLIPSIS { // N..
} else if r.end == rangeEllipsis { // N..
begin, end = r.begin, numTokens
if begin < 0 {
begin += numTokens + 1

View File

@ -6,14 +6,14 @@ func TestParseRange(t *testing.T) {
{
i := ".."
r, _ := ParseRange(&i)
if r.begin != RANGE_ELLIPSIS || r.end != RANGE_ELLIPSIS {
if r.begin != rangeEllipsis || r.end != rangeEllipsis {
t.Errorf("%s", r)
}
}
{
i := "3.."
r, _ := ParseRange(&i)
if r.begin != 3 || r.end != RANGE_ELLIPSIS {
if r.begin != 3 || r.end != rangeEllipsis {
t.Errorf("%s", r)
}
}

View File

@ -2,6 +2,7 @@ package fzf
import "time"
// Max returns the largest integer
func Max(first int, items ...int) int {
max := first
for _, item := range items {
@ -12,6 +13,7 @@ func Max(first int, items ...int) int {
return max
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
@ -19,6 +21,7 @@ func Max32(first int32, second int32) int32 {
return second
}
// Min returns the smallest integer
func Min(first int, items ...int) int {
min := first
for _, item := range items {
@ -29,6 +32,7 @@ func Min(first int, items ...int) int {
return min
}
// DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration {
if val < min {