mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-09-29 13:49:00 +00:00
c9f16b6430
When --with-nth is used, fzf used to preprocess each line and store the result as rune array, which was wasteful if the line only contains ascii characters.
243 lines
5.2 KiB
Go
243 lines
5.2 KiB
Go
package fzf
|
|
|
|
import (
|
|
"bytes"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/junegunn/fzf/src/util"
|
|
)
|
|
|
|
const rangeEllipsis = 0
|
|
|
|
// Range represents nth-expression
|
|
type Range struct {
|
|
begin int
|
|
end int
|
|
}
|
|
|
|
// Token contains the tokenized part of the strings and its prefix length
|
|
type Token struct {
|
|
text *util.Chars
|
|
prefixLength int32
|
|
}
|
|
|
|
// Delimiter for tokenizing the input
|
|
type Delimiter struct {
|
|
regex *regexp.Regexp
|
|
str *string
|
|
}
|
|
|
|
func newRange(begin int, end int) Range {
|
|
if begin == 1 {
|
|
begin = rangeEllipsis
|
|
}
|
|
if end == -1 {
|
|
end = rangeEllipsis
|
|
}
|
|
return Range{begin, end}
|
|
}
|
|
|
|
// ParseRange parses nth-expression and returns the corresponding Range object
|
|
func ParseRange(str *string) (Range, bool) {
|
|
if (*str) == ".." {
|
|
return newRange(rangeEllipsis, rangeEllipsis), true
|
|
} else if strings.HasPrefix(*str, "..") {
|
|
end, err := strconv.Atoi((*str)[2:])
|
|
if err != nil || end == 0 {
|
|
return Range{}, false
|
|
}
|
|
return newRange(rangeEllipsis, end), true
|
|
} else if strings.HasSuffix(*str, "..") {
|
|
begin, err := strconv.Atoi((*str)[:len(*str)-2])
|
|
if err != nil || begin == 0 {
|
|
return Range{}, false
|
|
}
|
|
return newRange(begin, rangeEllipsis), 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 || begin == 0 || end == 0 {
|
|
return Range{}, false
|
|
}
|
|
return newRange(begin, end), true
|
|
}
|
|
|
|
n, err := strconv.Atoi(*str)
|
|
if err != nil || n == 0 {
|
|
return Range{}, false
|
|
}
|
|
return newRange(n, n), true
|
|
}
|
|
|
|
func withPrefixLengths(tokens []string, begin int) []Token {
|
|
ret := make([]Token, len(tokens))
|
|
|
|
prefixLength := begin
|
|
for idx := range tokens {
|
|
chars := util.ToChars([]byte(tokens[idx]))
|
|
ret[idx] = Token{&chars, int32(prefixLength)}
|
|
prefixLength += chars.Length()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
const (
|
|
awkNil = iota
|
|
awkBlack
|
|
awkWhite
|
|
)
|
|
|
|
func awkTokenizer(input string) ([]string, int) {
|
|
// 9, 32
|
|
ret := []string{}
|
|
prefixLength := 0
|
|
state := awkNil
|
|
begin := 0
|
|
end := 0
|
|
for idx := 0; idx < len(input); idx++ {
|
|
r := input[idx]
|
|
white := r == 9 || r == 32
|
|
switch state {
|
|
case awkNil:
|
|
if white {
|
|
prefixLength++
|
|
} else {
|
|
state, begin, end = awkBlack, idx, idx+1
|
|
}
|
|
case awkBlack:
|
|
end = idx + 1
|
|
if white {
|
|
state = awkWhite
|
|
}
|
|
case awkWhite:
|
|
if white {
|
|
end = idx + 1
|
|
} else {
|
|
ret = append(ret, input[begin:end])
|
|
state, begin, end = awkBlack, idx, idx+1
|
|
}
|
|
}
|
|
}
|
|
if begin < end {
|
|
ret = append(ret, input[begin:end])
|
|
}
|
|
return ret, prefixLength
|
|
}
|
|
|
|
// Tokenize tokenizes the given string with the delimiter
|
|
func Tokenize(text string, delimiter Delimiter) []Token {
|
|
if delimiter.str == nil && delimiter.regex == nil {
|
|
// AWK-style (\S+\s*)
|
|
tokens, prefixLength := awkTokenizer(text)
|
|
return withPrefixLengths(tokens, prefixLength)
|
|
}
|
|
|
|
if delimiter.str != nil {
|
|
return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
|
|
}
|
|
|
|
// FIXME performance
|
|
var tokens []string
|
|
if delimiter.regex != nil {
|
|
for len(text) > 0 {
|
|
loc := delimiter.regex.FindStringIndex(text)
|
|
if loc == nil {
|
|
loc = []int{0, len(text)}
|
|
}
|
|
last := util.Max(loc[1], 1)
|
|
tokens = append(tokens, text[:last])
|
|
text = text[last:]
|
|
}
|
|
}
|
|
return withPrefixLengths(tokens, 0)
|
|
}
|
|
|
|
func joinTokens(tokens []Token) string {
|
|
var output bytes.Buffer
|
|
for _, token := range tokens {
|
|
output.WriteString(token.text.ToString())
|
|
}
|
|
return output.String()
|
|
}
|
|
|
|
// Transform is used to transform the input when --with-nth option is given
|
|
func Transform(tokens []Token, withNth []Range) []Token {
|
|
transTokens := make([]Token, len(withNth))
|
|
numTokens := len(tokens)
|
|
for idx, r := range withNth {
|
|
parts := []*util.Chars{}
|
|
minIdx := 0
|
|
if r.begin == r.end {
|
|
idx := r.begin
|
|
if idx == rangeEllipsis {
|
|
chars := util.ToChars([]byte(joinTokens(tokens)))
|
|
parts = append(parts, &chars)
|
|
} else {
|
|
if idx < 0 {
|
|
idx += numTokens + 1
|
|
}
|
|
if idx >= 1 && idx <= numTokens {
|
|
minIdx = idx - 1
|
|
parts = append(parts, tokens[idx-1].text)
|
|
}
|
|
}
|
|
} else {
|
|
var begin, end int
|
|
if r.begin == rangeEllipsis { // ..N
|
|
begin, end = 1, r.end
|
|
if end < 0 {
|
|
end += numTokens + 1
|
|
}
|
|
} else if r.end == rangeEllipsis { // 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 = util.Max(0, begin-1)
|
|
for idx := begin; idx <= end; idx++ {
|
|
if idx >= 1 && idx <= numTokens {
|
|
parts = append(parts, tokens[idx-1].text)
|
|
}
|
|
}
|
|
}
|
|
// Merge multiple parts
|
|
var merged util.Chars
|
|
switch len(parts) {
|
|
case 0:
|
|
merged = util.ToChars([]byte{})
|
|
case 1:
|
|
merged = *parts[0]
|
|
default:
|
|
var output bytes.Buffer
|
|
for _, part := range parts {
|
|
output.WriteString(part.ToString())
|
|
}
|
|
merged = util.ToChars([]byte(output.String()))
|
|
}
|
|
|
|
var prefixLength int32
|
|
if minIdx < numTokens {
|
|
prefixLength = tokens[minIdx].prefixLength
|
|
} else {
|
|
prefixLength = 0
|
|
}
|
|
transTokens[idx] = Token{&merged, prefixLength}
|
|
}
|
|
return transTokens
|
|
}
|