cat / 012
clone

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

Notebook UI: statements, comments and assigned names in the editor, runs and streams

Dami committed 02a7680
The highlighter and nu's prelude read names, comments and raw strings
from notebook.Parse, masking each name = in place; the context line says
which names a cell sets; a multi-statement cell streams its last
statement. UI, nbview and e2e tests (real nu) for the owner's cell.
commit
02a76805863e2aefa4ac6a944fe164a49dc8bbe2
parent
a164d6787e3770517fa77814c86cd7082ac90b89
author
Dami <dami.etoile@gmail.com>
kind
feat risk score 2.1 of 3, Jev's estimate
9 files changed, +231 -20
Me2e/notebook_test.go29++++++++
Minternal/ui/nbjob.go10++-
Minternal/ui/nblang.go11++-
Minternal/ui/nbrun.go9+-
Ainternal/ui/nbstmts_test.go112++++++++++++++++++++++++++++++
Minternal/ui/nbstream.go2+-
Minternal/ui/nbview/nuprelude.go12++-
Ainternal/ui/nbview/stmts_test.go59++++++++++++++++
Minternal/ui/nbview/syntax.go7--
M e2e/notebook_test.go +29 -0
M internal/ui/nbjob.go +7 -3
M internal/ui/nblang.go +9 -2
M internal/ui/nbrun.go +5 -4
A internal/ui/nbstmts_test.go +112 -0
@@ -0,0 +1,112 @@
1+package ui2+3+import (4+ "encoding/base64"5+ "slices"6+ "strings"7+ "testing"8+9+ "github.com/FelineStateMachine/012/internal/notebook"10+)11+12+// ownersCell is a cell of two statements: files is its own variable,13+// read by its second line.14+const ownersCell = "files = ls | where type == file\n$files | where size > 1kb | sort-by size --reverse"15+16+// varsOut is what nu prints for a run handing back variables: a line17+// for the output and each variable, its NUON as base64.18+func varsOut(out string, vars ...string) string {19+ enc := func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }20+ lines := []string{"__out " + enc(out)}21+ for i := 0; i+1 < len(vars); i += 2 {22+ lines = append(lines, vars[i]+" "+enc(vars[i+1]))23+ }24+ return strings.Join(lines, "\n") + "\n"25+}26+27+// nuCommand is what nu runs for src.28+func nuCommand(src string) string {29+ cmd, _, _ := notebook.Parse(src).Command()30+ return cmd31+}32+33+// The owner's cell runs as a script: files is the cell's own, not a cell34+// it reads, its output is its last line's, it has no name, and a later35+// cell reads $files from its run.36+func TestNotebookStatements(t *testing.T) {37+ big := "[[name, size]; [c.txt, 5kb], [a.txt, 2kb]]"38+ m, nu := notebookModel(t, map[string]string{39+ nuCommand(ownersCell): varsOut(big, "files", lsOut),40+ "$files | length": "3",41+ })42+ write(t, m, ownersCell)43+ write(t, m, "$files | length")44+ press(t, m, "<ctrl+enter>") // the second runs the first first45+ if got := nu.ran(); len(got) != 2 || !strings.HasPrefix(got[0], "let files = ls") {46+ t.Fatalf("ran %q", got)47+ }48+ if !slices.Equal(nu.jobs[0].Vars, []string{"files"}) || len(nu.jobs[0].Tables) != 0 {49+ t.Errorf("the first job %+v", nu.jobs[0])50+ }51+ if got := string(nu.jobs[1].Tables["files"]); got != lsOut {52+ t.Errorf("$files reached the second cell as %q", got)53+ }54+ s := screen(m)55+ if strings.Contains(s, "failed") || strings.Contains(s, "─ files ─") || !strings.Contains(s, "c.txt") {56+ t.Errorf("screen:\n%s", s)57+ }58+ cs := m.sheet.NotebookCells()59+ if o := m.book().Output(cs[0].ID); o == nil || string(o.NUON) != big || string(o.Vars["files"]) != lsOut {60+ t.Errorf("output %+v", o)61+ }62+ press(t, m, "<up>", "<up>", "<up>")63+ if i, _ := m.nbView().Selected(); i != 0 {64+ t.Fatalf("selected %d", i)65+ }66+ if got := line(m, contextLine); !strings.Contains(got, "sets $files") || strings.Contains(got, "reads") {67+ t.Errorf("context line %q", got)68+ }69+ // Running it again makes the reader stale.70+ press(t, m, "<ctrl+enter>")71+ if st := m.cellState(m.sheet, cs[1].ID); !st.Stale {72+ t.Error("the reader isn't stale")73+ }74+ // Naming the cell names its last statement.75+ press(t, m, "n")76+ send(m, pasteMsg("big"))77+ press(t, m, "<enter>")78+ if got := m.sheet.NotebookCells()[0].Source; got != "files = ls | where type == file\nbig = $files | where size > 1kb | sort-by size --reverse" {79+ t.Errorf("named %q", got)80+ }81+}82+83+// Comments are nu's: a commented-out assignment names nothing and a84+// commented-out $name reads nothing.85+func TestNotebookComments(t *testing.T) {86+ src := "# files = ls\nls # | where name == $nope"87+ m, nu := notebookModel(t, map[string]string{nuCommand(src): lsOut})88+ write(t, m, src)89+ press(t, m, "<ctrl+enter>")90+ if len(nu.jobs) != 1 || len(nu.jobs[0].Tables) != 0 || strings.Contains(nu.jobs[0].Command, "#") {91+ t.Fatalf("ran %+v", nu.jobs)92+ }93+ if strings.Contains(screen(m), "failed") || m.sheet.NotebookCells()[0].Name() != "" {94+ t.Errorf("screen:\n%s", screen(m))95+ }96+}97+98+// A cell of several statements streams its last one's values, its own99+// variables bound first and none handed back.100+func TestNotebookStreamStatements(t *testing.T) {101+ streamPolls = nil102+ m, _ := notebookModel(t, nil)103+ nu := newStreamNu()104+ m.SetShellRunner(nu)105+ write(t, m, "log = open app.log\n$log | lines")106+ press(t, m, "f")107+ defer m.stopCells()108+ script := <-nu.scripts109+ if !strings.Contains(script, "let log = open app.log\n$log | lines") || strings.Contains(script, "__out") {110+ t.Errorf("stream script %s", script)111+ }112+}
M internal/ui/nbstream.go +1 -1
M internal/ui/nbview/nuprelude.go +9 -3
A internal/ui/nbview/stmts_test.go +59 -0
@@ -0,0 +1,59 @@
1+package nbview2+3+import (4+ "context"5+ "fmt"6+ "strings"7+ "testing"8+9+ "github.com/FelineStateMachine/012/internal/notebook"10+)11+12+// A cell of several statements: each name assigned shows as a variable,13+// a comment's text as a comment, whatever it holds, and # in a string14+// or a raw string is the string's.15+func TestHighlightsStatements(t *testing.T) {16+ src := "files = ls # all\n# big = $files\nbig = $files | where name != \"a#b\" | append r#'c # d'#"17+ var got []string18+ for _, s := range (Tokens{}).Highlight(context.Background(), src) {19+ got = append(got, fmt.Sprintf("%s:%d", src[s.From:s.To], s.Kind))20+ }21+ want := fmt.Sprint([]string{"files:2", "=:5", "ls:0", "# all:6", "# big = $files:6", "big:2", "=:5", "$files:2", "|:5", "where:0",22+ "!=:5", `"a#b":1`, "|:5", "append:0", "r#'c # d'#:1"})23+ if fmt.Sprint(got) != want {24+ t.Errorf("spans %v\nwant %v", got, want)25+ }26+}27+28+// nu is asked about a cell of several statements with each `name =`29+// blanked, every byte in its place, and the names its lines assign for30+// the lines after declared.31+func TestPrepareStatements(t *testing.T) {32+ src := "files = ls # x = 1\nbig = $files | first"33+ p := prepare(src, []string{"sales"})34+ if body := p.text[p.at:]; body != " ls # x = 1\n $files | first" {35+ t.Errorf("masked %q", body)36+ }37+ pre := p.text[:p.at]38+ if !strings.Contains(pre, "let files: any = []\n") || strings.Contains(pre, "let big") || strings.Contains(pre, "let x") {39+ t.Errorf("prelude %q", pre)40+ }41+}42+43+// The context line says what a cell of several statements reads and44+// sets: not its own variables.45+func TestHeadStatements(t *testing.T) {46+ h := newHost("files = ls | where type == file\n$files | where size > 1kb | sort-by size --reverse", "$files | length")47+ v := newView(h, 60, 12)48+ v.Select(0, false)49+ if name, text := v.Head(); name != "cell 1" || text != "a nushell pipeline; sets $files" {50+ t.Errorf("head %q %q", name, text)51+ }52+ v.Select(1, false)53+ if _, text := v.Head(); text != "reads $files" {54+ t.Errorf("head of the reader %q", text)55+ }56+ if (notebook.Cell{Source: h.cells[0].Source}).Name() != "" {57+ t.Error("the cell is named")58+ }59+}
M internal/ui/nbview/syntax.go +0 -7