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 / nu.go
nu.go 6.5K
1package nbview23import (4 "context"5 "errors"6 "slices"7 "strings"8 "sync"9 "sync/atomic"10 "time"1112 "src.rest/cat/012/internal/nushell"13)1415// NuSession is what a session's questions to nu share: whether nu may16// be asked, whether it answers, and how many questions are out. Each17// question is a process of its own, asked in the background once18// typing pauses, at most two at a time, stopped when the text it's19// about is stale and after Timeout. While nu can't be asked (SetOn said20// no, it isn't installed, it's older than nushell.MinVersion, or it21// timed out three times in a row) the notebook's Nu answers as the22// built-ins do, and nu isn't started.23type NuSession struct {24 Runner nushell.Runner25 // Timeout bounds each question; 0 is a second and a half.26 Timeout time.Duration2728 on atomic.Bool29 off atomic.Bool // nu can't be asked this session30 slow atomic.Int32 // questions timed out in a row31 mu sync.Mutex32 checked bool // nu's version was asked33 sem chan struct{}34}3536// NewNuSession returns a session asking nu through r, once SetOn allows37// it.38func NewNuSession(r nushell.Runner) *NuSession {39 return &NuSession{Runner: r, sem: make(chan struct{}, 2)}40}4142// SetOn says whether nu may be asked: in 012 serve only as serve-shell43// allows, and only about a notebook whose cells may run here without44// asking (docs/nushell/notebooks.md#saving-and-trust).45func (s *NuSession) SetOn(on bool) { s.on.Store(on) }4647// Asking reports whether nu is asked: allowed, and not found missing,48// old or slow.49func (s *NuSession) Asking() bool { return s.on.Load() && !s.off.Load() }5051// slowAfter is how many questions in a row may time out before nu52// isn't asked again this session.53const slowAfter = 35455var errNotAsked = errors.New("nu isn't asked")5657// ask asks nu with f, within the timeout, once nu is known to answer.58func (s *NuSession) ask(ctx context.Context, f func(context.Context) error) error {59 if !s.Asking() {60 return errNotAsked61 }62 timeout := s.Timeout63 if timeout <= 0 {64 timeout = 1500 * time.Millisecond65 }66 select {67 case s.sem <- struct{}{}:68 defer func() { <-s.sem }()69 case <-ctx.Done():70 return ctx.Err()71 }72 qctx, cancel := context.WithTimeout(ctx, timeout)73 defer cancel()74 if !s.versionOK(qctx) {75 return errNotAsked76 }77 err := f(qctx)78 switch {79 case err == nil:80 s.slow.Store(0)81 case ctx.Err() != nil: // stale: nobody waits for the answer82 case errors.Is(err, context.DeadlineExceeded):83 if s.slow.Add(1) >= slowAfter {84 s.off.Store(true)85 }86 case errors.Is(err, nushell.ErrMissing), errors.Is(err, nushell.ErrOld):87 s.off.Store(true)88 }89 return err90}9192// versionOK asks nu's version the first time, turning nu off for the93// session if it's missing or too old.94func (s *NuSession) versionOK(ctx context.Context) bool {95 s.mu.Lock()96 defer s.mu.Unlock()97 if !s.checked {98 _, err := nushell.Version(ctx, s.Runner)99 if ctx.Err() != nil {100 return false // asked again next time101 }102 s.checked = true103 if err != nil {104 s.off.Store(true)105 }106 }107 return !s.off.Load()108}109110// Nu is a notebook's language as nu itself knows it: highlighting from111// its --ide-ast shapes, completions from --ide-complete after the112// notebook's own words, and problems from --ide-check.113//114// nu doesn't know the variables a notebook binds ($sales, $selection,115// $sheet.A1:C9) nor a cell's `name =`, so it's asked about the source116// with those declared before it, in a prelude, and the cell's name and117// ranges of sheets masked to as many bytes, so offsets map back by the118// prelude's length alone (see prepare).119type Nu struct {120 s *NuSession121 mu sync.Mutex122 names []string123 words []Word124}125126// For returns a notebook's Nu, asking through the session.127func (s *NuSession) For() *Nu { return &Nu{s: s} }128129// SetWords gives the notebook's names: the variables it binds besides130// $selection (its cells' names and linked files'), and the built-in131// completer's words. It's called where the notebook changes, so no132// question reads the notebook itself.133func (n *Nu) SetWords(names []string, words []Word) {134 n.mu.Lock()135 n.names, n.words = names, words136 n.mu.Unlock()137}138139func (n *Nu) known() ([]string, []Word) {140 n.mu.Lock()141 defer n.mu.Unlock()142 return n.names, n.words143}144145// Instant implements Instant: while nu isn't asked, the tokenizer146// answers at once.147func (n *Nu) Instant() bool { return !n.s.Asking() }148149// Draft implements Drafter: the tokenizer's answer, drawn until nu's150// comes.151func (n *Nu) Draft(src string) []Span { return Tokens{}.Highlight(context.Background(), src) }152153// Highlight implements Highlighter: nu's shapes, or the tokenizer's154// when nu doesn't answer.155func (n *Nu) Highlight(ctx context.Context, src string) []Span {156 names, _ := n.known()157 p := prepare(src, names)158 var shapes []nushell.Shape159 err := n.s.ask(ctx, func(ctx context.Context) (err error) {160 shapes, err = nushell.AST(ctx, n.s.Runner, p.text)161 return err162 })163 if err != nil {164 return n.Draft(src)165 }166 return p.spans(src, shapes)167}168169// maxProblems is the most problems nu's check reports.170const maxProblems = 20171172// Check implements Checker: nu's problems with the pipeline, leaving173// out those that only say it's unfinished at its end, as it is while174// it's typed.175func (n *Nu) Check(ctx context.Context, src string) []Diagnostic {176 names, _ := n.known()177 p := prepare(src, names)178 var probs []nushell.Problem179 err := n.s.ask(ctx, func(ctx context.Context) (err error) {180 probs, err = nushell.Check(ctx, n.s.Runner, p.text, maxProblems)181 return err182 })183 if err != nil {184 return nil185 }186 end := len(strings.TrimRight(src, " \t\n"))187 var out []Diagnostic188 for _, pr := range probs {189 from, to := pr.From-p.at, pr.To-p.at190 if from < 0 || to > len(src) || from == to && from >= end {191 continue192 }193 out = append(out, Diagnostic{From: from, To: to, Msg: pr.Msg})194 }195 return out196}197198// Complete implements Completer: the notebook's words, then nu's that199// they don't have.200func (n *Nu) Complete(ctx context.Context, src string, offset int) []Completion {201 offset = min(offset, len(src))202 names, words := n.known()203 out := Words(func() []Word { return words }).Complete(ctx, src, offset)204 p := prepare(src, names)205 var said []string206 if n.s.ask(ctx, func(ctx context.Context) (err error) {207 said, err = nushell.Complete(ctx, n.s.Runner, p.text, p.at+offset)208 return err209 }) != nil {210 return out211 }212 for _, w := range said {213 if strings.HasPrefix(w, "$__") || slices.ContainsFunc(out, func(c Completion) bool { return c.Text == w }) {214 continue // the prelude's own, or a word the notebook has215 }216 out = append(out, Completion{Text: w, From: replaced(src, offset, w), To: offset})217 }218 return out219}