From 501f28acdf450065f8a9a7c140f99bc632018ef8 Mon Sep 17 00:00:00 2001 From: Hector Tellez Date: Tue, 7 Jul 2026 02:07:51 -0700 Subject: [PATCH] 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 --- kittens/hints/hints_generator.go | 90 +++++++++++++++++++ ..._hints_test.go => hints_generator_test.go} | 44 +++++++-- kittens/hints/main.go | 40 +-------- kittens/hints/marks.go | 14 +-- kittens/hints/prefix_free_hints.go | 62 ------------- 5 files changed, 136 insertions(+), 114 deletions(-) create mode 100644 kittens/hints/hints_generator.go rename kittens/hints/{prefix_free_hints_test.go => hints_generator_test.go} (71%) delete mode 100644 kittens/hints/prefix_free_hints.go diff --git a/kittens/hints/hints_generator.go b/kittens/hints/hints_generator.go new file mode 100644 index 000000000..d50f5d25b --- /dev/null +++ b/kittens/hints/hints_generator.go @@ -0,0 +1,90 @@ +// License: GPLv3 Copyright: 2026, Kovid Goyal, + +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 +} diff --git a/kittens/hints/prefix_free_hints_test.go b/kittens/hints/hints_generator_test.go similarity index 71% rename from kittens/hints/prefix_free_hints_test.go rename to kittens/hints/hints_generator_test.go index 76596b33a..085a65549 100644 --- a/kittens/hints/prefix_free_hints_test.go +++ b/kittens/hints/hints_generator_test.go @@ -39,7 +39,7 @@ func TestPrefixFreeHints(t *testing.T) { skip := hints_to_skip(total_hints, l) hints := make([]string, 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 for i := range total_hints { @@ -82,9 +82,9 @@ func TestPrefixFreeHints(t *testing.T) { } expectedCodes1 := []string{"ab", "aa", "c", "b"} 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] { - 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"} 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] { - 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) + } } } } diff --git a/kittens/hints/main.go b/kittens/hints/main.go index 116133b11..57bd953c8 100644 --- a/kittens/hints/main.go +++ b/kittens/hints/main.go @@ -89,28 +89,6 @@ type Result struct { 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 { 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)) highlight_mark := func(m *Mark, mark_text string) string { - var hint string - if o.PrefixFree { - hint = generate_prefix_free_hint(m.Index, alphabet) - } else { - hint = encode_hint(m.Index, alphabet) - } + hint := encode_hint(m.Index, alphabet) if current_input != "" && !strings.HasPrefix(hint, current_input) { return faint(mark_text) } @@ -316,13 +289,7 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) { if changed { matches := []*Mark{} for idx, m := range index_map { - var eh string - if o.PrefixFree { - eh = generate_prefix_free_hint(idx, alphabet) - } else { - eh = encode_hint(idx, alphabet) - } - if strings.HasPrefix(eh, current_input) { + if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) { 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") { ev.Handled = true 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 { chosen = append(chosen, m) ignore_mark_indices.Add(idx) diff --git a/kittens/hints/marks.go b/kittens/hints/marks.go index 55c9b2d19..94be7d6ad 100644 --- a/kittens/hints/marks.go +++ b/kittens/hints/marks.go @@ -716,18 +716,10 @@ process_answer: index_map = make(map[int]*Mark, len(ans)) for i := range ans { m := &ans[i] - if opts.PrefixFree { - if opts.Ascending { - m.Index = i + offset + 1 - } else { - m.Index = (largest_index - m.Index) + offset + 1 - } + if opts.Ascending { + m.Index += offset } else { - if opts.Ascending { - m.Index += offset - } else { - m.Index = largest_index - m.Index + offset - } + m.Index = largest_index - m.Index + offset } index_map[m.Index] = m } diff --git a/kittens/hints/prefix_free_hints.go b/kittens/hints/prefix_free_hints.go deleted file mode 100644 index e308b47aa..000000000 --- a/kittens/hints/prefix_free_hints.go +++ /dev/null @@ -1,62 +0,0 @@ -// License: GPLv3 Copyright: 2026, Kovid Goyal, - -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 -}