From f3177305d5572b26f135fc045481358b4eb1bf69 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Jan 2015 04:49:30 +0900 Subject: [PATCH 01/51] Rewrite fzf in Go --- .gitignore | 2 + install | 126 +++++---- src/Dockerfile | 33 +++ src/LICENSE | 21 ++ src/Makefile | 49 ++++ src/README.md | 59 +++++ src/algo.go | 152 +++++++++++ src/algo_test.go | 44 ++++ src/atomicbool.go | 27 ++ src/atomicbool_test.go | 17 ++ src/cache.go | 47 ++++ src/chunklist.go | 73 ++++++ src/chunklist_test.go | 66 +++++ src/constants.go | 12 + src/core.go | 153 +++++++++++ src/curses/curses.go | 424 ++++++++++++++++++++++++++++++ src/eventbox.go | 48 ++++ src/fzf/main.go | 7 + src/item.go | 135 ++++++++++ src/item_test.go | 78 ++++++ src/matcher.go | 215 +++++++++++++++ src/options.go | 276 ++++++++++++++++++++ src/options_test.go | 37 +++ src/pattern.go | 305 ++++++++++++++++++++++ src/pattern_test.go | 87 +++++++ src/reader.go | 60 +++++ src/reader_test.go | 52 ++++ src/terminal.go | 580 +++++++++++++++++++++++++++++++++++++++++ src/tokenizer.go | 194 ++++++++++++++ src/tokenizer_test.go | 97 +++++++ src/util.go | 21 ++ src/util_test.go | 18 ++ 32 files changed, 3466 insertions(+), 49 deletions(-) create mode 100644 src/Dockerfile create mode 100644 src/LICENSE create mode 100644 src/Makefile create mode 100644 src/README.md create mode 100644 src/algo.go create mode 100644 src/algo_test.go create mode 100644 src/atomicbool.go create mode 100644 src/atomicbool_test.go create mode 100644 src/cache.go create mode 100644 src/chunklist.go create mode 100644 src/chunklist_test.go create mode 100644 src/constants.go create mode 100644 src/core.go create mode 100644 src/curses/curses.go create mode 100644 src/eventbox.go create mode 100644 src/fzf/main.go create mode 100644 src/item.go create mode 100644 src/item_test.go create mode 100644 src/matcher.go create mode 100644 src/options.go create mode 100644 src/options_test.go create mode 100644 src/pattern.go create mode 100644 src/pattern_test.go create mode 100644 src/reader.go create mode 100644 src/reader_test.go create mode 100644 src/terminal.go create mode 100644 src/tokenizer.go create mode 100644 src/tokenizer_test.go create mode 100644 src/util.go create mode 100644 src/util_test.go diff --git a/.gitignore b/.gitignore index 1627430..0915467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +bin +src/fzf/fzf_* pkg Gemfile.lock .DS_Store diff --git a/install b/install index 3176b27..6b64a3f 100755 --- a/install +++ b/install @@ -3,60 +3,81 @@ cd `dirname $BASH_SOURCE` fzf_base=`pwd` -# ruby executable -echo -n "Checking Ruby executable ... " -ruby=`which ruby` -if [ $? -ne 0 ]; then - echo "ruby executable not found!" - exit 1 -fi +ARCHI=$(uname -sm) -# System ruby is preferred -system_ruby=/usr/bin/ruby -if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then - $system_ruby --disable-gems -rcurses -e0 2> /dev/null - [ $? -eq 0 ] && ruby=$system_ruby -fi - -echo "OK ($ruby)" - -# Curses-support -echo -n "Checking Curses support ... " -"$ruby" -rcurses -e0 2> /dev/null -if [ $? -eq 0 ]; then - echo "OK" -else - echo "Not found" - echo "Installing 'curses' gem ... " - if (( EUID )); then - /usr/bin/env gem install curses --user-install +download() { + echo "Downloading fzf executable ($1) ..." + if curl -fLo "$fzf_base"/bin/fzf https://github.com/junegunn/fzf-bin/releases/download/snapshot/$1; then + chmod +x "$fzf_base"/bin/fzf else - /usr/bin/env gem install curses - fi - if [ $? -ne 0 ]; then - echo - echo "Failed to install 'curses' gem." - if [[ $(uname -r) =~ 'ARCH' ]]; then - echo "Make sure that base-devel package group is installed." - fi + echo "Failed to download $1" exit 1 fi -fi +} -# Ruby version -echo -n "Checking Ruby version ... " -"$ruby" -e 'exit RUBY_VERSION >= "1.9"' -if [ $? -eq 0 ]; then - echo ">= 1.9" - "$ruby" --disable-gems -rcurses -e0 2> /dev/null +mkdir -p "$fzf_base"/bin +if [ "$ARCHI" = "Darwin x86_64" ]; then + download fzf_darwin_amd64 +elif [ "$ARCHI" = "Linux x86_64" ]; then + download fzf_linux_amd64 +else # No prebuilt executable + echo "No prebuilt binary for $ARCHI ... Installing legacy Ruby version ..." + + # ruby executable + echo -n "Checking Ruby executable ... " + ruby=`which ruby` + if [ $? -ne 0 ]; then + echo "ruby executable not found!" + exit 1 + fi + + # System ruby is preferred + system_ruby=/usr/bin/ruby + if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then + $system_ruby --disable-gems -rcurses -e0 2> /dev/null + [ $? -eq 0 ] && ruby=$system_ruby + fi + + echo "OK ($ruby)" + + # Curses-support + echo -n "Checking Curses support ... " + "$ruby" -rcurses -e0 2> /dev/null if [ $? -eq 0 ]; then - fzf_cmd="$ruby --disable-gems $fzf_base/fzf" + echo "OK" else + echo "Not found" + echo "Installing 'curses' gem ... " + if (( EUID )); then + /usr/bin/env gem install curses --user-install + else + /usr/bin/env gem install curses + fi + if [ $? -ne 0 ]; then + echo + echo "Failed to install 'curses' gem." + if [[ $(uname -r) =~ 'ARCH' ]]; then + echo "Make sure that base-devel package group is installed." + fi + exit 1 + fi + fi + + # Ruby version + echo -n "Checking Ruby version ... " + "$ruby" -e 'exit RUBY_VERSION >= "1.9"' + if [ $? -eq 0 ]; then + echo ">= 1.9" + "$ruby" --disable-gems -rcurses -e0 2> /dev/null + if [ $? -eq 0 ]; then + fzf_cmd="$ruby --disable-gems $fzf_base/fzf" + else + fzf_cmd="$ruby $fzf_base/fzf" + fi + else + echo "< 1.9" fzf_cmd="$ruby $fzf_base/fzf" fi -else - echo "< 1.9" - fzf_cmd="$ruby $fzf_base/fzf" fi # Auto-completion @@ -85,10 +106,17 @@ for shell in bash zsh; do # Setup fzf function # ------------------ unalias fzf 2> /dev/null -fzf() { - $fzf_cmd "\$@" -} -export -f fzf > /dev/null +unset fzf 2> /dev/null +if [ -x "$fzf_base/bin/fzf" ]; then + if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then + export PATH="$fzf_base/bin:\$PATH" + fi +else + fzf() { + $fzf_cmd "\$@" + } + export -f fzf > /dev/null +fi # Auto-completion # --------------- diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..3c062ee --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:14.04 +MAINTAINER Junegunn Choi + +# apt-get +RUN apt-get update && apt-get -y upgrade +RUN apt-get install -y --force-yes git vim-nox curl procps sudo \ + build-essential libncurses-dev + +# Setup jg user with sudo privilege +RUN useradd -s /bin/bash -m jg && echo 'jg:jg' | chpasswd && \ + echo 'jg ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/jg + +# Setup dotfiles +USER jg +RUN cd ~ && git clone https://github.com/junegunn/dotfiles.git && \ + dotfiles/install > /dev/null + +# Install Go 1.4 +RUN cd ~ && curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz && \ + mv go go1.4 && \ + echo 'export GOROOT=~/go1.4' >> ~/dotfiles/bashrc-extra && \ + echo 'export PATH=~/go1.4/bin:$PATH' >> ~/dotfiles/bashrc-extra + +# Symlink fzf directory +RUN mkdir -p ~jg/go/src/github.com/junegunn && \ + ln -s /fzf ~jg/go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd ~jg/go/src/github.com/junegunn/fzf/src && /bin/bash -l + diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..fe4c31a --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..bae4c90 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,49 @@ +BINARY := fzf/fzf + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + BINARY := $(BINARY)_darwin +else ifeq ($(UNAME_S),Linux) + BINARY := $(BINARY)_linux +endif + +UNAME_M := $(shell uname -m) +ifneq ($(filter i386 i686,$(UNAME_M)),) +$(error "filtered is not supported, yet.") +endif + +ifeq ($(UNAME_M),x86_64) + BINARY := $(BINARY)_amd64 +else ifneq ($(filter i386 i686,$(UNAME_M)),) + BINARY := $(BINARY)_386 +else # TODO +$(error "$(UNAME_M) is not supported, yet.") +endif + +BINDIR = ../bin +SOURCES = $(wildcard *.go fzf/*.go) + +all: build + +build: $(BINARY) + +$(BINARY): $(SOURCES) + go get + go test -v + cd fzf && go build -o $(notdir $(BINARY)) + +install: $(BINARY) + mkdir -p $(BINDIR) + cp -f $(BINARY) $(BINDIR)/fzf + +clean: + rm -f $(BINARY) + +docker: + docker build -t junegunn/ubuntu-sandbox . + +linux64: + docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ + /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make build' + +.PHONY: build install linux64 clean docker run diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..2f3ca3b --- /dev/null +++ b/src/README.md @@ -0,0 +1,59 @@ +fzf in Go +========= + +This directory contains the source code for the new fzf implementation in Go. +This new version has the following benefits over the previous Ruby version. + +- Immensely faster + - No GIL. Performance is linearly proportional to the number of cores. + - It's so fast that I even decided to remove the sort limit (`--sort=N`) +- Does not require Ruby and distributed as an executable binary + - Ruby dependency is especially painful on Ruby 2.1 or above which + ships without curses gem + +Build +----- + +```sh +# Build fzf executable +make + +# Install the executable to ../bin directory +make install + +# Build executable for Linux x86_64 using Docker +make linux64 +``` + + +Prebuilt binaries +----------------- + +- Darwin x86_64 +- Linux x86_64 + +Third-party libraries used +-------------------------- + +- [ncurses](https://www.gnu.org/software/ncurses/) +- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) + - Licensed under [MIT](http://mattn.mit-license.org/2013) +- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) + - Licensed under [MIT](http://mattn.mit-license.org/2014) + +Contribution +------------ + +For the moment, I will not add or accept any new features until we can be sure +that the implementation is stable and we have a sufficient number of test +cases. However, fixes for obvious bugs and new test cases are welcome. + +I also care much about the performance of the implementation (that's the +reason I rewrote the whole thing in Go, right?), so please make sure that your +change does not result in performance regression. Please be minded that we +still don't have a quantitative measure of the performance. + +License +------- + +- [MIT](LICENSE) diff --git a/src/algo.go b/src/algo.go new file mode 100644 index 0000000..16790ba --- /dev/null +++ b/src/algo.go @@ -0,0 +1,152 @@ +package fzf + +import "strings" + +/* + * String matching algorithms here do not use strings.ToLower to avoid + * performance penalty. And they assume pattern runes are given in lowercase + * letters when caseSensitive is false. + * + * In short: They try to do as little work as possible. + */ + +func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + + // 0. (FIXME) How to find the shortest match? + // a_____b__c__abc + // ^^^^^^^^^^ ^^^ + // 1. forward scan (abc) + // *-----*-----*> + // a_____b___abc__ + // 2. reverse scan (cba) + // a_____b___abc__ + // <*** + pidx := 0 + sidx := -1 + eidx := -1 + + for index, char := range runes { + // This is considerably faster than blindly applying strings.ToLower to the + // whole string + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char == pattern[pidx] { + if sidx < 0 { + sidx = index + } + if pidx += 1; pidx == len(pattern) { + eidx = index + 1 + break + } + } + } + + if sidx >= 0 && eidx >= 0 { + pidx -= 1 + 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 { + sidx = index + break + } + } + } + return sidx, eidx + } + return -1, -1 +} + +func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) { + var str string + if caseSensitive { + str = *input + } else { + str = strings.ToLower(*input) + } + + if idx := strings.Index(str, string(pattern)); idx >= 0 { + prefixRuneLen := len([]rune((*input)[:idx])) + return prefixRuneLen, prefixRuneLen + len(pattern) + } + 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 + */ +func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + numRunes := len(runes) + plen := len(pattern) + if len(runes) < plen { + return -1, -1 + } + + pidx := 0 + for index := 0; index < numRunes; index++ { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if pattern[pidx] == char { + pidx += 1 + if pidx == plen { + return index - plen + 1, index + 1 + } + } else { + index -= pidx + pidx = 0 + } + } + return -1, -1 +} + +func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + if len(runes) < len(pattern) { + return -1, -1 + } + + for index, r := range pattern { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char != r { + return -1, -1 + } + } + return 0, len(pattern) +} + +func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(strings.TrimRight(*input, " ")) + trimmedLen := len(runes) + diff := trimmedLen - len(pattern) + if diff < 0 { + return -1, -1 + } + + for index, r := range pattern { + char := runes[index+diff] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char != r { + return -1, -1 + } + } + return trimmedLen - len(pattern), trimmedLen +} diff --git a/src/algo_test.go b/src/algo_test.go new file mode 100644 index 0000000..5da01a6 --- /dev/null +++ b/src/algo_test.go @@ -0,0 +1,44 @@ +package fzf + +import ( + "strings" + "testing" +) + +func assertMatch(t *testing.T, fun func(bool, *string, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { + if !caseSensitive { + pattern = strings.ToLower(pattern) + } + s, e := fun(caseSensitive, &input, []rune(pattern)) + if s != sidx { + t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) + } + if e != eidx { + t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern) + } +} + +func TestFuzzyMatch(t *testing.T) { + assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestExactMatchNaive(t *testing.T) { + assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) + assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) + assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestPrefixMatch(t *testing.T) { + assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) + assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) +} + +func TestSuffixMatch(t *testing.T) { + assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) + assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) +} diff --git a/src/atomicbool.go b/src/atomicbool.go new file mode 100644 index 0000000..f2f4894 --- /dev/null +++ b/src/atomicbool.go @@ -0,0 +1,27 @@ +package fzf + +import "sync" + +type AtomicBool struct { + mutex sync.Mutex + state bool +} + +func NewAtomicBool(initialState bool) *AtomicBool { + return &AtomicBool{ + mutex: sync.Mutex{}, + state: initialState} +} + +func (a *AtomicBool) Get() bool { + a.mutex.Lock() + defer a.mutex.Unlock() + return a.state +} + +func (a *AtomicBool) Set(newState bool) bool { + a.mutex.Lock() + defer a.mutex.Unlock() + a.state = newState + return a.state +} diff --git a/src/atomicbool_test.go b/src/atomicbool_test.go new file mode 100644 index 0000000..0af4570 --- /dev/null +++ b/src/atomicbool_test.go @@ -0,0 +1,17 @@ +package fzf + +import "testing" + +func TestAtomicBool(t *testing.T) { + if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() { + t.Error("Invalid initial value") + } + + ab := NewAtomicBool(true) + if ab.Set(false) { + t.Error("Invalid return value") + } + if ab.Get() { + t.Error("Invalid state") + } +} diff --git a/src/cache.go b/src/cache.go new file mode 100644 index 0000000..340f325 --- /dev/null +++ b/src/cache.go @@ -0,0 +1,47 @@ +package fzf + +import "sync" + +type QueryCache map[string][]*Item +type ChunkCache struct { + mutex sync.Mutex + cache map[*Chunk]*QueryCache +} + +func NewChunkCache() ChunkCache { + return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)} +} + +func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { + if len(key) == 0 || !chunk.IsFull() { + return + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if !ok { + cc.cache[chunk] = &QueryCache{} + qc = cc.cache[chunk] + } + (*qc)[key] = list +} + +func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { + if len(key) == 0 || !chunk.IsFull() { + return nil, false + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if ok { + list, ok := (*qc)[key] + if ok { + return list, true + } + } + return nil, false +} diff --git a/src/chunklist.go b/src/chunklist.go new file mode 100644 index 0000000..b1f9638 --- /dev/null +++ b/src/chunklist.go @@ -0,0 +1,73 @@ +package fzf + +import "sync" + +const CHUNK_SIZE int = 100 + +type Chunk []*Item // >>> []Item + +type Transformer func(*string, int) *Item + +type ChunkList struct { + chunks []*Chunk + count int + mutex sync.Mutex + trans Transformer +} + +func NewChunkList(trans Transformer) *ChunkList { + return &ChunkList{ + chunks: []*Chunk{}, + count: 0, + mutex: sync.Mutex{}, + trans: trans} +} + +func (c *Chunk) push(trans Transformer, data *string, index int) { + *c = append(*c, trans(data, index)) +} + +func (c *Chunk) IsFull() bool { + return len(*c) == CHUNK_SIZE +} + +func (cl *ChunkList) lastChunk() *Chunk { + return cl.chunks[len(cl.chunks)-1] +} + +func CountItems(cs []*Chunk) int { + if len(cs) == 0 { + return 0 + } + return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1])) +} + +func (cl *ChunkList) Count() int { + return cl.count +} + +func (cl *ChunkList) Chunks() []*Chunk { + return cl.chunks +} + +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)) + cl.chunks = append(cl.chunks, &newChunk) + } + + cl.lastChunk().push(cl.trans, &data, cl.count) + cl.count += 1 +} + +func (cl *ChunkList) Snapshot() []*Chunk { + cl.mutex.Lock() + defer cl.mutex.Unlock() + + ret := make([]*Chunk, len(cl.chunks)) + copy(ret, cl.chunks) + return ret +} diff --git a/src/chunklist_test.go b/src/chunklist_test.go new file mode 100644 index 0000000..a7daa47 --- /dev/null +++ b/src/chunklist_test.go @@ -0,0 +1,66 @@ +package fzf + +import ( + "fmt" + "testing" +) + +func TestChunkList(t *testing.T) { + cl := NewChunkList(func(s *string, i int) *Item { + return &Item{text: s, index: i * 2} + }) + + // Snapshot + snapshot := cl.Snapshot() + if len(snapshot) > 0 { + t.Error("Snapshot should be empty now") + } + + // Add some data + cl.Push("hello") + cl.Push("world") + + // Previously created snapshot should remain the same + if len(snapshot) > 0 { + t.Error("Snapshot should not have changed") + } + + // But the new snapshot should contain the added items + snapshot = cl.Snapshot() + if len(snapshot) != 1 { + t.Error("Snapshot should not be empty now") + } + + // Check the content of the ChunkList + chunk1 := snapshot[0] + if len(*chunk1) != 2 { + t.Error("Snapshot should contain only two items") + } + if *(*chunk1)[0].text != "hello" || (*chunk1)[0].index != 0 || + *(*chunk1)[1].text != "world" || (*chunk1)[1].index != 2 { + t.Error("Invalid data") + } + if chunk1.IsFull() { + t.Error("Chunk should not have been marked full yet") + } + + // Add more data + for i := 0; i < CHUNK_SIZE*2; i++ { + cl.Push(fmt.Sprintf("item %d", i)) + } + + // Previous snapshot should remain the same + if len(snapshot) != 1 { + t.Error("Snapshot should stay the same") + } + + // New snapshot + snapshot = cl.Snapshot() + if len(snapshot) != 3 || !snapshot[0].IsFull() || + !snapshot[1].IsFull() || snapshot[2].IsFull() { + t.Error("Expected two full chunks and one more chunk") + } + if len(*snapshot[2]) != 2 { + t.Error("Unexpected number of items") + } +} diff --git a/src/constants.go b/src/constants.go new file mode 100644 index 0000000..b0b64db --- /dev/null +++ b/src/constants.go @@ -0,0 +1,12 @@ +package fzf + +const VERSION = "0.9.0" + +const ( + EVT_READ_NEW EventType = iota + EVT_READ_FIN + EVT_SEARCH_NEW + EVT_SEARCH_PROGRESS + EVT_SEARCH_FIN + EVT_CLOSE +) diff --git a/src/core.go b/src/core.go new file mode 100644 index 0000000..2601397 --- /dev/null +++ b/src/core.go @@ -0,0 +1,153 @@ +package fzf + +import ( + "fmt" + "os" + "runtime" + "time" +) + +const COORDINATOR_DELAY time.Duration = 100 * 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) +*/ + +func Run(options *Options) { + initProcs() + + opts := ParseOptions() + + if opts.Version { + fmt.Println(VERSION) + os.Exit(0) + } + + // Event channel + eventBox := NewEventBox() + + // Chunk list + var chunkList *ChunkList + if len(opts.WithNth) == 0 { + chunkList = NewChunkList(func(data *string, index int) *Item { + return &Item{text: data, index: index} + }) + } else { + chunkList = NewChunkList(func(data *string, index int) *Item { + item := Item{text: data, index: index} + tokens := Tokenize(item.text, opts.Delimiter) + item.origText = item.text + item.text = Transform(tokens, opts.WithNth).whole + return &item + }) + } + + // Reader + reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} + go reader.ReadSource() + + // Matcher + patternBuilder := func(runes []rune) *Pattern { + return BuildPattern( + opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) + } + matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) + + // Defered-interactive / Non-interactive + // --select-1 | --exit-0 | --filter + if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 { + limit := 0 + var patternString string + if filtering { + patternString = *opts.Filter + } else { + if opts.Select1 || opts.Exit0 { + limit = 1 + } + patternString = opts.Query + } + pattern := patternBuilder([]rune(patternString)) + + looping := true + for looping { + eventBox.Wait(func(events *Events) { + for evt, _ := range *events { + switch evt { + case EVT_READ_FIN: + looping = false + return + } + } + }) + time.Sleep(COORDINATOR_DELAY) + } + + matches, cancelled := matcher.scan(MatchRequest{ + chunks: chunkList.Snapshot(), + pattern: pattern}, limit) + + if !cancelled && (filtering || opts.Exit0) { + if opts.PrintQuery { + fmt.Println(patternString) + } + for _, item := range matches { + item.Print() + } + os.Exit(0) + } + } + + // Go interactive + go matcher.Loop() + + // Terminal I/O + terminal := NewTerminal(opts, eventBox) + go terminal.Loop() + + // Event coordination + reading := true + ticks := 0 + for { + delay := true + ticks += 1 + 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 + terminal.UpdateCount(chunkList.Count(), !reading) + matcher.Reset(chunkList.Snapshot(), terminal.Input(), false) + + case EVT_SEARCH_NEW: + matcher.Reset(chunkList.Snapshot(), terminal.Input(), true) + delay = false + + case EVT_SEARCH_PROGRESS: + switch val := value.(type) { + case float32: + terminal.UpdateProgress(val) + } + + case EVT_SEARCH_FIN: + switch val := value.(type) { + case []*Item: + terminal.UpdateList(val) + } + } + } + }) + if ticks > 3 && delay && reading { + time.Sleep(COORDINATOR_DELAY) + } + } +} diff --git a/src/curses/curses.go b/src/curses/curses.go new file mode 100644 index 0000000..945a3ce --- /dev/null +++ b/src/curses/curses.go @@ -0,0 +1,424 @@ +package curses + +// #include +// #include +// #cgo LDFLAGS: -lncurses +import "C" + +import ( + "os" + "os/signal" + "syscall" + "time" + "unicode/utf8" +) + +const ( + 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 + ESC + + INVALID + MOUSE + + BTAB + + DEL + PGUP + PGDN + + ALT_B + ALT_F + ALT_D + ALT_BS +) + +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 +) + +type Event struct { + Type int + Char rune + MouseEvent *MouseEvent +} + +type MouseEvent struct { + Y int + X int + S int + Down bool + Double bool + Mod bool +} + +var ( + _buf []byte + _in *os.File + _color func(int, bool) C.int + _prevDownTime time.Time + _prevDownY int + _clickY []int +) + +func init() { + _prevDownTime = time.Unix(0, 0) + _clickY = []int{} +} + +func attrColored(pair int, bold bool) C.int { + var attr C.int = 0 + if pair > COL_NORMAL { + attr = C.COLOR_PAIR(C.int(pair)) + } + if bold { + attr = attr | C.A_BOLD + } + return attr +} + +func attrMono(pair int, bold bool) C.int { + var attr C.int = 0 + switch pair { + case COL_CURRENT: + if bold { + attr = C.A_REVERSE + } + case COL_MATCH: + attr = C.A_UNDERLINE + case COL_CURRENT_MATCH: + attr = C.A_UNDERLINE | C.A_REVERSE + } + if bold { + attr = attr | C.A_BOLD + } + return attr +} + +func MaxX() int { + return int(C.COLS) +} + +func MaxY() int { + return int(C.LINES) +} + +func getch(nonblock bool) int { + b := make([]byte, 1) + syscall.SetNonblock(int(_in.Fd()), nonblock) + _, err := _in.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func Init(color bool, color256 bool, black bool, mouse bool) { + { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + _in = in + // Break STDIN + // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) + } + + swapOutput() + + C.setlocale(C.LC_ALL, C.CString("")) + C.initscr() + if mouse { + C.mousemask(C.ALL_MOUSE_EVENTS, nil) + } + C.cbreak() + C.noecho() + C.raw() // stty dsusp undef + C.set_tabsize(4) // FIXME + + intChan := make(chan os.Signal, 1) + signal.Notify(intChan, os.Interrupt, os.Kill) + go func() { + <-intChan + Close() + os.Exit(1) + }() + + if color { + C.start_color() + var bg C.short + if black { + bg = C.COLOR_BLACK + } else { + C.use_default_colors() + 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) + } 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) + } + _color = attrColored + } else { + _color = attrMono + } +} + +func Close() { + C.endwin() + swapOutput() +} + +func swapOutput() { + syscall.Dup2(2, 3) + syscall.Dup2(1, 2) + syscall.Dup2(3, 1) +} + +func GetBytes() []byte { + c := getch(false) + _buf = append(_buf, byte(c)) + + for { + c = getch(true) + if c == -1 { + break + } + _buf = append(_buf, byte(c)) + } + + return _buf +} + +// 27 (91 79) 77 type x y +func mouseSequence(sz *int) Event { + if len(_buf) < 6 { + return Event{INVALID, 0, nil} + } + *sz = 6 + switch _buf[3] { + case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := _buf[3] >= 36 + down := _buf[3]%2 == 0 + x := int(_buf[4] - 33) + y := int(_buf[5] - 33) + double := false + if down { + now := time.Now() + if now.Sub(_prevDownTime) < DOUBLE_CLICK_DURATION { + _clickY = append(_clickY, y) + } else { + _clickY = []int{y} + } + _prevDownTime = now + } else { + if len(_clickY) > 1 && _clickY[0] == _clickY[1] && + time.Now().Sub(_prevDownTime) < DOUBLE_CLICK_DURATION { + double = true + } + } + 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{INVALID, 0, nil} +} + +func escSequence(sz *int) Event { + if len(_buf) < 2 { + return Event{ESC, 0, nil} + } + *sz = 2 + switch _buf[1] { + case 98: + return Event{ALT_B, 0, nil} + case 100: + return Event{ALT_D, 0, nil} + case 102: + return Event{ALT_F, 0, nil} + case 127: + return Event{ALT_BS, 0, nil} + case 91, 79: + if len(_buf) < 3 { + return Event{INVALID, 0, nil} + } + *sz = 3 + switch _buf[2] { + case 68: + return Event{CTRL_B, 0, nil} + case 67: + return Event{CTRL_F, 0, nil} + case 66: + return Event{CTRL_J, 0, nil} + case 65: + return Event{CTRL_K, 0, nil} + case 90: + return Event{BTAB, 0, nil} + case 72: + return Event{CTRL_A, 0, nil} + case 70: + return Event{CTRL_E, 0, nil} + case 77: + return mouseSequence(sz) + case 49, 50, 51, 52, 53, 54: + if len(_buf) < 4 { + return Event{INVALID, 0, nil} + } + *sz = 4 + switch _buf[2] { + case 50: + return Event{INVALID, 0, nil} // INS + case 51: + return Event{DEL, 0, nil} + case 52: + return Event{CTRL_E, 0, nil} + case 53: + return Event{PGUP, 0, nil} + case 54: + return Event{PGDN, 0, nil} + case 49: + switch _buf[3] { + case 126: + return Event{CTRL_A, 0, nil} + case 59: + if len(_buf) != 6 { + return Event{INVALID, 0, nil} + } + *sz = 6 + switch _buf[4] { + case 50: + switch _buf[5] { + case 68: + return Event{CTRL_A, 0, nil} + case 67: + return Event{CTRL_E, 0, nil} + } + case 53: + switch _buf[5] { + case 68: + return Event{ALT_B, 0, nil} + case 67: + return Event{ALT_F, 0, nil} + } + } // _buf[4] + } // _buf[3] + } // _buf[2] + } // _buf[2] + } // _buf[1] + return Event{INVALID, 0, nil} +} + +func GetChar() Event { + if len(_buf) == 0 { + _buf = GetBytes() + } + if len(_buf) == 0 { + panic("Empty _buffer") + } + + sz := 1 + defer func() { + _buf = _buf[sz:] + }() + + switch _buf[0] { + case CTRL_C, CTRL_G, CTRL_Q: + return Event{CTRL_C, 0, nil} + case 127: + return Event{CTRL_H, 0, nil} + case ESC: + return escSequence(&sz) + } + + // CTRL-A ~ CTRL-Z + if _buf[0] <= CTRL_Z { + return Event{int(_buf[0]), 0, nil} + } + r, rsz := utf8.DecodeRune(_buf) + sz = rsz + return Event{RUNE, r, nil} +} + +func Move(y int, x int) { + C.move(C.int(y), C.int(x)) +} + +func MoveAndClear(y int, x int) { + Move(y, x) + C.clrtoeol() +} + +func Print(text string) { + C.addstr(C.CString(text)) +} + +func CPrint(pair int, bold bool, text string) { + attr := _color(pair, bold) + C.attron(attr) + Print(text) + C.attroff(attr) +} + +func Clear() { + C.clear() +} + +func Refresh() { + C.refresh() +} diff --git a/src/eventbox.go b/src/eventbox.go new file mode 100644 index 0000000..6685e7c --- /dev/null +++ b/src/eventbox.go @@ -0,0 +1,48 @@ +package fzf + +import "sync" + +type EventType int + +type Events map[EventType]interface{} + +type EventBox struct { + events Events + cond *sync.Cond +} + +func NewEventBox() *EventBox { + return &EventBox{make(Events), sync.NewCond(&sync.Mutex{})} +} + +func (b *EventBox) Wait(callback func(*Events)) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + + if len(b.events) == 0 { + b.cond.Wait() + } + + callback(&b.events) +} + +func (b *EventBox) Set(event EventType, value interface{}) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + b.events[event] = value + b.cond.Broadcast() +} + +// Unsynchronized; should be called within Wait routine +func (events *Events) Clear() { + for event := range *events { + delete(*events, event) + } +} + +func (b *EventBox) Peak(event EventType) bool { + b.cond.L.Lock() + defer b.cond.L.Unlock() + _, ok := b.events[event] + return ok +} diff --git a/src/fzf/main.go b/src/fzf/main.go new file mode 100644 index 0000000..29d4767 --- /dev/null +++ b/src/fzf/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/junegunn/fzf/src" + +func main() { + fzf.Run(fzf.ParseOptions()) +} diff --git a/src/item.go b/src/item.go new file mode 100644 index 0000000..b70da93 --- /dev/null +++ b/src/item.go @@ -0,0 +1,135 @@ +package fzf + +import ( + "fmt" + "sort" +) + +type Offset [2]int + +type Item struct { + text *string + origText *string + offsets []Offset + index int + rank Rank + transformed *Transformed +} + +type Rank [3]int + +var NilRank = Rank{-1, 0, 0} + +func (i *Item) Rank() Rank { + if i.rank[0] > 0 { + return i.rank + } + sort.Sort(ByOrder(i.offsets)) + matchlen := 0 + prevEnd := 0 + for _, offset := range i.offsets { + begin := offset[0] + end := offset[1] + if prevEnd > begin { + begin = prevEnd + } + if end > prevEnd { + prevEnd = end + } + if end > begin { + matchlen += end - begin + } + } + i.rank = Rank{matchlen, len(*i.text), i.index} + return i.rank +} + +func (i *Item) Print() { + if i.origText != nil { + fmt.Println(*i.origText) + } else { + fmt.Println(*i.text) + } +} + +type ByOrder []Offset + +func (a ByOrder) Len() int { + return len(a) +} + +func (a ByOrder) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByOrder) Less(i, j int) bool { + ioff := a[i] + joff := a[j] + return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) +} + +type ByRelevance []*Item + +func (a ByRelevance) Len() int { + return len(a) +} + +func (a ByRelevance) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevance) Less(i, j int) bool { + irank := a[i].Rank() + jrank := a[j].Rank() + + return compareRanks(irank, jrank) +} + +func compareRanks(irank Rank, jrank Rank) bool { + for idx := range irank { + if irank[idx] < jrank[idx] { + return true + } else if irank[idx] > jrank[idx] { + return false + } + } + return true +} + +func SortMerge(partialResults [][]*Item) []*Item { + if len(partialResults) == 1 { + return partialResults[0] + } + + merged := []*Item{} + + for len(partialResults) > 0 { + minRank := Rank{0, 0, 0} + minIdx := -1 + + for idx, partialResult := range partialResults { + if len(partialResult) > 0 { + rank := partialResult[0].Rank() + if minIdx < 0 || compareRanks(rank, minRank) { + minRank = rank + minIdx = idx + } + } + } + + if minIdx >= 0 { + merged = append(merged, partialResults[minIdx][0]) + partialResults[minIdx] = partialResults[minIdx][1:] + } + + nonEmptyPartialResults := make([][]*Item, 0, len(partialResults)) + for _, partialResult := range partialResults { + if len(partialResult) > 0 { + nonEmptyPartialResults = append(nonEmptyPartialResults, partialResult) + } + } + partialResults = nonEmptyPartialResults + } + + return merged +} diff --git a/src/item_test.go b/src/item_test.go new file mode 100644 index 0000000..1e31629 --- /dev/null +++ b/src/item_test.go @@ -0,0 +1,78 @@ +package fzf + +import ( + "sort" + "testing" +) + +func TestOffsetSort(t *testing.T) { + offsets := []Offset{ + Offset{3, 5}, Offset{2, 7}, + Offset{1, 3}, Offset{2, 9}} + sort.Sort(ByOrder(offsets)) + + if offsets[0][0] != 1 || offsets[0][1] != 3 || + offsets[1][0] != 2 || offsets[1][1] != 7 || + offsets[2][0] != 2 || offsets[2][1] != 9 || + offsets[3][0] != 3 || offsets[3][1] != 5 { + t.Error("Invalid order:", offsets) + } +} + +func TestRankComparison(t *testing.T) { + if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || + !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || + !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || + !compareRanks(NilRank, Rank{0, 0, 0}) || + compareRanks(Rank{0, 0, 0}, NilRank) { + t.Error("Invalid order") + } +} + +// Match length, string length, index +func TestItemRank(t *testing.T) { + strs := []string{"foo", "foobar", "bar", "baz"} + item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + rank1 := item1.Rank() + if rank1[0] != 0 || rank1[1] != 3 || rank1[2] != 1 { + t.Error(item1.Rank()) + } + // Only differ in index + item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} + + items := []*Item{&item1, &item2} + sort.Sort(ByRelevance(items)) + if items[0] != &item2 || items[1] != &item1 { + t.Error(items) + } + + items = []*Item{&item2, &item1, &item1, &item2} + sort.Sort(ByRelevance(items)) + if items[0] != &item2 || items[1] != &item2 || + items[2] != &item1 || items[3] != &item1 { + t.Error(items) + } + + // Sort by relevance + item3 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} + sort.Sort(ByRelevance(items)) + if items[0] != &item2 || items[1] != &item1 || + items[2] != &item6 || items[3] != &item4 || + items[4] != &item5 || items[5] != &item3 { + t.Error(items) + } + + // Sort merged lists + lists := [][]*Item{ + []*Item{&item2, &item4, &item5}, []*Item{&item1, &item6}, []*Item{&item3}} + items = SortMerge(lists) + if items[0] != &item2 || items[1] != &item1 || + items[2] != &item6 || items[3] != &item4 || + items[4] != &item5 || items[5] != &item3 { + t.Error(items) + } +} diff --git a/src/matcher.go b/src/matcher.go new file mode 100644 index 0000000..363b07f --- /dev/null +++ b/src/matcher.go @@ -0,0 +1,215 @@ +package fzf + +import ( + "fmt" + "runtime" + "sort" + "time" +) + +type MatchRequest struct { + chunks []*Chunk + pattern *Pattern +} + +type Matcher struct { + patternBuilder func([]rune) *Pattern + sort bool + eventBox *EventBox + reqBox *EventBox + partitions int + queryCache QueryCache +} + +const ( + REQ_RETRY EventType = iota + REQ_RESET +) + +const ( + STAT_CANCELLED int = iota + STAT_QCH + STAT_CHUNKS +) + +const ( + PROGRESS_MIN_DURATION = 200 * time.Millisecond +) + +func NewMatcher(patternBuilder func([]rune) *Pattern, + sort bool, eventBox *EventBox) *Matcher { + return &Matcher{ + patternBuilder: patternBuilder, + sort: sort, + eventBox: eventBox, + reqBox: NewEventBox(), + partitions: runtime.NumCPU(), + queryCache: make(QueryCache)} +} + +func (m *Matcher) Loop() { + prevCount := 0 + + for { + var request MatchRequest + + m.reqBox.Wait(func(events *Events) { + for _, val := range *events { + switch val := val.(type) { + case MatchRequest: + request = val + default: + panic(fmt.Sprintf("Unexpected type: %T", val)) + } + } + events.Clear() + }) + + // Restart search + patternString := request.pattern.AsString() + allMatches := []*Item{} + cancelled := false + count := CountItems(request.chunks) + + foundCache := false + if count == prevCount { + // Look up queryCache + if cached, found := m.queryCache[patternString]; found { + foundCache = true + allMatches = cached + } + } else { + // Invalidate queryCache + prevCount = count + m.queryCache = make(QueryCache) + } + + if !foundCache { + allMatches, cancelled = m.scan(request, 0) + } + + if !cancelled { + m.queryCache[patternString] = allMatches + m.eventBox.Set(EVT_SEARCH_FIN, allMatches) + } + } +} + +func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { + perSlice := len(chunks) / m.partitions + + // No need to parallelize + if perSlice == 0 { + return [][]*Chunk{chunks} + } + + slices := make([][]*Chunk, m.partitions) + for i := 0; i < m.partitions; i++ { + start := i * perSlice + end := start + perSlice + if i == m.partitions-1 { + end = len(chunks) + } + slices[i] = chunks[start:end] + } + return slices +} + +type partialResult struct { + index int + matches []*Item +} + +func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { + startedAt := time.Now() + + numChunks := len(request.chunks) + if numChunks == 0 { + return []*Item{}, false + } + pattern := request.pattern + empty := pattern.IsEmpty() + cancelled := NewAtomicBool(false) + + slices := m.sliceChunks(request.chunks) + numSlices := len(slices) + resultChan := make(chan partialResult, numSlices) + countChan := make(chan int, numSlices) + + for idx, chunks := range slices { + go func(idx int, chunks []*Chunk) { + sliceMatches := []*Item{} + for _, chunk := range chunks { + var matches []*Item + if empty { + matches = *chunk + } else { + matches = request.pattern.Match(chunk) + } + sliceMatches = append(sliceMatches, matches...) + if cancelled.Get() { + return + } + countChan <- len(sliceMatches) + } + if !empty && m.sort { + sort.Sort(ByRelevance(sliceMatches)) + } + resultChan <- partialResult{idx, sliceMatches} + }(idx, chunks) + } + + count := 0 + matchCount := 0 + for matchesInChunk := range countChan { + count += 1 + matchCount += matchesInChunk + + if limit > 0 && matchCount > limit { + return nil, true // For --select-1 and --exit-0 + } + + if count == numChunks { + break + } + + if !empty && m.reqBox.Peak(REQ_RESET) { + cancelled.Set(true) + return nil, true + } + + if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION { + m.eventBox.Set(EVT_SEARCH_PROGRESS, float32(count)/float32(numChunks)) + } + } + + partialResults := make([][]*Item, numSlices) + for range slices { + partialResult := <-resultChan + partialResults[partialResult.index] = partialResult.matches + } + + var allMatches []*Item + if empty || !m.sort { + allMatches = []*Item{} + for _, matches := range partialResults { + allMatches = append(allMatches, matches...) + } + } else { + allMatches = SortMerge(partialResults) + } + + return allMatches, false +} + +func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { + pattern := m.patternBuilder(patternRunes) + + var event EventType + if cancel { + event = REQ_RESET + } else { + event = REQ_RETRY + } + m.reqBox.Set(event, MatchRequest{chunks, pattern}) +} diff --git a/src/options.go b/src/options.go new file mode 100644 index 0000000..4929dfd --- /dev/null +++ b/src/options.go @@ -0,0 +1,276 @@ +package fzf + +import ( + "fmt" + "github.com/junegunn/go-shellwords" + "os" + "regexp" + "strings" +) + +const USAGE = `usage: fzf [options] + + Search + -x, --extended Extended-search mode + -e, --extended-exact Extended-search mode (exact match) + -i Case-insensitive match (default: smart-case match) + +i Case-sensitive match + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) + --with-nth=N[,..] Transform the item using index expressions for search + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) + + Search result + -s, --sort Sort the result + +s, --no-sort Do not sort the result. Keep the sequence unchanged. + + Interface + -m, --multi Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + +c, --no-color Disable colors + +2, --no-256 Disable 256-color + --black Use black background + --reverse Reverse orientation + --prompt=STR Input prompt (default: '> ') + + Scripting + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line + + Environment variables + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m") + +` + +type Mode int + +const ( + MODE_FUZZY Mode = iota + MODE_EXTENDED + MODE_EXTENDED_EXACT +) + +type Case int + +const ( + CASE_SMART Case = iota + CASE_IGNORE + CASE_RESPECT +) + +type Options struct { + Mode Mode + Case Case + Nth []Range + WithNth []Range + Delimiter *regexp.Regexp + Sort int + Multi bool + Mouse bool + Color bool + Color256 bool + Black bool + Reverse bool + Prompt string + Query string + Select1 bool + Exit0 bool + Filter *string + PrintQuery bool + Version bool +} + +func DefaultOptions() *Options { + return &Options{ + Mode: MODE_FUZZY, + Case: CASE_SMART, + Nth: make([]Range, 0), + WithNth: make([]Range, 0), + Delimiter: nil, + Sort: 1000, + Multi: false, + Mouse: true, + Color: true, + Color256: strings.Contains(os.Getenv("TERM"), "256"), + Black: false, + Reverse: false, + Prompt: "> ", + Query: "", + Select1: false, + Exit0: false, + Filter: nil, + PrintQuery: false, + Version: false} +} + +func help(ok int) { + os.Stderr.WriteString(USAGE) + os.Exit(ok) +} + +func errorExit(msg string) { + os.Stderr.WriteString(msg + "\n") + help(1) +} + +func optString(arg string, prefix string) (bool, string) { + rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix)) + matches := rx.FindStringSubmatch(arg) + if len(matches) > 1 { + return true, matches[1] + } else { + return false, "" + } +} + +func nextString(args []string, i *int, message string) string { + if len(args) > *i+1 { + *i++ + } else { + errorExit(message) + } + return args[*i] +} + +func optionalNumeric(args []string, i *int) int { + if len(args) > *i+1 { + if strings.IndexAny(args[*i+1], "0123456789") == 0 { + *i++ + } + } + return 1 // Don't care +} + +func splitNth(str string) []Range { + if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match { + errorExit("invalid format: " + str) + } + + tokens := strings.Split(str, ",") + ranges := make([]Range, len(tokens)) + for idx, s := range tokens { + r, ok := ParseRange(&s) + if !ok { + errorExit("invalid format: " + str) + } + ranges[idx] = r + } + return ranges +} + +func delimiterRegexp(str string) *regexp.Regexp { + rx, e := regexp.Compile(str) + if e != nil { + str = regexp.QuoteMeta(str) + } + + rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) + if e != nil { + errorExit("invalid regular expression: " + e.Error()) + } + return rx +} + +func parseOptions(opts *Options, allArgs []string) { + for i := 0; i < len(allArgs); i++ { + arg := allArgs[i] + switch arg { + case "-h", "--help": + help(0) + case "-x", "--extended": + opts.Mode = MODE_EXTENDED + case "-e", "--extended-exact": + opts.Mode = MODE_EXTENDED_EXACT + case "+x", "--no-extended", "+e", "--no-extended-exact": + opts.Mode = MODE_FUZZY + case "-q", "--query": + opts.Query = nextString(allArgs, &i, "query string required") + case "-f", "--filter": + filter := nextString(allArgs, &i, "query string required") + opts.Filter = &filter + case "-d", "--delimiter": + opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) + case "-n", "--nth": + opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required")) + case "--with-nth": + opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) + case "-s", "--sort": + opts.Sort = optionalNumeric(allArgs, &i) + case "+s", "--no-sort": + opts.Sort = 0 + case "-i": + opts.Case = CASE_IGNORE + case "+i": + opts.Case = CASE_RESPECT + case "-m", "--multi": + opts.Multi = true + case "+m", "--no-multi": + opts.Multi = false + case "--no-mouse": + opts.Mouse = false + case "+c", "--no-color": + opts.Color = false + case "+2", "--no-256": + opts.Color256 = false + case "--black": + opts.Black = true + case "--no-black": + opts.Black = false + case "--reverse": + opts.Reverse = true + case "--no-reverse": + opts.Reverse = false + case "-1", "--select-1": + opts.Select1 = true + case "+1", "--no-select-1": + opts.Select1 = false + case "-0", "--exit-0": + opts.Exit0 = true + case "+0", "--no-exit-0": + opts.Exit0 = false + case "--print-query": + opts.PrintQuery = true + case "--no-print-query": + opts.PrintQuery = false + case "--prompt": + opts.Prompt = nextString(allArgs, &i, "prompt string required") + case "--version": + opts.Version = true + default: + if match, value := optString(arg, "-q|--query="); match { + opts.Query = value + } else if match, value := optString(arg, "-f|--filter="); match { + opts.Filter = &value + } else if match, value := optString(arg, "-d|--delimiter="); match { + opts.Delimiter = delimiterRegexp(value) + } else if match, value := optString(arg, "--prompt="); match { + opts.Prompt = value + } else if match, value := optString(arg, "-n|--nth="); match { + opts.Nth = splitNth(value) + } else if match, value := optString(arg, "--with-nth="); match { + opts.WithNth = splitNth(value) + } else if match, _ := optString(arg, "-s|--sort="); match { + opts.Sort = 1 // Don't care + } else { + errorExit("unknown option: " + arg) + } + } + } +} + +func ParseOptions() *Options { + opts := DefaultOptions() + + // Options from Env var + words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) + parseOptions(opts, words) + + // Options from command-line arguments + parseOptions(opts, os.Args[1:]) + return opts +} diff --git a/src/options_test.go b/src/options_test.go new file mode 100644 index 0000000..f0aa3a0 --- /dev/null +++ b/src/options_test.go @@ -0,0 +1,37 @@ +package fzf + +import "testing" + +func TestDelimiterRegex(t *testing.T) { + rx := delimiterRegexp("*") + tokens := rx.FindAllString("-*--*---**---", -1) + if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" || + tokens[3] != "*" || tokens[4] != "---" { + t.Errorf("%s %s %d", rx, tokens, len(tokens)) + } +} + +func TestSplitNth(t *testing.T) { + { + ranges := splitNth("..") + if len(ranges) != 1 || + ranges[0].begin != RANGE_ELLIPSIS || + ranges[0].end != RANGE_ELLIPSIS { + 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[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[6].begin != 2 || ranges[6].end != 2 || + ranges[7].begin != -2 || ranges[7].end != -2 { + t.Errorf("%s", ranges) + } + } +} diff --git a/src/pattern.go b/src/pattern.go new file mode 100644 index 0000000..533aa59 --- /dev/null +++ b/src/pattern.go @@ -0,0 +1,305 @@ +package fzf + +import ( + "regexp" + "sort" + "strings" +) + +const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// fuzzy +// 'exact +// ^exact-prefix +// exact-suffix$ +// !not-fuzzy +// !'not-exact +// !^not-exact-prefix +// !not-exact-suffix$ + +type TermType int + +const ( + TERM_FUZZY TermType = iota + TERM_EXACT + TERM_PREFIX + TERM_SUFFIX +) + +type Term struct { + typ TermType + inv bool + text []rune + origText []rune +} + +type Pattern struct { + mode Mode + caseSensitive bool + text []rune + terms []Term + hasInvTerm bool + delimiter *regexp.Regexp + nth []Range + procFun map[TermType]func(bool, *string, []rune) (int, int) +} + +var ( + _patternCache map[string]*Pattern + _splitRegex *regexp.Regexp + _cache ChunkCache +) + +func init() { + // We can uniquely identify the pattern for a given string since + // mode and caseMode do not change while the program is running + _patternCache = make(map[string]*Pattern) + _splitRegex = regexp.MustCompile("\\s+") + _cache = NewChunkCache() +} + +func clearPatternCache() { + _patternCache = make(map[string]*Pattern) +} + +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: + asString = strings.Trim(string(runes), " ") + default: + asString = string(runes) + } + + cached, found := _patternCache[asString] + if found { + return cached + } + + caseSensitive, hasInvTerm := true, false + terms := []Term{} + + switch caseMode { + case CASE_SMART: + if !strings.ContainsAny(asString, UPPERCASE) { + runes, caseSensitive = []rune(strings.ToLower(asString)), false + } + case CASE_IGNORE: + runes, caseSensitive = []rune(strings.ToLower(asString)), false + } + + switch mode { + case MODE_EXTENDED, MODE_EXTENDED_EXACT: + terms = parseTerms(mode, string(runes)) + for _, term := range terms { + if term.inv { + hasInvTerm = true + } + } + } + + ptr := &Pattern{ + mode: mode, + caseSensitive: caseSensitive, + text: runes, + terms: terms, + hasInvTerm: hasInvTerm, + nth: nth, + delimiter: delimiter, + 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 + + _patternCache[asString] = ptr + return ptr +} + +func parseTerms(mode Mode, str string) []Term { + tokens := _splitRegex.Split(str, -1) + terms := []Term{} + for _, token := range tokens { + typ, inv, text := TERM_FUZZY, false, token + origText := []rune(text) + if mode == MODE_EXTENDED_EXACT { + typ = TERM_EXACT + } + + if strings.HasPrefix(text, "!") { + inv = true + text = text[1:] + } + + if strings.HasPrefix(text, "'") { + if mode == MODE_EXTENDED { + typ = TERM_EXACT + text = text[1:] + } + } else if strings.HasPrefix(text, "^") { + typ = TERM_PREFIX + text = text[1:] + } else if strings.HasSuffix(text, "$") { + typ = TERM_SUFFIX + text = text[:len(text)-1] + } + + if len(text) > 0 { + terms = append(terms, Term{ + typ: typ, + inv: inv, + text: []rune(text), + origText: origText}) + } + } + return terms +} + +func (p *Pattern) IsEmpty() bool { + if p.mode == MODE_FUZZY { + return len(p.text) == 0 + } else { + return len(p.terms) == 0 + } +} + +func (p *Pattern) AsString() string { + return string(p.text) +} + +func (p *Pattern) CacheKey() string { + if p.mode == MODE_FUZZY { + return p.AsString() + } + cacheableTerms := []string{} + for _, term := range p.terms { + if term.inv { + continue + } + cacheableTerms = append(cacheableTerms, string(term.origText)) + } + sort.Strings(cacheableTerms) + return strings.Join(cacheableTerms, " ") +} + +func (p *Pattern) Match(chunk *Chunk) []*Item { + space := chunk + + // ChunkCache: Exact match + cacheKey := p.CacheKey() + if !p.hasInvTerm { // Because we're excluding Inv-term from cache key + if cached, found := _cache.Find(chunk, cacheKey); found { + return cached + } + } + + // ChunkCache: Prefix match + foundPrefixCache := false + for idx := len(cacheKey) - 1; idx > 0; idx-- { + if cached, found := _cache.Find(chunk, cacheKey[:idx]); found { + cachedChunk := Chunk(cached) + space = &cachedChunk + foundPrefixCache = true + break + } + } + + // ChunkCache: Suffix match + if !foundPrefixCache { + for idx := 1; idx < len(cacheKey); idx++ { + if cached, found := _cache.Find(chunk, cacheKey[idx:]); found { + cachedChunk := Chunk(cached) + space = &cachedChunk + break + } + } + } + + var matches []*Item + if p.mode == MODE_FUZZY { + matches = p.fuzzyMatch(space) + } else { + matches = p.extendedMatch(space) + } + + if !p.hasInvTerm { + _cache.Add(chunk, cacheKey, matches) + } + return matches +} + +func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { + matches := []*Item{} + for _, item := range *chunk { + input := p.prepareInput(item) + if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { + matches = append(matches, &Item{ + text: item.text, + index: item.index, + offsets: []Offset{Offset{sidx, eidx}}, + rank: NilRank}) + } + } + return matches +} + +func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { + matches := []*Item{} + for _, item := range *chunk { + input := p.prepareInput(item) + offsets := []Offset{} + Loop: + for _, term := range p.terms { + pfun := p.procFun[term.typ] + if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { + if term.inv { + break Loop + } + offsets = append(offsets, Offset{sidx, eidx}) + } else if term.inv { + offsets = append(offsets, Offset{0, 0}) + } + } + if len(offsets) == len(p.terms) { + matches = append(matches, &Item{ + text: item.text, + index: item.index, + offsets: offsets, + rank: NilRank}) + } + } + return matches +} + +func (p *Pattern) prepareInput(item *Item) *Transformed { + if item.transformed != nil { + return item.transformed + } + + var ret *Transformed + if len(p.nth) > 0 { + tokens := Tokenize(item.text, p.delimiter) + ret = Transform(tokens, p.nth) + } else { + trans := Transformed{ + whole: item.text, + parts: []Token{Token{text: item.text, prefixLength: 0}}} + ret = &trans + } + item.transformed = ret + return ret +} + +func (p *Pattern) iter(pfun func(bool, *string, []rune) (int, int), + inputs *Transformed, pattern []rune) (int, int) { + for _, part := range inputs.parts { + prefixLength := part.prefixLength + if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { + return sidx + prefixLength, eidx + prefixLength + } + } + return -1, -1 +} diff --git a/src/pattern_test.go b/src/pattern_test.go new file mode 100644 index 0000000..a1ce626 --- /dev/null +++ b/src/pattern_test.go @@ -0,0 +1,87 @@ +package fzf + +import "testing" + +func TestParseTermsExtended(t *testing.T) { + terms := parseTerms(MODE_EXTENDED, + "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 { + t.Errorf("%s", terms) + } + for idx, term := range terms { + if len(term.text) != 3 { + t.Errorf("%s", term) + } + if idx > 0 && len(term.origText) != 4+idx/5 { + t.Errorf("%s", term) + } + } +} + +func TestParseTermsExtendedExact(t *testing.T) { + terms := parseTerms(MODE_EXTENDED_EXACT, + "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 { + t.Errorf("%s", terms) + } +} + +func TestParseTermsEmpty(t *testing.T) { + terms := parseTerms(MODE_EXTENDED, "' $ ^ !' !^ !$") + if len(terms) != 0 { + t.Errorf("%s", terms) + } +} + +func TestExact(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, + []Range{}, nil, []rune("'abc")) + str := "aabbcc abc" + sidx, eidx := ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) + if sidx != 7 || eidx != 10 { + t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) + } +} + +func TestCaseSensitivity(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pat1 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("abc")) + clearPatternCache() + pat2 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("Abc")) + clearPatternCache() + pat3 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("abc")) + clearPatternCache() + pat4 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("Abc")) + clearPatternCache() + pat5 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("abc")) + clearPatternCache() + pat6 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("Abc")) + + if string(pat1.text) != "abc" || pat1.caseSensitive != false || + string(pat2.text) != "Abc" || pat2.caseSensitive != true || + string(pat3.text) != "abc" || pat3.caseSensitive != false || + string(pat4.text) != "abc" || pat4.caseSensitive != false || + string(pat5.text) != "abc" || pat5.caseSensitive != true || + string(pat6.text) != "Abc" || pat6.caseSensitive != true { + t.Error("Invalid case conversion") + } +} diff --git a/src/reader.go b/src/reader.go new file mode 100644 index 0000000..0e1f0a9 --- /dev/null +++ b/src/reader.go @@ -0,0 +1,60 @@ +package fzf + +// #include +import "C" + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" +) + +const DEFAULT_COMMAND = "find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null" + +type Reader struct { + pusher func(string) + eventBox *EventBox +} + +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 + } + r.readFromCommand(cmd) + } else { + r.readFromStdin() + } + r.eventBox.Set(EVT_READ_FIN, 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) + } + } +} + +func (r *Reader) readFromStdin() { + r.feed(os.Stdin) +} + +func (r *Reader) readFromCommand(cmd string) { + arg := fmt.Sprintf("%q", cmd) + listCommand := exec.Command("sh", "-c", arg[1:len(arg)-1]) + out, err := listCommand.StdoutPipe() + if err != nil { + return + } + err = listCommand.Start() + if err != nil { + return + } + defer listCommand.Wait() + r.feed(out) +} diff --git a/src/reader_test.go b/src/reader_test.go new file mode 100644 index 0000000..f51ccab --- /dev/null +++ b/src/reader_test.go @@ -0,0 +1,52 @@ +package fzf + +import "testing" + +func TestReadFromCommand(t *testing.T) { + strs := []string{} + eb := NewEventBox() + reader := Reader{ + pusher: func(s string) { strs = append(strs, s) }, + eventBox: eb} + + // Check EventBox + if eb.Peak(EVT_READ_NEW) { + t.Error("EVT_READ_NEW should not be set yet") + } + + // Normal command + reader.readFromCommand(`echo abc && echo def`) + if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { + t.Errorf("%s", strs) + } + + // Check EventBox again + if !eb.Peak(EVT_READ_NEW) { + t.Error("EVT_READ_NEW should be set yet") + } + + // Wait should return immediately + eb.Wait(func(events *Events) { + if _, found := (*events)[EVT_READ_NEW]; !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") + } + + // Failing command + reader.readFromCommand(`no-such-command`) + strs = []string{} + if len(strs) > 0 { + t.Errorf("%s", strs) + } + + // Check EventBox again + if eb.Peak(EVT_READ_NEW) { + t.Error("Command failed. EVT_READ_NEW should be set") + } +} diff --git a/src/terminal.go b/src/terminal.go new file mode 100644 index 0000000..b6c7154 --- /dev/null +++ b/src/terminal.go @@ -0,0 +1,580 @@ +package fzf + +import ( + "fmt" + C "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/go-runewidth" + "os" + "regexp" + "sort" + "sync" + "time" +) + +type Terminal struct { + prompt string + reverse bool + tac bool + cx int + cy int + offset int + yanked []rune + input []rune + multi bool + printQuery bool + count int + progress int + reading bool + list []*Item + selected map[*string]*string + reqBox *EventBox + eventBox *EventBox + mutex sync.Mutex + initFunc func() +} + +var _spinner []string = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} + +const ( + REQ_PROMPT EventType = iota + REQ_INFO + REQ_LIST + REQ_REDRAW + REQ_CLOSE + REQ_QUIT +) + +func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { + input := []rune(opts.Query) + return &Terminal{ + prompt: opts.Prompt, + tac: opts.Sort == 0, + reverse: opts.Reverse, + cx: displayWidth(input), + cy: 0, + offset: 0, + yanked: []rune{}, + input: input, + multi: opts.Multi, + printQuery: opts.PrintQuery, + list: []*Item{}, + selected: make(map[*string]*string), + reqBox: NewEventBox(), + eventBox: eventBox, + mutex: sync.Mutex{}, + initFunc: func() { + C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) + }} +} + +func (t *Terminal) Input() []rune { + t.mutex.Lock() + defer t.mutex.Unlock() + return copySlice(t.input) +} + +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) +} + +func (t *Terminal) UpdateProgress(progress float32) { + t.mutex.Lock() + t.progress = int(progress * 100) + t.mutex.Unlock() + t.reqBox.Set(REQ_INFO, nil) +} + +func (t *Terminal) UpdateList(list []*Item) { + t.mutex.Lock() + t.progress = 100 + t.list = list + t.mutex.Unlock() + t.reqBox.Set(REQ_INFO, nil) + t.reqBox.Set(REQ_LIST, nil) +} + +func (t *Terminal) listIndex(y int) int { + if t.tac { + return len(t.list) - y - 1 + } else { + return y + } +} + +func (t *Terminal) output() { + if t.printQuery { + fmt.Println(string(t.input)) + } + if len(t.selected) == 0 { + if len(t.list) > t.cy { + t.list[t.listIndex(t.cy)].Print() + } + } else { + for ptr, orig := range t.selected { + if orig != nil { + fmt.Println(*orig) + } else { + fmt.Println(*ptr) + } + } + } +} + +func displayWidth(runes []rune) int { + l := 0 + for _, r := range runes { + l += runewidth.RuneWidth(r) + } + return l +} + +func (t *Terminal) move(y int, x int, clear bool) { + maxy := C.MaxY() + if !t.reverse { + y = maxy - y - 1 + } + + if clear { + C.MoveAndClear(y, x) + } else { + C.Move(y, x) + } +} + +func (t *Terminal) placeCursor() { + t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false) +} + +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)) +} + +func (t *Terminal) printInfo() { + t.move(1, 0, true) + if t.reading { + duration := int64(200) * int64(time.Millisecond) + idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration + C.CPrint(C.COL_SPINNER, true, _spinner[idx]) + } + + t.move(1, 2, false) + output := fmt.Sprintf("%d/%d", len(t.list), t.count) + if t.multi && len(t.selected) > 0 { + output += fmt.Sprintf(" (%d)", len(t.selected)) + } + if t.progress > 0 && t.progress < 100 { + output += fmt.Sprintf(" (%d%%)", t.progress) + } + C.CPrint(C.COL_INFO, false, output) +} + +func (t *Terminal) printList() { + t.constrain() + + maxy := maxItems() + count := len(t.list) - t.offset + for i := 0; i < maxy; i++ { + t.move(i+2, 0, true) + if i < count { + t.printItem(t.list[t.listIndex(i+t.offset)], i == t.cy-t.offset) + } + } +} + +func (t *Terminal) printItem(item *Item, current bool) { + _, selected := t.selected[item.text] + if current { + C.CPrint(C.COL_CURSOR, true, ">") + if selected { + C.CPrint(C.COL_CURRENT, true, ">") + } else { + C.CPrint(C.COL_CURRENT, true, " ") + } + t.printHighlighted(item, true, C.COL_CURRENT, C.COL_CURRENT_MATCH) + } else { + C.CPrint(C.COL_CURSOR, true, " ") + if selected { + C.CPrint(C.COL_SELECTED, true, ">") + } else { + C.Print(" ") + } + t.printHighlighted(item, false, 0, C.COL_MATCH) + } +} + +func trimRight(runes []rune, width int) ([]rune, int) { + currentWidth := displayWidth(runes) + trimmed := 0 + + for currentWidth > width && len(runes) > 0 { + sz := len(runes) + currentWidth -= runewidth.RuneWidth(runes[sz-1]) + runes = runes[:sz-1] + trimmed += 1 + } + return runes, trimmed +} + +func trimLeft(runes []rune, width int) ([]rune, int) { + currentWidth := displayWidth(runes) + trimmed := 0 + + for currentWidth > width && len(runes) > 0 { + currentWidth -= runewidth.RuneWidth(runes[0]) + runes = runes[1:] + trimmed += 1 + } + return runes, trimmed +} + +func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { + maxe := 0 + for _, offset := range item.offsets { + if offset[1] > maxe { + maxe = offset[1] + } + } + + // Overflow + text := []rune(*item.text) + offsets := item.offsets + maxWidth := C.MaxX() - 3 + fullWidth := displayWidth(text) + if fullWidth > maxWidth { + // Stri.. + matchEndWidth := displayWidth(text[:maxe]) + if matchEndWidth <= maxWidth-2 { + text, _ = trimRight(text, maxWidth-2) + text = append(text, []rune("..")...) + } else { + // Stri.. + if matchEndWidth < fullWidth-2 { + text = append(text[:maxe], []rune("..")...) + } + // ..ri.. + var diff int + text, diff = trimLeft(text, maxWidth-2) + + // Transform offsets + offsets = make([]Offset, len(item.offsets)) + for idx, offset := range item.offsets { + b, e := offset[0], offset[1] + b += 2 - diff + e += 2 - diff + b = Max(b, 2) + if b < e { + offsets[idx] = Offset{b, e} + } + } + text = append([]rune(".."), text...) + } + } + + sort.Sort(ByOrder(offsets)) + index := 0 + for _, offset := range offsets { + b := Max(index, offset[0]) + e := Max(index, offset[1]) + C.CPrint(col1, bold, string(text[index:b])) + C.CPrint(col2, bold, string(text[b:e])) + index = e + } + if index < len(text) { + C.CPrint(col1, bold, string(text[index:])) + } +} + +func (t *Terminal) printAll() { + t.printList() + t.printInfo() + t.printPrompt() +} + +func (t *Terminal) refresh() { + t.placeCursor() + C.Refresh() +} + +func (t *Terminal) delChar() bool { + if len(t.input) > 0 && t.cx < len(t.input) { + t.input = append(t.input[:t.cx], t.input[t.cx+1:]...) + return true + } + return false +} + +func findLastMatch(pattern string, str string) int { + rx, err := regexp.Compile(pattern) + if err != nil { + return -1 + } + locs := rx.FindAllStringIndex(str, -1) + if locs == nil { + return -1 + } + return locs[len(locs)-1][0] +} + +func findFirstMatch(pattern string, str string) int { + rx, err := regexp.Compile(pattern) + if err != nil { + return -1 + } + loc := rx.FindStringIndex(str) + if loc == nil { + return -1 + } + return loc[0] +} + +func copySlice(slice []rune) []rune { + ret := make([]rune, len(slice)) + copy(ret, slice) + return ret +} + +func (t *Terminal) rubout(pattern string) { + pcx := t.cx + after := t.input[t.cx:] + t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1 + t.yanked = copySlice(t.input[t.cx:pcx]) + t.input = append(t.input[:t.cx], after...) +} + +func (t *Terminal) Loop() { + { // Late initialization + t.mutex.Lock() + t.initFunc() + t.printInfo() + t.printPrompt() + t.refresh() + t.mutex.Unlock() + } + + go func() { + for { + t.reqBox.Wait(func(events *Events) { + defer events.Clear() + t.mutex.Lock() + for req := range *events { + switch req { + case REQ_PROMPT: + t.printPrompt() + case REQ_INFO: + t.printInfo() + case REQ_LIST: + t.printList() + case REQ_REDRAW: + C.Clear() + t.printAll() + case REQ_CLOSE: + C.Close() + t.output() + os.Exit(0) + case REQ_QUIT: + C.Close() + os.Exit(1) + } + } + t.mutex.Unlock() + }) + t.refresh() + } + }() + + looping := true + for looping { + event := C.GetChar() + + t.mutex.Lock() + previousInput := t.input + events := []EventType{REQ_PROMPT} + toggle := func() { + item := t.list[t.listIndex(t.cy)] + if _, found := t.selected[item.text]; !found { + t.selected[item.text] = item.origText + } else { + delete(t.selected, item.text) + } + } + req := func(evts ...EventType) { + for _, event := range evts { + events = append(events, event) + if event == REQ_CLOSE || event == REQ_QUIT { + looping = false + } + } + } + switch event.Type { + case C.INVALID: + continue + case C.CTRL_A: + t.cx = 0 + case C.CTRL_B: + if t.cx > 0 { + t.cx -= 1 + } + case C.CTRL_C, C.CTRL_G, C.CTRL_Q, C.ESC: + req(REQ_QUIT) + case C.CTRL_D: + if !t.delChar() && t.cx == 0 { + req(REQ_QUIT) + } + case C.CTRL_E: + t.cx = len(t.input) + case C.CTRL_F: + if t.cx < len(t.input) { + t.cx += 1 + } + case C.CTRL_H: + if t.cx > 0 { + t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) + t.cx -= 1 + } + case C.TAB: + if t.multi && len(t.list) > 0 { + toggle() + t.vmove(-1) + req(REQ_LIST, REQ_INFO) + } + case C.BTAB: + if t.multi && len(t.list) > 0 { + toggle() + t.vmove(1) + req(REQ_LIST, REQ_INFO) + } + case C.CTRL_J, C.CTRL_N: + t.vmove(-1) + req(REQ_LIST) + case C.CTRL_K, C.CTRL_P: + t.vmove(1) + req(REQ_LIST) + case C.CTRL_M: + req(REQ_CLOSE) + case C.CTRL_L: + req(REQ_REDRAW) + case C.CTRL_U: + if t.cx > 0 { + t.yanked = copySlice(t.input[:t.cx]) + t.input = t.input[t.cx:] + t.cx = 0 + } + case C.CTRL_W: + if t.cx > 0 { + t.rubout("\\s\\S") + } + case C.ALT_BS: + if t.cx > 0 { + t.rubout("[^[:alnum:]][[:alnum:]]") + } + case C.CTRL_Y: + t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...) + t.cx += len(t.yanked) + case C.DEL: + t.delChar() + case C.PGUP: + t.vmove(maxItems() - 1) + req(REQ_LIST) + case C.PGDN: + t.vmove(-(maxItems() - 1)) + req(REQ_LIST) + case C.ALT_B: + t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 + case C.ALT_F: + t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + case C.ALT_D: + 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: + prefix := copySlice(t.input[:t.cx]) + t.input = append(append(prefix, event.Char), t.input[t.cx:]...) + t.cx += 1 + case C.MOUSE: + me := event.MouseEvent + mx, my := Min(len(t.input), Max(0, me.X-len(t.prompt))), me.Y + if !t.reverse { + my = C.MaxY() - my - 1 + } + if me.S != 0 { + // Scroll + if me.Mod { + toggle() + } + t.vmove(me.S) + req(REQ_LIST) + } else if me.Double { + // Double-click + if my >= 2 { + t.cy = my - 2 + req(REQ_CLOSE) + } + } else if me.Down { + if my == 0 && mx >= 0 { + // Prompt + t.cx = mx + req(REQ_PROMPT) + } else if my >= 2 { + // List + t.cy = my - 2 + if me.Mod { + toggle() + } + req(REQ_LIST) + } + } + } + changed := string(previousInput) != string(t.input) + t.mutex.Unlock() // Must be unlocked before touching reqBox + + if changed { + t.eventBox.Set(EVT_SEARCH_NEW, nil) + } + for _, event := range events { + t.reqBox.Set(event, nil) + } + } +} + +func (t *Terminal) constrain() { + count := len(t.list) + height := C.MaxY() - 2 + diffpos := t.cy - t.offset + + t.cy = Max(0, Min(t.cy, count-1)) + + if t.cy > t.offset+(height-1) { + // Ceil + t.offset = t.cy - (height - 1) + } else if t.offset > t.cy { + // Floor + t.offset = t.cy + } + + // Adjustment + if count-t.offset < height { + t.offset = Max(0, count-height) + t.cy = Max(0, Min(t.offset+diffpos, count-1)) + } +} + +func (t *Terminal) vmove(o int) { + if t.reverse { + t.cy -= o + } else { + t.cy += o + } +} + +func maxItems() int { + return C.MaxY() - 2 +} diff --git a/src/tokenizer.go b/src/tokenizer.go new file mode 100644 index 0000000..c187529 --- /dev/null +++ b/src/tokenizer.go @@ -0,0 +1,194 @@ +package fzf + +import ( + "regexp" + "strconv" + "strings" +) + +const RANGE_ELLIPSIS = 0 + +type Range struct { + begin int + end int +} + +type Transformed struct { + whole *string + parts []Token +} + +type Token struct { + text *string + prefixLength int +} + +func ParseRange(str *string) (Range, bool) { + if (*str) == ".." { + return Range{RANGE_ELLIPSIS, RANGE_ELLIPSIS}, 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 + } + } 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 + } + } else if strings.Contains(*str, "..") { + ns := strings.Split(*str, "..") + if len(ns) != 2 { + return Range{}, false + } + begin, err1 := strconv.Atoi(ns[0]) + end, err2 := strconv.Atoi(ns[1]) + if err1 != nil || err2 != nil { + return Range{}, false + } + return Range{begin, end}, true + } + + n, err := strconv.Atoi(*str) + if err != nil || n == 0 { + return Range{}, false + } + return Range{n, n}, true +} + +func withPrefixLengths(tokens []string, begin int) []Token { + ret := make([]Token, len(tokens)) + + prefixLength := begin + for idx, token := range tokens { + // Need to define a new local variable instead of the reused token to take + // the pointer to it + str := token + ret[idx] = Token{text: &str, prefixLength: prefixLength} + prefixLength += len([]rune(token)) + } + return ret +} + +const ( + AWK_NIL = iota + AWK_BLACK + AWK_WHITE +) + +func awkTokenizer(input *string) ([]string, int) { + // 9, 32 + ret := []string{} + str := []rune{} + prefixLength := 0 + state := AWK_NIL + for _, r := range []rune(*input) { + white := r == 9 || r == 32 + switch state { + case AWK_NIL: + if white { + prefixLength++ + } else { + state = AWK_BLACK + str = append(str, r) + } + case AWK_BLACK: + str = append(str, r) + if white { + state = AWK_WHITE + } + case AWK_WHITE: + if white { + str = append(str, r) + } else { + ret = append(ret, string(str)) + state = AWK_BLACK + str = []rune{r} + } + } + } + if len(str) > 0 { + ret = append(ret, string(str)) + } + return ret, prefixLength +} + +func Tokenize(str *string, delimiter *regexp.Regexp) []Token { + prefixLength := 0 + if delimiter == nil { + // AWK-style (\S+\s*) + tokens, prefixLength := awkTokenizer(str) + return withPrefixLengths(tokens, prefixLength) + } else { + tokens := delimiter.FindAllString(*str, -1) + return withPrefixLengths(tokens, prefixLength) + } +} + +func joinTokens(tokens []Token) string { + ret := "" + for _, token := range tokens { + ret += *token.text + } + return ret +} + +func Transform(tokens []Token, withNth []Range) *Transformed { + transTokens := make([]Token, len(withNth)) + numTokens := len(tokens) + whole := "" + for idx, r := range withNth { + part := "" + minIdx := 0 + if r.begin == r.end { + idx := r.begin + if idx == RANGE_ELLIPSIS { + part += joinTokens(tokens) + } else { + if idx < 0 { + idx += numTokens + 1 + } + if idx >= 1 && idx <= numTokens { + minIdx = idx - 1 + part += *tokens[idx-1].text + } + } + } else { + var begin, end int + if r.begin == RANGE_ELLIPSIS { // ..N + begin, end = 1, r.end + if end < 0 { + end += numTokens + 1 + } + } else if r.end == RANGE_ELLIPSIS { // N.. + begin, end = r.begin, numTokens + if begin < 0 { + begin += numTokens + 1 + } + } else { + begin, end = r.begin, r.end + if begin < 0 { + begin += numTokens + 1 + } + if end < 0 { + end += numTokens + 1 + } + } + minIdx = Max(0, begin-1) + for idx := begin; idx <= end; idx++ { + if idx >= 1 && idx <= numTokens { + part += *tokens[idx-1].text + } + } + } + whole += part + transTokens[idx] = Token{&part, tokens[minIdx].prefixLength} + } + return &Transformed{ + whole: &whole, + parts: transTokens} +} diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go new file mode 100644 index 0000000..ed77efe --- /dev/null +++ b/src/tokenizer_test.go @@ -0,0 +1,97 @@ +package fzf + +import "testing" + +func TestParseRange(t *testing.T) { + { + i := ".." + r, _ := ParseRange(&i) + if r.begin != RANGE_ELLIPSIS || r.end != RANGE_ELLIPSIS { + t.Errorf("%s", r) + } + } + { + i := "3.." + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != RANGE_ELLIPSIS { + t.Errorf("%s", r) + } + } + { + i := "3..5" + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != 5 { + t.Errorf("%s", r) + } + } + { + i := "-3..-5" + r, _ := ParseRange(&i) + if r.begin != -3 || r.end != -5 { + t.Errorf("%s", r) + } + } + { + i := "3" + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != 3 { + t.Errorf("%s", r) + } + } +} + +func TestTokenize(t *testing.T) { + // AWK-style + input := " abc: def: ghi " + tokens := Tokenize(&input, nil) + if *tokens[0].text != "abc: " || tokens[0].prefixLength != 2 { + t.Errorf("%s", tokens) + } + + // With delimiter + tokens = Tokenize(&input, delimiterRegexp(":")) + if *tokens[0].text != " abc:" || tokens[0].prefixLength != 0 { + t.Errorf("%s", tokens) + } +} + +func TestTransform(t *testing.T) { + input := " abc: def: ghi: jkl" + { + tokens := Tokenize(&input, nil) + { + ranges := splitNth("1,2,3") + tx := Transform(tokens, ranges) + if *tx.whole != "abc: def: ghi: " { + t.Errorf("%s", *tx) + } + } + { + ranges := splitNth("1..2,3,2..,1") + tx := Transform(tokens, ranges) + if *tx.whole != "abc: def: ghi: def: ghi: jklabc: " || + len(tx.parts) != 4 || + *tx.parts[0].text != "abc: def: " || tx.parts[0].prefixLength != 2 || + *tx.parts[1].text != "ghi: " || tx.parts[1].prefixLength != 14 || + *tx.parts[2].text != "def: ghi: jkl" || tx.parts[2].prefixLength != 8 || + *tx.parts[3].text != "abc: " || tx.parts[3].prefixLength != 2 { + t.Errorf("%s", *tx) + } + } + } + { + tokens := Tokenize(&input, delimiterRegexp(":")) + { + ranges := splitNth("1..2,3,2..,1") + tx := Transform(tokens, ranges) + if *tx.whole != " abc: def: ghi: def: ghi: jkl abc:" || + len(tx.parts) != 4 || + *tx.parts[0].text != " abc: def:" || tx.parts[0].prefixLength != 0 || + *tx.parts[1].text != " ghi:" || tx.parts[1].prefixLength != 12 || + *tx.parts[2].text != " def: ghi: jkl" || tx.parts[2].prefixLength != 6 || + *tx.parts[3].text != " abc:" || tx.parts[3].prefixLength != 0 { + t.Errorf("%s", *tx) + } + } + } +} diff --git a/src/util.go b/src/util.go new file mode 100644 index 0000000..2144e54 --- /dev/null +++ b/src/util.go @@ -0,0 +1,21 @@ +package fzf + +func Max(first int, items ...int) int { + max := first + for _, item := range items { + if item > max { + max = item + } + } + return max +} + +func Min(first int, items ...int) int { + min := first + for _, item := range items { + if item < min { + min = item + } + } + return min +} diff --git a/src/util_test.go b/src/util_test.go new file mode 100644 index 0000000..814b42c --- /dev/null +++ b/src/util_test.go @@ -0,0 +1,18 @@ +package fzf + +import "testing" + +func TestMax(t *testing.T) { + if Max(-2, 5, 1, 4, 3) != 5 { + t.Error("Invalid result") + } +} + +func TestMin(t *testing.T) { + if Min(2, -3) != -3 { + t.Error("Invalid result") + } + if Min(-2, 5, 1, 4, 3) != -2 { + t.Error("Invalid result") + } +} From baad26a0fd0d39640ec31f98d92a8745b022f755 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 01:36:33 +0900 Subject: [PATCH 02/51] Fix exit conditions of --select-1 and --exit-0 --- src/core.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core.go b/src/core.go index 2601397..4cdf79a 100644 --- a/src/core.go +++ b/src/core.go @@ -94,7 +94,8 @@ func Run(options *Options) { chunks: chunkList.Snapshot(), pattern: pattern}, limit) - if !cancelled && (filtering || opts.Exit0) { + if !cancelled && (filtering || + opts.Exit0 && len(matches) == 0 || opts.Select1 && len(matches) == 1) { if opts.PrintQuery { fmt.Println(patternString) } From 40d0a6347c65b523d60f8d7898eafc42e1e4a3b6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 01:47:59 +0900 Subject: [PATCH 03/51] Fix scan limit for --select-1 and --exit-0 options --- src/matcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcher.go b/src/matcher.go index 363b07f..ad782bd 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -150,7 +150,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { if cancelled.Get() { return } - countChan <- len(sliceMatches) + countChan <- len(matches) } if !empty && m.sort { sort.Sort(ByRelevance(sliceMatches)) From 9930a1d4d9cf92fe869f9352177dd24bdf1ac13f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 02:00:22 +0900 Subject: [PATCH 04/51] Update install script to download tarball --- install | 11 ++++++++--- src/Makefile | 7 +++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/install b/install index 6b64a3f..5a99930 100755 --- a/install +++ b/install @@ -6,16 +6,21 @@ fzf_base=`pwd` ARCHI=$(uname -sm) download() { + mkdir -p "$fzf_base"/bin + cd "$fzf_base"/bin echo "Downloading fzf executable ($1) ..." - if curl -fLo "$fzf_base"/bin/fzf https://github.com/junegunn/fzf-bin/releases/download/snapshot/$1; then - chmod +x "$fzf_base"/bin/fzf + if curl -fL \ + https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz | + tar -xz; then + mv $1 fzf + chmod +x fzf else echo "Failed to download $1" exit 1 fi + cd - > /dev/null } -mkdir -p "$fzf_base"/bin if [ "$ARCHI" = "Darwin x86_64" ]; then download fzf_darwin_amd64 elif [ "$ARCHI" = "Linux x86_64" ]; then diff --git a/src/Makefile b/src/Makefile index bae4c90..fecf7d2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -25,7 +25,10 @@ SOURCES = $(wildcard *.go fzf/*.go) all: build -build: $(BINARY) +build: $(BINARY).tar.gz + +$(BINARY).tar.gz: $(BINARY) + cd fzf && tar -czf $(notdir $(BINARY)).tar.gz $(notdir $(BINARY)) $(BINARY): $(SOURCES) go get @@ -37,7 +40,7 @@ install: $(BINARY) cp -f $(BINARY) $(BINDIR)/fzf clean: - rm -f $(BINARY) + rm -f $(BINARY) $(BINARY).tar.gz docker: docker build -t junegunn/ubuntu-sandbox . From 0a6cb62169ebe1abb63aa5c70868df7c40441b1c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 02:42:58 +0900 Subject: [PATCH 05/51] Fall back to Ruby version when download failed --- install | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/install b/install index 5a99930..46f6553 100755 --- a/install +++ b/install @@ -6,27 +6,49 @@ fzf_base=`pwd` ARCHI=$(uname -sm) download() { - mkdir -p "$fzf_base"/bin - cd "$fzf_base"/bin echo "Downloading fzf executable ($1) ..." - if curl -fL \ - https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz | - tar -xz; then - mv $1 fzf - chmod +x fzf - else - echo "Failed to download $1" - exit 1 + mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin + if [ $? -ne 0 ]; then + echo "- Failed to create bin directory." + return 1 fi - cd - > /dev/null + + local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz + if which curl > /dev/null; then + curl -fL $url | tar -xz + elif which wget > /dev/null; then + wget -O - $url | tar -xz + else + echo "- curl or wget required to download fzf executable." + return 1 + fi + + if [ ! -f $1 ]; then + echo "- Failed to download ${1}." + return 1 + fi + + mv $1 fzf && chmod +x fzf && cd - > /dev/null && echo } +# Try to download binary executable +binary_available=0 +downloaded=0 if [ "$ARCHI" = "Darwin x86_64" ]; then - download fzf_darwin_amd64 + binary_available=1 + download fzf_darwin_amd64 && downloaded=1 elif [ "$ARCHI" = "Linux x86_64" ]; then - download fzf_linux_amd64 -else # No prebuilt executable - echo "No prebuilt binary for $ARCHI ... Installing legacy Ruby version ..." + binary_available=1 + download fzf_linux_amd64 && downloaded=1 +fi + +if [ $downloaded -ne 1 ]; then + if [ $binary_available -eq 0 ]; then + echo -n "No prebuilt binary for $ARCHI ... " + else + echo -n "Failed to download binary executable ... " + fi + echo "Installing legacy Ruby version ..." # ruby executable echo -n "Checking Ruby executable ... " From 0dd024a09fc4dd37a596a07e7ff0043537895909 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 05:00:28 +0900 Subject: [PATCH 06/51] Remove unnecessary delay on non/defered interactive mode --- src/core.go | 3 ++- src/eventbox.go | 26 ++++++++++++++++++++-- src/eventbox_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/eventbox_test.go diff --git a/src/core.go b/src/core.go index 4cdf79a..7abee80 100644 --- a/src/core.go +++ b/src/core.go @@ -77,6 +77,7 @@ func Run(options *Options) { pattern := patternBuilder([]rune(patternString)) looping := true + eventBox.Unwatch(EVT_READ_NEW) for looping { eventBox.Wait(func(events *Events) { for evt, _ := range *events { @@ -87,7 +88,6 @@ func Run(options *Options) { } } }) - time.Sleep(COORDINATOR_DELAY) } matches, cancelled := matcher.scan(MatchRequest{ @@ -116,6 +116,7 @@ func Run(options *Options) { // Event coordination reading := true ticks := 0 + eventBox.Watch(EVT_READ_NEW) for { delay := true ticks += 1 diff --git a/src/eventbox.go b/src/eventbox.go index 6685e7c..95126cc 100644 --- a/src/eventbox.go +++ b/src/eventbox.go @@ -9,10 +9,14 @@ type Events map[EventType]interface{} type EventBox struct { events Events cond *sync.Cond + ignore map[EventType]bool } func NewEventBox() *EventBox { - return &EventBox{make(Events), sync.NewCond(&sync.Mutex{})} + return &EventBox{ + events: make(Events), + cond: sync.NewCond(&sync.Mutex{}), + ignore: make(map[EventType]bool)} } func (b *EventBox) Wait(callback func(*Events)) { @@ -30,7 +34,9 @@ func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Lock() defer b.cond.L.Unlock() b.events[event] = value - b.cond.Broadcast() + if _, found := b.ignore[event]; !found { + b.cond.Broadcast() + } } // Unsynchronized; should be called within Wait routine @@ -46,3 +52,19 @@ func (b *EventBox) Peak(event EventType) bool { _, ok := b.events[event] return ok } + +func (b *EventBox) Watch(events ...EventType) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + for _, event := range events { + delete(b.ignore, event) + } +} + +func (b *EventBox) Unwatch(events ...EventType) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + for _, event := range events { + b.ignore[event] = true + } +} diff --git a/src/eventbox_test.go b/src/eventbox_test.go new file mode 100644 index 0000000..fb0ceed --- /dev/null +++ b/src/eventbox_test.go @@ -0,0 +1,51 @@ +package fzf + +import "testing" + +func TestEventBox(t *testing.T) { + eb := NewEventBox() + + // Wait should return immediately + ch := make(chan bool) + + go func() { + eb.Set(EVT_READ_NEW, 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) + ch <- true + <-ch + eb.Set(EVT_SEARCH_FIN, 40) + ch <- true + <-ch + }() + + count := 0 + sum := 0 + looping := true + for looping { + <-ch + eb.Wait(func(events *Events) { + for _, value := range *events { + switch val := value.(type) { + case int: + sum += val + looping = sum < 100 + } + } + events.Clear() + }) + ch <- true + count += 1 + } + + if count != 3 { + t.Error("Invalid number of events", count) + } + if sum != 100 { + t.Error("Invalid sum", sum) + } +} From d2f7acbc69de26084c83bb07e2a175e05dce2fc2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 05:01:13 +0900 Subject: [PATCH 07/51] Remove race conditions when accessing the last chunk --- src/chunklist.go | 25 +++++++++++++++---------- src/chunklist_test.go | 20 ++++++++++++++------ src/core.go | 11 +++++++---- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index b1f9638..5bca6da 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -42,14 +42,6 @@ func CountItems(cs []*Chunk) int { return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1])) } -func (cl *ChunkList) Count() int { - return cl.count -} - -func (cl *ChunkList) Chunks() []*Chunk { - return cl.chunks -} - func (cl *ChunkList) Push(data string) { cl.mutex.Lock() defer cl.mutex.Unlock() @@ -63,11 +55,24 @@ func (cl *ChunkList) Push(data string) { cl.count += 1 } -func (cl *ChunkList) Snapshot() []*Chunk { +func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() defer cl.mutex.Unlock() ret := make([]*Chunk, len(cl.chunks)) copy(ret, cl.chunks) - return ret + + // Duplicate the last chunk + if cnt := len(ret); cnt > 0 { + ret[cnt-1] = ret[cnt-1].dupe() + } + return ret, cl.count +} + +func (c *Chunk) dupe() *Chunk { + newChunk := make(Chunk, len(*c)) + for idx, ptr := range *c { + newChunk[idx] = ptr + } + return &newChunk } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index a7daa47..b244ece 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -11,8 +11,8 @@ func TestChunkList(t *testing.T) { }) // Snapshot - snapshot := cl.Snapshot() - if len(snapshot) > 0 { + snapshot, count := cl.Snapshot() + if len(snapshot) > 0 || count > 0 { t.Error("Snapshot should be empty now") } @@ -26,8 +26,8 @@ func TestChunkList(t *testing.T) { } // But the new snapshot should contain the added items - snapshot = cl.Snapshot() - if len(snapshot) != 1 { + snapshot, count = cl.Snapshot() + if len(snapshot) != 1 && count != 2 { t.Error("Snapshot should not be empty now") } @@ -55,12 +55,20 @@ func TestChunkList(t *testing.T) { } // New snapshot - snapshot = cl.Snapshot() + snapshot, count = cl.Snapshot() if len(snapshot) != 3 || !snapshot[0].IsFull() || - !snapshot[1].IsFull() || snapshot[2].IsFull() { + !snapshot[1].IsFull() || snapshot[2].IsFull() || count != CHUNK_SIZE*2+2 { t.Error("Expected two full chunks and one more chunk") } if len(*snapshot[2]) != 2 { t.Error("Unexpected number of items") } + + cl.Push("hello") + cl.Push("world") + + lastChunkCount := len(*snapshot[len(snapshot)-1]) + if lastChunkCount != 2 { + t.Error("Unexpected number of items:", lastChunkCount) + } } diff --git a/src/core.go b/src/core.go index 7abee80..b6f0857 100644 --- a/src/core.go +++ b/src/core.go @@ -90,8 +90,9 @@ func Run(options *Options) { }) } + snapshot, _ := chunkList.Snapshot() matches, cancelled := matcher.scan(MatchRequest{ - chunks: chunkList.Snapshot(), + chunks: snapshot, pattern: pattern}, limit) if !cancelled && (filtering || @@ -127,11 +128,13 @@ func Run(options *Options) { case EVT_READ_NEW, EVT_READ_FIN: reading = reading && evt == EVT_READ_NEW - terminal.UpdateCount(chunkList.Count(), !reading) - matcher.Reset(chunkList.Snapshot(), terminal.Input(), false) + snapshot, count := chunkList.Snapshot() + terminal.UpdateCount(count, !reading) + matcher.Reset(snapshot, terminal.Input(), false) case EVT_SEARCH_NEW: - matcher.Reset(chunkList.Snapshot(), terminal.Input(), true) + snapshot, _ := chunkList.Snapshot() + matcher.Reset(snapshot, terminal.Input(), true) delay = false case EVT_SEARCH_PROGRESS: From 606d33e77e6e6aa2f03c4886db781260caff3a34 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 05:09:40 +0900 Subject: [PATCH 08/51] Remove race conditions from screen update --- src/terminal.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index b6c7154..4d5c9cc 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -297,7 +297,6 @@ func (t *Terminal) printAll() { } func (t *Terminal) refresh() { - t.placeCursor() C.Refresh() } @@ -353,6 +352,7 @@ func (t *Terminal) Loop() { t.initFunc() t.printInfo() t.printPrompt() + t.placeCursor() t.refresh() t.mutex.Unlock() } @@ -382,6 +382,7 @@ func (t *Terminal) Loop() { os.Exit(1) } } + t.placeCursor() t.mutex.Unlock() }) t.refresh() From f9f9b671c5dda3a88b515e9a5e9f9cbf292b849b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 14:29:42 +0900 Subject: [PATCH 09/51] Ask if fzf executable already exists --- install | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/install b/install index 46f6553..8ea1435 100755 --- a/install +++ b/install @@ -5,8 +5,17 @@ fzf_base=`pwd` ARCHI=$(uname -sm) +ask() { + read -p "$1 ([y]/n) " -n 1 -r + echo + [[ ! $REPLY =~ ^[Nn]$ ]] +} + download() { echo "Downloading fzf executable ($1) ..." + if [ -x "$fzf_base"/bin/fzf ]; then + ask "- fzf already exists. Download it again?" || return 0 + fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then echo "- Failed to create bin directory." @@ -108,15 +117,11 @@ if [ $downloaded -ne 1 ]; then fi # Auto-completion -read -p "Do you want to add auto-completion support? ([y]/n) " -n 1 -r -echo -[[ ! $REPLY =~ ^[Nn]$ ]] +ask "Do you want to add auto-completion support?" auto_completion=$? # Key-bindings -read -p "Do you want to add key bindings? ([y]/n) " -n 1 -r -echo -[[ ! $REPLY =~ ^[Nn]$ ]] +ask "Do you want to add key bindings?" key_bindings=$? echo From 53bce0581edeac68b49af7608cfc080d52ab5cc3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 14:35:13 +0900 Subject: [PATCH 10/51] Update fish function --- install | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/install b/install index 8ea1435..3a708a1 100755 --- a/install +++ b/install @@ -303,11 +303,19 @@ if [ -n "$(which fish)" ]; then has_fish=1 echo -n "Generate ~/.config/fish/functions/fzf.fish ... " mkdir -p ~/.config/fish/functions - cat > ~/.config/fish/functions/fzf.fish << EOFZF + if [ $downloaded -eq 0 ]; then + cat > ~/.config/fish/functions/fzf.fish << EOFZF function fzf $fzf_cmd \$argv end EOFZF + else + cat > ~/.config/fish/functions/fzf.fish << EOFZF +function fzf + $fzf_base/bin/fzf \$argv +end +EOFZF + fi echo "OK" if [ $key_bindings -eq 0 ]; then From 755773756950ae3124eb82224c21a42e605b6194 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 00:52:08 +0900 Subject: [PATCH 11/51] Remove outdated information from README --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index 0b52864..998db0c 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,6 @@ fzf is a general-purpose fuzzy finder for your shell. It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and the likes. -Requirements ------------- - -fzf requires Ruby (>= 1.8.5). - Installation ------------ @@ -436,21 +431,6 @@ If you have any rendering issues, check the followings: option. And if it solves your problem, I recommend including it in `FZF_DEFAULT_OPTS` for further convenience. 4. If you still have problem, try `--no-256` option or even `--no-color`. -5. Ruby 1.9 or above is required for correctly displaying unicode characters. - -### Ranking algorithm - -fzf sorts the result first by the length of the matched substring, then by the -length of the whole string. However it only does so when the number of matches -is less than the limit which is by default 1000, in order to avoid the cost of -sorting a large list and limit the response time of the query. - -This limit can be adjusted with `-s` option, or with the environment variable -`FZF_DEFAULT_OPTS`. - -```sh -export FZF_DEFAULT_OPTS="--sort 20000" -``` ### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` @@ -545,12 +525,6 @@ function fe end ``` -### Windows - -fzf works on [Cygwin](http://www.cygwin.com/) and -[MSYS2](http://sourceforge.net/projects/msys2/). You may need to use `--black` -option on MSYS2 to avoid rendering issues. - ### Handling UTF-8 NFD paths on OSX Use iconv to convert NFD paths to NFC: From 8e5ecf6b383c35c7f33f7933e35959c2fc9b893c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 01:25:54 +0900 Subject: [PATCH 12/51] Update Makefile and installer to use version number --- install | 17 ++++++++--------- src/Makefile | 45 ++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/install b/install index 3a708a1..e331d31 100755 --- a/install +++ b/install @@ -3,8 +3,6 @@ cd `dirname $BASH_SOURCE` fzf_base=`pwd` -ARCHI=$(uname -sm) - ask() { read -p "$1 ([y]/n) " -n 1 -r echo @@ -22,7 +20,7 @@ download() { return 1 fi - local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz + local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tgz if which curl > /dev/null; then curl -fL $url | tar -xz elif which wget > /dev/null; then @@ -41,19 +39,20 @@ download() { } # Try to download binary executable -binary_available=0 +archi=$(uname -sm) downloaded=0 -if [ "$ARCHI" = "Darwin x86_64" ]; then +binary_available=0 +if [ "$archi" = "Darwin x86_64" ]; then binary_available=1 - download fzf_darwin_amd64 && downloaded=1 -elif [ "$ARCHI" = "Linux x86_64" ]; then + download fzf-0.9.0-darwin_amd64 && downloaded=1 +elif [ "$archi" = "Linux x86_64" ]; then binary_available=1 - download fzf_linux_amd64 && downloaded=1 + download fzf-0.9.0-linux_amd64 && downloaded=1 fi if [ $downloaded -ne 1 ]; then if [ $binary_available -eq 0 ]; then - echo -n "No prebuilt binary for $ARCHI ... " + echo -n "No prebuilt binary for $archi ... " else echo -n "Failed to download binary executable ... " fi diff --git a/src/Makefile b/src/Makefile index fecf7d2..10429a1 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,52 +1,51 @@ -BINARY := fzf/fzf - UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) - BINARY := $(BINARY)_darwin + SUFFIX := darwin else ifeq ($(UNAME_S),Linux) - BINARY := $(BINARY)_linux + SUFFIX := linux endif UNAME_M := $(shell uname -m) -ifneq ($(filter i386 i686,$(UNAME_M)),) -$(error "filtered is not supported, yet.") -endif - ifeq ($(UNAME_M),x86_64) - BINARY := $(BINARY)_amd64 + SUFFIX := $(SUFFIX)_amd64 else ifneq ($(filter i386 i686,$(UNAME_M)),) - BINARY := $(BINARY)_386 + SUFFIX := $(SUFFIX)_386 else # TODO $(error "$(UNAME_M) is not supported, yet.") endif -BINDIR = ../bin -SOURCES = $(wildcard *.go fzf/*.go) +BINARY := fzf-$(SUFFIX) +BINDIR := ../bin +SOURCES := $(wildcard *.go fzf/*.go) +RELEASE = fzf-$(shell fzf/$(BINARY) --version)-$(SUFFIX) -all: build +all: release -build: $(BINARY).tar.gz +release: build + cd fzf && \ + cp $(BINARY) $(RELEASE) && \ + tar -czf $(RELEASE).tgz $(RELEASE) && \ + rm $(RELEASE) -$(BINARY).tar.gz: $(BINARY) - cd fzf && tar -czf $(notdir $(BINARY)).tar.gz $(notdir $(BINARY)) +build: fzf/$(BINARY) -$(BINARY): $(SOURCES) +fzf/$(BINARY): $(SOURCES) go get go test -v - cd fzf && go build -o $(notdir $(BINARY)) + cd fzf && go build -o $(BINARY) -install: $(BINARY) +install: fzf/$(BINARY) mkdir -p $(BINDIR) - cp -f $(BINARY) $(BINDIR)/fzf + cp -f fzf/$(BINARY) $(BINDIR)/fzf clean: - rm -f $(BINARY) $(BINARY).tar.gz + cd fzf && rm -f $(BINARY) $(RELEASE).tgz docker: docker build -t junegunn/ubuntu-sandbox . linux64: docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ - /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make build' + /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make' -.PHONY: build install linux64 clean docker run +.PHONY: build release install linux64 clean docker run From dee0909d2bd665a885855c366ed2b2137819d8fb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 01:40:19 +0900 Subject: [PATCH 13/51] Fix mouse click offset when list is scrolled --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 4d5c9cc..28a9a33 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -526,7 +526,7 @@ func (t *Terminal) Loop() { req(REQ_PROMPT) } else if my >= 2 { // List - t.cy = my - 2 + t.cy = t.offset + my - 2 if me.Mod { toggle() } From ea25e9674f84071dab194a6e35973373ef03e02a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 02:17:26 +0900 Subject: [PATCH 14/51] Refactor install script --- install | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install b/install index e331d31..855e056 100755 --- a/install +++ b/install @@ -1,5 +1,7 @@ #!/usr/bin/env bash +version=0.9.0 + cd `dirname $BASH_SOURCE` fzf_base=`pwd` @@ -44,10 +46,10 @@ downloaded=0 binary_available=0 if [ "$archi" = "Darwin x86_64" ]; then binary_available=1 - download fzf-0.9.0-darwin_amd64 && downloaded=1 + download fzf-$version-darwin_amd64 && downloaded=1 elif [ "$archi" = "Linux x86_64" ]; then binary_available=1 - download fzf-0.9.0-linux_amd64 && downloaded=1 + download fzf-$version-linux_amd64 && downloaded=1 fi if [ $downloaded -ne 1 ]; then From 4a5142c60b1e833425a19de744b48ad1753f0543 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 02:32:18 +0900 Subject: [PATCH 15/51] Do not sort terms when building cache key --- src/pattern.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 533aa59..7c27f52 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -2,7 +2,6 @@ package fzf import ( "regexp" - "sort" "strings" ) @@ -181,7 +180,6 @@ func (p *Pattern) CacheKey() string { } cacheableTerms = append(cacheableTerms, string(term.origText)) } - sort.Strings(cacheableTerms) return strings.Join(cacheableTerms, " ") } From 82156d34ccf109b95b626852741bee8ee74f8378 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 12:21:26 +0900 Subject: [PATCH 16/51] Update Makefile and install script fzf may not run correctly on some OS even when the binary the platform is successfully downloaded. The install script is updated to check if the system has no problem running the executable and fall back to Ruby version when necessary. --- install | 52 ++++++++++++++++++++++++++++++++-------------------- src/Makefile | 4 ++-- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/install b/install index 855e056..b454715 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ version=0.9.0 -cd `dirname $BASH_SOURCE` -fzf_base=`pwd` +cd $(dirname $BASH_SOURCE) +fzf_base=$(pwd) ask() { read -p "$1 ([y]/n) " -n 1 -r @@ -11,15 +11,26 @@ ask() { [[ ! $REPLY =~ ^[Nn]$ ]] } +check_binary() { + echo "- Checking fzf executable" + echo -n " - " + if ! "$fzf_base"/bin/fzf --version; then + binary_error="Error occurred" + fi +} + download() { echo "Downloading fzf executable ($1) ..." if [ -x "$fzf_base"/bin/fzf ]; then - ask "- fzf already exists. Download it again?" || return 0 + if ! ask "- fzf already exists. Download it again?"; then + check_binary + return + fi fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then - echo "- Failed to create bin directory." - return 1 + binary_error="Failed to create bin directory" + return fi local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tgz @@ -28,35 +39,36 @@ download() { elif which wget > /dev/null; then wget -O - $url | tar -xz else - echo "- curl or wget required to download fzf executable." - return 1 + binary_error="curl or wget not found" + return fi if [ ! -f $1 ]; then - echo "- Failed to download ${1}." - return 1 + binary_error="Failed to download ${1}" + return fi - mv $1 fzf && chmod +x fzf && cd - > /dev/null && echo + mv $1 fzf && chmod +x fzf && check_binary } # Try to download binary executable archi=$(uname -sm) -downloaded=0 -binary_available=0 +binary_available=1 +binary_error="" if [ "$archi" = "Darwin x86_64" ]; then - binary_available=1 - download fzf-$version-darwin_amd64 && downloaded=1 + download fzf-$version-darwin_amd64 elif [ "$archi" = "Linux x86_64" ]; then - binary_available=1 - download fzf-$version-linux_amd64 && downloaded=1 + download fzf-$version-linux_amd64 +else + binary_available=0 fi -if [ $downloaded -ne 1 ]; then +cd "$fzf_base" +if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then - echo -n "No prebuilt binary for $archi ... " + echo "No prebuilt binary for $archi ... " else - echo -n "Failed to download binary executable ... " + echo " - $binary_error ... " fi echo "Installing legacy Ruby version ..." @@ -304,7 +316,7 @@ if [ -n "$(which fish)" ]; then has_fish=1 echo -n "Generate ~/.config/fish/functions/fzf.fish ... " mkdir -p ~/.config/fish/functions - if [ $downloaded -eq 0 ]; then + if [ -n "$binary_error" ]; then cat > ~/.config/fish/functions/fzf.fish << EOFZF function fzf $fzf_cmd \$argv diff --git a/src/Makefile b/src/Makefile index 10429a1..a7235bc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,8 +44,8 @@ clean: docker: docker build -t junegunn/ubuntu-sandbox . -linux64: +linux64: docker docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make' -.PHONY: build release install linux64 clean docker run +.PHONY: build release install linux64 clean docker From b42dcdb7a747cd5c7a412ca1dc8b7eb73b64f084 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 12:21:56 +0900 Subject: [PATCH 17/51] Update README for Go - System requirements --- src/README.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/README.md b/src/README.md index 2f3ca3b..7c47759 100644 --- a/src/README.md +++ b/src/README.md @@ -1,8 +1,9 @@ fzf in Go ========= -This directory contains the source code for the new fzf implementation in Go. -This new version has the following benefits over the previous Ruby version. +This directory contains the source code for the new fzf implementation in +[Go][go]. This new version has the following benefits over the previous Ruby +version. - Immensely faster - No GIL. Performance is linearly proportional to the number of cores. @@ -25,17 +26,28 @@ make install make linux64 ``` +System requirements +------------------- -Prebuilt binaries ------------------ +Currently prebuilt binaries are provided only for 64 bit OS X and Linux. +The install script will fall back to the legacy Ruby version on the other +systems, but if you have Go installed, you can try building it yourself. +(`make install`) -- Darwin x86_64 -- Linux x86_64 +However, as pointed out in [golang.org/doc/install][req], the Go version will +not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby +version instead. + +The Go version depends on [ncurses][ncurses] and some Unix system calls, so it +shouldn't run natively on Windows at the moment. But it should be not +impossible to support Windows by falling back to a cross-platform alternative +such as [termbox][termbox] only on Windows. If you're interested in making fzf +work on Windows, please let me know. Third-party libraries used -------------------------- -- [ncurses](https://www.gnu.org/software/ncurses/) +- [ncurses][ncurses] - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - Licensed under [MIT](http://mattn.mit-license.org/2013) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) @@ -56,4 +68,9 @@ still don't have a quantitative measure of the performance. License ------- -- [MIT](LICENSE) +[MIT](LICENSE) + +[go]: https://golang.org/ +[ncurses]: https://www.gnu.org/software/ncurses/ +[req]: http://golang.org/doc/install +[termbox]: https://github.com/nsf/termbox-go From ee2ee025993421b243ef668e4d4ee395a5201820 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 19:32:44 +0900 Subject: [PATCH 18/51] Fix index out of bounds error during Transform --- src/tokenizer.go | 8 +++++++- src/tokenizer_test.go | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tokenizer.go b/src/tokenizer.go index c187529..bc1ca3a 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -186,7 +186,13 @@ func Transform(tokens []Token, withNth []Range) *Transformed { } } whole += part - transTokens[idx] = Token{&part, tokens[minIdx].prefixLength} + var prefixLength int + if minIdx < numTokens { + prefixLength = tokens[minIdx].prefixLength + } else { + prefixLength = 0 + } + transTokens[idx] = Token{&part, prefixLength} } return &Transformed{ whole: &whole, diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index ed77efe..1ae0c7e 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -95,3 +95,7 @@ func TestTransform(t *testing.T) { } } } + +func TestTransformIndexOutOfBounds(t *testing.T) { + Transform([]Token{}, splitNth("1")) +} From 3e6c950e12c5cdaa0a5e17915fc75ccd6e3648c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 6 Jan 2015 02:04:06 +0900 Subject: [PATCH 19/51] Build i386 binary as well --- install | 14 ++++----- src/Dockerfile | 33 -------------------- src/Dockerfile.arch | 25 +++++++++++++++ src/Dockerfile.centos | 25 +++++++++++++++ src/Dockerfile.ubuntu | 30 ++++++++++++++++++ src/Makefile | 71 +++++++++++++++++++++++++++---------------- src/README.md | 4 +-- 7 files changed, 133 insertions(+), 69 deletions(-) delete mode 100644 src/Dockerfile create mode 100644 src/Dockerfile.arch create mode 100644 src/Dockerfile.centos create mode 100644 src/Dockerfile.ubuntu diff --git a/install b/install index b454715..b83920f 100755 --- a/install +++ b/install @@ -55,13 +55,13 @@ download() { archi=$(uname -sm) binary_available=1 binary_error="" -if [ "$archi" = "Darwin x86_64" ]; then - download fzf-$version-darwin_amd64 -elif [ "$archi" = "Linux x86_64" ]; then - download fzf-$version-linux_amd64 -else - binary_available=0 -fi +case "$archi" in + "Darwin x86_64") download fzf-$version-darwin_amd64 ;; +# "Darwin i[36]86") download fzf-$version-darwin_386 ;; + "Linux x86_64") download fzf-$version-linux_amd64 ;; +# "Linux i[36]86") download fzf-$version-linux_386 ;; + *) binary_available=0 ;; +esac cd "$fzf_base" if [ -n "$binary_error" ]; then diff --git a/src/Dockerfile b/src/Dockerfile deleted file mode 100644 index 3c062ee..0000000 --- a/src/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM ubuntu:14.04 -MAINTAINER Junegunn Choi - -# apt-get -RUN apt-get update && apt-get -y upgrade -RUN apt-get install -y --force-yes git vim-nox curl procps sudo \ - build-essential libncurses-dev - -# Setup jg user with sudo privilege -RUN useradd -s /bin/bash -m jg && echo 'jg:jg' | chpasswd && \ - echo 'jg ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/jg - -# Setup dotfiles -USER jg -RUN cd ~ && git clone https://github.com/junegunn/dotfiles.git && \ - dotfiles/install > /dev/null - -# Install Go 1.4 -RUN cd ~ && curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz && \ - mv go go1.4 && \ - echo 'export GOROOT=~/go1.4' >> ~/dotfiles/bashrc-extra && \ - echo 'export PATH=~/go1.4/bin:$PATH' >> ~/dotfiles/bashrc-extra - -# Symlink fzf directory -RUN mkdir -p ~jg/go/src/github.com/junegunn && \ - ln -s /fzf ~jg/go/src/github.com/junegunn/fzf - -# Volume -VOLUME /fzf - -# Default CMD -CMD cd ~jg/go/src/github.com/junegunn/fzf/src && /bin/bash -l - diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch new file mode 100644 index 0000000..9fa4ea3 --- /dev/null +++ b/src/Dockerfile.arch @@ -0,0 +1,25 @@ +FROM base/archlinux:2014.07.03 +MAINTAINER Junegunn Choi + +# apt-get +RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +# Symlink fzf directory +RUN mkdir -p /go/src/github.com/junegunn && \ + ln -s /fzf /go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash + diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos new file mode 100644 index 0000000..e791dc6 --- /dev/null +++ b/src/Dockerfile.centos @@ -0,0 +1,25 @@ +FROM centos:centos7 +MAINTAINER Junegunn Choi + +# yum +RUN yum install -y git gcc make tar ncurses-devel + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +# Symlink fzf directory +RUN mkdir -p /go/src/github.com/junegunn && \ + ln -s /fzf /go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash + diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu new file mode 100644 index 0000000..b7f6232 --- /dev/null +++ b/src/Dockerfile.ubuntu @@ -0,0 +1,30 @@ +FROM ubuntu:14.04 +MAINTAINER Junegunn Choi + +# apt-get +RUN apt-get update && apt-get -y upgrade && \ + apt-get install -y --force-yes git curl build-essential libncurses-dev + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +# For i386 build +RUN apt-get install -y lib32ncurses5-dev && \ + cd $GOROOT/src && GOARCH=386 ./make.bash + +# Symlink fzf directory +RUN mkdir -p /go/src/github.com/junegunn && \ + ln -s /fzf /go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash + diff --git a/src/Makefile b/src/Makefile index a7235bc..037fb61 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,51 +1,68 @@ UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) - SUFFIX := darwin + GOOS := darwin else ifeq ($(UNAME_S),Linux) - SUFFIX := linux + GOOS := linux endif -UNAME_M := $(shell uname -m) -ifeq ($(UNAME_M),x86_64) - SUFFIX := $(SUFFIX)_amd64 -else ifneq ($(filter i386 i686,$(UNAME_M)),) - SUFFIX := $(SUFFIX)_386 -else # TODO -$(error "$(UNAME_M) is not supported, yet.") +ifneq ($(shell uname -m),x86_64) +$(error "Build on $(UNAME_M) is not supported, yet.") endif -BINARY := fzf-$(SUFFIX) -BINDIR := ../bin -SOURCES := $(wildcard *.go fzf/*.go) -RELEASE = fzf-$(shell fzf/$(BINARY) --version)-$(SUFFIX) +SOURCES := $(wildcard *.go fzf/*.go) +BINDIR := ../bin + +BINARY32 := fzf-$(GOOS)_386 +BINARY64 := fzf-$(GOOS)_amd64 +RELEASE32 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_386 +RELEASE64 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_amd64 all: release release: build cd fzf && \ - cp $(BINARY) $(RELEASE) && \ - tar -czf $(RELEASE).tgz $(RELEASE) && \ - rm $(RELEASE) + cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ + cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ + rm $(RELEASE32) $(RELEASE64) -build: fzf/$(BINARY) +build: fzf/$(BINARY32) fzf/$(BINARY64) -fzf/$(BINARY): $(SOURCES) +test: $(SOURCES) go get go test -v - cd fzf && go build -o $(BINARY) -install: fzf/$(BINARY) +fzf/$(BINARY32): test + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) + +fzf/$(BINARY64): test + cd fzf && go build -o $(BINARY64) + +install: fzf/$(BINARY64) mkdir -p $(BINDIR) - cp -f fzf/$(BINARY) $(BINDIR)/fzf + cp -f fzf/$(BINARY64) $(BINDIR)/fzf clean: - cd fzf && rm -f $(BINARY) $(RELEASE).tgz + cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz + +DISTRO := ubuntu docker: - docker build -t junegunn/ubuntu-sandbox . + docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) -linux64: docker - docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ - /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make' +linux: docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' -.PHONY: build release install linux64 clean docker +$(DISTRO): docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +arch: docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/arch-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +centos: docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/centos-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +.PHONY: build release install linux clean docker $(DISTRO) diff --git a/src/README.md b/src/README.md index 7c47759..70b2c1d 100644 --- a/src/README.md +++ b/src/README.md @@ -22,8 +22,8 @@ make # Install the executable to ../bin directory make install -# Build executable for Linux x86_64 using Docker -make linux64 +# Build executables for Linux using Docker +make linux ``` System requirements From 383f908cf79ed8e97e7728db8ec2e0ab7e270bc3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 6 Jan 2015 02:04:27 +0900 Subject: [PATCH 20/51] Remove unnecessary event dispatch --- src/terminal.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 28a9a33..a935bd3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -523,7 +523,6 @@ func (t *Terminal) Loop() { if my == 0 && mx >= 0 { // Prompt t.cx = mx - req(REQ_PROMPT) } else if my >= 2 { // List t.cy = t.offset + my - 2 From 6109a0fe4442007cb3cd3df53730f74418ceaf37 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 6 Jan 2015 02:07:30 +0900 Subject: [PATCH 21/51] Refactor Makefile --- src/Makefile | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Makefile b/src/Makefile index 037fb61..3e57a17 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,6 +44,7 @@ install: fzf/$(BINARY64) clean: cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz +# Linux distribution to build fzf on DISTRO := ubuntu docker: @@ -57,12 +58,4 @@ $(DISTRO): docker docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -arch: docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/arch-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' - -centos: docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/centos-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' - .PHONY: build release install linux clean docker $(DISTRO) From b277f5ae6fe9b263410945796263aa52d95d4ab5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 00:24:05 +0900 Subject: [PATCH 22/51] Fix i386 build --- install | 13 +++++++------ src/Dockerfile.arch | 6 ++++++ src/Makefile | 2 +- src/README.md | 7 +++---- src/curses/curses.go | 23 ++++++++++++----------- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/install b/install index b83920f..ce5ec79 100755 --- a/install +++ b/install @@ -15,6 +15,7 @@ check_binary() { echo "- Checking fzf executable" echo -n " - " if ! "$fzf_base"/bin/fzf --version; then + rm -v "$fzf_base"/bin/fzf binary_error="Error occurred" fi } @@ -33,7 +34,7 @@ download() { return fi - local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tgz + local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz if which curl > /dev/null; then curl -fL $url | tar -xz elif which wget > /dev/null; then @@ -56,11 +57,11 @@ archi=$(uname -sm) binary_available=1 binary_error="" case "$archi" in - "Darwin x86_64") download fzf-$version-darwin_amd64 ;; -# "Darwin i[36]86") download fzf-$version-darwin_386 ;; - "Linux x86_64") download fzf-$version-linux_amd64 ;; -# "Linux i[36]86") download fzf-$version-linux_386 ;; - *) binary_available=0 ;; + Darwin\ x86_64) download fzf-$version-darwin_amd64 ;; + Darwin\ i*86) download fzf-$version-darwin_386 ;; + Linux\ x86_64) download fzf-$version-linux_amd64 ;; + Linux\ i*86) download fzf-$version-linux_386 ;; + *) binary_available=0 ;; esac cd "$fzf_base" diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index 9fa4ea3..8f942db 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -13,6 +13,12 @@ ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH +# For i386 build +RUN echo '[multilib]' >> /etc/pacman.conf && \ + echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \ + pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ + cd $GOROOT/src && GOARCH=386 ./make.bash + # Symlink fzf directory RUN mkdir -p /go/src/github.com/junegunn && \ ln -s /fzf /go/src/github.com/junegunn/fzf diff --git a/src/Makefile b/src/Makefile index 3e57a17..12767d3 100644 --- a/src/Makefile +++ b/src/Makefile @@ -45,7 +45,7 @@ clean: cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz # Linux distribution to build fzf on -DISTRO := ubuntu +DISTRO := arch docker: docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) diff --git a/src/README.md b/src/README.md index 70b2c1d..06b915a 100644 --- a/src/README.md +++ b/src/README.md @@ -29,10 +29,9 @@ make linux System requirements ------------------- -Currently prebuilt binaries are provided only for 64 bit OS X and Linux. -The install script will fall back to the legacy Ruby version on the other -systems, but if you have Go installed, you can try building it yourself. -(`make install`) +Currently prebuilt binaries are provided only for OS X and Linux. The install +script will fall back to the legacy Ruby version on the other systems, but if +you have Go installed, you can try building it yourself. (`make install`) However, as pointed out in [golang.org/doc/install][req], the Go version will not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby diff --git a/src/curses/curses.go b/src/curses/curses.go index 945a3ce..e4a6575 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -1,8 +1,15 @@ package curses -// #include -// #include -// #cgo LDFLAGS: -lncurses +/* +#include +#include +#cgo LDFLAGS: -lncurses +void swapOutput() { + FILE* temp = stdout; + stdout = stderr; + stderr = temp; +} +*/ import "C" import ( @@ -162,7 +169,7 @@ func Init(color bool, color256 bool, black bool, mouse bool) { // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) } - swapOutput() + C.swapOutput() C.setlocale(C.LC_ALL, C.CString("")) C.initscr() @@ -218,13 +225,7 @@ func Init(color bool, color256 bool, black bool, mouse bool) { func Close() { C.endwin() - swapOutput() -} - -func swapOutput() { - syscall.Dup2(2, 3) - syscall.Dup2(1, 2) - syscall.Dup2(3, 1) + C.swapOutput() } func GetBytes() []byte { From 8a0ab20a70954a5957a648a77a64e05013fbdf9a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 01:14:35 +0900 Subject: [PATCH 23/51] Update vim plugin to use Go binary --- plugin/fzf.vim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index db3c649..22fb4cc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -1,4 +1,4 @@ -" Copyright (c) 2014 Junegunn Choi +" Copyright (c) 2015 Junegunn Choi " " MIT License " @@ -25,6 +25,7 @@ let s:min_tmux_width = 10 let s:min_tmux_height = 3 let s:default_tmux_height = '40%' let s:launcher = 'xterm -e bash -ic %s' +let s:fzf_go = expand(':h:h').'/bin/fzf' let s:fzf_rb = expand(':h:h').'/fzf' let s:cpo_save = &cpo @@ -34,7 +35,8 @@ function! s:fzf_exec() if !exists('s:exec') call system('type fzf') if v:shell_error - let s:exec = executable(s:fzf_rb) ? s:fzf_rb : '' + let s:exec = executable(s:fzf_go) ? + \ s:fzf_go : (executable(s:fzf_rb) ? s:fzf_rb : '') else let s:exec = 'fzf' endif From 3e129ac68c08d63d304b9d4bf7229ef1152c4163 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 02:24:13 +0900 Subject: [PATCH 24/51] Remove extraneous quote-escape --- src/reader.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/reader.go b/src/reader.go index 0e1f0a9..39fa70c 100644 --- a/src/reader.go +++ b/src/reader.go @@ -5,13 +5,12 @@ import "C" import ( "bufio" - "fmt" "io" "os" "os/exec" ) -const DEFAULT_COMMAND = "find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null" +const DEFAULT_COMMAND = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` type Reader struct { pusher func(string) @@ -45,8 +44,7 @@ func (r *Reader) readFromStdin() { } func (r *Reader) readFromCommand(cmd string) { - arg := fmt.Sprintf("%q", cmd) - listCommand := exec.Command("sh", "-c", arg[1:len(arg)-1]) + listCommand := exec.Command("sh", "-c", cmd) out, err := listCommand.StdoutPipe() if err != nil { return From f99f66570bdbc296f021ec102b41ab944c7b74a3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 12:46:45 +0900 Subject: [PATCH 25/51] Add small initial delay to screen update To avoid flickering when the input is small --- src/terminal.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index a935bd3..32c8458 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -31,6 +31,7 @@ type Terminal struct { eventBox *EventBox mutex sync.Mutex initFunc func() + suppress bool } var _spinner []string = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} @@ -39,11 +40,17 @@ const ( REQ_PROMPT EventType = iota REQ_INFO REQ_LIST + REQ_REFRESH REQ_REDRAW REQ_CLOSE REQ_QUIT ) +const ( + INITIAL_DELAY = 100 * time.Millisecond + SPINNER_DURATION = 200 * time.Millisecond +) + func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ @@ -62,6 +69,7 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { reqBox: NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, + suppress: true, initFunc: func() { C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) }} @@ -79,6 +87,9 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { t.reading = !final t.mutex.Unlock() t.reqBox.Set(REQ_INFO, nil) + if final { + t.reqBox.Set(REQ_REFRESH, nil) + } } func (t *Terminal) UpdateProgress(progress float32) { @@ -158,7 +169,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { t.move(1, 0, true) if t.reading { - duration := int64(200) * int64(time.Millisecond) + duration := int64(SPINNER_DURATION) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration C.CPrint(C.COL_SPINNER, true, _spinner[idx]) } @@ -297,7 +308,9 @@ func (t *Terminal) printAll() { } func (t *Terminal) refresh() { - C.Refresh() + if !t.suppress { + C.Refresh() + } } func (t *Terminal) delChar() bool { @@ -350,11 +363,16 @@ func (t *Terminal) Loop() { { // Late initialization t.mutex.Lock() t.initFunc() - t.printInfo() t.printPrompt() t.placeCursor() - t.refresh() + C.Refresh() + t.printInfo() t.mutex.Unlock() + go func() { + timer := time.NewTimer(INITIAL_DELAY) + <-timer.C + t.reqBox.Set(REQ_REFRESH, nil) + }() } go func() { @@ -370,6 +388,8 @@ func (t *Terminal) Loop() { t.printInfo() case REQ_LIST: t.printList() + case REQ_REFRESH: + t.suppress = false case REQ_REDRAW: C.Clear() t.printAll() From 23f27f3ce539e324b5c2c4919a059e0845edcf12 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 20:08:05 +0900 Subject: [PATCH 26/51] Improve install script --- install | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/install b/install index ce5ec79..f8307cf 100755 --- a/install +++ b/install @@ -12,21 +12,25 @@ ask() { } check_binary() { - echo "- Checking fzf executable" - echo -n " - " + echo -n " - Checking fzf executable ... " if ! "$fzf_base"/bin/fzf --version; then - rm -v "$fzf_base"/bin/fzf + rm -f "$fzf_base"/bin/fzf binary_error="Error occurred" fi } +symlink() { + echo " - Creating symlink: bin/$1 -> bin/fzf" + rm -f "$fzf_base"/bin/fzf + ln -sf "$fzf_base"/bin/$1 "$fzf_base"/bin/fzf +} + download() { - echo "Downloading fzf executable ($1) ..." - if [ -x "$fzf_base"/bin/fzf ]; then - if ! ask "- fzf already exists. Download it again?"; then - check_binary - return - fi + echo "Downloading bin/$1 ..." + if [ -x "$fzf_base"/bin/$1 ]; then + echo " - Already exists" + symlink $1 + check_binary && return fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then @@ -49,7 +53,7 @@ download() { return fi - mv $1 fzf && chmod +x fzf && check_binary + chmod +x $1 && symlink $1 && check_binary } # Try to download binary executable @@ -69,7 +73,7 @@ if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then echo "No prebuilt binary for $archi ... " else - echo " - $binary_error ... " + echo " - $binary_error !!!" fi echo "Installing legacy Ruby version ..." @@ -77,7 +81,7 @@ if [ -n "$binary_error" ]; then echo -n "Checking Ruby executable ... " ruby=`which ruby` if [ $? -ne 0 ]; then - echo "ruby executable not found!" + echo "ruby executable not found !!!" exit 1 fi From 3ed86445e1906eb47679bd2c8a5ee1a01632c883 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 8 Jan 2015 11:04:25 +0900 Subject: [PATCH 27/51] Remove call to ncurses set_tabsize() Not available on old verions of ncurses --- src/curses/curses.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index e4a6575..736ccf6 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -178,8 +178,7 @@ func Init(color bool, color256 bool, black bool, mouse bool) { } C.cbreak() C.noecho() - C.raw() // stty dsusp undef - C.set_tabsize(4) // FIXME + C.raw() // stty dsusp undef intChan := make(chan os.Signal, 1) signal.Notify(intChan, os.Interrupt, os.Kill) From efec9acd6f655c7e63d6cda61486c961fdaed443 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 8 Jan 2015 22:04:12 +0900 Subject: [PATCH 28/51] Fix missing mutex unlock --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index 32c8458..a442d34 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -434,6 +434,7 @@ func (t *Terminal) Loop() { } switch event.Type { case C.INVALID: + t.mutex.Unlock() continue case C.CTRL_A: t.cx = 0 From f401c42f9c22de9df7a40ee31727ff0eab5dd30e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 8 Jan 2015 22:07:04 +0900 Subject: [PATCH 29/51] Adjust initial coordinator delay --- src/core.go | 10 +++++++--- src/util.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core.go b/src/core.go index b6f0857..5a81efa 100644 --- a/src/core.go +++ b/src/core.go @@ -7,7 +7,8 @@ import ( "time" ) -const COORDINATOR_DELAY time.Duration = 100 * time.Millisecond +const COORDINATOR_DELAY_MAX time.Duration = 100 * time.Millisecond +const COORDINATOR_DELAY_STEP time.Duration = 10 * time.Millisecond func initProcs() { runtime.GOMAXPROCS(runtime.NumCPU()) @@ -151,8 +152,11 @@ func Run(options *Options) { } } }) - if ticks > 3 && delay && reading { - time.Sleep(COORDINATOR_DELAY) + if delay && reading { + dur := DurWithin( + time.Duration(ticks)*COORDINATOR_DELAY_STEP, + 0, COORDINATOR_DELAY_MAX) + time.Sleep(dur) } } } diff --git a/src/util.go b/src/util.go index 2144e54..cc8d4f5 100644 --- a/src/util.go +++ b/src/util.go @@ -1,5 +1,7 @@ package fzf +import "time" + func Max(first int, items ...int) int { max := first for _, item := range items { @@ -19,3 +21,14 @@ func Min(first int, items ...int) int { } return min } + +func DurWithin( + val time.Duration, min time.Duration, max time.Duration) time.Duration { + if val < min { + return min + } + if val > max { + return max + } + return val +} From d303c5b3ebc6d56af6d3a03c6b4cdb361a2b022c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Jan 2015 02:35:20 +0900 Subject: [PATCH 30/51] Minor refactoring --- src/algo.go | 2 +- src/tokenizer.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/algo.go b/src/algo.go index 16790ba..e0c173f 100644 --- a/src/algo.go +++ b/src/algo.go @@ -90,7 +90,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in runes := []rune(*input) numRunes := len(runes) plen := len(pattern) - if len(runes) < plen { + if numRunes < plen { return -1, -1 } diff --git a/src/tokenizer.go b/src/tokenizer.go index bc1ca3a..d62f395 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -118,14 +118,13 @@ func awkTokenizer(input *string) ([]string, int) { } func Tokenize(str *string, delimiter *regexp.Regexp) []Token { - prefixLength := 0 if delimiter == nil { // AWK-style (\S+\s*) tokens, prefixLength := awkTokenizer(str) return withPrefixLengths(tokens, prefixLength) } else { tokens := delimiter.FindAllString(*str, -1) - return withPrefixLengths(tokens, prefixLength) + return withPrefixLengths(tokens, 0) } } From aa05bf5206768965e575b6032543745c830e6eea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Jan 2015 02:37:08 +0900 Subject: [PATCH 31/51] Reduce memory footprint --- src/chunklist_test.go | 6 +++--- src/core.go | 11 ++++++----- src/item.go | 41 +++++++++++++++++++++++++---------------- src/item_test.go | 17 ++++++++--------- src/pattern.go | 10 ++++------ src/terminal.go | 18 +++++++++--------- src/util.go | 7 +++++++ 7 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/chunklist_test.go b/src/chunklist_test.go index b244ece..09e4aad 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -7,7 +7,7 @@ import ( func TestChunkList(t *testing.T) { cl := NewChunkList(func(s *string, i int) *Item { - return &Item{text: s, index: i * 2} + return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} }) // Snapshot @@ -36,8 +36,8 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - if *(*chunk1)[0].text != "hello" || (*chunk1)[0].index != 0 || - *(*chunk1)[1].text != "world" || (*chunk1)[1].index != 2 { + if *(*chunk1)[0].text != "hello" || (*chunk1)[0].rank.index != 0 || + *(*chunk1)[1].text != "world" || (*chunk1)[1].rank.index != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index 5a81efa..e5bdb12 100644 --- a/src/core.go +++ b/src/core.go @@ -39,14 +39,15 @@ func Run(options *Options) { var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { - return &Item{text: data, index: index} + return &Item{text: data, rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { - item := Item{text: data, index: index} - tokens := Tokenize(item.text, opts.Delimiter) - item.origText = item.text - item.text = Transform(tokens, opts.WithNth).whole + tokens := Tokenize(data, opts.Delimiter) + item := Item{ + text: Transform(tokens, opts.WithNth).whole, + origText: data, + rank: Rank{0, 0, uint32(index)}} return &item }) } diff --git a/src/item.go b/src/item.go index b70da93..4c8f13d 100644 --- a/src/item.go +++ b/src/item.go @@ -5,31 +5,32 @@ import ( "sort" ) -type Offset [2]int +type Offset [2]int32 type Item struct { text *string origText *string offsets []Offset - index int rank Rank transformed *Transformed } -type Rank [3]int - -var NilRank = Rank{-1, 0, 0} +type Rank struct { + matchlen uint16 + strlen uint16 + index uint32 +} func (i *Item) Rank() Rank { - if i.rank[0] > 0 { + if i.rank.matchlen > 0 || i.rank.strlen > 0 { return i.rank } sort.Sort(ByOrder(i.offsets)) matchlen := 0 prevEnd := 0 for _, offset := range i.offsets { - begin := offset[0] - end := offset[1] + begin := int(offset[0]) + end := int(offset[1]) if prevEnd > begin { begin = prevEnd } @@ -40,7 +41,7 @@ func (i *Item) Rank() Rank { matchlen += end - begin } } - i.rank = Rank{matchlen, len(*i.text), i.index} + i.rank = Rank{uint16(matchlen), uint16(len(*i.text)), i.rank.index} return i.rank } @@ -86,14 +87,22 @@ func (a ByRelevance) Less(i, j int) bool { } func compareRanks(irank Rank, jrank Rank) bool { - for idx := range irank { - if irank[idx] < jrank[idx] { - return true - } else if irank[idx] > jrank[idx] { - return false - } + if irank.matchlen < jrank.matchlen { + return true + } else if irank.matchlen > jrank.matchlen { + return false } - return true + + if irank.strlen < jrank.strlen { + return true + } else if irank.strlen > jrank.strlen { + return false + } + + if irank.index <= jrank.index { + return true + } + return false } func SortMerge(partialResults [][]*Item) []*Item { diff --git a/src/item_test.go b/src/item_test.go index 1e31629..23b8718 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -23,8 +23,7 @@ func TestRankComparison(t *testing.T) { if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || - !compareRanks(NilRank, Rank{0, 0, 0}) || - compareRanks(Rank{0, 0, 0}, NilRank) { + !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) { t.Error("Invalid order") } } @@ -32,13 +31,13 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestItemRank(t *testing.T) { strs := []string{"foo", "foobar", "bar", "baz"} - item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + item1 := Item{text: &strs[0], rank: Rank{0, 0, 1}, offsets: []Offset{}} rank1 := item1.Rank() - if rank1[0] != 0 || rank1[1] != 3 || rank1[2] != 1 { + if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 { t.Error(item1.Rank()) } // Only differ in index - item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} + item2 := Item{text: &strs[0], rank: Rank{0, 0, 0}, offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) @@ -54,10 +53,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - item3 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item3 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item2 || items[1] != &item1 || diff --git a/src/pattern.go b/src/pattern.go index 7c27f52..7b29425 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -236,9 +236,8 @@ func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { matches = append(matches, &Item{ text: item.text, - index: item.index, - offsets: []Offset{Offset{sidx, eidx}}, - rank: NilRank}) + offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, + rank: Rank{0, 0, item.rank.index}}) } } return matches @@ -256,7 +255,7 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { if term.inv { break Loop } - offsets = append(offsets, Offset{sidx, eidx}) + offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) } else if term.inv { offsets = append(offsets, Offset{0, 0}) } @@ -264,9 +263,8 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { if len(offsets) == len(p.terms) { matches = append(matches, &Item{ text: item.text, - index: item.index, offsets: offsets, - rank: NilRank}) + rank: Rank{0, 0, item.rank.index}}) } } return matches diff --git a/src/terminal.go b/src/terminal.go index a442d34..77a70f7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -232,9 +232,9 @@ func trimRight(runes []rune, width int) ([]rune, int) { return runes, trimmed } -func trimLeft(runes []rune, width int) ([]rune, int) { +func trimLeft(runes []rune, width int) ([]rune, int32) { currentWidth := displayWidth(runes) - trimmed := 0 + var trimmed int32 = 0 for currentWidth > width && len(runes) > 0 { currentWidth -= runewidth.RuneWidth(runes[0]) @@ -245,7 +245,7 @@ func trimLeft(runes []rune, width int) ([]rune, int) { } func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { - maxe := 0 + var maxe int32 = 0 for _, offset := range item.offsets { if offset[1] > maxe { maxe = offset[1] @@ -269,7 +269,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { text = append(text[:maxe], []rune("..")...) } // ..ri.. - var diff int + var diff int32 text, diff = trimLeft(text, maxWidth-2) // Transform offsets @@ -278,7 +278,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { b, e := offset[0], offset[1] b += 2 - diff e += 2 - diff - b = Max(b, 2) + b = Max32(b, 2) if b < e { offsets[idx] = Offset{b, e} } @@ -288,15 +288,15 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { } sort.Sort(ByOrder(offsets)) - index := 0 + var index int32 = 0 for _, offset := range offsets { - b := Max(index, offset[0]) - e := Max(index, offset[1]) + b := Max32(index, offset[0]) + e := Max32(index, offset[1]) C.CPrint(col1, bold, string(text[index:b])) C.CPrint(col2, bold, string(text[b:e])) index = e } - if index < len(text) { + if index < int32(len(text)) { C.CPrint(col1, bold, string(text[index:])) } } diff --git a/src/util.go b/src/util.go index cc8d4f5..de6f365 100644 --- a/src/util.go +++ b/src/util.go @@ -12,6 +12,13 @@ func Max(first int, items ...int) int { return max } +func Max32(first int32, second int32) int32 { + if first > second { + return first + } + return second +} + func Min(first int, items ...int) int { min := first for _, item := range items { From b7bb1008107fb079e68f9ebeeca699c65cc966c9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 01:06:08 +0900 Subject: [PATCH 32/51] Improve response time by only looking at top-N items --- src/core.go | 11 ++++--- src/item.go | 38 ---------------------- src/item_test.go | 10 ------ src/matcher.go | 39 ++++++++-------------- src/merger.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/merger_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++ src/terminal.go | 28 ++++++++-------- 7 files changed, 193 insertions(+), 92 deletions(-) create mode 100644 src/merger.go create mode 100644 src/merger_test.go diff --git a/src/core.go b/src/core.go index e5bdb12..98973f8 100644 --- a/src/core.go +++ b/src/core.go @@ -93,17 +93,18 @@ func Run(options *Options) { } snapshot, _ := chunkList.Snapshot() - matches, cancelled := matcher.scan(MatchRequest{ + merger, cancelled := matcher.scan(MatchRequest{ chunks: snapshot, pattern: pattern}, limit) if !cancelled && (filtering || - opts.Exit0 && len(matches) == 0 || opts.Select1 && len(matches) == 1) { + opts.Exit0 && merger.Length() == 0 || + opts.Select1 && merger.Length() == 1) { if opts.PrintQuery { fmt.Println(patternString) } - for _, item := range matches { - item.Print() + for i := 0; i < merger.Length(); i++ { + merger.Get(i).Print() } os.Exit(0) } @@ -147,7 +148,7 @@ func Run(options *Options) { case EVT_SEARCH_FIN: switch val := value.(type) { - case []*Item: + case *Merger: terminal.UpdateList(val) } } diff --git a/src/item.go b/src/item.go index 4c8f13d..60355b4 100644 --- a/src/item.go +++ b/src/item.go @@ -104,41 +104,3 @@ func compareRanks(irank Rank, jrank Rank) bool { } return false } - -func SortMerge(partialResults [][]*Item) []*Item { - if len(partialResults) == 1 { - return partialResults[0] - } - - merged := []*Item{} - - for len(partialResults) > 0 { - minRank := Rank{0, 0, 0} - minIdx := -1 - - for idx, partialResult := range partialResults { - if len(partialResult) > 0 { - rank := partialResult[0].Rank() - if minIdx < 0 || compareRanks(rank, minRank) { - minRank = rank - minIdx = idx - } - } - } - - if minIdx >= 0 { - merged = append(merged, partialResults[minIdx][0]) - partialResults[minIdx] = partialResults[minIdx][1:] - } - - nonEmptyPartialResults := make([][]*Item, 0, len(partialResults)) - for _, partialResult := range partialResults { - if len(partialResult) > 0 { - nonEmptyPartialResults = append(nonEmptyPartialResults, partialResult) - } - } - partialResults = nonEmptyPartialResults - } - - return merged -} diff --git a/src/item_test.go b/src/item_test.go index 23b8718..87d8be4 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -64,14 +64,4 @@ func TestItemRank(t *testing.T) { items[4] != &item5 || items[5] != &item3 { t.Error(items) } - - // Sort merged lists - lists := [][]*Item{ - []*Item{&item2, &item4, &item5}, []*Item{&item1, &item6}, []*Item{&item3}} - items = SortMerge(lists) - if items[0] != &item2 || items[1] != &item1 || - items[2] != &item6 || items[3] != &item4 || - items[4] != &item5 || items[5] != &item3 { - t.Error(items) - } } diff --git a/src/matcher.go b/src/matcher.go index ad782bd..234e703 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -18,7 +18,7 @@ type Matcher struct { eventBox *EventBox reqBox *EventBox partitions int - queryCache QueryCache + mergerCache map[string]*Merger } const ( @@ -44,7 +44,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern, eventBox: eventBox, reqBox: NewEventBox(), partitions: runtime.NumCPU(), - queryCache: make(QueryCache)} + mergerCache: make(map[string]*Merger)} } func (m *Matcher) Loop() { @@ -67,30 +67,30 @@ func (m *Matcher) Loop() { // Restart search patternString := request.pattern.AsString() - allMatches := []*Item{} + var merger *Merger cancelled := false count := CountItems(request.chunks) foundCache := false if count == prevCount { - // Look up queryCache - if cached, found := m.queryCache[patternString]; found { + // Look up mergerCache + if cached, found := m.mergerCache[patternString]; found { foundCache = true - allMatches = cached + merger = cached } } else { - // Invalidate queryCache + // Invalidate mergerCache prevCount = count - m.queryCache = make(QueryCache) + m.mergerCache = make(map[string]*Merger) } if !foundCache { - allMatches, cancelled = m.scan(request, 0) + merger, cancelled = m.scan(request, 0) } if !cancelled { - m.queryCache[patternString] = allMatches - m.eventBox.Set(EVT_SEARCH_FIN, allMatches) + m.mergerCache[patternString] = merger + m.eventBox.Set(EVT_SEARCH_FIN, merger) } } } @@ -120,12 +120,12 @@ type partialResult struct { matches []*Item } -func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { +func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { startedAt := time.Now() numChunks := len(request.chunks) if numChunks == 0 { - return []*Item{}, false + return EmptyMerger, false } pattern := request.pattern empty := pattern.IsEmpty() @@ -188,18 +188,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - - var allMatches []*Item - if empty || !m.sort { - allMatches = []*Item{} - for _, matches := range partialResults { - allMatches = append(allMatches, matches...) - } - } else { - allMatches = SortMerge(partialResults) - } - - return allMatches, false + return NewMerger(partialResults, !empty && m.sort), false } func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { diff --git a/src/merger.go b/src/merger.go new file mode 100644 index 0000000..7ff3243 --- /dev/null +++ b/src/merger.go @@ -0,0 +1,79 @@ +package fzf + +var EmptyMerger *Merger = NewMerger([][]*Item{}, false) + +type Merger struct { + lists [][]*Item + merged []*Item + cursors []int + done bool +} + +func NewMerger(lists [][]*Item, sorted bool) *Merger { + mg := Merger{ + lists: lists, + merged: []*Item{}, + cursors: make([]int, len(lists)), + done: false} + if !sorted { + for _, list := range lists { + mg.merged = append(mg.merged, list...) + } + mg.done = true + } + return &mg +} + +func (mg *Merger) Length() int { + cnt := 0 + for _, list := range mg.lists { + cnt += len(list) + } + return cnt +} + +func (mg *Merger) Get(idx int) *Item { + if mg.done { + return mg.merged[idx] + } else if len(mg.lists) == 1 { + return mg.lists[0][idx] + } + mg.buildUpto(idx) + return mg.merged[idx] +} + +func (mg *Merger) buildUpto(upto int) { + numBuilt := len(mg.merged) + if numBuilt > upto { + return + } + + for i := numBuilt; i <= upto; i++ { + minRank := Rank{0, 0, 0} + minIdx := -1 + for listIdx, list := range mg.lists { + cursor := mg.cursors[listIdx] + if cursor < 0 || cursor == len(list) { + mg.cursors[listIdx] = -1 + continue + } + if cursor >= 0 { + rank := list[cursor].Rank() + if minIdx < 0 || compareRanks(rank, minRank) { + minRank = rank + minIdx = listIdx + } + } + mg.cursors[listIdx] = cursor + } + + if minIdx >= 0 { + chosen := mg.lists[minIdx] + mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) + mg.cursors[minIdx] += 1 + } else { + mg.done = true + return + } + } +} diff --git a/src/merger_test.go b/src/merger_test.go new file mode 100644 index 0000000..19941b1 --- /dev/null +++ b/src/merger_test.go @@ -0,0 +1,80 @@ +package fzf + +import ( + "math/rand" + "sort" + "testing" +) + +func assert(t *testing.T, cond bool, msg ...string) { + if !cond { + t.Error(msg) + } +} + +func randItem() *Item { + return &Item{ + rank: Rank{uint16(rand.Uint32()), uint16(rand.Uint32()), rand.Uint32()}} +} + +func TestEmptyMerger(t *testing.T) { + assert(t, EmptyMerger.Length() == 0, "Not empty") +} + +func buildLists(partiallySorted bool) ([][]*Item, []*Item) { + numLists := 4 + lists := make([][]*Item, numLists) + cnt := 0 + for i := 0; i < numLists; i++ { + numItems := rand.Int() % 20 + cnt += numItems + lists[i] = make([]*Item, numItems) + for j := 0; j < numItems; j++ { + item := randItem() + lists[i][j] = item + } + if partiallySorted { + sort.Sort(ByRelevance(lists[i])) + } + } + items := []*Item{} + for _, list := range lists { + items = append(items, list...) + } + return lists, items +} + +func TestMergerUnsorted(t *testing.T) { + lists, items := buildLists(false) + cnt := len(items) + + // Not sorted: same order + mg := NewMerger(lists, false) + assert(t, cnt == mg.Length(), "Invalid Length") + for i := 0; i < cnt; i++ { + assert(t, items[i] == mg.Get(i), "Invalid Get") + } +} + +func TestMergerSorted(t *testing.T) { + lists, items := buildLists(true) + cnt := len(items) + + // Sorted sorted order + mg := NewMerger(lists, true) + assert(t, cnt == mg.Length(), "Invalid Length") + sort.Sort(ByRelevance(items)) + for i := 0; i < cnt; i++ { + if items[i] != mg.Get(i) { + t.Error("Not sorted", items[i], mg.Get(i)) + } + } + + // Inverse order + mg2 := NewMerger(lists, true) + for i := cnt - 1; i >= cnt; i-- { + if items[i] != mg2.Get(i) { + t.Error("Not sorted", items[i], mg2.Get(i)) + } + } +} diff --git a/src/terminal.go b/src/terminal.go index 77a70f7..7b83a47 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -25,7 +25,7 @@ type Terminal struct { count int progress int reading bool - list []*Item + merger *Merger selected map[*string]*string reqBox *EventBox eventBox *EventBox @@ -64,7 +64,7 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { input: input, multi: opts.Multi, printQuery: opts.PrintQuery, - list: []*Item{}, + merger: EmptyMerger, selected: make(map[*string]*string), reqBox: NewEventBox(), eventBox: eventBox, @@ -99,10 +99,10 @@ func (t *Terminal) UpdateProgress(progress float32) { t.reqBox.Set(REQ_INFO, nil) } -func (t *Terminal) UpdateList(list []*Item) { +func (t *Terminal) UpdateList(merger *Merger) { t.mutex.Lock() t.progress = 100 - t.list = list + t.merger = merger t.mutex.Unlock() t.reqBox.Set(REQ_INFO, nil) t.reqBox.Set(REQ_LIST, nil) @@ -110,7 +110,7 @@ func (t *Terminal) UpdateList(list []*Item) { func (t *Terminal) listIndex(y int) int { if t.tac { - return len(t.list) - y - 1 + return t.merger.Length() - y - 1 } else { return y } @@ -121,8 +121,8 @@ func (t *Terminal) output() { fmt.Println(string(t.input)) } if len(t.selected) == 0 { - if len(t.list) > t.cy { - t.list[t.listIndex(t.cy)].Print() + if t.merger.Length() > t.cy { + t.merger.Get(t.listIndex(t.cy)).Print() } } else { for ptr, orig := range t.selected { @@ -175,7 +175,7 @@ func (t *Terminal) printInfo() { } t.move(1, 2, false) - output := fmt.Sprintf("%d/%d", len(t.list), t.count) + output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) if t.multi && len(t.selected) > 0 { output += fmt.Sprintf(" (%d)", len(t.selected)) } @@ -189,11 +189,11 @@ func (t *Terminal) printList() { t.constrain() maxy := maxItems() - count := len(t.list) - t.offset + count := t.merger.Length() - t.offset for i := 0; i < maxy; i++ { t.move(i+2, 0, true) if i < count { - t.printItem(t.list[t.listIndex(i+t.offset)], i == t.cy-t.offset) + t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset) } } } @@ -417,7 +417,7 @@ func (t *Terminal) Loop() { previousInput := t.input events := []EventType{REQ_PROMPT} toggle := func() { - item := t.list[t.listIndex(t.cy)] + item := t.merger.Get(t.listIndex(t.cy)) if _, found := t.selected[item.text]; !found { t.selected[item.text] = item.origText } else { @@ -460,13 +460,13 @@ func (t *Terminal) Loop() { t.cx -= 1 } case C.TAB: - if t.multi && len(t.list) > 0 { + if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) req(REQ_LIST, REQ_INFO) } case C.BTAB: - if t.multi && len(t.list) > 0 { + if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) req(REQ_LIST, REQ_INFO) @@ -567,7 +567,7 @@ func (t *Terminal) Loop() { } func (t *Terminal) constrain() { - count := len(t.list) + count := t.merger.Length() height := C.MaxY() - 2 diffpos := t.cy - t.offset From 8b02ae650c99b3fffe9c572e96ada007cc1ccad5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 01:16:13 +0900 Subject: [PATCH 33/51] Update src/README.md --- src/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/README.md b/src/README.md index 06b915a..f179ae7 100644 --- a/src/README.md +++ b/src/README.md @@ -2,12 +2,13 @@ fzf in Go ========= This directory contains the source code for the new fzf implementation in -[Go][go]. This new version has the following benefits over the previous Ruby +[Go][go]. The new version has the following benefits over the previous Ruby version. - Immensely faster - No GIL. Performance is linearly proportional to the number of cores. - - It's so fast that I even decided to remove the sort limit (`--sort=N`) + - It's so fast that I even decided to remove the sort limit. `--sort=N` is + no longer required. - Does not require Ruby and distributed as an executable binary - Ruby dependency is especially painful on Ruby 2.1 or above which ships without curses gem @@ -16,7 +17,7 @@ Build ----- ```sh -# Build fzf executable +# Build fzf executables make # Install the executable to ../bin directory @@ -31,9 +32,9 @@ System requirements Currently prebuilt binaries are provided only for OS X and Linux. The install script will fall back to the legacy Ruby version on the other systems, but if -you have Go installed, you can try building it yourself. (`make install`) +you have Go 1.4 installed, you can try building it yourself. (`make install`) -However, as pointed out in [golang.org/doc/install][req], the Go version will +However, as pointed out in [golang.org/doc/install][req], the Go version may not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby version instead. From 188c90bf2564adcade3eb283ecf72fa74b7dc6dd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 12:21:17 +0900 Subject: [PATCH 34/51] Fix incorrect behaviors of mouse events when --multi enabled --- src/terminal.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 7b83a47..73bf396 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -416,14 +416,6 @@ func (t *Terminal) Loop() { t.mutex.Lock() previousInput := t.input events := []EventType{REQ_PROMPT} - toggle := func() { - item := t.merger.Get(t.listIndex(t.cy)) - if _, found := t.selected[item.text]; !found { - t.selected[item.text] = item.origText - } else { - delete(t.selected, item.text) - } - } req := func(evts ...EventType) { for _, event := range evts { events = append(events, event) @@ -432,6 +424,18 @@ func (t *Terminal) Loop() { } } } + toggle := func() { + idx := t.listIndex(t.cy) + if idx < t.merger.Length() { + item := t.merger.Get(idx) + if _, found := t.selected[item.text]; !found { + t.selected[item.text] = item.origText + } else { + delete(t.selected, item.text) + } + req(REQ_INFO) + } + } switch event.Type { case C.INVALID: t.mutex.Unlock() @@ -463,13 +467,13 @@ func (t *Terminal) Loop() { if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) - req(REQ_LIST, REQ_INFO) + req(REQ_LIST) } case C.BTAB: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) - req(REQ_LIST, REQ_INFO) + req(REQ_LIST) } case C.CTRL_J, C.CTRL_N: t.vmove(-1) @@ -529,11 +533,13 @@ func (t *Terminal) Loop() { } if me.S != 0 { // Scroll - if me.Mod { - toggle() + if t.merger.Length() > 0 { + if t.multi && me.Mod { + toggle() + } + t.vmove(me.S) + req(REQ_LIST) } - t.vmove(me.S) - req(REQ_LIST) } else if me.Double { // Double-click if my >= 2 { @@ -547,7 +553,7 @@ func (t *Terminal) Loop() { } else if my >= 2 { // List t.cy = t.offset + my - 2 - if me.Mod { + if t.multi && me.Mod { toggle() } req(REQ_LIST) From b8a9861f9530ec63467c062f974e93546240fb53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 12:26:11 +0900 Subject: [PATCH 35/51] Fix double click on an empty row not to close fzf --- src/terminal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 73bf396..c9db8ff 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -544,7 +544,9 @@ func (t *Terminal) Loop() { // Double-click if my >= 2 { t.cy = my - 2 - req(REQ_CLOSE) + if t.listIndex(t.cy) < t.merger.Length() { + req(REQ_CLOSE) + } } } else if me.Down { if my == 0 && mx >= 0 { From 2d9b38b93eb16b341e91ec5d8eaaa9898f1d68f6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 14:22:00 +0900 Subject: [PATCH 36/51] Constrain cy in vmove() --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index c9db8ff..fb17ce5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -602,6 +602,7 @@ func (t *Terminal) vmove(o int) { } else { t.cy += o } + t.cy = Max(0, Min(t.cy, t.merger.Length()-1)) } func maxItems() int { From 6e86fee588bdcd769501ab671fa21a8e8e2de828 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 14:24:12 +0900 Subject: [PATCH 37/51] Change Merger implementation on --no-sort --- src/merger.go | 51 +++++++++++++++++++++++----------------------- src/merger_test.go | 5 ++++- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/merger.go b/src/merger.go index 7ff3243..08a3d15 100644 --- a/src/merger.go +++ b/src/merger.go @@ -1,12 +1,15 @@ package fzf +import "fmt" + var EmptyMerger *Merger = NewMerger([][]*Item{}, false) type Merger struct { lists [][]*Item merged []*Item cursors []int - done bool + sorted bool + count int } func NewMerger(lists [][]*Item, sorted bool) *Merger { @@ -14,41 +17,37 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger { lists: lists, merged: []*Item{}, cursors: make([]int, len(lists)), - done: false} - if !sorted { - for _, list := range lists { - mg.merged = append(mg.merged, list...) - } - mg.done = true + sorted: sorted, + count: 0} + + for _, list := range mg.lists { + mg.count += len(list) } return &mg } func (mg *Merger) Length() int { - cnt := 0 - for _, list := range mg.lists { - cnt += len(list) - } - return cnt + return mg.count } func (mg *Merger) Get(idx int) *Item { - if mg.done { - return mg.merged[idx] - } else if len(mg.lists) == 1 { + if len(mg.lists) == 1 { return mg.lists[0][idx] + } else if !mg.sorted { + for _, list := range mg.lists { + numItems := len(list) + if idx < numItems { + return list[idx] + } + idx -= numItems + } + panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } - mg.buildUpto(idx) - return mg.merged[idx] + return mg.mergedGet(idx) } -func (mg *Merger) buildUpto(upto int) { - numBuilt := len(mg.merged) - if numBuilt > upto { - return - } - - for i := numBuilt; i <= upto; i++ { +func (mg *Merger) mergedGet(idx int) *Item { + for i := len(mg.merged); i <= idx; i++ { minRank := Rank{0, 0, 0} minIdx := -1 for listIdx, list := range mg.lists { @@ -72,8 +71,8 @@ func (mg *Merger) buildUpto(upto int) { mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) mg.cursors[minIdx] += 1 } else { - mg.done = true - return + panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count)) } } + return mg.merged[idx] } diff --git a/src/merger_test.go b/src/merger_test.go index 19941b1..32a1228 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -19,6 +19,9 @@ func randItem() *Item { func TestEmptyMerger(t *testing.T) { assert(t, EmptyMerger.Length() == 0, "Not empty") + assert(t, EmptyMerger.count == 0, "Invalid count") + assert(t, len(EmptyMerger.lists) == 0, "Invalid lists") + assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") } func buildLists(partiallySorted bool) ([][]*Item, []*Item) { @@ -72,7 +75,7 @@ func TestMergerSorted(t *testing.T) { // Inverse order mg2 := NewMerger(lists, true) - for i := cnt - 1; i >= cnt; i-- { + for i := cnt - 1; i >= 0; i-- { if items[i] != mg2.Get(i) { t.Error("Not sorted", items[i], mg2.Get(i)) } From f670f4f076867d6876bcbc832a9b464bbe4f8f68 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 14:50:24 +0900 Subject: [PATCH 38/51] Make sure that cy is properly limited --- src/terminal.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index fb17ce5..7039e57 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -543,8 +543,7 @@ func (t *Terminal) Loop() { } else if me.Double { // Double-click if my >= 2 { - t.cy = my - 2 - if t.listIndex(t.cy) < t.merger.Length() { + if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() { req(REQ_CLOSE) } } @@ -554,8 +553,7 @@ func (t *Terminal) Loop() { t.cx = mx } else if my >= 2 { // List - t.cy = t.offset + my - 2 - if t.multi && me.Mod { + if t.vset(t.offset+my-2) && t.multi && me.Mod { toggle() } req(REQ_LIST) @@ -598,11 +596,15 @@ func (t *Terminal) constrain() { func (t *Terminal) vmove(o int) { if t.reverse { - t.cy -= o + t.vset(t.cy - o) } else { - t.cy += o + t.vset(t.cy + o) } - t.cy = Max(0, Min(t.cy, t.merger.Length()-1)) +} + +func (t *Terminal) vset(o int) bool { + t.cy = Max(0, Min(o, t.merger.Length()-1)) + return t.cy == o } func maxItems() int { From 4f4031443365659de357ad4da15af8b5e3245137 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 01:15:44 +0900 Subject: [PATCH 39/51] Fix --with-nth option when query is non-empty --- src/pattern.go | 14 ++++++++------ src/pattern_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 7b29425..93dbaf9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -235,9 +235,10 @@ func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { input := p.prepareInput(item) if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { matches = append(matches, &Item{ - text: item.text, - offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, - rank: Rank{0, 0, item.rank.index}}) + text: item.text, + origText: item.origText, + offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, + rank: Rank{0, 0, item.rank.index}}) } } return matches @@ -262,9 +263,10 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { } if len(offsets) == len(p.terms) { matches = append(matches, &Item{ - text: item.text, - offsets: offsets, - rank: Rank{0, 0, item.rank.index}}) + text: item.text, + origText: item.origText, + offsets: offsets, + rank: Rank{0, 0, item.rank.index}}) } } return matches diff --git a/src/pattern_test.go b/src/pattern_test.go index a1ce626..a776e30 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -85,3 +85,21 @@ func TestCaseSensitivity(t *testing.T) { t.Error("Invalid case conversion") } } + +func TestOrigText(t *testing.T) { + strptr := func(str string) *string { + return &str + } + + pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, []Range{}, nil, []rune("jg")) + for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { + chunk := Chunk{ + &Item{text: strptr("junegunn"), origText: strptr("junegunn.choi")}, + } + matches := fun(&chunk) + if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || + matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 { + t.Error("Invalid match result", matches) + } + } +} From ca4bdfb4bd61e1cb9991146ac5b6bafbf5391072 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 01:47:46 +0900 Subject: [PATCH 40/51] Fix Transform result cache to speed up subsequent searches --- src/item.go | 2 +- src/pattern.go | 22 ++++++++++++---------- src/pattern_test.go | 14 ++++++++++---- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/item.go b/src/item.go index 60355b4..9f90b8d 100644 --- a/src/item.go +++ b/src/item.go @@ -10,9 +10,9 @@ type Offset [2]int32 type Item struct { text *string origText *string + transformed *Transformed offsets []Offset rank Rank - transformed *Transformed } type Rank struct { diff --git a/src/pattern.go b/src/pattern.go index 93dbaf9..a936411 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -229,16 +229,22 @@ func (p *Pattern) Match(chunk *Chunk) []*Item { return matches } +func dupItem(item *Item, offsets []Offset) *Item { + return &Item{ + text: item.text, + origText: item.origText, + transformed: item.transformed, + offsets: offsets, + rank: Rank{0, 0, item.rank.index}} +} + func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { matches := []*Item{} for _, item := range *chunk { input := p.prepareInput(item) if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { - matches = append(matches, &Item{ - text: item.text, - origText: item.origText, - offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, - rank: Rank{0, 0, item.rank.index}}) + matches = append(matches, + dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) } } return matches @@ -262,11 +268,7 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { } } if len(offsets) == len(p.terms) { - matches = append(matches, &Item{ - text: item.text, - origText: item.origText, - offsets: offsets, - rank: Rank{0, 0, item.rank.index}}) + matches = append(matches, dupItem(item, offsets)) } } return matches diff --git a/src/pattern_test.go b/src/pattern_test.go index a776e30..2635b6c 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -86,19 +86,25 @@ func TestCaseSensitivity(t *testing.T) { } } -func TestOrigText(t *testing.T) { +func TestOrigTextAndTransformed(t *testing.T) { strptr := func(str string) *string { return &str } - pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, []Range{}, nil, []rune("jg")) + tokens := Tokenize(strptr("junegunn"), nil) + trans := Transform(tokens, []Range{Range{1, 1}}) + for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { chunk := Chunk{ - &Item{text: strptr("junegunn"), origText: strptr("junegunn.choi")}, + &Item{ + text: strptr("junegunn"), + origText: strptr("junegunn.choi"), + transformed: trans}, } matches := fun(&chunk) if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || - matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 { + matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || + matches[0].transformed != trans { t.Error("Invalid match result", matches) } } From e293cd4d088ec1fc4b6a7b14a19825138a20c597 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 02:20:54 +0900 Subject: [PATCH 41/51] Add test cases for ChunkCache --- src/cache_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/cache_test.go diff --git a/src/cache_test.go b/src/cache_test.go new file mode 100644 index 0000000..2a8b048 --- /dev/null +++ b/src/cache_test.go @@ -0,0 +1,40 @@ +package fzf + +import "testing" + +func TestChunkCache(t *testing.T) { + cache := NewChunkCache() + chunk2 := make(Chunk, CHUNK_SIZE) + chunk1p := &Chunk{} + chunk2p := &chunk2 + items1 := []*Item{&Item{}} + items2 := []*Item{&Item{}, &Item{}} + cache.Add(chunk1p, "foo", items1) + cache.Add(chunk2p, "foo", items1) + cache.Add(chunk2p, "bar", items2) + + { // chunk1 is not full + cached, found := cache.Find(chunk1p, "foo") + if found { + t.Error("Cached disabled for non-empty chunks", found, cached) + } + } + { + cached, found := cache.Find(chunk2p, "foo") + if !found || len(cached) != 1 { + t.Error("Expected 1 item cached", found, cached) + } + } + { + cached, found := cache.Find(chunk2p, "bar") + if !found || len(cached) != 2 { + t.Error("Expected 2 items cached", found, cached) + } + } + { + cached, found := cache.Find(chunk1p, "foobar") + if found { + t.Error("Expected 0 item cached", found, cached) + } + } +} From bd7331ecf5f2dc6dd7b5e7e20979b8dc8021e04f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 03:45:49 +0900 Subject: [PATCH 42/51] Remove unnecessary loop label --- src/pattern.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index a936411..2aa45c2 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -255,12 +255,11 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { for _, item := range *chunk { input := p.prepareInput(item) offsets := []Offset{} - Loop: for _, term := range p.terms { pfun := p.procFun[term.typ] if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { if term.inv { - break Loop + break } offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) } else if term.inv { From 313578a1a07e79aba273f47a281247e76e6329d6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 03:53:07 +0900 Subject: [PATCH 43/51] Improve prefix/suffix cache lookup --- src/pattern.go | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 2aa45c2..31ba813 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -194,24 +194,19 @@ func (p *Pattern) Match(chunk *Chunk) []*Item { } } - // ChunkCache: Prefix match - foundPrefixCache := false - for idx := len(cacheKey) - 1; idx > 0; idx-- { - if cached, found := _cache.Find(chunk, cacheKey[:idx]); found { - cachedChunk := Chunk(cached) - space = &cachedChunk - foundPrefixCache = true - break - } - } - - // ChunkCache: Suffix match - if !foundPrefixCache { - for idx := 1; idx < len(cacheKey); idx++ { - if cached, found := _cache.Find(chunk, cacheKey[idx:]); found { + // ChunkCache: Prefix/suffix match +Loop: + for idx := 1; idx < len(cacheKey); idx++ { + // [---------| ] | [ |---------] + // [--------| ] | [ |--------] + // [-------| ] | [ |-------] + prefix := cacheKey[:len(cacheKey)-idx] + suffix := cacheKey[idx:] + for _, substr := range [2]*string{&prefix, &suffix} { + if cached, found := _cache.Find(chunk, *substr); found { cachedChunk := Chunk(cached) space = &cachedChunk - break + break Loop } } } From 6c3489087c2067f42158b558c1d0a2217e1b16b3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 14:19:50 +0900 Subject: [PATCH 44/51] Refactor Makefile and Dockerfiles --- src/Dockerfile.arch | 6 +----- src/Dockerfile.centos | 6 +----- src/Dockerfile.ubuntu | 6 +----- src/Makefile | 40 ++++++++++++++++++++++++++-------------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index 8f942db..054b95c 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -19,12 +19,8 @@ RUN echo '[multilib]' >> /etc/pacman.conf && \ pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ cd $GOROOT/src && GOARCH=386 ./make.bash -# Symlink fzf directory -RUN mkdir -p /go/src/github.com/junegunn && \ - ln -s /fzf /go/src/github.com/junegunn/fzf - # Volume -VOLUME /fzf +VOLUME /go # Default CMD CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index e791dc6..5b27925 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -13,12 +13,8 @@ ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH -# Symlink fzf directory -RUN mkdir -p /go/src/github.com/junegunn && \ - ln -s /fzf /go/src/github.com/junegunn/fzf - # Volume -VOLUME /fzf +VOLUME /go # Default CMD CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index b7f6232..91bf780 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -18,12 +18,8 @@ ENV PATH /go1.4/bin:$PATH RUN apt-get install -y lib32ncurses5-dev && \ cd $GOROOT/src && GOARCH=386 ./make.bash -# Symlink fzf directory -RUN mkdir -p /go/src/github.com/junegunn && \ - ln -s /fzf /go/src/github.com/junegunn/fzf - # Volume -VOLUME /fzf +VOLUME /go # Default CMD CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Makefile b/src/Makefile index 12767d3..7108f0d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,3 +1,7 @@ +ifndef GOPATH +$(error GOPATH is undefined) +endif + UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) GOOS := darwin @@ -9,7 +13,7 @@ ifneq ($(shell uname -m),x86_64) $(error "Build on $(UNAME_M) is not supported, yet.") endif -SOURCES := $(wildcard *.go fzf/*.go) +SOURCES := $(wildcard *.go */*.go) BINDIR := ../bin BINARY32 := fzf-$(GOOS)_386 @@ -17,7 +21,7 @@ BINARY64 := fzf-$(GOOS)_amd64 RELEASE32 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_386 RELEASE64 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_amd64 -all: release +all: test release release: build cd fzf && \ @@ -27,23 +31,31 @@ release: build build: fzf/$(BINARY32) fzf/$(BINARY64) -test: $(SOURCES) +test: go get go test -v -fzf/$(BINARY32): test - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) +install: $(BINDIR)/fzf -fzf/$(BINARY64): test - cd fzf && go build -o $(BINARY64) - -install: fzf/$(BINARY64) - mkdir -p $(BINDIR) - cp -f fzf/$(BINARY64) $(BINDIR)/fzf +uninstall: + rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) clean: cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz +fzf/$(BINARY32): $(SOURCES) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) + +fzf/$(BINARY64): $(SOURCES) + cd fzf && go build -o $(BINARY64) + +$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) + cp -f fzf/$(BINARY64) $(BINDIR) + cd $(BINDIR) && ln -sf $(BINARY64) fzf + +$(BINDIR): + mkdir -p $@ + # Linux distribution to build fzf on DISTRO := arch @@ -51,11 +63,11 @@ docker: docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) linux: docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' $(DISTRO): docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -.PHONY: build release install linux clean docker $(DISTRO) +.PHONY: all build release test install uninstall clean docker linux $(DISTRO) From 1c313526753c1ad48b11574883c70bd850af78d1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 16:59:57 +0900 Subject: [PATCH 45/51] Update src/README.md and package comment --- src/README.md | 65 +++++++++++++++++++++++++++++++++++---------------- src/core.go | 25 ++++++++++++++++++++ 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/README.md b/src/README.md index f179ae7..fb17e68 100644 --- a/src/README.md +++ b/src/README.md @@ -5,34 +5,44 @@ This directory contains the source code for the new fzf implementation in [Go][go]. The new version has the following benefits over the previous Ruby version. -- Immensely faster - - No GIL. Performance is linearly proportional to the number of cores. - - It's so fast that I even decided to remove the sort limit. `--sort=N` is - no longer required. -- Does not require Ruby and distributed as an executable binary - - Ruby dependency is especially painful on Ruby 2.1 or above which - ships without curses gem +Motivation +---------- -Build ------ +### No Ruby dependency -```sh -# Build fzf executables -make +There have always been complaints about fzf being a Ruby script. To make +matters worse, Ruby 2.1 dropped ncurses support from its standard libary. +Because of the change, users running Ruby 2.1 or above were forced to build C +extensions of curses gem to meet the requirement of fzf. The new Go version +will be distributed as an executable binary so it will be much more accessible +and easier to setup. -# Install the executable to ../bin directory -make install +### Performance -# Build executables for Linux using Docker -make linux -``` +With the presence of [GIL][gil], Ruby cannot utilize multiple CPU cores. Even +though the Ruby version of fzf was pretty responsive even for 100k+ lines, +which is well above the size of the usual input, it was obvious that we could +do better. Now with the Go version, GIL is gone, and the search performance +scales proportional to the number of cores. On my Macbook Pro (Mid 2012), it +was shown to be an order of magnitude faster on certain cases. It also starts +much faster than before though the difference shouldn't be really noticeable. + +Differences with Ruby version +----------------------------- + +The Go version is designed to be perfectly compatible with the previous Ruby +version. The only behavioral difference is that the new version ignores the +numeric argument to `--sort=N` option and always sorts the result regardless +of the number of matches. The value was introduced to limit the response time +of the query, but the Go version is blazingly fast (almost instant response +even for 1M+ items) so I decided that it's no longer required. System requirements ------------------- Currently prebuilt binaries are provided only for OS X and Linux. The install script will fall back to the legacy Ruby version on the other systems, but if -you have Go 1.4 installed, you can try building it yourself. (`make install`) +you have Go 1.4 installed, you can try building it yourself. However, as pointed out in [golang.org/doc/install][req], the Go version may not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby @@ -44,6 +54,20 @@ impossible to support Windows by falling back to a cross-platform alternative such as [termbox][termbox] only on Windows. If you're interested in making fzf work on Windows, please let me know. +Build +----- + +```sh +# Build fzf executables and tarballs +make + +# Install the executable to ../bin directory +make install + +# Build executables and tarballs for Linux using Docker +make linux +``` + Third-party libraries used -------------------------- @@ -56,8 +80,8 @@ Third-party libraries used Contribution ------------ -For the moment, I will not add or accept any new features until we can be sure -that the implementation is stable and we have a sufficient number of test +For the time being, I will not add or accept any new features until we can be +sure that the implementation is stable and we have a sufficient number of test cases. However, fixes for obvious bugs and new test cases are welcome. I also care much about the performance of the implementation (that's the @@ -71,6 +95,7 @@ License [MIT](LICENSE) [go]: https://golang.org/ +[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock [ncurses]: https://www.gnu.org/software/ncurses/ [req]: http://golang.org/doc/install [termbox]: https://github.com/nsf/termbox-go diff --git a/src/core.go b/src/core.go index 98973f8..2970038 100644 --- a/src/core.go +++ b/src/core.go @@ -1,3 +1,28 @@ +/* +Package fzf implements fzf, a command-line fuzzy finder. + +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ package fzf import ( From 1db68a3976cfb10ed7d6ab88d7b468bb1b93ee34 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 21:56:55 +0900 Subject: [PATCH 46/51] Avoid unnecessary update of search progress --- src/terminal.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 7039e57..7d8bc5b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -94,9 +94,14 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Lock() - t.progress = int(progress * 100) + newProgress := int(progress * 100) + changed := t.progress != newProgress + t.progress = newProgress t.mutex.Unlock() - t.reqBox.Set(REQ_INFO, nil) + + if changed { + t.reqBox.Set(REQ_INFO, nil) + } } func (t *Terminal) UpdateList(merger *Merger) { From 9dbf6b02d24b52ae43e36905bbb1e83087e1dfe9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 23:49:12 +0900 Subject: [PATCH 47/51] Fix race conditions - Wait for completions of goroutines when cancelling a search - Remove shared access to rank field of Item --- src/core.go | 6 +++++- src/item.go | 22 +++++++++++----------- src/item_test.go | 8 ++++---- src/matcher.go | 17 +++++++++++++---- src/merger.go | 2 +- src/merger_test.go | 12 +++++++++++- src/pattern.go | 5 ++++- 7 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/core.go b/src/core.go index 2970038..ab2a48f 100644 --- a/src/core.go +++ b/src/core.go @@ -64,7 +64,10 @@ func Run(options *Options) { var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { - return &Item{text: data, rank: Rank{0, 0, uint32(index)}} + return &Item{ + text: data, + index: uint32(index), + rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { @@ -72,6 +75,7 @@ func Run(options *Options) { item := Item{ text: Transform(tokens, opts.WithNth).whole, origText: data, + index: uint32(index), rank: Rank{0, 0, uint32(index)}} return &item }) diff --git a/src/item.go b/src/item.go index 9f90b8d..41aa34b 100644 --- a/src/item.go +++ b/src/item.go @@ -1,9 +1,6 @@ package fzf -import ( - "fmt" - "sort" -) +import "fmt" type Offset [2]int32 @@ -11,6 +8,7 @@ type Item struct { text *string origText *string transformed *Transformed + index uint32 offsets []Offset rank Rank } @@ -21,11 +19,10 @@ type Rank struct { index uint32 } -func (i *Item) Rank() Rank { - if i.rank.matchlen > 0 || i.rank.strlen > 0 { +func (i *Item) Rank(cache bool) Rank { + if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) { return i.rank } - sort.Sort(ByOrder(i.offsets)) matchlen := 0 prevEnd := 0 for _, offset := range i.offsets { @@ -41,8 +38,11 @@ func (i *Item) Rank() Rank { matchlen += end - begin } } - i.rank = Rank{uint16(matchlen), uint16(len(*i.text)), i.rank.index} - return i.rank + rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index} + if cache { + i.rank = rank + } + return rank } func (i *Item) Print() { @@ -80,8 +80,8 @@ func (a ByRelevance) Swap(i, j int) { } func (a ByRelevance) Less(i, j int) bool { - irank := a[i].Rank() - jrank := a[j].Rank() + irank := a[i].Rank(true) + jrank := a[j].Rank(true) return compareRanks(irank, jrank) } diff --git a/src/item_test.go b/src/item_test.go index 87d8be4..0e83631 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -31,13 +31,13 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestItemRank(t *testing.T) { strs := []string{"foo", "foobar", "bar", "baz"} - item1 := Item{text: &strs[0], rank: Rank{0, 0, 1}, offsets: []Offset{}} - rank1 := item1.Rank() + item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + rank1 := item1.Rank(true) if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 { - t.Error(item1.Rank()) + t.Error(item1.Rank(true)) } // Only differ in index - item2 := Item{text: &strs[0], rank: Rank{0, 0, 0}, offsets: []Offset{}} + item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) diff --git a/src/matcher.go b/src/matcher.go index 234e703..713b4dd 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -4,6 +4,7 @@ import ( "fmt" "runtime" "sort" + "sync" "time" ) @@ -134,10 +135,13 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { slices := m.sliceChunks(request.chunks) numSlices := len(slices) resultChan := make(chan partialResult, numSlices) - countChan := make(chan int, numSlices) + countChan := make(chan int, numChunks) + waitGroup := sync.WaitGroup{} for idx, chunks := range slices { + waitGroup.Add(1) go func(idx int, chunks []*Chunk) { + defer func() { waitGroup.Done() }() sliceMatches := []*Item{} for _, chunk := range chunks { var matches []*Item @@ -159,6 +163,12 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { }(idx, chunks) } + wait := func() bool { + cancelled.Set(true) + waitGroup.Wait() + return true + } + count := 0 matchCount := 0 for matchesInChunk := range countChan { @@ -166,7 +176,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { matchCount += matchesInChunk if limit > 0 && matchCount > limit { - return nil, true // For --select-1 and --exit-0 + return nil, wait() // For --select-1 and --exit-0 } if count == numChunks { @@ -174,8 +184,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { } if !empty && m.reqBox.Peak(REQ_RESET) { - cancelled.Set(true) - return nil, true + return nil, wait() } if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION { diff --git a/src/merger.go b/src/merger.go index 08a3d15..16afdaf 100644 --- a/src/merger.go +++ b/src/merger.go @@ -57,7 +57,7 @@ func (mg *Merger) mergedGet(idx int) *Item { continue } if cursor >= 0 { - rank := list[cursor].Rank() + rank := list[cursor].Rank(false) if minIdx < 0 || compareRanks(rank, minRank) { minRank = rank minIdx = listIdx diff --git a/src/merger_test.go b/src/merger_test.go index 32a1228..f79da09 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -1,6 +1,7 @@ package fzf import ( + "fmt" "math/rand" "sort" "testing" @@ -13,8 +14,17 @@ func assert(t *testing.T, cond bool, msg ...string) { } func randItem() *Item { + str := fmt.Sprintf("%d", rand.Uint32()) + offsets := make([]Offset, rand.Int()%3) + for idx := range offsets { + sidx := int32(rand.Uint32() % 20) + eidx := sidx + int32(rand.Uint32()%20) + offsets[idx] = Offset{sidx, eidx} + } return &Item{ - rank: Rank{uint16(rand.Uint32()), uint16(rand.Uint32()), rand.Uint32()}} + text: &str, + index: rand.Uint32(), + offsets: offsets} } func TestEmptyMerger(t *testing.T) { diff --git a/src/pattern.go b/src/pattern.go index 31ba813..2e7d6f9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -2,6 +2,7 @@ package fzf import ( "regexp" + "sort" "strings" ) @@ -225,12 +226,14 @@ Loop: } func dupItem(item *Item, offsets []Offset) *Item { + sort.Sort(ByOrder(offsets)) return &Item{ text: item.text, origText: item.origText, transformed: item.transformed, + index: item.index, offsets: offsets, - rank: Rank{0, 0, item.rank.index}} + rank: Rank{0, 0, item.index}} } func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { From 7a2bc2cada971c7a390d09b0afda34780ff56fb6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 12 Jan 2015 03:01:24 +0900 Subject: [PATCH 48/51] Lint --- src/algo.go | 29 +++--- src/atomicbool.go | 5 + src/cache.go | 6 ++ src/cache_test.go | 2 +- src/chunklist.go | 20 +++- src/chunklist_test.go | 4 +- src/constants.go | 19 ++-- src/core.go | 43 +++++---- src/curses/curses.go | 218 +++++++++++++++++++++--------------------- src/eventbox.go | 11 ++- src/eventbox_test.go | 14 +-- src/item.go | 16 ++-- src/matcher.go | 31 +++--- src/merger.go | 10 +- src/options.go | 43 +++++---- src/options_test.go | 10 +- src/pattern.go | 77 ++++++++------- src/pattern_test.go | 54 +++++------ src/reader.go | 10 +- src/reader_test.go | 18 ++-- src/terminal.go | 188 ++++++++++++++++++------------------ src/tokenizer.go | 47 ++++----- src/tokenizer_test.go | 4 +- src/util.go | 4 + 24 files changed, 478 insertions(+), 405 deletions(-) diff --git a/src/algo.go b/src/algo.go index e0c173f..5f15ab3 100644 --- a/src/algo.go +++ b/src/algo.go @@ -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) diff --git a/src/atomicbool.go b/src/atomicbool.go index f2f4894..b264724 100644 --- a/src/atomicbool.go +++ b/src/atomicbool.go @@ -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() diff --git a/src/cache.go b/src/cache.go index 340f325..f2f84a0 100644 --- a/src/cache.go +++ b/src/cache.go @@ -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 diff --git a/src/cache_test.go b/src/cache_test.go index 2a8b048..3975eaa 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -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{}} diff --git a/src/chunklist.go b/src/chunklist.go index 5bca6da..73983b1 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -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() diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 09e4aad..02288d9 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -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 { diff --git a/src/constants.go b/src/constants.go index b0b64db..80eb634 100644 --- a/src/constants.go +++ b/src/constants.go @@ -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 ) diff --git a/src/core.go b/src/core.go index ab2a48f..65e641c 100644 --- a/src/core.go +++ b/src/core.go @@ -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) } } diff --git a/src/curses/curses.go b/src/curses/curses.go index 736ccf6..8ebb583 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -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) { diff --git a/src/eventbox.go b/src/eventbox.go index 95126cc..0c8f922 100644 --- a/src/eventbox.go +++ b/src/eventbox.go @@ -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() diff --git a/src/eventbox_test.go b/src/eventbox_test.go index fb0ceed..1cd7f22 100644 --- a/src/eventbox_test.go +++ b/src/eventbox_test.go @@ -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 { diff --git a/src/item.go b/src/item.go index 41aa34b..4cbd3f9 100644 --- a/src/item.go +++ b/src/item.go @@ -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 { diff --git a/src/matcher.go b/src/matcher.go index 713b4dd..b8be287 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -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}) } diff --git a/src/merger.go b/src/merger.go index 16afdaf..bd2158d 100644 --- a/src/merger.go +++ b/src/merger.go @@ -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)) } diff --git a/src/options.go b/src/options.go index 4929dfd..cf0608b 100644 --- a/src/options.go +++ b/src/options.go @@ -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")) diff --git a/src/options_test.go b/src/options_test.go index f0aa3a0..e10ec56 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -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) diff --git a/src/pattern.go b/src/pattern.go index 2e7d6f9..9f32de6 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -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) diff --git a/src/pattern_test.go b/src/pattern_test.go index 2635b6c..c006c45 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -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}}) diff --git a/src/reader.go b/src/reader.go index 39fa70c..269a2fd 100644 --- a/src/reader.go +++ b/src/reader.go @@ -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) } } } diff --git a/src/reader_test.go b/src/reader_test.go index f51ccab..630f6fa 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -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") } } diff --git a/src/terminal.go b/src/terminal.go index 7d8bc5b..daf63c5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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) diff --git a/src/tokenizer.go b/src/tokenizer.go index d62f395..294329b 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -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 diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 1ae0c7e..5195a1b 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -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) } } diff --git a/src/util.go b/src/util.go index de6f365..5461705 100644 --- a/src/util.go +++ b/src/util.go @@ -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 { From cd847affb79ea6438c9721635724efc6f58e2215 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 12 Jan 2015 12:56:17 +0900 Subject: [PATCH 49/51] Reorganize source code --- src/Makefile | 2 +- src/{ => algo}/algo.go | 2 +- src/{ => algo}/algo_test.go | 2 +- src/chunklist.go | 10 ++++---- src/constants.go | 9 +++---- src/core.go | 10 ++++---- src/matcher.go | 18 +++++++------- src/options.go | 3 ++- src/pattern.go | 12 ++++++---- src/pattern_test.go | 8 +++++-- src/reader.go | 9 ++++--- src/reader_test.go | 10 +++++--- src/terminal.go | 39 +++++++++++++++++-------------- src/tokenizer.go | 4 +++- src/{ => util}/atomicbool.go | 2 +- src/{ => util}/atomicbool_test.go | 2 +- src/{ => util}/eventbox.go | 5 +++- src/{ => util}/eventbox_test.go | 12 +++++++++- src/{ => util}/util.go | 31 ++++++++++++++++-------- src/util/util_test.go | 22 +++++++++++++++++ src/util_test.go | 18 -------------- 21 files changed, 139 insertions(+), 91 deletions(-) rename src/{ => algo}/algo.go (99%) rename src/{ => algo}/algo_test.go (99%) rename src/{ => util}/atomicbool.go (98%) rename src/{ => util}/atomicbool_test.go (95%) rename src/{ => util}/eventbox.go (95%) rename src/{ => util}/eventbox_test.go (85%) rename src/{ => util}/util.go (60%) create mode 100644 src/util/util_test.go delete mode 100644 src/util_test.go diff --git a/src/Makefile b/src/Makefile index 7108f0d..43a7bc0 100644 --- a/src/Makefile +++ b/src/Makefile @@ -33,7 +33,7 @@ build: fzf/$(BINARY32) fzf/$(BINARY64) test: go get - go test -v + go test -v ./... install: $(BINDIR)/fzf diff --git a/src/algo.go b/src/algo/algo.go similarity index 99% rename from src/algo.go rename to src/algo/algo.go index 5f15ab3..bc4e538 100644 --- a/src/algo.go +++ b/src/algo/algo.go @@ -1,4 +1,4 @@ -package fzf +package algo import "strings" diff --git a/src/algo_test.go b/src/algo/algo_test.go similarity index 99% rename from src/algo_test.go rename to src/algo/algo_test.go index 5da01a6..363b6ee 100644 --- a/src/algo_test.go +++ b/src/algo/algo_test.go @@ -1,4 +1,4 @@ -package fzf +package algo import ( "strings" diff --git a/src/chunklist.go b/src/chunklist.go index 73983b1..571a59a 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -8,20 +8,20 @@ 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 +// ItemBuilder is a closure type that builds Item object from a pointer to a // string and an integer -type Transformer func(*string, int) *Item +type ItemBuilder func(*string, int) *Item // ChunkList is a list of Chunks type ChunkList struct { chunks []*Chunk count int mutex sync.Mutex - trans Transformer + trans ItemBuilder } // NewChunkList returns a new ChunkList -func NewChunkList(trans Transformer) *ChunkList { +func NewChunkList(trans ItemBuilder) *ChunkList { return &ChunkList{ chunks: []*Chunk{}, count: 0, @@ -29,7 +29,7 @@ func NewChunkList(trans Transformer) *ChunkList { trans: trans} } -func (c *Chunk) push(trans Transformer, data *string, index int) { +func (c *Chunk) push(trans ItemBuilder, data *string, index int) { *c = append(*c, trans(data, index)) } diff --git a/src/constants.go b/src/constants.go index 80eb634..a871570 100644 --- a/src/constants.go +++ b/src/constants.go @@ -1,14 +1,15 @@ package fzf +import ( + "github.com/junegunn/fzf/src/util" +) + // Current version const Version = "0.9.0" -// EventType is the type for fzf events -type EventType int - // fzf events const ( - EvtReadNew EventType = iota + EvtReadNew util.EventType = iota EvtReadFin EvtSearchNew EvtSearchProgress diff --git a/src/core.go b/src/core.go index 65e641c..ee90413 100644 --- a/src/core.go +++ b/src/core.go @@ -30,6 +30,8 @@ import ( "os" "runtime" "time" + + "github.com/junegunn/fzf/src/util" ) const coordinatorDelayMax time.Duration = 100 * time.Millisecond @@ -59,7 +61,7 @@ func Run(options *Options) { } // Event channel - eventBox := NewEventBox() + eventBox := util.NewEventBox() // Chunk list var chunkList *ChunkList @@ -111,7 +113,7 @@ func Run(options *Options) { looping := true eventBox.Unwatch(EvtReadNew) for looping { - eventBox.Wait(func(events *Events) { + eventBox.Wait(func(events *util.Events) { for evt := range *events { switch evt { case EvtReadFin: @@ -154,7 +156,7 @@ func Run(options *Options) { for { delay := true ticks++ - eventBox.Wait(func(events *Events) { + eventBox.Wait(func(events *util.Events) { defer events.Clear() for evt, value := range *events { switch evt { @@ -185,7 +187,7 @@ func Run(options *Options) { } }) if delay && reading { - dur := DurWithin( + dur := util.DurWithin( time.Duration(ticks)*coordinatorDelayStep, 0, coordinatorDelayMax) time.Sleep(dur) diff --git a/src/matcher.go b/src/matcher.go index b8be287..1ea9541 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -6,6 +6,8 @@ import ( "sort" "sync" "time" + + "github.com/junegunn/fzf/src/util" ) // MatchRequest represents a search request @@ -18,14 +20,14 @@ type MatchRequest struct { type Matcher struct { patternBuilder func([]rune) *Pattern sort bool - eventBox *EventBox - reqBox *EventBox + eventBox *util.EventBox + reqBox *util.EventBox partitions int mergerCache map[string]*Merger } const ( - reqRetry EventType = iota + reqRetry util.EventType = iota reqReset ) @@ -35,12 +37,12 @@ const ( // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, - sort bool, eventBox *EventBox) *Matcher { + sort bool, eventBox *util.EventBox) *Matcher { return &Matcher{ patternBuilder: patternBuilder, sort: sort, eventBox: eventBox, - reqBox: NewEventBox(), + reqBox: util.NewEventBox(), partitions: runtime.NumCPU(), mergerCache: make(map[string]*Merger)} } @@ -52,7 +54,7 @@ func (m *Matcher) Loop() { for { var request MatchRequest - m.reqBox.Wait(func(events *Events) { + m.reqBox.Wait(func(events *util.Events) { for _, val := range *events { switch val := val.(type) { case MatchRequest: @@ -128,7 +130,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { } pattern := request.pattern empty := pattern.IsEmpty() - cancelled := NewAtomicBool(false) + cancelled := util.NewAtomicBool(false) slices := m.sliceChunks(request.chunks) numSlices := len(slices) @@ -202,7 +204,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { pattern := m.patternBuilder(patternRunes) - var event EventType + var event util.EventType if cancel { event = reqReset } else { diff --git a/src/options.go b/src/options.go index cf0608b..e1dba29 100644 --- a/src/options.go +++ b/src/options.go @@ -2,10 +2,11 @@ package fzf import ( "fmt" - "github.com/junegunn/go-shellwords" "os" "regexp" "strings" + + "github.com/junegunn/go-shellwords" ) const usage = `usage: fzf [options] diff --git a/src/pattern.go b/src/pattern.go index 9f32de6..17e3b6b 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -4,6 +4,8 @@ import ( "regexp" "sort" "strings" + + "github.com/junegunn/fzf/src/algo" ) const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -112,10 +114,10 @@ func BuildPattern(mode Mode, caseMode Case, delimiter: delimiter, procFun: make(map[termType]func(bool, *string, []rune) (int, int))} - ptr.procFun[termFuzzy] = FuzzyMatch - ptr.procFun[termExact] = ExactMatchNaive - ptr.procFun[termPrefix] = PrefixMatch - ptr.procFun[termSuffix] = SuffixMatch + ptr.procFun[termFuzzy] = algo.FuzzyMatch + ptr.procFun[termExact] = algo.ExactMatchNaive + ptr.procFun[termPrefix] = algo.PrefixMatch + ptr.procFun[termSuffix] = algo.SuffixMatch _patternCache[asString] = ptr return ptr @@ -245,7 +247,7 @@ func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { matches := []*Item{} for _, item := range *chunk { input := p.prepareInput(item) - if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { + if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 { matches = append(matches, dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) } diff --git a/src/pattern_test.go b/src/pattern_test.go index c006c45..4d36eda 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -1,6 +1,10 @@ package fzf -import "testing" +import ( + "testing" + + "github.com/junegunn/fzf/src/algo" +) func TestParseTermsExtended(t *testing.T) { terms := parseTerms(ModeExtended, @@ -55,7 +59,7 @@ func TestExact(t *testing.T) { pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("'abc")) str := "aabbcc abc" - sidx, eidx := ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) + sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) if sidx != 7 || eidx != 10 { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } diff --git a/src/reader.go b/src/reader.go index 269a2fd..2c10b8a 100644 --- a/src/reader.go +++ b/src/reader.go @@ -1,13 +1,12 @@ package fzf -// #include -import "C" - import ( "bufio" "io" "os" "os/exec" + + "github.com/junegunn/fzf/src/util" ) const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` @@ -15,12 +14,12 @@ const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l // Reader reads from command or standard input type Reader struct { pusher func(string) - eventBox *EventBox + eventBox *util.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 { + if util.IsTty() { cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { cmd = defaultCommand diff --git a/src/reader_test.go b/src/reader_test.go index 630f6fa..5800b3f 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -1,10 +1,14 @@ package fzf -import "testing" +import ( + "testing" + + "github.com/junegunn/fzf/src/util" +) func TestReadFromCommand(t *testing.T) { strs := []string{} - eb := NewEventBox() + eb := util.NewEventBox() reader := Reader{ pusher: func(s string) { strs = append(strs, s) }, eventBox: eb} @@ -26,7 +30,7 @@ func TestReadFromCommand(t *testing.T) { } // Wait should return immediately - eb.Wait(func(events *Events) { + eb.Wait(func(events *util.Events) { if _, found := (*events)[EvtReadNew]; !found { t.Errorf("%s", events) } diff --git a/src/terminal.go b/src/terminal.go index daf63c5..4204a1d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -2,13 +2,16 @@ package fzf import ( "fmt" - C "github.com/junegunn/fzf/src/curses" - "github.com/junegunn/go-runewidth" "os" "regexp" "sort" "sync" "time" + + C "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" + + "github.com/junegunn/go-runewidth" ) // Terminal represents terminal input/output @@ -28,8 +31,8 @@ type Terminal struct { reading bool merger *Merger selected map[*string]*string - reqBox *EventBox - eventBox *EventBox + reqBox *util.EventBox + eventBox *util.EventBox mutex sync.Mutex initFunc func() suppress bool @@ -38,7 +41,7 @@ type Terminal struct { var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} const ( - reqPrompt EventType = iota + reqPrompt util.EventType = iota reqInfo reqList reqRefresh @@ -53,7 +56,7 @@ const ( ) // NewTerminal returns new Terminal object -func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { +func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ prompt: opts.Prompt, @@ -68,7 +71,7 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { printQuery: opts.PrintQuery, merger: EmptyMerger, selected: make(map[*string]*string), - reqBox: NewEventBox(), + reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, @@ -288,7 +291,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { b, e := offset[0], offset[1] b += 2 - diff e += 2 - diff - b = Max32(b, 2) + b = util.Max32(b, 2) if b < e { offsets[idx] = Offset{b, e} } @@ -300,8 +303,8 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { sort.Sort(ByOrder(offsets)) var index int32 for _, offset := range offsets { - b := Max32(index, offset[0]) - e := Max32(index, offset[1]) + b := util.Max32(index, offset[0]) + e := util.Max32(index, offset[1]) C.CPrint(col1, bold, string(text[index:b])) C.CPrint(col2, bold, string(text[b:e])) index = e @@ -388,7 +391,7 @@ func (t *Terminal) Loop() { go func() { for { - t.reqBox.Wait(func(events *Events) { + t.reqBox.Wait(func(events *util.Events) { defer events.Clear() t.mutex.Lock() for req := range *events { @@ -426,8 +429,8 @@ func (t *Terminal) Loop() { t.mutex.Lock() previousInput := t.input - events := []EventType{reqPrompt} - req := func(evts ...EventType) { + events := []util.EventType{reqPrompt} + req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) if event == reqClose || event == reqQuit { @@ -538,7 +541,7 @@ func (t *Terminal) Loop() { t.cx++ case C.Mouse: me := event.MouseEvent - mx, my := Min(len(t.input), Max(0, me.X-len(t.prompt))), me.Y + mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y if !t.reverse { my = C.MaxY() - my - 1 } @@ -588,7 +591,7 @@ func (t *Terminal) constrain() { height := C.MaxY() - 2 diffpos := t.cy - t.offset - t.cy = Max(0, Min(t.cy, count-1)) + t.cy = util.Constrain(t.cy, 0, count-1) if t.cy > t.offset+(height-1) { // Ceil @@ -600,8 +603,8 @@ func (t *Terminal) constrain() { // Adjustment if count-t.offset < height { - t.offset = Max(0, count-height) - t.cy = Max(0, Min(t.offset+diffpos, count-1)) + t.offset = util.Max(0, count-height) + t.cy = util.Constrain(t.offset+diffpos, 0, count-1) } } @@ -614,7 +617,7 @@ func (t *Terminal) vmove(o int) { } func (t *Terminal) vset(o int) bool { - t.cy = Max(0, Min(o, t.merger.Length()-1)) + t.cy = util.Constrain(o, 0, t.merger.Length()-1) return t.cy == o } diff --git a/src/tokenizer.go b/src/tokenizer.go index 294329b..26aebd9 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -4,6 +4,8 @@ import ( "regexp" "strconv" "strings" + + "github.com/junegunn/fzf/src/util" ) const rangeEllipsis = 0 @@ -180,7 +182,7 @@ func Transform(tokens []Token, withNth []Range) *Transformed { end += numTokens + 1 } } - minIdx = Max(0, begin-1) + minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { part += *tokens[idx-1].text diff --git a/src/atomicbool.go b/src/util/atomicbool.go similarity index 98% rename from src/atomicbool.go rename to src/util/atomicbool.go index b264724..9e1bdc8 100644 --- a/src/atomicbool.go +++ b/src/util/atomicbool.go @@ -1,4 +1,4 @@ -package fzf +package util import "sync" diff --git a/src/atomicbool_test.go b/src/util/atomicbool_test.go similarity index 95% rename from src/atomicbool_test.go rename to src/util/atomicbool_test.go index 0af4570..1feff79 100644 --- a/src/atomicbool_test.go +++ b/src/util/atomicbool_test.go @@ -1,4 +1,4 @@ -package fzf +package util import "testing" diff --git a/src/eventbox.go b/src/util/eventbox.go similarity index 95% rename from src/eventbox.go rename to src/util/eventbox.go index 0c8f922..568ad9f 100644 --- a/src/eventbox.go +++ b/src/util/eventbox.go @@ -1,7 +1,10 @@ -package fzf +package util import "sync" +// EventType is the type for fzf events +type EventType int + // Events is a type that associates EventType to any data type Events map[EventType]interface{} diff --git a/src/eventbox_test.go b/src/util/eventbox_test.go similarity index 85% rename from src/eventbox_test.go rename to src/util/eventbox_test.go index 1cd7f22..5a9dc30 100644 --- a/src/eventbox_test.go +++ b/src/util/eventbox_test.go @@ -1,7 +1,17 @@ -package fzf +package util import "testing" +// fzf events +const ( + EvtReadNew EventType = iota + EvtReadFin + EvtSearchNew + EvtSearchProgress + EvtSearchFin + EvtClose +) + func TestEventBox(t *testing.T) { eb := NewEventBox() diff --git a/src/util.go b/src/util/util.go similarity index 60% rename from src/util.go rename to src/util/util.go index 5461705..14833c0 100644 --- a/src/util.go +++ b/src/util/util.go @@ -1,6 +1,12 @@ -package fzf +package util -import "time" +// #include +import "C" + +import ( + "os" + "time" +) // Max returns the largest integer func Max(first int, items ...int) int { @@ -21,15 +27,15 @@ 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 { - if item < min { - min = item - } +// Constrain limits the given integer with the upper and lower bounds +func Constrain(val int, min int, max int) int { + if val < min { + return min } - return min + if val > max { + return max + } + return val } // DurWithin limits the given time.Duration with the upper and lower bounds @@ -43,3 +49,8 @@ func DurWithin( } return val } + +// IsTty returns true is stdin is a terminal +func IsTty() bool { + return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 +} diff --git a/src/util/util_test.go b/src/util/util_test.go new file mode 100644 index 0000000..06cfd4f --- /dev/null +++ b/src/util/util_test.go @@ -0,0 +1,22 @@ +package util + +import "testing" + +func TestMax(t *testing.T) { + if Max(-2, 5, 1, 4, 3) != 5 { + t.Error("Invalid result") + } +} + +func TestContrain(t *testing.T) { + if Constrain(-3, -1, 3) != -1 { + t.Error("Expected", -1) + } + if Constrain(2, -1, 3) != 2 { + t.Error("Expected", 2) + } + + if Constrain(5, -1, 3) != 3 { + t.Error("Expected", 3) + } +} diff --git a/src/util_test.go b/src/util_test.go deleted file mode 100644 index 814b42c..0000000 --- a/src/util_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package fzf - -import "testing" - -func TestMax(t *testing.T) { - if Max(-2, 5, 1, 4, 3) != 5 { - t.Error("Invalid result") - } -} - -func TestMin(t *testing.T) { - if Min(2, -3) != -3 { - t.Error("Invalid result") - } - if Min(-2, 5, 1, 4, 3) != -2 { - t.Error("Invalid result") - } -} From 2c86e728b56a54304f4a4404f14a678b5279adc9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Jan 2015 02:05:37 +0900 Subject: [PATCH 50/51] Update src/README.md --- src/README.md | 80 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/README.md b/src/README.md index fb17e68..c071865 100644 --- a/src/README.md +++ b/src/README.md @@ -2,30 +2,45 @@ fzf in Go ========= This directory contains the source code for the new fzf implementation in -[Go][go]. The new version has the following benefits over the previous Ruby -version. +[Go][go]. -Motivation ----------- +Upgrade from Ruby version +------------------------- + +The install script has been updated to download the right binary for your +system. If you already have installed fzf, simply git-pull the repository and +rerun the install script. + +```sh +cd ~/.fzf +git pull +./install +``` + +Motivations +----------- ### No Ruby dependency There have always been complaints about fzf being a Ruby script. To make -matters worse, Ruby 2.1 dropped ncurses support from its standard libary. -Because of the change, users running Ruby 2.1 or above were forced to build C +matters worse, Ruby 2.1 removed ncurses binding from its standard libary. +Because of the change, users running Ruby 2.1 or above are forced to build C extensions of curses gem to meet the requirement of fzf. The new Go version will be distributed as an executable binary so it will be much more accessible -and easier to setup. +and should be easier to setup. ### Performance -With the presence of [GIL][gil], Ruby cannot utilize multiple CPU cores. Even -though the Ruby version of fzf was pretty responsive even for 100k+ lines, -which is well above the size of the usual input, it was obvious that we could -do better. Now with the Go version, GIL is gone, and the search performance -scales proportional to the number of cores. On my Macbook Pro (Mid 2012), it -was shown to be an order of magnitude faster on certain cases. It also starts -much faster than before though the difference shouldn't be really noticeable. +Many people have been surprised to see how fast fzf is even when it was +written in Ruby. It stays quite responsive even for 100k+ lines, which is +well above the size of the usual input. + +The new Go version, of course, is significantly faster than that. It has all +the performance optimization techniques used in Ruby implementation and more. +It also doesn't suffer from [GIL][gil], so the search performance scales +proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new +version was shown to be an order of magnitude faster on certain cases. It also +starts much faster though the difference may not be noticeable. Differences with Ruby version ----------------------------- @@ -40,19 +55,19 @@ even for 1M+ items) so I decided that it's no longer required. System requirements ------------------- -Currently prebuilt binaries are provided only for OS X and Linux. The install +Currently, prebuilt binaries are provided only for OS X and Linux. The install script will fall back to the legacy Ruby version on the other systems, but if you have Go 1.4 installed, you can try building it yourself. However, as pointed out in [golang.org/doc/install][req], the Go version may -not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby -version instead. +not run on CentOS/RHEL 5.x, and if that's the case, the install script will +choose the Ruby version instead. The Go version depends on [ncurses][ncurses] and some Unix system calls, so it -shouldn't run natively on Windows at the moment. But it should be not -impossible to support Windows by falling back to a cross-platform alternative -such as [termbox][termbox] only on Windows. If you're interested in making fzf -work on Windows, please let me know. +shouldn't run natively on Windows at the moment. But it won't be impossible to +support Windows by falling back to a cross-platform alternative such as +[termbox][termbox] only on Windows. If you're interested in making fzf work on +Windows, please let me know. Build ----- @@ -68,6 +83,17 @@ make install make linux ``` +Contribution +------------ + +For the time being, I will not add or accept any new features until we can be +sure that the implementation is stable and we have a sufficient number of test +cases. However, fixes for obvious bugs and new test cases are welcome. + +I also care much about the performance of the implementation, so please make +sure that your change does not result in performance regression. And please be +noted that we don't have a quantitative measure of the performance yet. + Third-party libraries used -------------------------- @@ -77,18 +103,6 @@ Third-party libraries used - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - Licensed under [MIT](http://mattn.mit-license.org/2014) -Contribution ------------- - -For the time being, I will not add or accept any new features until we can be -sure that the implementation is stable and we have a sufficient number of test -cases. However, fixes for obvious bugs and new test cases are welcome. - -I also care much about the performance of the implementation (that's the -reason I rewrote the whole thing in Go, right?), so please make sure that your -change does not result in performance regression. Please be minded that we -still don't have a quantitative measure of the performance. - License ------- From 5c491d573a147573c68aa7e56a6032dbe4b84635 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Jan 2015 02:39:00 +0900 Subject: [PATCH 51/51] Fix fzf.{bash,zsh} when Go version is not supported --- install | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/install b/install index f8307cf..4098c87 100755 --- a/install +++ b/install @@ -152,20 +152,29 @@ for shell in bash zsh; do fzf_completion="# $fzf_completion" fi - cat > $src << EOF + if [ -n "$binary_error" ]; then + cat > $src << EOF # Setup fzf function # ------------------ unalias fzf 2> /dev/null +fzf() { + $fzf_cmd "\$@" +} +export -f fzf > /dev/null + +# Auto-completion +# --------------- +$fzf_completion + +EOF + else + cat > $src << EOF +# Setup fzf +# --------- +unalias fzf 2> /dev/null unset fzf 2> /dev/null -if [ -x "$fzf_base/bin/fzf" ]; then - if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then - export PATH="$fzf_base/bin:\$PATH" - fi -else - fzf() { - $fzf_cmd "\$@" - } - export -f fzf > /dev/null +if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then + export PATH="$fzf_base/bin:\$PATH" fi # Auto-completion @@ -173,6 +182,7 @@ fi $fzf_completion EOF + fi if [ $key_bindings -eq 0 ]; then if [ $shell = bash ]; then