diff --git a/src/core.go b/src/core.go index da7da137395..03abc2857af 100644 --- a/src/core.go +++ b/src/core.go @@ -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) diff --git a/src/keymap.go b/src/keymap.go new file mode 100644 index 00000000000..96f32dd1c33 --- /dev/null +++ b/src/keymap.go @@ -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', + 'б': ',', + 'ю': '.', + }, +} diff --git a/src/options.go b/src/options.go index 5cb9f14ab7e..208a7bb94eb 100644 --- a/src/options.go +++ b/src/options.go @@ -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 @@ -646,6 +648,7 @@ type Options struct { BlockProfile string MutexProfile string TtyDefault string + KeymapConvert bool } func filterNonEmpty(input []string) []string { @@ -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": diff --git a/src/pattern.go b/src/pattern.go index 8e6966c34bd..d870ffc9dea 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -54,6 +54,7 @@ type Pattern struct { forward bool withPos bool text []rune + altText []rune termSets []termSet sortable bool cacheable bool @@ -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 { @@ -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, @@ -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, @@ -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 @@ -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 } @@ -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}} @@ -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 diff --git a/src/pattern_test.go b/src/pattern_test.go index 8e566263470..896c510af56 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -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) @@ -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)) @@ -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 || @@ -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}}) @@ -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()) } @@ -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()) }