@@ -13,6 +13,7 @@ import (
1313 "strings"
1414 "sync/atomic"
1515 "unicode"
16+ "unicode/utf8"
1617
1718 "codeberg.org/tslocum/cbind"
1819 "github.com/gdamore/tcell/v3"
@@ -50,9 +51,51 @@ func (substr searchWord) MatchString(target string) bool {
5051}
5152
5253// searchWord FindAll searches for strings and returns the index of the match.
54+ // The returned indexes are positions in target, not in its lowercased form,
55+ // because lowercasing can change the byte length of a rune (İ, K, ẞ...).
5356func (substr searchWord ) FindAll (target string ) [][]int {
54- target = strings .ToLower (target )
55- return allStringIndex (target , substr .word )
57+ lower , offsets := toLowerWithOffsets (target )
58+ indexes := allStringIndex (lower , substr .word )
59+ if offsets == nil {
60+ return indexes
61+ }
62+ for _ , idx := range indexes {
63+ idx [0 ] = offsets [idx [0 ]]
64+ idx [1 ] = offsets [idx [1 ]]
65+ }
66+ return indexes
67+ }
68+
69+ // toLowerWithOffsets lowercases s the same way [strings.ToLower] does and also
70+ // returns a table that maps every byte offset of the result back to a byte
71+ // offset of s. The table is nil when s is ASCII only, since the offsets are
72+ // then identical and no mapping is needed.
73+ func toLowerWithOffsets (s string ) (string , []int ) {
74+ if isASCII (s ) {
75+ return strings .ToLower (s ), nil
76+ }
77+
78+ lower := make ([]byte , 0 , len (s ))
79+ offsets := make ([]int , 0 , len (s )+ 1 )
80+ for i , r := range s {
81+ n := len (lower )
82+ lower = utf8 .AppendRune (lower , unicode .ToLower (r ))
83+ for range len (lower ) - n {
84+ offsets = append (offsets , i )
85+ }
86+ }
87+ offsets = append (offsets , len (s ))
88+ return string (lower ), offsets
89+ }
90+
91+ // isASCII returns true if s consists of ASCII bytes only.
92+ func isASCII (s string ) bool {
93+ for i := range len (s ) {
94+ if s [i ] >= utf8 .RuneSelf {
95+ return false
96+ }
97+ }
98+ return true
5699}
57100
58101// searchWord String returns the search word.
0 commit comments