mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-23 00:38:10 +02:00
Merge branch 'master' of https://github.com/tellezhector/kitty
This commit is contained in:
@@ -192,7 +192,12 @@ 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 {
|
||||||
hint := encode_hint(m.Index, alphabet)
|
var hint string
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -311,7 +316,13 @@ 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 {
|
||||||
if eh := encode_hint(idx, alphabet); strings.HasPrefix(eh, current_input) {
|
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) {
|
||||||
matches = append(matches, m)
|
matches = append(matches, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,16 @@ The offset (from zero) at which to start hint numbering. Note that only numbers
|
|||||||
greater than or equal to zero are respected.
|
greater than or equal to zero are respected.
|
||||||
|
|
||||||
|
|
||||||
|
--prefix-free
|
||||||
|
type=bool-set
|
||||||
|
Generate hints such that no hint is a prefix of another. For example, with
|
||||||
|
alphabet "abc" and 4 matches, hints are "b", "c", "aa", "ab", instead of "a",
|
||||||
|
"b", "c", "aa" (where "a" prefixes "aa"). This works by applying a dynamic
|
||||||
|
offset large enough so that the hints are prefix-free. When combined with a
|
||||||
|
:option:`--hints-offset` greater than 0, the effective offset is whichever is
|
||||||
|
larger.
|
||||||
|
|
||||||
|
|
||||||
--alphabet
|
--alphabet
|
||||||
The list of characters to use for hints. The default is to use numbers and
|
The list of characters to use for hints. The default is to use numbers and
|
||||||
lowercase English alphabets. Specify your preference as a string of characters.
|
lowercase English alphabets. Specify your preference as a string of characters.
|
||||||
|
|||||||
@@ -706,13 +706,28 @@ process_answer:
|
|||||||
}
|
}
|
||||||
largest_index := ans[len(ans)-1].Index
|
largest_index := ans[len(ans)-1].Index
|
||||||
offset := max(0, opts.HintsOffset)
|
offset := max(0, opts.HintsOffset)
|
||||||
|
if opts.PrefixFree {
|
||||||
|
alphabetLength := len(opts.Alphabet)
|
||||||
|
if alphabetLength == 0 {
|
||||||
|
alphabetLength = len(DEFAULT_HINT_ALPHABET)
|
||||||
|
}
|
||||||
|
offset = max(offset, hints_to_skip(len(ans), alphabetLength))
|
||||||
|
}
|
||||||
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.Ascending {
|
if opts.PrefixFree {
|
||||||
m.Index += offset
|
if opts.Ascending {
|
||||||
|
m.Index = i + offset + 1
|
||||||
|
} else {
|
||||||
|
m.Index = (largest_index - m.Index) + offset + 1
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
m.Index = largest_index - m.Index + offset
|
if opts.Ascending {
|
||||||
|
m.Index += offset
|
||||||
|
} else {
|
||||||
|
m.Index = largest_index - m.Index + offset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
index_map[m.Index] = m
|
index_map[m.Index] = m
|
||||||
}
|
}
|
||||||
|
|||||||
62
kittens/hints/prefix_free_hints.go
Normal file
62
kittens/hints/prefix_free_hints.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
123
kittens/hints/prefix_free_hints_test.go
Normal file
123
kittens/hints/prefix_free_hints_test.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// License: GPLv3 Copyright: 2026, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package hints
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/kitty"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrefixFreeHints(t *testing.T) {
|
||||||
|
// Test hints_to_skip
|
||||||
|
testsToSkip := []struct {
|
||||||
|
n, alphabetSize, expected int
|
||||||
|
}{
|
||||||
|
{5, 3, 1},
|
||||||
|
{6, 3, 2},
|
||||||
|
{4, 3, 1},
|
||||||
|
{3, 3, 0},
|
||||||
|
{2, 3, 0},
|
||||||
|
{1, 3, 0},
|
||||||
|
{0, 3, 0},
|
||||||
|
}
|
||||||
|
for _, tc := range testsToSkip {
|
||||||
|
actual := hints_to_skip(tc.n, tc.alphabetSize)
|
||||||
|
if actual != tc.expected {
|
||||||
|
t.Errorf("hints_to_skip(%d, %d) = %d, expected %d", tc.n, tc.alphabetSize, actual, tc.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test prefix-free property verification
|
||||||
|
alphabets := []string{"abc", "0123456789"}
|
||||||
|
for _, alph := range alphabets {
|
||||||
|
l := len(alph)
|
||||||
|
for n := 1; n <= 10; n++ {
|
||||||
|
total_hints := int(math.Pow(float64(2), float64(n)))
|
||||||
|
skip := hints_to_skip(total_hints, l)
|
||||||
|
hints := make([]string, total_hints)
|
||||||
|
for i := 0; i < total_hints; i++ {
|
||||||
|
hints[i] = generate_prefix_free_hint(skip+1+i, alph)
|
||||||
|
}
|
||||||
|
// Verify that no hint is a prefix of another hint
|
||||||
|
for i := 0; i < total_hints; i++ {
|
||||||
|
for j := 0; j < total_hints; j++ {
|
||||||
|
if i == j {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(hints[j], hints[i]) {
|
||||||
|
t.Errorf("For alphabet %q and n=%d: %q is a prefix of %q (skip=%d)", alph, total_hints, hints[i], hints[j], skip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HintsOffset + PrefixFree Test case 1: HintsOffset is smaller than dynamic
|
||||||
|
// offset (dynamic offset wins)
|
||||||
|
//
|
||||||
|
// With 4 matches, and alphabet size 3:
|
||||||
|
// hints_to_skip(4, 3) = 1.
|
||||||
|
// HintsOffset = 0.
|
||||||
|
// Effective offset = max(0, 1) = 1.
|
||||||
|
// Ascending = true.
|
||||||
|
// Match 0: 0 + 1 + 1 = 2 -> "b"
|
||||||
|
// Match 1: 1 + 1 + 1 = 3 -> "c"
|
||||||
|
// Match 2: 2 + 1 + 1 = 4 -> "aa"
|
||||||
|
// Match 3: 3 + 1 + 1 = 5 -> "ab"
|
||||||
|
opts1 := &Options{
|
||||||
|
Type: "url",
|
||||||
|
UrlPrefixes: "default",
|
||||||
|
Regex: kitty.HintsDefaultRegex,
|
||||||
|
PrefixFree: true,
|
||||||
|
Alphabet: "abc",
|
||||||
|
HintsOffset: 0,
|
||||||
|
Ascending: false, // default
|
||||||
|
}
|
||||||
|
_, marks1, _, err := find_marks("http://a.com http://b.com http://c.com http://d.com", opts1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("find_marks failed: %v", err)
|
||||||
|
}
|
||||||
|
expectedCodes1 := []string{"ab", "aa", "c", "b"}
|
||||||
|
for i, m := range marks1 {
|
||||||
|
hint := generate_prefix_free_hint(m.Index, opts1.Alphabet)
|
||||||
|
if hint != expectedCodes1[i] {
|
||||||
|
t.Errorf("Case 1 - Match %d: got hint %q, expected %q", i, hint, expectedCodes1[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HintsOffset + PrefixFree Test case 2: HintsOffset is larger than dynamic
|
||||||
|
// offset (HintsOffset wins)
|
||||||
|
//
|
||||||
|
// With 4 matches, and alphabet size 3:
|
||||||
|
// hints_to_skip(4, 3) = 1.
|
||||||
|
// HintsOffset = 3.
|
||||||
|
// Effective offset = max(3, 1) = 3.
|
||||||
|
// Ascending = true.
|
||||||
|
// Match 0: 0 + 3 + 1 = 4 -> "aa"
|
||||||
|
// Match 1: 1 + 3 + 1 = 5 -> "ab"
|
||||||
|
// Match 2: 2 + 3 + 1 = 6 -> "ac"
|
||||||
|
// Match 3: 3 + 3 + 1 = 7 -> "ba"
|
||||||
|
opts2 := &Options{
|
||||||
|
Type: "url",
|
||||||
|
UrlPrefixes: "default",
|
||||||
|
Regex: kitty.HintsDefaultRegex,
|
||||||
|
PrefixFree: true,
|
||||||
|
Alphabet: "abc",
|
||||||
|
HintsOffset: 3,
|
||||||
|
Ascending: true,
|
||||||
|
}
|
||||||
|
_, marks2, _, err := find_marks("http://a.com http://b.com http://c.com http://d.com", opts2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("find_marks failed: %v", err)
|
||||||
|
}
|
||||||
|
expectedCodes2 := []string{"aa", "ab", "ac", "ba"}
|
||||||
|
for i, m := range marks2 {
|
||||||
|
hint := generate_prefix_free_hint(m.Index, opts2.Alphabet)
|
||||||
|
if hint != expectedCodes2[i] {
|
||||||
|
t.Errorf("Case 2 - Match %d: got hint %q, expected %q", i, hint, expectedCodes2[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user