cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / ui / nbview / highlight.go
highlight.go 4.7K
1package nbview23import (4 "context"5 "slices"67 tea "charm.land/bubbletea/v2"89 "src.rest/cat/012/internal/notebook"10)1112// Highlighting: what the highlighter said of each source, kept as a13// role per rune. A slow highlighter (nu) is asked in the background:14// about the cells drawn, as they're drawn, and about the cell being15// edited once typing pauses, never per key. Until it answers, the code16// shows its draft (the tokenizer's roles), and the cell being edited17// keeps the answer about its last text where the text is unchanged, so18// nothing flickers between keys.1920// syntaxCache keeps what the highlighter said of each source, as a role21// per rune, and which sources it has been asked about.22type syntaxCache struct {23 spans map[string][]Span24 roles map[string][]int825 drafts map[string][]int826 asked map[string]bool27 want []string // sources drawn without an answer yet28 last string // the edited text last answered about29}3031// Instant is a highlighter quick enough to ask while drawing, as the32// built-in tokenizer is.33type Instant interface{ Instant() bool }3435// Instant implements Instant: the tokenizer is.36func (Tokens) Instant() bool { return true }3738// Drafter is a slow highlighter with a quick draft, drawn until its39// answer comes, and in place of one while it's Instant.40type Drafter interface{ Draft(src string) []Span }4142// isInstant reports whether a highlighter or checker answers at once43// (or, a checker, not at all), so it needn't wait for typing to pause.44func isInstant(p any) bool {45 in, ok := p.(Instant)46 return ok && in.Instant()47}4849// asksLater reports whether the language is asked once typing pauses:50// its highlighter or checker is slow.51func (v *View) asksLater() bool {52 hl, ck := v.Providers.Highlighter, v.Providers.Checker53 return hl != nil && !isInstant(hl) || ck != nil && !isInstant(ck)54}5556// spansFor is src's roles, a role per rune, or nil until the57// highlighter has answered or drafted. The cell being edited (edited)58// isn't asked about here, but once typing pauses.59func (v *View) spansFor(src string, kind notebook.Kind, edited bool) []int8 {60 hl := v.Providers.Highlighter61 if kind != notebook.Code || hl == nil {62 return nil63 }64 c := &v.syntax65 if roles, ok := c.roles[src]; ok {66 return roles67 }68 d, drafts := hl.(Drafter)69 if isInstant(hl) && !drafts {70 v.keepSpans(src, hl.Highlight(context.Background(), src))71 return c.roles[src]72 }73 if !edited && !isInstant(hl) {74 c.ask(src)75 }76 if !drafts {77 return nil78 }79 draft := c.draft(src, d)80 if was, ok := c.roles[c.last]; ok && edited {81 return blend(c.last, src, was, draft)82 }83 return draft84}8586// ask notes that src is drawn without an answer, for Fetch.87func (c *syntaxCache) ask(src string) {88 if c.asked == nil {89 c.asked = map[string]bool{}90 }91 if !c.asked[src] {92 c.asked[src] = true93 c.want = append(c.want, src)94 }95}9697// draft is d's roles for src, kept.98func (c *syntaxCache) draft(src string, d Drafter) []int8 {99 if roles, ok := c.drafts[src]; ok {100 return roles101 }102 if c.drafts == nil || len(c.drafts) > 512 {103 c.drafts = map[string][]int8{}104 }105 roles := rolesOf(src, d.Draft(src))106 c.drafts[src] = roles107 return roles108}109110// keepSpans keeps what the highlighter said of src.111func (v *View) keepSpans(src string, spans []Span) {112 c := &v.syntax113 if c.roles == nil || len(c.roles) > 512 {114 c.roles, c.spans = map[string][]int8{}, map[string][]Span{}115 }116 c.spans[src] = spans117 c.roles[src] = rolesOf(src, spans)118}119120// rolesOf is a role per rune of src, -1 for none, from spans; a later121// span overrides an earlier one.122func rolesOf(src string, spans []Span) []int8 {123 roles := make([]int8, 0, len(src))124 for bi := range src {125 r := int8(-1)126 for _, s := range spans {127 if bi >= s.From && bi < s.To {128 r = int8(s.Kind)129 }130 }131 roles = append(roles, r)132 }133 return roles134}135136// blend is src's values: was's (of old) where src keeps old's runes,137// its start and its end, and draft's in what changed between.138func blend[T any](old, src string, was, draft []T) []T {139 o, n := []rune(old), []rune(src)140 if len(was) != len(o) || len(draft) != len(n) {141 return draft142 }143 p := 0144 for p < len(o) && p < len(n) && o[p] == n[p] {145 p++146 }147 s := 0148 for s < len(o)-p && s < len(n)-p && o[len(o)-1-s] == n[len(n)-1-s] {149 s++150 }151 out := slices.Clone(draft)152 copy(out, was[:p])153 copy(out[len(n)-s:], was[len(o)-s:])154 return out155}156157// Fetch asks the highlighter about the sources drawn without an answer,158// in the background.159func (v *View) Fetch() tea.Cmd {160 hl := v.Providers.Highlighter161 want := v.syntax.want162 v.syntax.want = nil163 if hl == nil || len(want) == 0 {164 return nil165 }166 cmds := make([]tea.Cmd, len(want))167 for i, src := range want {168 cmds[i] = func() tea.Msg {169 return highlightMsg{view: v, src: src, spans: hl.Highlight(context.Background(), src)}170 }171 }172 return tea.Batch(cmds...)173}