mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-16 21:45:03 +02:00
Fixes bug where hints starting with the first alphabet's character are _almost never_ generated.
* Switches to "positional notation" to fix the bug. * Consolidates `encode_hint` and `generate_prefix_free_hint` into `encode_hint`. * Moves prefix_free_hints.go to hints_generator.go; `encode_hint` and `decode_hint` were moved here too. * The "prefix-free" logic was made 0-index based instead of 1-index based for two reasons: 1. To simplify 1-off discrepancies. 2. To keep backwards compatibility with the old usage of `encode_hint`. * Slight perfomance improvement added by reusing the runt-to-index map instead of rebuilding for each call to `decode_hint`. * Unit tests added; for all new logic and old affected logic. Fixes #10231
This commit is contained in:
90
kittens/hints/hints_generator.go
Normal file
90
kittens/hints/hints_generator.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// License: GPLv3 Copyright: 2026, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package hints
|
||||||
|
|
||||||
|
// hints_to_skip calculates how many hints to skip based on the given n and
|
||||||
|
// alphabet size so that the next n hints generated by expanding the prefix tree
|
||||||
|
// of an alphabet of size alphabetSize will be prefix-free.
|
||||||
|
//
|
||||||
|
// The result is 1-based, meaning that the first hint to be skipped is the first
|
||||||
|
// non-empty hint.
|
||||||
|
//
|
||||||
|
// For example, if alphabet was "abc" and n was 5, the first non-empty hints
|
||||||
|
// from that alphabet would be: "a", "b", "c", "aa", "ab", "ac", ...
|
||||||
|
// the result of hints_to_skip would be 1, and this would mean that if you skip
|
||||||
|
// "a" from the list above, the next 5 hints are prefix-free:
|
||||||
|
// "b", "c", "aa", "ab", "ac".
|
||||||
|
//
|
||||||
|
// See issue #10195 for an informal proof on why this function works.
|
||||||
|
func hints_to_skip(n int, alphabetSize int) int {
|
||||||
|
if n < 2 {
|
||||||
|
n = 2
|
||||||
|
}
|
||||||
|
return (n - 2) / (alphabetSize - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode_hint generates the hint corresponding to the given index in the
|
||||||
|
// breadth-first traversal of the prefix tree generated by the given alphabet.
|
||||||
|
//
|
||||||
|
// For example, given the alphabet "abc", the hints would be generated in
|
||||||
|
// the following order:
|
||||||
|
// 0. "a"
|
||||||
|
// 1. "b"
|
||||||
|
// 2. "c"
|
||||||
|
// 3. "aa"
|
||||||
|
// 4. "ab"
|
||||||
|
// 5. "ac"
|
||||||
|
// 6. "ba"
|
||||||
|
// 7. "bb"
|
||||||
|
// 8. "bc"
|
||||||
|
// 9. "ca"
|
||||||
|
// 10. "cb"
|
||||||
|
// 11. "cc"
|
||||||
|
// 12. "aaa"
|
||||||
|
// ...
|
||||||
|
//
|
||||||
|
// Indices are 0-based on non-empty hints; index=0 the hint of length 1 with
|
||||||
|
// the first character of the alphabet.
|
||||||
|
func encode_hint(num int, alphabet string) (res string) {
|
||||||
|
l := len(alphabet)
|
||||||
|
|
||||||
|
for num >= 0 {
|
||||||
|
mod := num % l
|
||||||
|
res = string(alphabet[mod]) + res
|
||||||
|
num /= l
|
||||||
|
num -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func rune_to_index_map(alphabet string) *map[rune]int {
|
||||||
|
runeToIndexMap := make(map[rune]int)
|
||||||
|
for index, char := range alphabet {
|
||||||
|
runeToIndexMap[char] = index
|
||||||
|
}
|
||||||
|
return &runeToIndexMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode_hint converts a hint to its corresponding index as it would appear in
|
||||||
|
// the breadth-first traversal of the prefix tree generated by the given
|
||||||
|
// alphabet.
|
||||||
|
//
|
||||||
|
// For example, given the alphabet "abc", the hints would be generated in
|
||||||
|
// the following order:
|
||||||
|
// 0. "a"
|
||||||
|
// 1. "b"
|
||||||
|
// 2. "c"
|
||||||
|
// 3. "aa"
|
||||||
|
// 4. "ab"
|
||||||
|
// ...
|
||||||
|
//
|
||||||
|
// hint is expected to be non-empty.
|
||||||
|
func decode_hint(hint string, char_to_index *map[rune]int) (res int) {
|
||||||
|
base := len(*char_to_index)
|
||||||
|
|
||||||
|
for _, c := range hint {
|
||||||
|
res = res*base + ((*char_to_index)[c] + 1)
|
||||||
|
}
|
||||||
|
return res - 1
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ func TestPrefixFreeHints(t *testing.T) {
|
|||||||
skip := hints_to_skip(total_hints, l)
|
skip := hints_to_skip(total_hints, l)
|
||||||
hints := make([]string, total_hints)
|
hints := make([]string, total_hints)
|
||||||
for i := range total_hints {
|
for i := range total_hints {
|
||||||
hints[i] = generate_prefix_free_hint(skip+1+i, alph)
|
hints[i] = encode_hint(skip+1+i, alph)
|
||||||
}
|
}
|
||||||
// Verify that no hint is a prefix of another hint
|
// Verify that no hint is a prefix of another hint
|
||||||
for i := range total_hints {
|
for i := range total_hints {
|
||||||
@@ -82,9 +82,9 @@ func TestPrefixFreeHints(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expectedCodes1 := []string{"ab", "aa", "c", "b"}
|
expectedCodes1 := []string{"ab", "aa", "c", "b"}
|
||||||
for i, m := range marks1 {
|
for i, m := range marks1 {
|
||||||
hint := generate_prefix_free_hint(m.Index, opts1.Alphabet)
|
hint := encode_hint(m.Index, opts1.Alphabet)
|
||||||
if hint != expectedCodes1[i] {
|
if hint != expectedCodes1[i] {
|
||||||
t.Errorf("Case 1 - Match %d: got hint %q, expected %q", i, hint, expectedCodes1[i])
|
t.Errorf("Case 1 - Match %d (Index %d): got hint %q, expected %q", i, m.Index, hint, expectedCodes1[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,9 +115,43 @@ func TestPrefixFreeHints(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expectedCodes2 := []string{"aa", "ab", "ac", "ba"}
|
expectedCodes2 := []string{"aa", "ab", "ac", "ba"}
|
||||||
for i, m := range marks2 {
|
for i, m := range marks2 {
|
||||||
hint := generate_prefix_free_hint(m.Index, opts2.Alphabet)
|
hint := encode_hint(m.Index, opts2.Alphabet)
|
||||||
if hint != expectedCodes2[i] {
|
if hint != expectedCodes2[i] {
|
||||||
t.Errorf("Case 2 - Match %d: got hint %q, expected %q", i, hint, expectedCodes2[i])
|
t.Errorf("Case 2 - Match %d (Index %d): got hint %q, expected %q", i, m.Index, hint, expectedCodes2[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeHint(t *testing.T) {
|
||||||
|
// enchode_hint sample tests
|
||||||
|
tests := []struct {
|
||||||
|
num int
|
||||||
|
alphabet string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{0, "abc", "a"},
|
||||||
|
{1, "abc", "b"},
|
||||||
|
{2, "abc", "c"},
|
||||||
|
{3, "abc", "aa"},
|
||||||
|
{11, "abc", "cc"},
|
||||||
|
{12, "abc", "aaa"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
actual := encode_hint(tc.num, tc.alphabet)
|
||||||
|
if actual != tc.expected {
|
||||||
|
t.Errorf("encode_hint(%d, %q) = %q, expected %q", tc.num, tc.alphabet, actual, tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode_hint should reverse encode_hint (round-trip test)
|
||||||
|
for _, alph := range []string{"abc", "0123456789", DEFAULT_HINT_ALPHABET} {
|
||||||
|
char_to_index := rune_to_index_map(alph)
|
||||||
|
for num := range 200 {
|
||||||
|
hint := encode_hint(num, alph)
|
||||||
|
decoded := decode_hint(hint, char_to_index)
|
||||||
|
if decoded != num {
|
||||||
|
t.Errorf("decode_hint(encode_hint(%d, %q)) = %d, expected %d", num, alph, decoded, num)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,28 +89,6 @@ type Result struct {
|
|||||||
Cwd string `json:"cwd"`
|
Cwd string `json:"cwd"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode_hint(num int, alphabet string) (res string) {
|
|
||||||
runes := []rune(alphabet)
|
|
||||||
d := len(runes)
|
|
||||||
for res == "" || num > 0 {
|
|
||||||
res = string(runes[num%d]) + res
|
|
||||||
num /= d
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func decode_hint(x string, alphabet string) (ans int) {
|
|
||||||
base := len(alphabet)
|
|
||||||
index_map := make(map[rune]int, len(alphabet))
|
|
||||||
for i, c := range alphabet {
|
|
||||||
index_map[c] = i
|
|
||||||
}
|
|
||||||
for _, char := range x {
|
|
||||||
ans = ans*base + index_map[char]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func as_rgb(c uint32) [3]float32 {
|
func as_rgb(c uint32) [3]float32 {
|
||||||
return [3]float32{float32((c>>16)&255) / 255.0, float32((c>>8)&255) / 255.0, float32(c&255) / 255.0}
|
return [3]float32{float32((c>>16)&255) / 255.0, float32((c>>8)&255) / 255.0, float32(c&255) / 255.0}
|
||||||
}
|
}
|
||||||
@@ -192,12 +170,7 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
|
|||||||
text_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", o.HintsTextColor, o.HintsTextBackgroundColor))
|
text_style := fctx.SprintFunc(fmt.Sprintf("fg=%s bg=%s bold", o.HintsTextColor, o.HintsTextBackgroundColor))
|
||||||
|
|
||||||
highlight_mark := func(m *Mark, mark_text string) string {
|
highlight_mark := func(m *Mark, mark_text string) string {
|
||||||
var hint string
|
hint := encode_hint(m.Index, alphabet)
|
||||||
if o.PrefixFree {
|
|
||||||
hint = generate_prefix_free_hint(m.Index, alphabet)
|
|
||||||
} else {
|
|
||||||
hint = encode_hint(m.Index, alphabet)
|
|
||||||
}
|
|
||||||
if current_input != "" && !strings.HasPrefix(hint, current_input) {
|
if current_input != "" && !strings.HasPrefix(hint, current_input) {
|
||||||
return faint(mark_text)
|
return faint(mark_text)
|
||||||
}
|
}
|
||||||
@@ -316,13 +289,7 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
|
|||||||
if changed {
|
if changed {
|
||||||
matches := []*Mark{}
|
matches := []*Mark{}
|
||||||
for idx, m := range index_map {
|
for idx, m := range index_map {
|
||||||
var eh string
|
if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) {
|
||||||
if o.PrefixFree {
|
|
||||||
eh = generate_prefix_free_hint(idx, alphabet)
|
|
||||||
} else {
|
|
||||||
eh = encode_hint(idx, alphabet)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(eh, current_input) {
|
|
||||||
matches = append(matches, m)
|
matches = append(matches, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,7 +322,8 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
|
|||||||
} else if ev.MatchesPressOrRepeat("enter") || ev.MatchesPressOrRepeat("space") {
|
} else if ev.MatchesPressOrRepeat("enter") || ev.MatchesPressOrRepeat("space") {
|
||||||
ev.Handled = true
|
ev.Handled = true
|
||||||
if current_input != "" {
|
if current_input != "" {
|
||||||
idx := decode_hint(current_input, alphabet)
|
char_to_index := rune_to_index_map(alphabet)
|
||||||
|
idx := decode_hint(current_input, char_to_index)
|
||||||
if m := index_map[idx]; m != nil {
|
if m := index_map[idx]; m != nil {
|
||||||
chosen = append(chosen, m)
|
chosen = append(chosen, m)
|
||||||
ignore_mark_indices.Add(idx)
|
ignore_mark_indices.Add(idx)
|
||||||
|
|||||||
@@ -716,18 +716,10 @@ process_answer:
|
|||||||
index_map = make(map[int]*Mark, len(ans))
|
index_map = make(map[int]*Mark, len(ans))
|
||||||
for i := range ans {
|
for i := range ans {
|
||||||
m := &ans[i]
|
m := &ans[i]
|
||||||
if opts.PrefixFree {
|
if opts.Ascending {
|
||||||
if opts.Ascending {
|
m.Index += offset
|
||||||
m.Index = i + offset + 1
|
|
||||||
} else {
|
|
||||||
m.Index = (largest_index - m.Index) + offset + 1
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if opts.Ascending {
|
m.Index = largest_index - m.Index + offset
|
||||||
m.Index += offset
|
|
||||||
} else {
|
|
||||||
m.Index = largest_index - m.Index + offset
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
index_map[m.Index] = m
|
index_map[m.Index] = m
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// License: GPLv3 Copyright: 2026, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
package hints
|
|
||||||
|
|
||||||
// hints_to_skip calculates how many hints to skip based on the given n and
|
|
||||||
// alphabet size so that the next n hints generated by expanding the prefix tree
|
|
||||||
// of an alphabet of size alphabetSize will be prefix-free.
|
|
||||||
//
|
|
||||||
// The result is 1-based, meaning that the first hint to be skipped is the first
|
|
||||||
// non-empty hint.
|
|
||||||
//
|
|
||||||
// For example, if alphabet was "abc" and n was 5, the first non-empty hints
|
|
||||||
// from that alphabet would be: "a", "b", "c", "aa", "ab", "ac", ...
|
|
||||||
// the result of hints_to_skip would be 1, and this would mean that if you skip
|
|
||||||
// "a" from the list above, the next 5 hints are prefix-free:
|
|
||||||
// "b", "c", "aa", "ab", "ac".
|
|
||||||
//
|
|
||||||
// See issue #10195 for an informal proof on why this function works.
|
|
||||||
func hints_to_skip(n int, alphabetSize int) int {
|
|
||||||
if n < 2 {
|
|
||||||
n = 2
|
|
||||||
}
|
|
||||||
return (n - 2) / (alphabetSize - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate_prefix_free_hint generates the hint corresponding to the given index
|
|
||||||
// in the breadth-first traversal of the prefix tree generated by the given
|
|
||||||
// alphabet.
|
|
||||||
//
|
|
||||||
// For example, given the alphabet "abc", the hints would be generated in
|
|
||||||
// the following order:
|
|
||||||
// 1. "a"
|
|
||||||
// 2. "b"
|
|
||||||
// 3. "c"
|
|
||||||
// 4. "aa"
|
|
||||||
// 5. "ab"
|
|
||||||
// 6. "ac"
|
|
||||||
// 7. "ba"
|
|
||||||
// 8. "bb"
|
|
||||||
// 9. "bc"
|
|
||||||
// 10. "ca"
|
|
||||||
// 11. "cb"
|
|
||||||
// 12. "cc"
|
|
||||||
// 13. "aaa"
|
|
||||||
// ...
|
|
||||||
//
|
|
||||||
// Indices are 1-based on non-empty hints; index=0 returns an empty (invalid)
|
|
||||||
// hint.
|
|
||||||
func generate_prefix_free_hint(index int, alphabet string) string {
|
|
||||||
l := len(alphabet)
|
|
||||||
|
|
||||||
hint := ""
|
|
||||||
index -= 1
|
|
||||||
for index >= 0 {
|
|
||||||
mod := index % l
|
|
||||||
char := string(alphabet[mod])
|
|
||||||
index /= l
|
|
||||||
hint = char + hint
|
|
||||||
index -= 1
|
|
||||||
}
|
|
||||||
return hint
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user