1package ui23import (4 "bytes"5 "context"6 "encoding/json"7 "fmt"8 "io"9 "slices"10 "strconv"11 "strings"12 "testing"1314 tea "charm.land/bubbletea/v2"1516 "src.rest/cat/012/internal/nushell"17 "src.rest/cat/012/internal/sheet"18 "src.rest/cat/012/internal/ui/cmdhelp"19 "src.rest/cat/012/internal/ui/shortcuts"20)2122// ideNu is fakeNu answering the code editor's questions too, as nu23// 0.116 would about any pipeline, and noting them. Its check finds24// problem wherever the pipeline has it.25type ideNu struct {26 *fakeNu27 ide []string28 problem string29 // help is the hover of every command, a command's help as nu30 // writes it.31 help string32}3334func (f *ideNu) Run(ctx context.Context, job nushell.Job, script string, stdout io.Writer) error {35 if len(job.IDE) == 0 {36 return f.fakeNu.Run(ctx, job, script, stdout)37 }38 f.ide = append(f.ide, job.IDE[0])39 answer := map[string]string{"--version": "0.116.0", "--ide-ast": "[]", "--ide-complete": `{"completions": []}`}[job.IDE[0]]40 if i := strings.Index(script, f.problem); job.IDE[0] == "--ide-check" && f.problem != "" && i >= 0 {41 answer = fmt.Sprintf(`{"type":"diagnostic","severity":"Error","message":"Not a flag.","span":{"start":%d,"end":%d}}`, i, i+len(f.problem))42 }43 if job.IDE[0] == "--ide-hover" && f.help != "" {44 answer = f.hover(script, job.IDE[1])45 }46 _, err := io.WriteString(stdout, answer)47 return err48}4950// asked are the questions nu was asked since the last call.51func (f *ideNu) asked() []string {52 out := f.ide53 f.ide = nil54 return out55}5657// Writing a cell asks nu about it once typing pauses; a file from58// another computer isn't asked about until its cells are trusted, and59// 012 serve asks nu only as serve-shell allows.60func TestNotebookAsksNuAsTrusted(t *testing.T) {61 nu := &ideNu{fakeNu: &fakeNu{out: map[string]string{"ls": lsOut}}}62 m := newModel()63 m.SetShellRunner(nu)64 m.SetMachine("here")65 m.nb.still = true66 run(m, m.runCommand("nb.open"))67 press(t, m, "b", "<enter>", "l")68 endPauses(m)69 if got := nu.asked(); !slices.Contains(got, "--ide-ast") || !slices.Contains(got, "--ide-check") {70 t.Fatalf("a notebook made here: asked %v", got)71 }72 press(t, m, "<esc>")7374 var b bytes.Buffer75 if err := m.book().Write(&b); err != nil {76 t.Fatal(err)77 }78 s, err := sheet.Read(strings.NewReader(strings.Replace(b.String(), `"macroOrigin": "here"`, `"macroOrigin": "elsewhere"`, 1)))79 if err != nil {80 t.Fatal(err)81 }82 m2 := New(s, "")83 m2.Update(tea.WindowSizeMsg{Width: 80, Height: 20})84 m2.SetShellRunner(nu)85 m2.SetMachine("here")86 m2.nb.still = true87 run(m2, m2.runCommand("nb.open"))88 press(t, m2, "<enter>", "s")89 endPauses(m2)90 if got := nu.asked(); len(got) != 0 {91 t.Errorf("a file from elsewhere, not trusted: asked %v", got)92 }93 press(t, m2, "<esc>")94 run(m2, m2.runCommand("nb.run_all"))95 press(t, m2, "<enter>") // Run this file's notebook cells? Run96 press(t, m2, "<enter>", "s")97 endPauses(m2)98 if got := nu.asked(); !slices.Contains(got, "--ide-ast") {99 t.Errorf("trusted: asked %v", got)100 }101 press(t, m2, "<esc>")102 m2.nb.served = true103 press(t, m2, "<enter>", "s")104 endPauses(m2)105 if got := nu.asked(); len(got) != 0 {106 t.Errorf("served without serve-shell: asked %v", got)107 }108}109110// hover answers as nu does about the word at byte at of script: a111// flag's "flag", a command's help.112func (f *ideNu) hover(script, at string) string {113 i, _ := strconv.Atoi(at)114 from, to := i, i115 for from > 0 && script[from-1] != ' ' && script[from-1] != '\n' {116 from--117 }118 for to < len(script) && script[to] != ' ' {119 to++120 }121 text := f.help122 switch {123 case from == to:124 return ""125 case script[from] == '-':126 text = "flag"127 case script[from] == '$' || script[from] == '|':128 return ""129 }130 b, _ := json.Marshal(map[string]any{"hover": text, "span": map[string]int{"start": from, "end": to}})131 return string(b)132}133134// sortByHelp is sort-by's hover, as nu 0.116 writes it.135const sortByHelp = "Sort by the given cell path or closure.\n### Usage\n```\n sort-by {flags} <...comparator>\n```\n\n### Flags\n\n" +136 " `-h`, `--help` - Display the help message for this command\\\n `-r`, `--reverse` - Sort in reverse order.\\\n" +137 " `-i`, `--ignore-case` - Sort string-based data case-insensitively.\n\n### Parameters\n\n" +138 " `...comparator: oneof<cell-path, closure>` - The cell path(s) or closure(s) to compare elements by.\n\n\n" +139 "### Input/output types\n\n```\n list<any> | list<any>\n table | table\n\n```\n### Example(s)\n```\n```\n" +140 " Sort files by modified date.\n```\n ls | sort-by modified\n\n"141142// The caret resting on a command says its signature on the context143// line; F1 opens its help, and Esc goes back to the cell being edited.144// F1 where there's no command opens the shortcuts.145func TestNotebookWordHelp(t *testing.T) {146 nu := &ideNu{fakeNu: &fakeNu{out: map[string]string{}}, help: sortByHelp}147 m := newModel()148 m.SetShellRunner(nu)149 m.SetMachine("here")150 m.nb.still = true151 m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})152 run(m, m.runCommand("nb.open"))153 press(t, m, "b", "<enter>", "ls | sort-by name -r", "<home>", "<right>", "<right>", "<right>", "<right>", "<right>", "<right>")154 endPauses(m)155 if got := line(m, contextLine); !strings.Contains(got, "sort-by <...comparator: cell-path|closure> --reverse") ||156 !strings.Contains(got, "Sort by the given cell path or closure.") || !strings.Contains(got, "F1") {157 t.Fatalf("context line %q", got)158 }159 press(t, m, "<end>")160 endPauses(m)161 if got := line(m, contextLine); !strings.Contains(got, "sort-by --reverse, -r") || !strings.Contains(got, "Sort in reverse order.") {162 t.Errorf("on the flag: %q", got)163 }164 press(t, m, "<f1>")165 if _, ok := m.overlay.(*cmdhelp.View); !ok {166 t.Fatalf("F1 opened %T", m.overlay)167 }168 if s := screen(m); !strings.Contains(s, "https://www.nushell.sh/commands/docs/sort-by.html") || !strings.Contains(s, "-r, --reverse") ||169 !strings.Contains(s, "ls | sort-by modified") || !strings.Contains(line(m, 0), "HELP") {170 t.Errorf("help\n%s", s)171 }172 press(t, m, "<esc>")173 if m.overlay != nil || !m.nbView().Editing() {174 t.Errorf("Esc: overlay %T, editing %v", m.overlay, m.nbView().Editing())175 }176 press(t, m, " ", "<f1>")177 if _, ok := m.overlay.(*shortcuts.View); !ok {178 t.Errorf("F1 on no word opened %T", m.overlay)179 }180}