Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func Run(opts *Options) (int, error) {
denyMutex.Unlock()
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy, opts.KeymapConvert)
}
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)

Expand Down
66 changes: 66 additions & 0 deletions src/keymap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package fzf

var Keymap = map[string]map[rune]rune{
"hebrew": {
'א': 't',
'ב': 'c',
'ג': 'd',
'ד': 's',
'ה': 'v',
'ו': 'u',
'ז': 'z',
'ח': 'j',
'ט': 'y',
'י': 'h',
'כ': 'f',
'ך': 'f',
'ל': 'k',
'מ': 'n',
'ם': 'n',
'נ': 'b',
'ס': 'x',
'ע': 'g',
'פ': 'p',
'ף': 'p',
'צ': 'm',
'ץ': 'm',
'ק': 'e',
'ר': 'r',
'ש': 'a',
'ת': 'w',
},
"russian": {
'й': 'q',
'ц': 'w',
'у': 'e',
'к': 'r',
'е': 't',
'н': 'y',
'г': 'u',
'ш': 'i',
'щ': 'o',
'з': 'p',
'х': '[',
'ъ': ']',
'ф': 'a',
'ы': 's',
'в': 'd',
'а': 'f',
'п': 'g',
'р': 'h',
'о': 'j',
'л': 'k',
'д': 'l',
'ж': ';',
'э': '\'',
'я': 'z',
'ч': 'x',
'с': 'c',
'м': 'v',
'и': 'b',
'т': 'n',
'ь': 'm',
'б': ',',
'ю': '.',
},
}
7 changes: 7 additions & 0 deletions src/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Usage: fzf [options]
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result
--literal Do not normalize latin script letters
--keymap-convert, +C Enable keyboard layout conversion (e.g. Hebrew to QWERTY)
--no-keymap-convert, -C Disable keyboard layout conversion (default)
--tail=NUM Maximum number of items to keep in memory
--disabled Do not perform search
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
Expand Down Expand Up @@ -646,6 +648,7 @@ type Options struct {
BlockProfile string
MutexProfile string
TtyDefault string
KeymapConvert bool
}

func filterNonEmpty(input []string) []string {
Expand Down Expand Up @@ -2587,6 +2590,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Multi = 0
case "--ansi":
opts.Ansi = true
case "--keymap-convert", "+C":
opts.KeymapConvert = true
case "--no-keymap-convert", "-C":
opts.KeymapConvert = false
case "--no-ansi":
opts.Ansi = false
case "--no-mouse":
Expand Down
92 changes: 83 additions & 9 deletions src/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Pattern struct {
forward bool
withPos bool
text []rune
altText []rune
termSets []termSet
sortable bool
cacheable bool
Expand All @@ -64,17 +65,27 @@ type Pattern struct {
procFun map[termType]algo.Algo
cache *ChunkCache
denylist map[int32]struct{}
keymapConvert bool
}

var _splitRegex *regexp.Regexp
var (
_splitRegex *regexp.Regexp
qwertyMapping map[rune]rune
)

func init() {
_splitRegex = regexp.MustCompile(" +")
qwertyMapping = make(map[rune]rune)
for _, kmap := range Keymap {
for k, v := range kmap {
qwertyMapping[k] = v
}
}
}

// BuildPattern builds Pattern object from the given arguments
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}, keymapConvert bool) *Pattern {

var asString string
if extended {
Expand Down Expand Up @@ -129,6 +140,29 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
}
}

var altText []rune
textRunes := []rune(asString)
if keymapConvert {
needsConversion := false
for _, r := range textRunes {
if _, ok := qwertyMapping[r]; ok {
needsConversion = true
break
}
}
if needsConversion {
altRunes := make([]rune, len(textRunes))
for i, r := range textRunes {
if qwerty, ok := qwertyMapping[r]; ok {
altRunes[i] = qwerty
} else {
altRunes[i] = r
}
}
altText = altRunes
}
}

ptr := &Pattern{
fuzzy: fuzzy,
fuzzyAlgo: fuzzyAlgo,
Expand All @@ -137,7 +171,8 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
normalize: normalize,
forward: forward,
withPos: withPos,
text: []rune(asString),
text: textRunes,
altText: altText,
termSets: termSets,
sortable: sortable,
cacheable: cacheable,
Expand All @@ -146,7 +181,8 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
delimiter: delimiter,
cache: cache,
denylist: denylist,
procFun: make(map[termType]algo.Algo)}
procFun: make(map[termType]algo.Algo),
keymapConvert: keymapConvert}

ptr.cacheKey = ptr.buildCacheKey()
ptr.procFun[termFuzzy] = fuzzyAlgo
Expand Down Expand Up @@ -344,7 +380,7 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if p.extended {
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab, p.keymapConvert); len(offsets) == len(p.termSets) {
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
}
Expand All @@ -366,13 +402,24 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
} else {
input = p.transformInput(item)
}
if p.fuzzy {
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)

matchFunc := func(pattern []rune) (Offset, int, *[]int) {
if p.fuzzy {
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, pattern, withPos, slab)
}
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, pattern, withPos, slab)
}

offset, bonus, pos := matchFunc(p.text)
if p.altText != nil {
if altOffset, altBonus, altPos := matchFunc(p.altText); altBonus > bonus {
return altOffset, altBonus, altPos
}
}
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
return offset, bonus, pos
}

func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab, keymapConvert bool) ([]Offset, int, *[]int) {
var input []Token
if len(p.nth) == 0 {
input = []Token{{text: &item.text, prefixLength: 0}}
Expand All @@ -392,6 +439,33 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
for _, term := range termSet {
pfun := p.procFun[term.typ]
off, score, pos := p.iter(pfun, input, term.caseSensitive, term.normalize, p.forward, term.text, withPos, slab)

altText := term.text
if keymapConvert {
needsConversion := false
for _, r := range altText {
if _, ok := qwertyMapping[r]; ok {
needsConversion = true
break
}
}
if needsConversion {
altRunes := make([]rune, len(altText))
for i, r := range altText {
if qwerty, ok := qwertyMapping[r]; ok {
altRunes[i] = qwerty
} else {
altRunes[i] = r
}
}
altText = altRunes
}
}

if altOff, altScore, altPos := p.iter(pfun, input, term.caseSensitive, term.normalize, p.forward, altText, withPos, slab); altScore > score {
off, score, pos = altOff, altScore, altPos
}

if sidx := off[0]; sidx >= 0 {
if term.inv {
continue
Expand Down
28 changes: 14 additions & 14 deletions src/pattern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ func TestParseTermsEmpty(t *testing.T) {
}

func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune, keymapConvert bool) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
withPos, cacheable, nth, delimiter, revision{}, runes, nil, keymapConvert)
}

func TestExact(t *testing.T) {
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true,
false, true, []Range{}, Delimiter{}, []rune("'abc"), false)
chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
Expand All @@ -86,7 +86,7 @@ func TestExact(t *testing.T) {
}

func TestEqual(t *testing.T) {
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"), false)

match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str))
Expand All @@ -107,12 +107,12 @@ func TestEqual(t *testing.T) {
}

func TestCaseSensitivity(t *testing.T) {
pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"), false)
pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"), false)
pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"), false)
pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"), false)
pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"), false)
pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"), false)

if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
Expand All @@ -125,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
}

func TestOrigTextAndTransformed(t *testing.T) {
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"), false)
tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}})

Expand Down Expand Up @@ -159,7 +159,7 @@ func TestOrigTextAndTransformed(t *testing.T) {

func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) {
pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr), false)
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
Expand All @@ -181,7 +181,7 @@ func TestCacheKey(t *testing.T) {

func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) {
pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str), false)
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
Expand Down