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 / hover_test.go
hover_test.go 6.6K
1package nbview23import (4 "context"5 "strings"6 "testing"78 tea "charm.land/bubbletea/v2"9 "github.com/charmbracelet/x/ansi"1011 "src.rest/cat/012/internal/notebook"12)1314// nu's hover, as testdata/nu records it: a command's signature and15// summary, a flag in its command's help, a variable's type, and the16// notebook's own word for a name it binds.17func TestNuHovers(t *testing.T) {18 _, n := nuFor(recordedNu(t))19 for _, c := range []struct{ word, text, desc string }{20 {"sort-by", "sort-by <...comparator: cell-path|closure> --reverse --ignore-case --natural --custom", "Sort by the given cell path or closure."},21 {"-r", "sort-by --reverse, -r", "Sort in reverse order."},22 {"--decimals", "into string --decimals <int>, -d", "Decimal digits to which to round."},23 {"$n", "$n: int", ""},24 {"$files", "$files: a cell's output", ""},25 } {26 at := strings.Index(hoverSrc, c.word+" ")27 if c.word == "$n" {28 at = strings.LastIndex(hoverSrc, c.word)29 }30 h, ok := n.Hover(context.Background(), hoverSrc, at+1)31 if !ok || h.Text != c.text || h.Desc != c.desc || hoverSrc[h.From:h.To] != c.word {32 t.Errorf("%s: %+v %v", c.word, h, ok)33 }34 if wantHelp := c.desc != ""; (h.Help != nil) != wantHelp {35 t.Errorf("%s: help %v", c.word, h.Help)36 }37 }38 n.SetWords([]string{"files"}, []Word{{Text: "$sheet.", Desc: "a range of a sheet: $sheet.A1:C9"}})39 if h, ok := n.Hover(context.Background(), "$sheet.A1:C9 | first", 3); !ok || h.Text != "$sheet.A1:C9: a range of a sheet" {40 t.Errorf("a range: %+v", h)41 }42}4344func TestCommandStart(t *testing.T) {45 for _, c := range []struct{ src, flag, want string }{46 {"ls | sort-by size -r", "-r", "sort-by"},47 {"ls | each {|f| $f | sort-by x -r }", "-r", "sort-by"},48 {"let big = ls -a", "-a", "ls"},49 {"ls\n | where ('a|b' | str contains -i x)", "-i", "str"},50 {"^git log --oneline", "--oneline", "git"},51 } {52 src := c.src53 at, ok := commandStart(src, notebook.Parse(src).Strings, strings.Index(src, c.flag))54 if !ok || !strings.HasPrefix(src[at:], c.want) {55 t.Errorf("%q: %d %q", src, at, src[at:])56 }57 }58}5960// settle runs cmd and hands the view the messages it makes, and theirs,61// with typing's pauses over at once.62func settle(v *View, cmd tea.Cmd) []tea.Msg {63 var out []tea.Msg64 for _, msg := range drain(cmd) {65 if p, ok := msg.(paused); ok {66 msg = p.msg67 }68 out = append(out, msg)69 if next, ok := v.Update(msg); ok {70 out = append(out, settle(v, next)...)71 }72 }73 return out74}7576// press types keys, then lets the caret rest.77func press(v *View, keys ...string) {78 var cmd tea.Cmd79 for _, k := range keys {80 cmd, _ = v.Key(key(k))81 }82 settle(v, cmd)83}8485// The caret resting on a command shows its signature on the context86// line, F1 asks for its help, and a problem at the caret wins the line.87func TestHoverOnTheContextLine(t *testing.T) {88 f := recordedNu(t)89 h := newHost(hoverSrc)90 v := newView(h, 120, 20)91 v.EditKeys["f1"] = "nb.word_help"92 _, n := nuFor(f)93 v.Providers = Providers{Highlighter: n, Checker: n, Hoverer: n}94 settle(v, v.StartEdit())95 press(v, "home", "right", "right", "right", "right", "right", "right", "right", "right", "right", "right")96 left, right := v.ContextLine()97 if got := ansi.Strip(left); !strings.HasPrefix(got, "sort-by <...comparator: cell-path|closure>") || !strings.Contains(got, "Sort by the given") {98 t.Errorf("context line %q", got)99 }100 if !strings.Contains(ansi.Strip(right), "F1 help") {101 t.Errorf("keys %q", ansi.Strip(right))102 }103 msgs := settle(v, v.WordHelp())104 if len(msgs) != 1 || msgs[0].(HelpMsg).Help == nil || msgs[0].(HelpMsg).Help.Name != "sort-by" {105 t.Errorf("F1: %v", msgs)106 }107108 v.edit.diags, v.edit.diagFor = []Diagnostic{{From: 19, To: 26, Msg: "Something's wrong."}}, v.edit.text()109 if left, _ := v.ContextLine(); ansi.Strip(left) != "Something's wrong." {110 t.Errorf("the problem gives way: %q", ansi.Strip(left))111 }112 v.edit.diags = nil113114 cmd, _ := v.Key(key("home")) // the answer about the word left is dropped115 if _, ok := v.Update(hoveredMsg{view: v, moves: v.edit.moves - 1, src: v.edit.text(), hover: Hover{Text: "stale", To: 99}, ok: true}); !ok || v.edit.hover.Text == "stale" {116 t.Error("a stale answer was kept")117 }118 settle(v, cmd)119 if left, _ := v.ContextLine(); strings.Contains(ansi.Strip(left), "sort-by") {120 t.Errorf("the caret left the word: %q", ansi.Strip(left))121 }122}123124// F1 on no command asks for the shortcuts.125func TestWordHelpWithoutAWord(t *testing.T) {126 h := newHost("ls ")127 v := newView(h, 80, 20)128 settle(v, v.StartEdit())129 msgs := settle(v, v.WordHelp())130 if len(msgs) != 1 || msgs[0].(HelpMsg).Help != nil {131 t.Errorf("F1 after a space: %v", msgs)132 }133}134135// A $name the notebook binds says its cell and its value's shape,136// without asking nu.137func TestCellHover(t *testing.T) {138 h := newHost("files = ls", "x = 4\ny = [1 2 3]", "$files | append $x | append $y | append $gone")139 h.outs[1] = &notebook.Output{Count: 1, NUON: []byte("[{name: a, size: 1, type: file}, {name: b, size: 2, type: dir}]")}140 h.outs[2] = &notebook.Output{Count: 2, NUON: []byte("[1, 2, 3]"), Vars: map[string][]byte{"x": []byte("4")}}141 v := newView(h, 100, 30)142 v.Select(2, false)143 settle(v, v.StartEdit())144 src := v.edit.text()145 for word, want := range map[string]string{146 "$files": "$files: table, 2 rows × 3 columns from cell 1",147 "$x": "$x: int from cell 2",148 "$y": "$y: list, 3 items from cell 2",149 } {150 v.edit.area.Pos = len([]rune(src[:strings.Index(src, word)+2]))151 settle(v, v.rest())152 if left, _ := v.ContextLine(); ansi.Strip(left) != want {153 t.Errorf("%s: %q", word, ansi.Strip(left))154 }155 }156 h.outs[1] = nil157 v.edit.area.Pos = 2158 settle(v, v.rest())159 if left, _ := v.ContextLine(); ansi.Strip(left) != "$files: from cell 1, not run yet" {160 t.Errorf("not run: %q", ansi.Strip(left))161 }162 v.edit.area.Pos = len([]rune(src)) - 2163 settle(v, v.rest())164 if left, _ := v.ContextLine(); ansi.Strip(left) != "" {165 t.Errorf("a name no cell binds: %q", ansi.Strip(left))166 }167}168169// A long signature gives up its flags, then its description, to fit.170func TestHoverLineFits(t *testing.T) {171 th := newHost().Theme()172 h := Hover{Text: "sort-by <...comparator: cell-path> --reverse --ignore-case", Brief: "sort-by <...comparator> …", Desc: "Sort by the given cell path or closure."}173 for room, want := range map[int]string{174 200: "sort-by <...comparator: cell-path> --reverse --ignore-case Sort by the given cell path or closure.",175 90: "sort-by <...comparator: cell-path> --reverse … Sort by the given cell path or closure.",176 60: "sort-by <...comparator> … Sort by the given cell path or c…",177 40: "sort-by <...comparator> … Sort by the …",178 } {179 if got := ansi.Strip(hoverLine(th, h, room)); got != want || ansi.StringWidth(got) > room {180 t.Errorf("%d: %q", room, got)181 }182 }183}