The hover's and the language's 80 ms pauses are timed by View.After, tea.Tick's when nil. nbview's tests end pauses when they settle; ui's keep them in pauses until a test ends them with endPauses. go test -count=1 ./internal/ui: 15.3 s before, 6.9 s after (9.2 s before the hover came); ./internal/ui/nbview 2.8 s to 1.7 s.
- commit
- 61b082d30b9ea3e8d71ca6e991f5e51f08ccb096
- parent
- 6c03099a895951ce3844d7178f97887bf1693d6b
- author
- Dami <dami.etoile@gmail.com>
- kind
- refactor risk score 1.3 of 3, Jev's estimate
| M | docs/ | 6 | ++++- |
| M | internal/ | 4 | +++ |
| M | internal/ | 1 | + |
| M | internal/ | 6 | +++++ |
| M | internal/ | 10 | ++++++++- |
| M | internal/ | 4 | +--- |
| M | internal/ | 6 | ++++- |
| M | internal/ | 4 | +++ |
| M | internal/ | 6 | +++++ |
| M | internal/ | 8 | ++++++- |
| A | internal/ | 36 | ++++++++++++++++++++++++++++++ |
@@ -58,7 +58,11 @@ the seed draws, and its accepted steps are undone with the rest.
5858 5959 The UI is tested by sending6060 Bubble Tea messages (keys, mouse, paste, window size) to the model and61-reading what `View` renders, without a terminal. Sessions sharing a61+reading what `View` renders, without a terminal. No unit test waits62+on a debounce: a notebook times typing's pauses on a clock the tests63+give it (`nbview.View.After`), which hands them the message the pause64+would, and a test that wants the pause over delivers it (`endPauses`).65+Sessions sharing a6266 workbook are driven the same way through `ui.Shared`, each room's6367 changes handed to the others as their programs would get them6468 (`share_test.go`), and in real Bubble Tea programs typing at once under@@ -102,6 +102,10 @@ func runVia(update func(tea.Msg) tea.Cmd, cmd tea.Cmd) tea.Msg {
102102 if _, ok := out.(autoscrollMsg); ok {103103 return nil104104 }105+ if msg, ok := out.(pausedMsg); ok {106+ pauses = append(pauses, msg.msg) // tests end them with endPauses107+ return nil108+ }105109 if msg, ok := out.(nbStreamMsg); ok {106110 streamPolls = append(streamPolls, msg) // tests take them with pumpStreams107111 return nil@@ -487,6 +487,7 @@ func TestMonochromeNuProblem(t *testing.T) {
487487 run(m, m.runCommand("nb.open"))488488 press(t, m, "b", "<enter>")489489 send(m, pasteMsg("ls | sort-by name --revrse"))490+ endPauses(m)490491 l := findLine(t, m, "sort-by")491492 if c := cellsOf(l, "--revrse"); !every(c, true, func(c monoCell) bool { return c.underline == "3" }) {492493 t.Errorf("the problem isn't underlined: %+v", c)@@ -65,6 +65,7 @@ func TestNotebookAsksNuAsTrusted(t *testing.T) {
6565 m.nb.still = true6666 run(m, m.runCommand("nb.open"))6767 press(t, m, "b", "<enter>", "l")68+ endPauses(m)6869 if got := nu.asked(); !slices.Contains(got, "--ide-ast") || !slices.Contains(got, "--ide-check") {6970 t.Fatalf("a notebook made here: asked %v", got)7071 }@@ -85,6 +86,7 @@ func TestNotebookAsksNuAsTrusted(t *testing.T) {
8586 m2.nb.still = true8687 run(m2, m2.runCommand("nb.open"))8788 press(t, m2, "<enter>", "s")89+ endPauses(m2)8890 if got := nu.asked(); len(got) != 0 {8991 t.Errorf("a file from elsewhere, not trusted: asked %v", got)9092 }@@ -92,12 +94,14 @@ func TestNotebookAsksNuAsTrusted(t *testing.T) {
9294 run(m2, m2.runCommand("nb.run_all"))9395 press(t, m2, "<enter>") // Run this file's notebook cells? Run9496 press(t, m2, "<enter>", "s")97+ endPauses(m2)9598 if got := nu.asked(); !slices.Contains(got, "--ide-ast") {9699 t.Errorf("trusted: asked %v", got)97100 }98101 press(t, m2, "<esc>")99102 m2.nb.served = true100103 press(t, m2, "<enter>", "s")104+ endPauses(m2)101105 if got := nu.asked(); len(got) != 0 {102106 t.Errorf("served without serve-shell: asked %v", got)103107 }@@ -147,11 +151,13 @@ func TestNotebookWordHelp(t *testing.T) {
147151 m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})148152 run(m, m.runCommand("nb.open"))149153 press(t, m, "b", "<enter>", "ls | sort-by name -r", "<home>", "<right>", "<right>", "<right>", "<right>", "<right>", "<right>")154+ endPauses(m)150155 if got := line(m, contextLine); !strings.Contains(got, "sort-by <...comparator: cell-path|closure> --reverse") ||151156 !strings.Contains(got, "Sort by the given cell path or closure.") || !strings.Contains(got, "F1") {152157 t.Fatalf("context line %q", got)153158 }154159 press(t, m, "<end>")160+ endPauses(m)155161 if got := line(m, contextLine); !strings.Contains(got, "sort-by --reverse, -r") || !strings.Contains(got, "Sort in reverse order.") {156162 t.Errorf("on the flag: %q", got)157163 }@@ -21,6 +21,14 @@ import (
2121 // pause is how long typing pauses before the language is asked.2222 const pause = 80 * time.Millisecond2323 24+// afterPause gives msg once typing has paused, on the view's clock.25+func (v *View) afterPause(msg tea.Msg) tea.Cmd {26+ if v.After != nil {27+ return v.After(pause, msg)28+ }29+ return tea.Tick(pause, func(time.Time) tea.Msg { return msg })30+}31+2432 // editor is the cell being edited.2533 type editor struct {2634 on bool@@ -208,7 +216,7 @@ func (v *View) changed() tea.Cmd {
208216 return rest209217 }210218 version := v.edit.version211- return tea.Batch(rest, tea.Tick(pause, func(time.Time) tea.Msg { return pausedMsg{view: v, version: version} }))219+ return tea.Batch(rest, v.afterPause(pausedMsg{view: v, version: version}))212220 }213221 214222 // cancel stops the questions about the text being edited that are@@ -4,7 +4,6 @@ import (
44 "context"55 "slices"66 "strings"7- "time"87 98 tea "charm.land/bubbletea/v2"109 "github.com/charmbracelet/x/ansi"@@ -68,8 +67,7 @@ func (v *View) rest() tea.Cmd {
6867 e := &v.edit6968 e.moves++7069 e.stopHover()71- moves := e.moves72- return tea.Tick(pause, func(time.Time) tea.Msg { return restedMsg{view: v, moves: moves} })70+ return v.afterPause(restedMsg{view: v, moves: e.moves})7371 }7472 7573 func (e *editor) stopHover() {@@ -57,10 +57,14 @@ func TestCommandStart(t *testing.T) {
5757 }5858 }5959 60-// settle runs cmd and hands the view the messages it makes, and theirs.60+// settle runs cmd and hands the view the messages it makes, and theirs,61+// with typing's pauses over at once.6162 func settle(v *View, cmd tea.Cmd) []tea.Msg {6263 var out []tea.Msg6364 for _, msg := range drain(cmd) {65+ if p, ok := msg.(paused); ok {66+ msg = p.msg67+ }6468 out = append(out, msg)6569 if next, ok := v.Update(msg); ok {6670 out = append(out, settle(v, next)...)@@ -73,6 +73,10 @@ type View struct {
7373 // "d d" is d pressed twice.7474 Keys, EditKeys map[string]string7575 Providers Providers76+ // After is the clock typing's pauses are timed on: a command that77+ // gives msg once d has passed, tea.Tick's when nil. Tests give one78+ // that hands them msg, to deliver when they want the pause over.79+ After func(d time.Duration, msg tea.Msg) tea.Cmd7680 7781 sel int // the active cell7882 anchor int // the other end of the cells selected with it, or -1@@ -5,6 +5,7 @@ import (
55 "fmt"66 "strings"77 "testing"8+ "time"89 910 tea "charm.land/bubbletea/v2"1011 "github.com/charmbracelet/x/ansi"@@ -121,10 +122,15 @@ func newView(h *fakeHost, w, ht int) *View {
121122 v.Keys = map[string]string{"enter": "nb.edit", "d d": "nb.delete", "o": "nb.toggle_output", "shift+enter": "nb.run_next"}122123 v.EditKeys = map[string]string{"esc": "nb.command_mode", "shift+enter": "nb.run_next"}123124 v.Providers = Providers{Highlighter: Tokens{}}125+ v.After = func(_ time.Duration, msg tea.Msg) tea.Cmd { return func() tea.Msg { return paused{msg} } }124126 v.Resize(w, ht)125127 return v126128 }127129 130+// paused is a message the view asked for once typing pauses: no test131+// waits for the pause; settle ends it at once, and the others never.132+type paused struct{ msg tea.Msg }133+128134 func text(v *View) string { return ansi.Strip(strings.Join(v.Lines(), "\n")) }129135 130136 func key(s string) tea.KeyPressMsg {@@ -4,6 +4,7 @@ import (
44 "cmp"55 "slices"66 "strconv"7+ "time"78 89 tea "charm.land/bubbletea/v2"910 @@ -22,6 +23,11 @@ import (
2223 // running them is nbrun.go's, sending an output to a sheet nbsend.go's,2324 // and drawing the tab nbscreen.go's.2425 26+// nbAfter is the clock notebooks time typing's pauses on27+// (nbview.View.After); nil is the real one. The tests' hands them the28+// pauses to end when they choose (pauses_test.go).29+var nbAfter func(time.Duration, tea.Msg) tea.Cmd30+2531 // nbKeys are command mode's keys, as Jupyter's; "d d" is d twice.2632 var nbKeys = map[string]string{2733 "enter": "nb.edit",@@ -189,7 +195,7 @@ func (m *Model) viewOf(s *sheet.Sheet) *nbview.View {
189195 v := m.nb.views[s]190196 if v == nil {191197 v = nbview.New(nbHost{m, s})192- v.Keys, v.EditKeys = nbKeys, nbEditKeys198+ v.Keys, v.EditKeys, v.After = nbKeys, nbEditKeys, nbAfter193199 lang := m.nbLang().For()194200 v.Providers = nbview.Providers{Highlighter: lang, Completer: lang, Checker: lang, Hoverer: lang}195201 m.nb.views[s] = v@@ -0,0 +1,36 @@
1+package ui2+3+import (4+ "time"5+6+ tea "charm.land/bubbletea/v2"7+)8+9+// Notebooks ask about the text being edited, and the word at the caret,10+// once typing pauses. No test waits for a pause: the notebook's clock11+// hands the message the pause would give to run, which keeps it in12+// pauses, and a test that wants a pause over ends it with endPauses.13+func init() {14+ nbAfter = func(_ time.Duration, msg tea.Msg) tea.Cmd {15+ return func() tea.Msg { return pausedMsg{msg} }16+ }17+}18+19+// pausedMsg is a message a notebook asked for once typing pauses.20+type pausedMsg struct{ msg tea.Msg }21+22+// pauses are the messages waiting for typing's pauses to end.23+var pauses []tea.Msg24+25+// endPauses ends the pauses waiting, and those they start: the26+// notebook asks about the text and the word at the caret, and m gets27+// the answers.28+func endPauses(m *Model) {29+ for i := 0; i < 10 && len(pauses) > 0; i++ {30+ msgs := pauses31+ pauses = nil32+ for _, msg := range msgs {33+ send(m, msg)34+ }35+ }36+}