Start work on porting unicode input kitten to Go

This commit is contained in:
Kovid Goyal
2023-02-09 19:07:55 +05:30
parent a2e4efbb14
commit 53e33a80ba
20 changed files with 39037 additions and 76658 deletions

View File

@@ -10,6 +10,7 @@ import (
"kitty/tools/cmd/clipboard"
"kitty/tools/cmd/edit_in_kitty"
"kitty/tools/cmd/icat"
"kitty/tools/cmd/unicode_input"
"kitty/tools/cmd/update_self"
"kitty/tools/tui"
)
@@ -29,6 +30,8 @@ func KittyToolEntryPoints(root *cli.Command) {
clipboard.EntryPoint(root)
// icat
icat.EntryPoint(root)
// unicode_input
unicode_input.EntryPoint(root)
// __hold_till_enter__
root.AddSubCommand(&cli.Command{
Name: "__hold_till_enter__",

View File

@@ -0,0 +1,470 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_input
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
"kitty/tools/cli"
"kitty/tools/tui"
"kitty/tools/tui/loop"
"kitty/tools/tui/readline"
"kitty/tools/unicode_names"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
const INDEX_CHAR string = "."
const INDEX_BASE = 36
const InvalidChar rune = unicode.MaxRune + 1
const default_set_of_symbols string = `
«» 😀😛😇😈😉😍😎😮👍👎 §©® ·°±×÷¼½½¾
µ¢£¿¡¨´¸ˆ˜ ÀÁÂÃÄÅÆÇÈÉÊË ÌÍÎÏÐÑÒÓÔÕÖØ ŒŠÙÚÛÜÝŸÞßàá âãäåæçèéêëìí
îïðñòóôõöøœš ùúûüýÿþªºαΩ
`
var DEFAULT_SET []rune
var EMOTICONS_SET []rune
const DEFAULT_MODE string = "HEX"
func build_sets() {
DEFAULT_SET = make([]rune, 0, len(default_set_of_symbols))
for _, ch := range default_set_of_symbols {
if !unicode.IsSpace(ch) {
DEFAULT_SET = append(DEFAULT_SET, ch)
}
}
EMOTICONS_SET = make([]rune, 0, 0x1f64f-0x1f600+1)
for i := 0x1f600; i <= 0x1f64f; i++ {
DEFAULT_SET = append(DEFAULT_SET, rune(i))
}
}
func codepoint_ok(code rune) bool {
return !(code <= 32 || code == 127 || (128 <= code && code <= 159) || (0xd800 <= code && code <= 0xdbff) || (0xDC00 <= code && code <= 0xDFFF) || code > unicode.MaxRune)
}
func parse_favorites(raw string) (ans []rune) {
ans = make([]rune, 0, 128)
for _, line := range utils.Splitlines(raw) {
line = strings.TrimSpace(line)
if len(line) == 0 || strings.HasPrefix(line, "#") {
continue
}
idx := strings.Index(line, "#")
if idx > -1 {
line = line[:idx]
}
code_text, _, _ := strings.Cut(line, " ")
code, err := strconv.ParseUint(code_text, 16, 32)
if err == nil && codepoint_ok(rune(code)) {
ans = append(ans, rune(code))
}
}
return
}
func serialize_favorites(favs []rune) string {
b := strings.Builder{}
b.Grow(8192)
b.WriteString(`# Favorite characters for unicode input
# Enter the hex code for each favorite character on a new line. Blank lines are
# ignored and anything after a # is considered a comment.
`)
for _, ch := range favs {
b.WriteString(fmt.Sprintf("%x # %s %s", ch, string(ch), unicode_names.NameForCodePoint(ch)))
}
return b.String()
}
var loaded_favorites []rune
func favorites_path() string {
return filepath.Join(utils.ConfigDir(), "unicode-input-favorites.conf")
}
func load_favorites(refresh bool) []rune {
if refresh || loaded_favorites == nil {
raw, err := os.ReadFile(favorites_path())
if err == nil {
loaded_favorites = parse_favorites(utils.UnsafeBytesToString(raw))
} else {
loaded_favorites = parse_favorites("")
}
}
return loaded_favorites
}
type CachedData struct {
Recent []rune `json:"recent,omitempty"`
Mode string `json:"mode,omitempty"`
}
var cached_data *CachedData
type Mode int
const (
HEX Mode = iota
NAME
EMOTICONS
FAVORITES
)
type ModeData struct {
mode Mode
key string
title string
}
var all_modes [4]ModeData
type checkpoints_key struct {
mode Mode
text string
codepoints []rune
}
func (self *checkpoints_key) clear() {
*self = checkpoints_key{}
}
func (self *checkpoints_key) is_equal(other checkpoints_key) bool {
return self.mode == other.mode && self.text == other.text && slices.Equal(self.codepoints, other.codepoints)
}
type handler struct {
mode Mode
recent []rune
current_char rune
err error
lp *loop.Loop
ctx style.Context
rl *readline.Readline
choice_line string
emoji_variation string
checkpoints_key checkpoints_key
table table
current_tab_formatter, tab_bar_formatter, chosen_formatter, chosen_name_formatter, dim_formatter func(...any) string
}
func (self *handler) initialize() {
self.lp.AllowLineWrapping(false)
self.table.initialize(self.emoji_variation, self.ctx)
self.lp.SetWindowTitle("Unicode input")
self.ctx.AllowEscapeCodes = true
self.current_char = InvalidChar
self.current_tab_formatter = self.ctx.SprintFunc("reverse=false bold=true")
self.tab_bar_formatter = self.ctx.SprintFunc("reverse=true")
self.chosen_formatter = self.ctx.SprintFunc("fg=green")
self.chosen_name_formatter = self.ctx.SprintFunc("italic=true dim=true")
self.dim_formatter = self.ctx.SprintFunc("dim=true")
self.rl = readline.New(self.lp, readline.RlInit{Prompt: "> "})
self.rl.Start()
self.draw_screen()
}
func (self *handler) finalize() string {
self.rl.End()
self.rl.Shutdown()
return ""
}
func (self *handler) resolved_char() string {
if self.current_char == InvalidChar {
return ""
}
return resolved_char(self.current_char, self.emoji_variation)
}
func is_index(word string) bool {
if !strings.HasSuffix(word, INDEX_CHAR) {
return false
}
word = strings.TrimLeft(word, INDEX_CHAR)
_, err := strconv.ParseUint(word, 36, 32)
return err == nil
}
func (self *handler) update_codepoints() {
var codepoints []rune
var index_word int
var q checkpoints_key
q.mode = self.mode
switch self.mode {
case HEX:
codepoints = self.recent
case EMOTICONS:
codepoints = EMOTICONS_SET
case FAVORITES:
codepoints = load_favorites(false)
q.codepoints = codepoints
case NAME:
q.text = self.rl.AllText()
if !q.is_equal(self.checkpoints_key) {
words := strings.Split(q.text, " ")
words = utils.RemoveAll(words, INDEX_CHAR)
words = utils.Filter(words, is_index)
if len(words) > 0 {
iw := strings.TrimLeft(words[0], INDEX_CHAR)
words = words[1:]
n, err := strconv.ParseUint(iw, INDEX_BASE, 32)
if err == nil {
index_word = int(n)
}
}
codepoints = unicode_names.CodePointsForQuery(strings.Join(words, " "))
}
}
if !q.is_equal(self.checkpoints_key) {
self.checkpoints_key = q
self.table.set_codepoints(codepoints, self.mode, index_word)
}
}
func (self *handler) update_current_char() {
self.update_codepoints()
self.current_char = InvalidChar
text := self.rl.AllText()
switch self.mode {
case HEX:
if strings.HasPrefix(text, INDEX_CHAR) {
if len(text) > 1 {
self.current_char = self.table.codepoint_at_hint(text[1:])
}
} else if len(text) > 0 {
code, err := strconv.ParseUint(text, 16, 32)
if err == nil && code <= unicode.MaxRune {
self.current_char = rune(code)
}
}
case NAME:
cc := self.table.current_codepoint()
if cc > 0 && cc <= unicode.MaxRune {
self.current_char = rune(cc)
}
default:
if len(text) > 0 {
self.current_char = self.table.codepoint_at_hint(strings.TrimLeft(text, INDEX_CHAR))
}
}
if !codepoint_ok(self.current_char) {
self.current_char = InvalidChar
}
}
func (self *handler) update_prompt() {
self.update_current_char()
ch := "??"
color := "red"
self.choice_line = ""
if self.current_char != InvalidChar {
ch, color = self.resolved_char(), "green"
self.choice_line = fmt.Sprintf(
"Chosen: %s U+%x %s", self.chosen_formatter(ch), self.current_char,
self.chosen_name_formatter(unicode_names.NameForCodePoint(self.current_char)))
}
prompt := fmt.Sprintf("%s> ", self.ctx.SprintFunc("fg="+color)(ch))
self.rl.SetPrompt(prompt)
}
func (self *handler) draw_title_bar() {
entries := make([]string, 0, len(all_modes))
for _, md := range all_modes {
entry := fmt.Sprintf(" %s (%s) ", md.title, md.key)
if md.mode == self.mode {
entry = self.current_tab_formatter(entry)
}
entries = append(entries, entry)
}
sz, _ := self.lp.ScreenSize()
text := fmt.Sprintf("Search by:%s", strings.Join(entries, ""))
extra := int(sz.WidthCells) - wcswidth.Stringwidth(text)
if extra > 0 {
text += strings.Repeat(" ", extra)
}
self.lp.Println(self.tab_bar_formatter(text))
}
func (self *handler) draw_screen() {
self.lp.StartAtomicUpdate()
defer self.lp.EndAtomicUpdate()
self.lp.ClearScreen()
self.draw_title_bar()
y := 1
writeln := func(text ...any) {
self.lp.Println(text...)
y += 1
}
switch self.mode {
case NAME:
writeln("Enter words from the name of the character")
case HEX:
writeln("Enter the hex code for the character")
default:
writeln("Enter the index for the character you want from the list below")
}
self.rl.RedrawNonAtomic()
self.lp.SaveCursorPosition()
defer self.lp.RestoreCursorPosition()
writeln()
writeln(self.choice_line)
switch self.mode {
case HEX:
writeln(self.dim_formatter(fmt.Sprintf("Type %s followed by the index for the recent entries below", INDEX_CHAR)))
case NAME:
writeln(self.dim_formatter(fmt.Sprintf("Use Tab or arrow keys to choose a character. Type space and %s to select by index", INDEX_CHAR)))
case FAVORITES:
writeln(self.dim_formatter("Press F12 to edit the list of favorites"))
}
sz, _ := self.lp.ScreenSize()
q := self.table.layout(int(sz.HeightCells)-y, int(sz.WidthCells))
if q != "" {
self.lp.QueueWriteString(q)
}
}
func (self *handler) on_text(text string, from_key_event, in_bracketed_paste bool) error {
err := self.rl.OnText(text, from_key_event, in_bracketed_paste)
if err != nil {
return err
}
self.refresh()
return nil
}
func (self *handler) on_key_event(event *loop.KeyEvent) (err error) {
// TODO: Implement rest of this
err = self.rl.OnKeyEvent(event)
if err != nil {
if err == readline.ErrAcceptInput {
self.refresh()
self.lp.Quit(0)
return nil
}
return err
}
if event.Handled {
self.refresh()
}
return
}
func (self *handler) refresh() {
self.update_prompt()
self.draw_screen()
}
func run_loop(opts *Options) (lp *loop.Loop, err error) {
output := tui.KittenOutputSerializer()
lp, err = loop.New()
if err != nil {
return
}
cv := utils.NewCachedValues("unicode-input", &CachedData{Recent: DEFAULT_SET, Mode: DEFAULT_MODE})
cached_data = cv.Load()
defer cv.Save()
h := handler{recent: cached_data.Recent, lp: lp, emoji_variation: opts.EmojiVariation}
switch cached_data.Mode {
case "HEX":
h.mode = HEX
case "NAME":
h.mode = NAME
case "EMOTICONS":
h.mode = EMOTICONS
case "FAVORITES":
h.mode = FAVORITES
}
all_modes[0] = ModeData{mode: HEX, title: "Code", key: "F1"}
all_modes[1] = ModeData{mode: NAME, title: "Name", key: "F2"}
all_modes[2] = ModeData{mode: EMOTICONS, title: "Emoticons", key: "F3"}
all_modes[3] = ModeData{mode: FAVORITES, title: "Favorites", key: "F4"}
lp.OnInitialize = func() (string, error) {
h.initialize()
return "", nil
}
lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
h.refresh()
return nil
}
lp.OnResumeFromStop = func() error {
h.refresh()
return nil
}
lp.OnText = h.on_text
lp.OnFinalize = h.finalize
lp.OnKeyEvent = h.on_key_event
err = lp.Run()
if err != nil {
return
}
if h.err == nil {
switch h.mode {
case HEX:
cached_data.Mode = "HEX"
case NAME:
cached_data.Mode = "NAME"
case EMOTICONS:
cached_data.Mode = "EMOTICONS"
case FAVORITES:
cached_data.Mode = "FAVORITES"
}
if h.current_char != InvalidChar {
cached_data.Recent = h.recent
idx := slices.Index(cached_data.Recent, h.current_char)
if idx > -1 {
cached_data.Recent = slices.Delete(cached_data.Recent, idx, idx+1)
}
cached_data.Recent = slices.Insert(cached_data.Recent, 0, h.current_char)[:len(DEFAULT_SET)]
ans := h.resolved_char()
o, err := output(ans)
if err != nil {
return lp, err
}
fmt.Println(o)
}
}
err = h.err
return
}
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
go unicode_names.Initialize() // start parsing name data in the background
build_sets()
lp, err := run_loop(o)
if err != nil {
return 1, err
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return 1, nil
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

View File

@@ -0,0 +1,233 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_input
import (
"fmt"
"strconv"
"strings"
"kitty/tools/unicode_names"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
func resolved_char(ch rune, emoji_variation string) string {
ans := string(ch)
if wcswidth.IsEmojiPresentationBase(ch) {
switch emoji_variation {
case "text":
ans += "\ufe0e"
case "graphic":
ans += "\ufe0f"
}
}
return ans
}
func decode_hint(text string) int {
x, err := strconv.ParseUint(text, INDEX_BASE, 32)
if err != nil {
return -1
}
return int(x)
}
func encode_hint(num int) string {
return strconv.FormatUint(uint64(num), INDEX_BASE)
}
func ljust(s string, sz int) string {
x := wcswidth.Stringwidth(s)
if x < sz {
s += strings.Repeat(" ", sz-x)
}
return s
}
type table struct {
emoji_variation string
layout_dirty bool
last_rows, last_cols int
codepoints []rune
current_idx, scroll_rows int
text string
num_cols, num_rows int
mode Mode
green, reversed, not_reversed, intense_gray func(...any) string
}
func (self *table) initialize(emoji_variation string, ctx style.Context) {
self.emoji_variation = emoji_variation
self.layout_dirty = true
self.last_cols, self.last_rows = -1, -1
self.green = ctx.SprintFunc("fg=green")
self.reversed = ctx.SprintFunc("reverse=true")
self.not_reversed = ctx.SprintFunc("reverse=false")
self.intense_gray = ctx.SprintFunc("fg=intense-gray")
}
func (self *table) current_codepoint() rune {
if len(self.codepoints) > 0 {
return self.codepoints[self.current_idx]
}
return InvalidChar
}
func (self *table) set_codepoints(codepoints []rune, mode Mode, current_idx int) {
self.codepoints = codepoints
self.mode = mode
self.layout_dirty = true
if self.current_idx >= len(self.codepoints) {
self.current_idx = 0
}
self.scroll_rows = 0
}
func (self *table) codepoint_at_hint(hint string) rune {
idx := decode_hint(hint)
if idx >= 0 && idx < len(self.codepoints) {
return self.codepoints[idx]
}
return InvalidChar
}
type cell_data struct {
idx, ch, desc string
}
func (self *table) layout(rows, cols int) string {
if !self.layout_dirty && self.last_cols == cols && self.last_rows == rows {
return self.text
}
self.last_cols, self.last_rows = cols, rows
self.layout_dirty = false
var as_parts func(int, rune) cell_data
var cell func(int, cell_data)
var idx_size, space_for_desc int
output := strings.Builder{}
output.Grow(4096)
switch self.mode {
case NAME:
as_parts = func(i int, codepoint rune) cell_data {
return cell_data{idx: ljust(encode_hint(i), idx_size), ch: resolved_char(codepoint, self.emoji_variation), desc: unicode_names.NameForCodePoint(codepoint)}
}
cell = func(i int, cd cell_data) {
is_current := i == self.current_idx
text := self.green(cd.idx) + " \x1b[49m" + cd.ch + " "
w := wcswidth.Stringwidth(cd.ch)
if w < 2 {
text += strings.Repeat(" ", (2 - w))
}
if len(cd.desc) > space_for_desc {
text += cd.desc[:space_for_desc-1] + "…"
} else {
text += cd.desc
extra := space_for_desc - len(cd.desc)
if extra > 0 {
text += strings.Repeat(" ", extra)
}
}
if is_current {
text = self.reversed(text)
} else {
text = self.not_reversed(text)
}
output.WriteString(text)
}
default:
as_parts = func(i int, codepoint rune) cell_data {
return cell_data{idx: ljust(encode_hint(i), idx_size), ch: resolved_char(codepoint, self.emoji_variation)}
}
cell = func(i int, cd cell_data) {
output.WriteString(self.green(cd.idx))
output.WriteString(" ")
output.WriteString(self.intense_gray(cd.ch))
w := wcswidth.Stringwidth(cd.ch)
if w < 2 {
output.WriteString(strings.Repeat(" ", (2 - w)))
}
}
}
num := len(self.codepoints)
if num < 1 {
self.text = ""
self.num_cols = 0
self.num_rows = 0
return self.text
}
idx_size = len(encode_hint(num - 1))
parts := make([]cell_data, len(self.codepoints))
for i, ch := range self.codepoints {
parts[i] = as_parts(i, ch)
}
longest := 0
switch self.mode {
case NAME:
for _, p := range parts {
longest = utils.Max(longest, idx_size+2+len(p.desc)+2)
}
default:
longest = idx_size + 3
}
col_width := longest + 2
col_width = utils.Min(col_width, 40)
space_for_desc = col_width - 2 - idx_size - 4
self.num_cols = utils.Max(cols/col_width, 1)
self.num_rows = rows
rows_left := rows
skip_scroll := self.scroll_rows * self.num_cols
for i, cd := range parts {
if skip_scroll > 0 {
skip_scroll -= 1
continue
}
cell(i, cd)
output.WriteString(" ")
if i > 0 && (i+1)%self.num_cols == 0 {
rows_left -= 1
if rows_left == 0 {
break
}
output.WriteString("\r\n")
}
}
self.text = output.String()
return self.text
}
func (self *table) move_current(rows, cols int) {
if len(self.codepoints) == 0 {
return
}
if cols != 0 {
self.current_idx = (self.current_idx + len(self.codepoints) + cols) % len(self.codepoints)
self.layout_dirty = true
}
if rows != 0 {
amt := rows * self.num_cols
self.current_idx += amt
self.current_idx = utils.Max(0, utils.Min(self.current_idx, len(self.codepoints)-1))
self.layout_dirty = true
}
first_visible := self.scroll_rows * self.num_cols
last_visible := first_visible + ((self.num_cols * self.num_rows) - 1)
scroll_amount := self.num_rows
if self.current_idx < first_visible {
self.scroll_rows = utils.Max(self.scroll_rows-scroll_amount, 0)
}
if self.current_idx > last_visible {
self.scroll_rows += scroll_amount
}
}

View File

@@ -195,11 +195,11 @@ func (self *Loop) Println(args ...any) {
self.QueueWriteString("\r\n")
}
func (self *Loop) SaveCursor() {
func (self *Loop) SaveCursorPosition() {
self.QueueWriteString("\x1b7")
}
func (self *Loop) RestoreCursor() {
func (self *Loop) RestoreCursorPosition() {
self.QueueWriteString("\x1b8")
}
@@ -341,6 +341,12 @@ func (self *Loop) AllowLineWrapping(allow bool) {
}
}
func (self *Loop) SetWindowTitle(title string) {
title = strings.ReplaceAll(title, "\033", "")
title = strings.ReplaceAll(title, "\x9c", "")
self.QueueWriteString("\033]2;" + title + "\033\\")
}
func (self *Loop) ClearScreen() {
self.QueueWriteString("\x1b[H\x1b[2J")
}

View File

@@ -168,6 +168,10 @@ func New(loop *loop.Loop, r RlInit) *Readline {
return ans
}
func (self *Readline) SetPrompt(prompt string) {
self.prompt = self.make_prompt(prompt, false)
}
func (self *Readline) Shutdown() {
self.history.Shutdown()
}

37
tools/tui/ui_kitten.go Normal file
View File

@@ -0,0 +1,37 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package tui
import (
"encoding/json"
"fmt"
"os"
"kitty/tools/utils"
)
var _ = fmt.Print
func KittenOutputSerializer() func(any) (string, error) {
write_with_escape_code := os.Getenv("KITTEN_RUNNING_AS_UI") != ""
os.Unsetenv("KITTEN_RUNNING_AS_UI")
if write_with_escape_code {
return func(what any) (string, error) {
data, err := json.Marshal(what)
if err != nil {
return "", err
}
return "\x1bP@kitty-kitten-result|" + utils.UnsafeBytesToString(data) + "\x1b\\", nil
}
}
return func(what any) (string, error) {
if sval, ok := what.(string); ok {
return sval, nil
}
data, err := json.MarshalIndent(what, "", " ")
if err != nil {
return "", err
}
return utils.UnsafeBytesToString(data), nil
}
}

37998
tools/unicode_names/names.txt generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_names
import (
"bytes"
"compress/zlib"
_ "embed"
"encoding/binary"
"fmt"
"io"
"strings"
"sync"
"time"
"kitty/tools/utils"
"kitty/tools/utils/images"
)
type mark_set = *utils.Set[uint16]
//go:embed data_generated.bin
var unicode_name_data string
var _ = fmt.Print
var names map[rune]string
var marks []rune
var word_map map[string][]uint16
func add_word(codepoint uint16, word []byte) {
w := utils.UnsafeBytesToString(word)
word_map[w] = append(word_map[w], codepoint)
}
func add_words(codepoint uint16, raw []byte) {
for len(raw) > 0 {
idx := bytes.IndexByte(raw, ' ')
if idx < 0 {
add_word(codepoint, raw)
break
}
if idx > 0 {
add_word(codepoint, raw[:idx])
}
raw = raw[idx+1:]
}
}
func parse_record(record []byte, mark uint16) {
codepoint := rune(binary.LittleEndian.Uint32(record))
record = record[4:]
marks[mark] = codepoint
namelen := binary.LittleEndian.Uint16(record)
record = record[2:]
name := utils.UnsafeBytesToString(record[:namelen])
names[codepoint] = name
add_words(mark, record[:namelen])
if len(record) > int(namelen) {
add_words(mark, record[namelen:])
}
}
var parse_once sync.Once
func read_all(r io.Reader, expected_size int) ([]byte, error) {
b := make([]byte, 0, expected_size)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return b, err
}
}
}
func parse_data() {
compressed := utils.UnsafeStringToBytes(unicode_name_data)
uncompressed_size := binary.LittleEndian.Uint32(compressed)
r, _ := zlib.NewReader(bytes.NewReader(compressed[4:]))
defer r.Close()
raw, err := read_all(r, int(uncompressed_size))
if err != nil {
panic(err)
}
num_of_lines := binary.LittleEndian.Uint32(raw)
raw = raw[4:]
num_of_words := binary.LittleEndian.Uint32(raw)
raw = raw[4:]
names = make(map[rune]string, num_of_lines)
word_map = make(map[string][]uint16, num_of_words)
marks = make([]rune, num_of_lines)
var mark uint16
for len(raw) > 0 {
record_len := binary.LittleEndian.Uint16(raw)
raw = raw[2:]
parse_record(raw[:record_len], mark)
mark += 1
raw = raw[record_len:]
}
}
func Initialize() {
parse_once.Do(parse_data)
}
func NameForCodePoint(cp rune) string {
Initialize()
return names[cp]
}
func find_matching_codepoints(prefix string) (ans mark_set) {
for q, marks := range word_map {
if strings.HasPrefix(q, prefix) {
if ans == nil {
ans = utils.NewSet[uint16](len(marks) * 2)
}
ans.AddItems(marks...)
}
}
return ans
}
func marks_for_query(query string) (ans mark_set) {
Initialize()
prefixes := strings.Split(strings.ToLower(query), " ")
results := make(chan mark_set, len(prefixes))
ctx := images.Context{}
ctx.Parallel(0, len(prefixes), func(nums <-chan int) {
for i := range nums {
results <- find_matching_codepoints(prefixes[i])
}
})
close(results)
for x := range results {
if ans == nil {
ans = x
} else {
ans = ans.Intersect(x)
}
}
if ans == nil {
ans = utils.NewSet[uint16](0)
}
return
}
func CodePointsForQuery(query string) (ans []rune) {
x := marks_for_query(query)
ans = make([]rune, x.Len())
i := 0
for m := range x.Iterable() {
ans[i] = marks[m]
i += 1
}
return
}
func Develop() {
start := time.Now()
Initialize()
fmt.Println("Parsing unicode name data took:", time.Since(start))
start = time.Now()
num := CodePointsForQuery("arr")
fmt.Println("Querying arr took:", time.Since(start), "and found:", len(num))
start = time.Now()
num = CodePointsForQuery("arr right")
fmt.Println("Querying arr right took:", time.Since(start), "and found:", len(num))
}

View File

@@ -0,0 +1,35 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package unicode_names
import (
"fmt"
"kitty/tools/utils"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
)
var _ = fmt.Print
func TestUnicodeInputQueries(t *testing.T) {
ts := func(query string, expected ...rune) {
if expected == nil {
expected = make([]rune, 0)
}
expected = utils.Sort(expected, func(a, b rune) bool { return a < b })
actual := CodePointsForQuery(query)
actual = utils.Sort(actual, func(a, b rune) bool { return a < b })
diff := cmp.Diff(expected, actual)
if diff != "" {
t.Fatalf("Failed query: %#v\n%s", query, diff)
}
}
ts("horiz ell", 0x2026, 0x22ef, 0x2b2c, 0x2b2d, 0xfe19)
ts("horizontal ell", 0x2026, 0x22ef, 0x2b2c, 0x2b2d, 0xfe19)
ts("kfjhgkjdsfhgkjds")
if slices.Index(CodePointsForQuery("bee"), 0x1f41d) < 0 {
t.Fatalf("The query bee did not match the codepoint: 0x1f41d")
}
}