- commit
- 6fb7f09d6b9afd480871dfb9968761afc1c40903
- parent
- efbed527a23a21792eb5d163ac1ead7138961e22
- parent
- 6c03099a895951ce3844d7178f97887bf1693d6b
- author
- Dami <dami.etoile@gmail.com>
@@ -22,17 +22,6 @@ The grid is Excel's, 1,048,576 x 16,384, and memory is bounded by the
2222 rises in measured steps; see [Bounds of support](docs/contributing/limits.md#what-would-raise-the-bounds).2323 Tables past it are read in place as [linked sources](docs/files/sources.md).2424 25-### 2. A TUI Jupyter for nushell26-27-Notebook tabs are a TUI Jupyter for nushell28-([Notebooks](docs/nushell/notebooks.md)): code and note cells, outputs in29-place, sent to sheets as live regions, and cells highlighted, checked and30-completed by nu as they're written.31-32-| Item | Result | Size |33-|---|---|---|34-| A command's signature and description for the word at the caret, on the context line, through `nu --ide-hover` or `nu --lsp` | Writing a cell without leaving for `help` | S |35-3625 ## Shelved3726 3827 Explored, not scheduled; the fit is still open.@@ -118,6 +107,7 @@ needs cgo.
118107 - Notebook tabs, a TUI Jupyter for nushell: code and note cells with Jupyter's keys, outputs drawn in place and opened full-screen, `$name` between cells, stale marks and a reactive mode, outputs sent to sheets as live regions (`nu.name`), saved up to a cap, and earlier notebook sheets converted on open: [Notebooks](docs/nushell/notebooks.md)119108 - Cells run as streams: a pipeline that never ends (`tail -f`, `watch`) followed live, its rows reaching the output and its sheet as nu prints them: [Notebooks](docs/nushell/notebooks.md#streams)120109 - Code cells highlighted, checked and completed as they're written by nu itself (`--ide-ast`, `--ide-check`, `--ide-complete`), in the background, falling back to 012's own when nu is missing, old, slow or not trusted: [Notebooks](docs/nushell/notebooks.md#writing-a-cell)110+- The word at the caret on the context line: a command's signature and summary (`--ide-hover`), F1 for its whole help and docs page, and a `$name`'s cell and shape: [Notebooks](docs/nushell/notebooks.md#writing-a-cell)121111 - Table and record outputs drawn and worked as 012's own grid: formats by type, fitted and resizable columns, select, copy, sort, filter and find in place, full-screen, charts and pivots on the sheet the output is sent to: [Notebooks](docs/nushell/notebooks.md#outputs-as-grids)122112 123113 **Files**@@ -29,7 +29,7 @@ saved and saves them again, byte for byte
2929 ([File fixtures](releasing.md#file-fixtures)).3030 3131 `TestRandomEdits` (`internal/sheet/randedit_test.go`) checks what holds32-between features: from each of a thousand seeds it builds a workbook32+between features: from each of 3,000 seeds it builds a workbook3333 of two sheets and a notebook tab, makes up to 40 edits drawn from the3434 seed (entries, pastes, fills, series, sorts, inserted and deleted rows3535 and columns, formats, borders, merges, moves, notes, rules, names,@@ -43,7 +43,7 @@ scratch), and recalculating everything in place changes no value.
4343 Outputs' rows come from outside the undo history, so the test sends4444 them again after each edit, undo and reopening, as the UI does. A4545 failure names the fewest of the seed's edits that still fail. It takes46-about a second; `-randedit.seeds=20000` runs more, and46+a few seconds; `-randedit.seeds=20000` runs more, and4747 `FuzzRandomEdits` (in `make fuzz`) lets the fuzzer choose the edits.4848 4949 `TestRandomSharedEdits` (`randedit_shared_test.go`) makes the same@@ -70,7 +70,10 @@ was typed first, so a sheet looks the same when opened again. An array that woul
7070 spill into cells its formula reads, `=SORT(B8:D9)` in A9, is a circular7171 dependency and shows `#REF!` too, as are arrays that would spill into7272 each other's inputs, even when one of them, blocked and read as73-`#REF!`, would come out smaller and free the other.73+`#REF!`, would come out smaller and free the other, or is blocked by a74+value only at the size the other's spill gives it. So is an array that75+would spill over a notebook output or linked file whose cells it reads:76+the output would give way, and its `#REF!` free the array again.7477 7578 ## Names in a formula: LET and LAMBDA7679 Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
@@ -241,6 +241,7 @@ delete, copy, cut, move, make notes or code, hide or clear outputs.
241241 | Up, Down, Home, End | Move by the lines on screen, wrapped lines too |242242 | Ctrl+A, Ctrl+E | The start, the end of the line |243243 | Tab | Complete the word at the caret: a cell's `$name`, `$selection`, a linked file, then what nu completes there (commands, flags, paths); one completion goes in at once ([Writing a cell](#writing-a-cell)) |244+| F1 | The help of the command at the caret, with its page in nushell's docs ([Writing a cell](#writing-a-cell)) |244245 245246 A terminal without the kitty keyboard protocol sends Shift+Enter and246247 Ctrl+Enter as Enter ([Keys the terminal has to tell@@ -291,6 +292,31 @@ it the context line says what nu said:
291292 292293 Tab asks `nu --ide-complete` too, after the notebook's own names, so it293294 completes flags, subcommands and paths as well as cells and commands.295+296+Once the caret rests on a word, the context line says what it is, so297+writing a cell doesn't mean leaving it for `help`. On a command, `nu298+--ide-hover` gives its signature (its parameters with their types,299+then its flags) and what it does; on a flag, what the flag does. Where300+the line is short, the flags give way first, then the types, then the301+end of the description. A problem at the caret takes the line instead.302+303+304+305+306+F1 opens the command's whole help over the notebook: what it does, a307+link to its page in nushell's docs, its usage, flags, parameters, input308+and output types and examples. Up and Down scroll it, Esc goes back to309+the cell. F1 on a word that isn't a command opens the keyboard shortcuts,310+as it does anywhere else.311+312+313+314+315+On a `$name` another cell binds, the line says the cell and the shape316+of its value as it last ran (`$files: table, 12 rows × 4 columns from317+cell 3`), from the notebook's own outputs without asking nu; on318+`$selection`, a linked file or `$sheet.A1:C9`, what it reads; on a319+variable the cell binds itself with `let`, the type nu gives it.294320 nu doesn't know the variables a notebook binds, so it's asked about the295321 cell with `$name` for each name the cells assign and each linked file,296322 `$selection` and `$sheet` declared before it; a `$sheet.A1:C9` range@@ -189,6 +189,7 @@ on a sheet, `!` starts an entry as any character does.
189189 | Esc | Back to command mode, keeping what was typed |190190 | Shift+Enter, Ctrl+Enter, Alt+Enter | Run, as in command mode |191191 | Enter, Tab | A new line; complete the word at the caret, with what nu completes there ([Writing a cell](../nushell/notebooks.md#writing-a-cell)) |192+| F1 | The help of the nushell command at the caret, with its page in nushell's docs; the shortcuts where there's none |192193 | Up, Down, Home, End, Ctrl+A, Ctrl+E | Move by the lines on screen; the start, the end of the line |193194 | Word keys | As [anywhere text is edited](#entering-data); Ctrl+U deletes to the start of the line, not the cell |194195 @@ -6,6 +6,9 @@ import (
66 "path/filepath"77 "strings"88 "testing"9+ "unicode/utf8"10+11+ ghostty "go.mitchellh.com/libghostty"912 )1013 1114 // Notebooks: cells written and run with the real nu, one reading@@ -101,6 +104,32 @@ func TestNotebookHighlightsAndCompletesWithNu(t *testing.T) {
101104 s.waitFor("sort-by size --reverse")102105 }103106 107+// With the real nu, the caret resting on a command says its signature108+// on the context line, and on a flag what the flag does; F1 opens the109+// command's help, with its page in nushell's docs as a link. On a110+// cell's $name, the line says the cell and its output's shape.111+func TestNotebookHoverWithNu(t *testing.T) {112+ needNu(t)113+ s := startWith(t, options{cols: 120, rows: 30, startsOn: "EDIT"}, "nu")114+ s.keys("n = [[k v]; [1 a] [2 b] [3 c]]", "<shift+enter>")115+ s.waitFor("Out[1]:")116+ s.keys("<enter>", "$n | sort-by k --reverse", "<home>")117+ s.waitFor("$n: table, 3 rows × 2 columns from cell 1")118+ s.keys("<right>", "<right>", "<right>", "<right>", "<right>", "<right>")119+ s.waitFor("sort-by <...comparator: cell-path|closure>")120+ s.waitFor("Sort by the given cell path or closure.")121+ s.keys("<end>")122+ s.waitFor("sort-by --reverse, -r")123+ s.keys("<f1>")124+ s.waitFor("Docs: https://www.nushell.sh/commands/docs/sort-by.html")125+ s.waitFor("HELP")126+ if got := s.linkAt("https://www.nushell.sh"); got != "https://www.nushell.sh/commands/docs/sort-by.html" {127+ t.Errorf("the docs link to %q", got)128+ }129+ s.keys("<esc>")130+ s.waitFor("EDIT")131+}132+104133 // Data > Shell from a workbook makes a Notebook tab; $selection is105134 // the range selected on the sheet shown before it.106135 func TestNotebookReadsSelection(t *testing.T) {@@ -143,3 +172,20 @@ func TestNotebookLongPipeline(t *testing.T) {
143172 t.Errorf("the pipeline isn't whole after editing:\n%s", s.screen())144173 }145174 }175+176+// linkAt is the hyperlink under the first cell of text on the screen.177+func (s *session) linkAt(text string) string {178+ for y, line := range strings.Split(s.screen(), "\n") {179+ if i := strings.Index(line, text); i >= 0 {180+ s.mu.Lock()181+ defer s.mu.Unlock()182+ ref, err := s.vt.GridRef(ghostty.Point{Tag: ghostty.PointTagActive, X: uint16(utf8.RuneCountInString(line[:i])), Y: uint32(y)})183+ if err != nil {184+ return ""185+ }186+ u, _ := ref.HyperlinkURI()187+ return u188+ }189+ }190+ return ""191+}@@ -118,6 +118,20 @@ var notebookScreens = []screen{
118118 s.keys("<enter>", "$files | sort-by size --revrse")119119 s.waitFor("doesn't have flag `revrse`")120120 }},121+ {name: "notebook-nu-hover", setup: func(s *session) {122+ filesNotebook(s)123+ s.keys("<enter>", "$files | sort-by size --reverse", "<home>")124+ s.keys(strings.Split(strings.Repeat("<right>,", 9), ",")[:9]...)125+ s.waitFor("Sort by the given")126+ }},127+ {name: "notebook-nu-help", setup: func(s *session) {128+ filesNotebook(s)129+ s.keys("<enter>", "$files | sort-by size --reverse", "<home>")130+ s.keys(strings.Split(strings.Repeat("<right>,", 9), ",")[:9]...)131+ s.waitFor("Sort by the given")132+ s.keys("<f1>")133+ s.waitFor("Docs:")134+ }},121135 {name: "notebook-select", opts: options{cols: 120, rows: 30}, setup: func(s *session) {122136 filesNotebook(s)123137 s.keys("<up>", "<up>", "<shift+up>")@@ -36,6 +36,8 @@ var docScreens = []docScreen{
3636 {name: "palette", from: "palette", cols: 80, rows: 18},3737 {name: "notebook-cells", from: "notebook-cells", cols: 80, rows: 26},3838 {name: "notebook-nu-error", from: "notebook-nu-error", cols: 80, rows: 16},39+ {name: "notebook-nu-hover", from: "notebook-nu-hover", cols: 100, rows: 16},40+ {name: "notebook-nu-help", from: "notebook-nu-help", cols: 100, rows: 24},3941 {name: "notebook-grid", from: "notebook-grid", cols: 80, rows: 24},4042 {name: "share-presence", from: "share-presence", cols: 80, rows: 13},4143 {name: "live-suggestion", from: "live-suggestion", cols: 80, rows: 12},@@ -8,8 +8,8 @@ for arg; do script=$arg; done
88 # The code editor's questions (--ide-ast and the rest, about the file99 # named last) are answered as nu 0.116 answered them about the same1010 # text, recorded in ide/: name.nu, and its name.ast.json,11-# name.check.jsonl or name.complete.json. Text not recorded fails, and12-# 012 falls back to its own highlighting.11+# name.check.jsonl, name.complete.json or name.hover-<byte>.json. Text12+# not recorded fails, and 012 falls back to its own highlighting.1313 case " $* " in1414 *' --version '*)1515 echo 0.116.0@@ -17,6 +17,12 @@ case " $* " in
1717 *' --ide-ast '*) answer=ast.json ;;1818 *' --ide-check '*) answer=check.jsonl ;;1919 *' --ide-complete '*) answer=complete.json ;;20+*' --ide-hover '*)21+ prev=22+ for arg; do23+ [ "$prev" = --ide-hover ] && answer=hover-$arg.json24+ prev=$arg25+ done ;;2026 esac2127 if [ -n "$answer" ]; then2228 for nu in "$(dirname "$0")"/ide/*.nu; do@@ -0,0 +1,24 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐</span><span style="color:var(--vt-palette-0)">e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">Docs:</span> <span style="color:var(--vt-palette-4);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</span> <span style="color:var(--vt-palette-8)">│</span>9+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-4)">sort-by</span> {<span style="color:var(--vt-palette-4)">flags</span>} <span style="font-weight:bold"><</span>...comparator> <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</span> <span style="color:var(--vt-palette-8)">│</span>19+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof<cell-path,</span> <span style="font-weight:bold">closure></span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-19</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">42</span> <span style="color:var(--vt-palette-8)">─┘</span> <span style="color:var(--vt-palette-2)">▶</span>23+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>24+Sort by the given cell path or closure. <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">scroll</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file@@ -0,0 +1,24 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Docs:</span> <span style="color:var(--vt-palette-12);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</span> <span style="color:var(--vt-palette-8)">│</span>9+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-6)">sort-by</span> {<span style="color:var(--vt-palette-6)">flags</span>} <span style="font-weight:bold"><</span>...comparator> <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</span> <span style="color:var(--vt-palette-8)">│</span>19+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof<cell-path,</span> <span style="font-weight:bold">closure></span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-19</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">42</span> <span style="color:var(--vt-palette-8)">─┘</span> <span style="color:var(--vt-palette-2)">▶</span>23+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>24+Sort by the given cell path or closure. <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -0,0 +1,16 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">EDIT</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶ Run <span style="color:var(--vt-palette-8)">■</span> <span style="color:var(--vt-palette-8)">Stop</span> ↻ Restart ▶▶ Run all <span style="color:var(--vt-palette-8)">│</span> + Add ✂ Cut ⧉ Copy <span style="color:var(--vt-palette-8)">⎘</span> <span style="color:var(--vt-palette-8)">Paste</span> <span style="color:var(--vt-palette-8)">│</span> Code ▾ <span style="color:var(--vt-palette-0)">nu</span> <span style="color:var(--vt-palette-0)">○</span> <span style="color:var(--vt-palette-0)">idle</span>3+ sort-by <...comparator: cell-path|closure> … <span style="color:var(--vt-palette-0)">Sort</span> <span style="color:var(--vt-palette-0)">by</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">given</span> <span style="color:var(--vt-palette-0)">cell</span> <span style="color:var(--vt-palette-0)">path</span> <span style="color:var(--vt-palette-0)">or</span> <span style="color:var(--vt-palette-0)">closure.</span> <span style="color:var(--vt-palette-8)">──</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F1</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">help</span>4+5+ <span style="color:var(--vt-palette-8)">╭──────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">✓</span> <span style="color:var(--vt-palette-0)"><1s</span> <span style="color:var(--vt-palette-8)">─╮</span>6+ <span style="color:var(--vt-palette-0)">[2]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">where</span> size <span style="font-weight:bold">></span> <span style="color:var(--vt-palette-1)">1kb</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>7+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>8+ <span style="color:var(--vt-palette-0)">Out[2]:</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">name</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">type</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">size</span><span style="background:var(--vt-palette-7)"> </span>9+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> 012 file 4.2 kB10+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> README.md file 9.8 kB11+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> demos dir 3.1 kB12+13+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>14+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-0)">[</span> <span style="color:var(--vt-palette-0)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>15+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>16+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">❯</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Notebook</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file@@ -0,0 +1,16 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">EDIT</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶ Run <span style="color:var(--vt-palette-8)">■</span> <span style="color:var(--vt-palette-8)">Stop</span> ↻ Restart ▶▶ Run all <span style="color:var(--vt-palette-8)">│</span> + Add ✂ Cut ⧉ Copy <span style="color:var(--vt-palette-8)">⎘</span> <span style="color:var(--vt-palette-8)">Paste</span> <span style="color:var(--vt-palette-8)">│</span> Code ▾ <span style="color:var(--vt-palette-8)">nu</span> <span style="color:var(--vt-palette-8)">○</span> <span style="color:var(--vt-palette-8)">idle</span>3+ sort-by <...comparator: cell-path|closure> … <span style="color:var(--vt-palette-8)">Sort</span> <span style="color:var(--vt-palette-8)">by</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">given</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="color:var(--vt-palette-8)">path</span> <span style="color:var(--vt-palette-8)">or</span> <span style="color:var(--vt-palette-8)">closure.</span> <span style="color:var(--vt-palette-8)">──</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F1</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">help</span>4+5+ <span style="color:var(--vt-palette-8)">╭──────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">✓</span> <span style="color:var(--vt-palette-8)"><1s</span> <span style="color:var(--vt-palette-8)">─╮</span>6+ <span style="color:var(--vt-palette-8)">[2]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">where</span> size <span style="font-weight:bold">></span> <span style="color:var(--vt-palette-3)">1kb</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>7+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>8+ <span style="color:var(--vt-palette-8)">Out[2]:</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">name</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">type</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">size</span><span style="background:var(--vt-palette-8)"> </span>9+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> 012 file 4.2 kB10+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> README.md file 9.8 kB11+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> demos dir 3.1 kB12+13+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>14+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">[</span> <span style="color:var(--vt-palette-8)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>15+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>16+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">❯</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Notebook</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file@@ -14,5 +14,5 @@
1414 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as HTML <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">231</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">232</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 Start a new, empty sheet <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">move</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Enter</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">run</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file@@ -14,5 +14,5 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as HTML <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 Start a new, empty sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">move</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">run</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -25,6 +25,6 @@
2525 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">21</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Deselect <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>2626 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">22</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select all <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+A</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>2727 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">23</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select columns <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+Space</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>28-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-25</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">132</span> <span style="color:var(--vt-palette-8)">─┘</span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-25</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">133</span> <span style="color:var(--vt-palette-8)">─┘</span>2929 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">25</span><span style="background:var(--vt-palette-7)"> </span>3030 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">scroll</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file@@ -11,6 +11,6 @@
1111 <span style="color:var(--vt-palette-8)">│</span> Last used cell <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+End</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>1212 <span style="color:var(--vt-palette-8)">│</span> Go to <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+G</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F5</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>1313 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>14-<span style="color:var(--vt-palette-8)">└───────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-11</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">132</span> <span style="color:var(--vt-palette-8)">─┘</span>14+<span style="color:var(--vt-palette-8)">└───────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-11</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">133</span> <span style="color:var(--vt-palette-8)">─┘</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -40,6 +40,6 @@
4040 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">36</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Date <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Shift+3</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>4141 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">37</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Notebooks</span> Time <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Shift+2</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>4242 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">38</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move between cells and outputs <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">j</span><span style="background:var(--vt-palette-8)"> </span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">k</span><span style="background:var(--vt-palette-8)"> </span> Bold <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+B</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>43-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">39</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-40</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">66</span> <span style="color:var(--vt-palette-8)">─┘</span>43+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">39</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-40</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">67</span> <span style="color:var(--vt-palette-8)">─┘</span>4444 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">40</span><span style="background:var(--vt-palette-8)"> </span>4545 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -25,6 +25,6 @@
2525 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Deselect <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>2626 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select all <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+A</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>2727 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Select columns <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Space</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>28-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-25</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">132</span> <span style="color:var(--vt-palette-8)">─┘</span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-25</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">133</span> <span style="color:var(--vt-palette-8)">─┘</span>2929 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">25</span><span style="background:var(--vt-palette-8)"> </span>3030 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">F</span><span style="color:#ffffff;background:#1f1f1f">ile</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">E</span><span style="color:#ffffff;background:#1f1f1f">dit</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">V</span><span style="color:#ffffff;background:#1f1f1f">iew</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">I</span><span style="color:#ffffff;background:#1f1f1f">nsert</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">F</span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">o</span><span style="color:#ffffff;background:#1f1f1f">rmat</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">D</span><span style="color:#ffffff;background:#1f1f1f">ata</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">H</span><span style="color:#ffffff;background:#1f1f1f">elp</span><span style="background:#1f1f1f"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">HELP</span><span style="background:#40e0ff"> </span>2+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▶</span><span style="color:#757575;background:#000000">┌─</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">sort-by</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">────────────────────────────────────────────────────────────────────────────────────┐</span><span style="color:#959595;background:#000000">e</span><span style="background:#000000"> </span>3+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>4+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">by</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">the</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">given</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cell</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">path</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">or</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">closure.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>5+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>6+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Docs:</span><span style="background:#000000"> </span><span style="color:#a0c0ff;background:#000000;text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>7+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>8+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Usage</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">▶</span>9+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">sort-by</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">{</span><span style="color:#40e0ff;background:#000000">flags</span><span style="color:#ffffff;background:#000000">}</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold"><</span><span style="color:#ffffff;background:#000000">...comparator></span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>10+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>11+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Flags</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>12+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-r,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--reverse</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">in</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">reverse</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">order.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>13+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-i,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--ignore-case</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">string-based</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">data</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">case-insensitively.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>14+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-n,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--natural</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">alphanumeric</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">string-based</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">data</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">naturally</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">(1,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">9,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">99,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">100,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">...).</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>15+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">-c,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">--custom</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Use</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">closures</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">specify</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">a</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">custom</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">sort</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">order,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">rather</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">than</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">compute</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">a</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>16+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">comparison</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">key.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>17+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>18+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Parameters</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">▶</span>19+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">...comparator:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">oneof<cell-path,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">closure></span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">The</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cell</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">path(s)</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">or</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">closure(s)</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">compare</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>20+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">elements</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">by.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>21+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>22+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Input</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">and</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">output</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>23+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">list<any></span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">|</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">list<any></span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>24+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">record</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">|</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">table</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>25+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">table</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">|</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">table</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>26+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">▶</span>27+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">Examples</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span>28+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">└─────────────────────────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">1-25</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">of</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">42</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">─┘</span><span style="background:#000000"> </span>29+<span style="background:#000000"> </span>30+<span style="color:#ffffff;background:#1f1f1f">Sort</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">by</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">the</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">given</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">cell</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">path</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">or</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">closure.</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Up/Down</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">scroll</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Esc</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">close</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐</span><span style="color:var(--vt-palette-0)">e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">Docs:</span> <span style="color:var(--vt-palette-4);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>9+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-4)">sort-by</span> {<span style="color:var(--vt-palette-4)">flags</span>} <span style="font-weight:bold"><</span>...comparator> <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>19+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof<cell-path,</span> <span style="font-weight:bold">closure></span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Input</span> <span style="font-weight:bold">and</span> <span style="font-weight:bold">output</span> <span style="color:var(--vt-palette-8)">│</span>23+ <span style="color:var(--vt-palette-8)">│</span> list<any> | list<any> <span style="color:var(--vt-palette-8)">│</span>24+ <span style="color:var(--vt-palette-8)">│</span> record | table <span style="color:var(--vt-palette-8)">│</span>25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> table | table <span style="color:var(--vt-palette-8)">│</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Examples</span> <span style="color:var(--vt-palette-8)">│</span>28+ <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">1-25</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">42</span> <span style="color:var(--vt-palette-8)">─┘</span>29+30+Sort by the given cell path or closure. <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">scroll</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">HELP</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶<span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">sort-by</span> <span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────┐e</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>4+ <span style="color:var(--vt-palette-8)">│</span> Sort by the given cell path or closure. <span style="color:var(--vt-palette-8)">│</span>5+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>6+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Docs:</span> <span style="color:var(--vt-palette-12);text-decoration:underline">https://www.nushell.sh/commands/docs/sort-by.html</span> <span style="color:var(--vt-palette-8)">│</span>7+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>8+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Usage</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>9+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-6)">sort-by</span> {<span style="color:var(--vt-palette-6)">flags</span>} <span style="font-weight:bold"><</span>...comparator> <span style="color:var(--vt-palette-8)">│</span>10+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>11+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Flags</span> <span style="color:var(--vt-palette-8)">│</span>12+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-r,</span> <span style="font-weight:bold">--reverse</span> Sort in reverse order. <span style="color:var(--vt-palette-8)">│</span>13+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-i,</span> <span style="font-weight:bold">--ignore-case</span> Sort string-based data case-insensitively. <span style="color:var(--vt-palette-8)">│</span>14+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-n,</span> <span style="font-weight:bold">--natural</span> Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...). <span style="color:var(--vt-palette-8)">│</span>15+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">-c,</span> <span style="font-weight:bold">--custom</span> Use closures to specify a custom sort order, rather than to compute a <span style="color:var(--vt-palette-8)">│</span>16+ <span style="color:var(--vt-palette-8)">│</span> comparison key. <span style="color:var(--vt-palette-8)">│</span>17+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>18+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Parameters</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>19+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">...comparator:</span> <span style="font-weight:bold">oneof<cell-path,</span> <span style="font-weight:bold">closure></span> The cell path(s) or closure(s) to compare <span style="color:var(--vt-palette-8)">│</span>20+ <span style="color:var(--vt-palette-8)">│</span> elements by. <span style="color:var(--vt-palette-8)">│</span>21+ <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span>22+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Input</span> <span style="font-weight:bold">and</span> <span style="font-weight:bold">output</span> <span style="color:var(--vt-palette-8)">│</span>23+ <span style="color:var(--vt-palette-8)">│</span> list<any> | list<any> <span style="color:var(--vt-palette-8)">│</span>24+ <span style="color:var(--vt-palette-8)">│</span> record | table <span style="color:var(--vt-palette-8)">│</span>25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> table | table <span style="color:var(--vt-palette-8)">│</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">Examples</span> <span style="color:var(--vt-palette-8)">│</span>28+ <span style="color:var(--vt-palette-8)">└─────────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">1-25</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">42</span> <span style="color:var(--vt-palette-8)">─┘</span>29+30+Sort by the given cell path or closure. <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">scroll</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">F</span><span style="color:#ffffff;background:#1f1f1f">ile</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">E</span><span style="color:#ffffff;background:#1f1f1f">dit</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">V</span><span style="color:#ffffff;background:#1f1f1f">iew</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">I</span><span style="color:#ffffff;background:#1f1f1f">nsert</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">F</span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">o</span><span style="color:#ffffff;background:#1f1f1f">rmat</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">D</span><span style="color:#ffffff;background:#1f1f1f">ata</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">H</span><span style="color:#ffffff;background:#1f1f1f">elp</span><span style="background:#1f1f1f"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">EDIT</span><span style="background:#40e0ff"> </span>2+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▶</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Run</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">■</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">Stop</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">↻</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Restart</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▶▶</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Run</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">all</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">+</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Add</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">✂</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Cut</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">⧉</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Copy</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">⎘</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">Paste</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">Code</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">▾</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">nu</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">○</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">idle</span><span style="background:#000000"> </span>3+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">sort-by</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000"><...comparator:</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cell-path|closure></span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">…</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Sort</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">by</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">the</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">given</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">cell</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">path</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">or</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">closure.</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">──</span><span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">F1</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">help</span><span style="background:#000000"> </span>4+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold;text-decoration:underline">Files</span><span style="background:#000000"> </span>5+<span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">What's</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">big</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">in</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">the</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">repo,</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">from</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">ls</span><span style="color:#ffffff;background:#000000">.</span><span style="background:#000000"> </span>6+<span style="background:#000000"> </span>7+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╭─</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">files</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">──────────────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">✓</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000"><1s</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">─╮</span><span style="background:#000000"> </span>8+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">[1]:</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ff80ff;background:#000000">files</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">=</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">ls</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">▶</span>9+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╰──────────────────────────────────────────────────────────────────────────────────────╯</span><span style="background:#000000"> </span>10+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">Out[1]:</span><span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">name</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">type</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">size</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span>11+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">012</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4.2</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>12+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">2</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">CLAUDE.md</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1.0</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>13+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">3</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">README.md</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">9.8</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>14+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">4</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">demos</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">dir</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">3.1</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>15+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">5</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">go.mod</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">812</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">B</span><span style="background:#000000"> </span>16+<span style="background:#000000"> </span>17+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╭──────────────────────────────────────────────────────────────────────────────</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">✓</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000"><1s</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">─╮</span><span style="background:#000000"> </span>18+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">[2]:</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#ff80ff;background:#000000">$files</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">|</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">where</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">size</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">></span><span style="background:#000000"> </span><span style="color:#ffe040;background:#000000">1kb</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">▶</span>19+<span style="background:#000000"> </span><span style="color:#757575;background:#000000">╰──────────────────────────────────────────────────────────────────────────────────────╯</span><span style="background:#000000"> </span>20+<span style="background:#000000"> </span><span style="color:#959595;background:#000000">Out[2]:</span><span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">name</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">type</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">size</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span>21+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">012</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4.2</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>22+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">2</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">README.md</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">file</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">9.8</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>23+<span style="background:#000000"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">3</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">demos</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">dir</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">3.1</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">kB</span><span style="background:#000000"> </span>24+<span style="background:#000000"> </span>25+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span><span style="background:#000000"> </span>26+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">[</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">]:</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┃</span><span style="background:#000000"> </span><span style="color:#ff80ff;background:#000000">$files</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">|</span><span style="background:#000000"> </span><span style="color:#40e0ff;background:#000000">sort-by</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">size</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">--reverse</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┃</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">▶</span>27+<span style="color:#40e040;background:#000000">▌</span><span style="background:#000000"> </span><span style="color:#40e040;background:#000000">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span><span style="background:#000000"> </span>28+<span style="background:#000000"> </span>29+<span style="background:#000000"> </span>30+<span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">❯</span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">Notebook</span><span style="background:#40e0ff"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">+</span><span style="background:#1f1f1f"> </span><span style="color:#757575;background:#1f1f1f">│</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">untitled</span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">modified</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Ctrl+K</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">search</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Ctrl+/</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">shortcuts</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">F10</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">menu</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">EDIT</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶ Run <span style="color:var(--vt-palette-8)">■</span> <span style="color:var(--vt-palette-8)">Stop</span> ↻ Restart ▶▶ Run all <span style="color:var(--vt-palette-8)">│</span> + Add ✂ Cut ⧉ Copy <span style="color:var(--vt-palette-8)">⎘</span> <span style="color:var(--vt-palette-8)">Paste</span> <span style="color:var(--vt-palette-8)">│</span> Code ▾ <span style="color:var(--vt-palette-0)">nu</span> <span style="color:var(--vt-palette-0)">○</span> <span style="color:var(--vt-palette-0)">idle</span>3+ sort-by <...comparator: cell-path|closure> … <span style="color:var(--vt-palette-0)">Sort</span> <span style="color:var(--vt-palette-0)">by</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">given</span> <span style="color:var(--vt-palette-0)">cell</span> <span style="color:var(--vt-palette-0)">path</span> <span style="color:var(--vt-palette-0)">or</span> <span style="color:var(--vt-palette-0)">closure.</span> <span style="color:var(--vt-palette-8)">──</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F1</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">help</span>4+ <span style="font-weight:bold;text-decoration:underline">Files</span>5+ What's big in the repo, from <span style="color:var(--vt-palette-4)">ls</span>.6+7+ <span style="color:var(--vt-palette-8)">╭─</span> <span style="color:var(--vt-palette-0)">files</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">✓</span> <span style="color:var(--vt-palette-0)"><1s</span> <span style="color:var(--vt-palette-8)">─╮</span>8+ <span style="color:var(--vt-palette-0)">[1]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">files</span> <span style="font-weight:bold">=</span> <span style="color:var(--vt-palette-4)">ls</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>9+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>10+ <span style="color:var(--vt-palette-0)">Out[1]:</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">name</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">type</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">size</span><span style="background:var(--vt-palette-7)"> </span>11+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> 012 file 4.2 kB12+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> CLAUDE.md file 1.0 kB13+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> README.md file 9.8 kB14+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> demos dir 3.1 kB15+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> go.mod file 812 B16+17+ <span style="color:var(--vt-palette-8)">╭──────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">✓</span> <span style="color:var(--vt-palette-0)"><1s</span> <span style="color:var(--vt-palette-8)">─╮</span>18+ <span style="color:var(--vt-palette-0)">[2]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">where</span> size <span style="font-weight:bold">></span> <span style="color:var(--vt-palette-1)">1kb</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">▶</span>19+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>20+ <span style="color:var(--vt-palette-0)">Out[2]:</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">name</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">type</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">size</span><span style="background:var(--vt-palette-7)"> </span>21+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> 012 file 4.2 kB22+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> README.md file 9.8 kB23+ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> demos dir 3.1 kB24+25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-0)">[</span> <span style="color:var(--vt-palette-0)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-4)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>28+29+30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">❯</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Notebook</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">EDIT</span><span style="background:var(--vt-palette-6)"> </span>2+ ▶ Run <span style="color:var(--vt-palette-8)">■</span> <span style="color:var(--vt-palette-8)">Stop</span> ↻ Restart ▶▶ Run all <span style="color:var(--vt-palette-8)">│</span> + Add ✂ Cut ⧉ Copy <span style="color:var(--vt-palette-8)">⎘</span> <span style="color:var(--vt-palette-8)">Paste</span> <span style="color:var(--vt-palette-8)">│</span> Code ▾ <span style="color:var(--vt-palette-8)">nu</span> <span style="color:var(--vt-palette-8)">○</span> <span style="color:var(--vt-palette-8)">idle</span>3+ sort-by <...comparator: cell-path|closure> … <span style="color:var(--vt-palette-8)">Sort</span> <span style="color:var(--vt-palette-8)">by</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">given</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="color:var(--vt-palette-8)">path</span> <span style="color:var(--vt-palette-8)">or</span> <span style="color:var(--vt-palette-8)">closure.</span> <span style="color:var(--vt-palette-8)">──</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F1</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">help</span>4+ <span style="font-weight:bold;text-decoration:underline">Files</span>5+ What's big in the repo, from <span style="color:var(--vt-palette-6)">ls</span>.6+7+ <span style="color:var(--vt-palette-8)">╭─</span> <span style="color:var(--vt-palette-8)">files</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">✓</span> <span style="color:var(--vt-palette-8)"><1s</span> <span style="color:var(--vt-palette-8)">─╮</span>8+ <span style="color:var(--vt-palette-8)">[1]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">files</span> <span style="font-weight:bold">=</span> <span style="color:var(--vt-palette-6)">ls</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>9+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>10+ <span style="color:var(--vt-palette-8)">Out[1]:</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">name</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">type</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">size</span><span style="background:var(--vt-palette-8)"> </span>11+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> 012 file 4.2 kB12+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> CLAUDE.md file 1.0 kB13+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> README.md file 9.8 kB14+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> demos dir 3.1 kB15+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> go.mod file 812 B16+17+ <span style="color:var(--vt-palette-8)">╭──────────────────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">✓</span> <span style="color:var(--vt-palette-8)"><1s</span> <span style="color:var(--vt-palette-8)">─╮</span>18+ <span style="color:var(--vt-palette-8)">[2]:</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">where</span> size <span style="font-weight:bold">></span> <span style="color:var(--vt-palette-3)">1kb</span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">▶</span>19+ <span style="color:var(--vt-palette-8)">╰──────────────────────────────────────────────────────────────────────────────────────╯</span>20+ <span style="color:var(--vt-palette-8)">Out[2]:</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">name</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">type</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">size</span><span style="background:var(--vt-palette-8)"> </span>21+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> 012 file 4.2 kB22+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> README.md file 9.8 kB23+ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> demos dir 3.1 kB24+25+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓</span>26+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-8)">[</span> <span style="color:var(--vt-palette-8)">]:</span> <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-5)">$files</span> <span style="font-weight:bold">|</span> <span style="color:var(--vt-palette-6)">sort-by</span> <span style="color:var(--vt-palette-2)">size</span> --reverse <span style="color:var(--vt-palette-2)">┃</span> <span style="color:var(--vt-palette-2)">▶</span>27+<span style="color:var(--vt-palette-2)">▌</span> <span style="color:var(--vt-palette-2)">┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛</span>28+29+30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">❯</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Notebook</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file@@ -12,5 +12,5 @@
1212 <span style="color:var(--vt-palette-8)">│</span> Sort <span style="color:var(--vt-palette-3);font-weight:bold">s</span>heet Z to <span style="color:var(--vt-palette-3);font-weight:bold">A</span> <span style="color:var(--vt-palette-8)">│</span>1313 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Read</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">source</span> <span style="color:var(--vt-palette-8)">again</span> <span style="color:var(--vt-palette-8)">│</span>1414 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Ask</span> <span style="color:var(--vt-palette-8)">JEV</span> <span style="color:var(--vt-palette-8)">again</span> <span style="color:var(--vt-palette-8)">│</span>15-<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">68</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>15+<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">68</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1616 Save the sheet u… <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">move</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">run</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Reset <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umn width <span style="color:var(--vt-palette-0)">Format</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">11</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Insert <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umns right <span style="color:var(--vt-palette-0)">Insert</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">12</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Delete rows or <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umns <span style="color:var(--vt-palette-0)">Edit</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+Alt+-</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">48</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">48</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span>1919 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span>2020 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span>@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Reset <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umn width <span style="color:var(--vt-palette-8)">Format</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Insert <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umns right <span style="color:var(--vt-palette-8)">Insert</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Delete rows or <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umns <span style="color:var(--vt-palette-8)">Edit</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+Alt+-</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">48</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">48</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-3);font-weight:bold">S</span>av<span style="color:var(--vt-palette-3);font-weight:bold">e</span> as <span style="color:var(--vt-palette-8)">Fi</span><span style="color:var(--vt-palette-3);font-weight:bold">l</span><span style="color:var(--vt-palette-8)">e</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Code</span> <span style="color:var(--vt-palette-8)">Data</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Shell</span> <span style="color:var(--vt-palette-8)">y</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Link a <span style="color:var(--vt-palette-3);font-weight:bold">s</span>ourc<span style="color:var(--vt-palette-3);font-weight:bold">e</span> <span style="color:var(--vt-palette-8)">Data</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-3);font-weight:bold">L</span><span style="color:var(--vt-palette-8)">inked</span> <span style="color:var(--vt-palette-8)">file</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">63</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">63</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>@@ -14,7 +14,7 @@
1414 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1515 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>1616 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Download as HTML <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>17-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">231</span> <span style="color:var(--vt-palette-8)">─┘</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">232</span> <span style="color:var(--vt-palette-8)">─┘</span>1818 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>1919 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>2020 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>@@ -0,0 +1,285 @@
1+package nushell2+3+import (4+ "cmp"5+ "context"6+ "encoding/json"7+ "fmt"8+ "strconv"9+ "strings"10+)11+12+// What nu says of the word at a byte of a script, through13+//14+// nu --ide-hover 7 file {"hover":"Sort by the given cell path or closure.\n### Usage\n...","span":{"start":5,"end":12}}15+//16+// A command's hover is its help in Markdown (Help reads it), a17+// variable's is its type ("int"), a flag's is just "flag", and a byte18+// on no word prints nothing. nu --lsp answers the same through the19+// language server's protocol; a process per question, as the other20+// questions are, needs no session with it.21+22+// Hover is nu's text about the word at a byte, and the word's bytes.23+type Hover struct {24+ From, To int25+ Text string26+}27+28+// HoverAt is what nu says of the word at offset, a byte of script;29+// false when it's on none.30+func HoverAt(ctx context.Context, r Runner, script string, offset int) (Hover, bool, error) {31+ out, err := ask(ctx, r, script, "--ide-hover", strconv.Itoa(offset))32+ if err != nil {33+ return Hover{}, false, err34+ }35+ return ParseHover(out)36+}37+38+// ParseHover reads what --ide-hover printed.39+func ParseHover(out []byte) (Hover, bool, error) {40+ if len(out) == 0 {41+ return Hover{}, false, nil42+ }43+ var h struct {44+ Hover *string45+ Span *span46+ }47+ if err := json.Unmarshal(out, &h); err != nil || h.Hover == nil || h.Span == nil {48+ return Hover{}, false, fmt.Errorf("%w: %s", ErrOld, firstLine(out))49+ }50+ return Hover{From: h.Span.Start, To: h.Span.End, Text: *h.Hover}, true, nil51+}52+53+// Help is a command's help, as its hover has it.54+type Help struct {55+ Name string // sort-by, str join56+ Desc string // what it does, its paragraphs57+ Usage string // sort-by {flags} <...comparator>58+ // Flags are its flags but --help; Params its positional59+ // parameters, in order.60+ Flags []Flag61+ Params []Param62+ // Types are its input and output types, "table | table".63+ Types []string64+ Examples []Example65+}66+67+// Flag is a command's flag: -r, --reverse, the type of its value if it68+// takes one, and what it does.69+type Flag struct {70+ Short, Long, Arg, Desc string71+}72+73+// Param is a positional parameter: comparator, its type as nu writes it74+// (oneof<cell-path, closure>), and what it is.75+type Param struct {76+ Name, Type, Desc string77+}78+79+// Example is one of a command's examples: what it does, and the code.80+type Example struct {81+ Desc, Code string82+}83+84+// ParseHelp reads a command's hover; false when it isn't one (a85+// variable's type, a flag's "flag").86+func ParseHelp(md string) (Help, bool) {87+ desc, rest, ok := strings.Cut(md, "### Usage")88+ if !ok {89+ return Help{}, false90+ }91+ h := Help{Desc: strings.TrimSpace(desc)}92+ for _, sec := range strings.Split("### Usage"+rest, "\n### ") {93+ head, body, _ := strings.Cut(strings.TrimPrefix(sec, "### "), "\n")94+ switch strings.TrimSpace(head) {95+ case "Usage":96+ h.Usage = strings.TrimSpace(strings.Trim(strings.TrimSpace(body), "`"))97+ h.Name = commandName(h.Usage)98+ case "Flags":99+ h.Flags = parseFlags(body)100+ case "Parameters":101+ h.Params = parseParams(body)102+ case "Input/output types":103+ h.Types = fenced(body)104+ case "Example(s)":105+ h.Examples = parseExamples(body)106+ }107+ }108+ return h, h.Name != ""109+}110+111+// commandName is the words of usage before its flags and parameters.112+func commandName(usage string) string {113+ var words []string114+ for w := range strings.FieldsSeq(usage) {115+ if strings.ContainsAny(w[:1], "{<(.[") {116+ break117+ }118+ words = append(words, w)119+ }120+ return strings.Join(words, " ")121+}122+123+// fenced are the lines of body's first code fence, trimmed.124+func fenced(body string) []string {125+ var out []string126+ in := false127+ for line := range strings.SplitSeq(body, "\n") {128+ t := strings.TrimSpace(line)129+ switch {130+ case strings.HasPrefix(t, "```") && in:131+ return out132+ case strings.HasPrefix(t, "```"):133+ in = true134+ case in && t != "":135+ out = append(out, t)136+ }137+ }138+ return out139+}140+141+// ticked splits `-r`, `--reverse` `<int>` - Sort in reverse order.\142+// into its backquoted parts and what follows " - ".143+func ticked(line string) ([]string, string) {144+ line = strings.TrimSuffix(strings.TrimSpace(line), "\\")145+ var parts []string146+ for strings.HasPrefix(line, "`") {147+ end := strings.Index(line[1:], "`")148+ if end < 0 {149+ break150+ }151+ parts = append(parts, line[1:end+1])152+ line = strings.TrimLeft(line[end+2:], ", ")153+ }154+ return parts, strings.TrimSpace(strings.TrimPrefix(line, "- "))155+}156+157+func parseFlags(body string) []Flag {158+ var out []Flag159+ for line := range strings.SplitSeq(body, "\n") {160+ parts, desc := ticked(line)161+ var f Flag162+ for _, p := range parts {163+ switch {164+ case strings.HasPrefix(p, "--"):165+ f.Long = p166+ case strings.HasPrefix(p, "-"):167+ f.Short = p168+ case strings.HasPrefix(p, "<"):169+ f.Arg = strings.Trim(p, "<>")170+ }171+ }172+ if (f.Long != "" || f.Short != "") && f.Long != "--help" {173+ f.Desc = desc174+ out = append(out, f)175+ }176+ }177+ return out178+}179+180+func parseParams(body string) []Param {181+ var out []Param182+ for line := range strings.SplitSeq(body, "\n") {183+ parts, desc := ticked(line)184+ if len(parts) == 0 {185+ continue186+ }187+ name, typ, _ := strings.Cut(parts[0], ":")188+ out = append(out, Param{Name: strings.TrimSpace(name), Type: strings.TrimSpace(typ), Desc: desc})189+ }190+ return out191+}192+193+// parseExamples reads the examples: an empty fence, then each194+// example's description outside a fence and its code inside one.195+func parseExamples(body string) []Example {196+ segs := strings.Split(body, "```")197+ var out []Example198+ for i := 2; i+1 < len(segs); i += 2 {199+ desc := strings.TrimSpace(segs[i])200+ var code []string201+ for line := range strings.SplitSeq(strings.Trim(segs[i+1], "\n"), "\n") {202+ code = append(code, strings.TrimPrefix(line, " "))203+ }204+ c := strings.TrimSpace(strings.Join(code, "\n"))205+ if desc != "" || c != "" {206+ out = append(out, Example{Desc: desc, Code: c})207+ }208+ }209+ return out210+}211+212+// Summary is the first paragraph of the description.213+func (h Help) Summary() string {214+ first, _, _ := strings.Cut(h.Desc, "\n\n")215+ return strings.Join(strings.Fields(first), " ")216+}217+218+// Signature is the command's usage with its parameters' types and its219+// flags by name: sort-by <...comparator: cell-path|closure> --reverse.220+func (h Help) Signature() string {221+ sig := []string{h.Name}222+ rest := strings.TrimSpace(strings.TrimPrefix(h.Usage, h.Name))223+ for w := range strings.FieldsSeq(rest) {224+ if w == "{flags}" {225+ continue226+ }227+ if p, ok := h.param(w); ok && p.Type != "" {228+ w = w[:len(w)-1] + ": " + typeWords(p.Type) + w[len(w)-1:]229+ }230+ sig = append(sig, w)231+ }232+ for _, f := range h.Flags {233+ name := cmp.Or(f.Long, f.Short)234+ if f.Arg != "" {235+ name += " <" + f.Arg + ">"236+ }237+ sig = append(sig, name)238+ }239+ return strings.Join(sig, " ")240+}241+242+// Brief is the signature where there's no room for it: the usage243+// without types, and … for its flags: sort-by <...comparator> ….244+func (h Help) Brief() string {245+ brief := strings.Join(strings.Fields(strings.Replace(h.Usage, "{flags}", "", 1)), " ")246+ if len(h.Flags) > 0 {247+ brief += " …"248+ }249+ return brief250+}251+252+// param is the parameter usage's word w names: <...comparator>,253+// <rows?>.254+func (h Help) param(w string) (Param, bool) {255+ name := strings.Trim(w, "<>()[]?.")256+ for _, p := range h.Params {257+ if strings.Trim(p.Name, ".?") == name {258+ return p, true259+ }260+ }261+ return Param{}, false262+}263+264+// typeWords writes oneof<int, filesize> as int|filesize.265+func typeWords(t string) string {266+ if in, ok := strings.CutPrefix(t, "oneof<"); ok {267+ return strings.ReplaceAll(strings.TrimSuffix(in, ">"), ", ", "|")268+ }269+ return t270+}271+272+// Flag is the command's flag named w, long (--reverse) or short (-r).273+func (h Help) Flag(w string) (Flag, bool) {274+ for _, f := range h.Flags {275+ if w != "" && (f.Long == w || f.Short == w) {276+ return f, true277+ }278+ }279+ return Flag{}, false280+}281+282+// DocsURL is the page of nushell's documentation for command name.283+func DocsURL(name string) string {284+ return "https://www.nushell.sh/commands/docs/" + strings.ReplaceAll(name, " ", "_") + ".html"285+}@@ -0,0 +1,97 @@
1+package nushell2+3+import (4+ "context"5+ "errors"6+ "os/exec"7+ "slices"8+ "strings"9+ "testing"10+)11+12+// hoverAsked are the bytes of testdata/ide/hover.nu that hover-*.json13+// record nu's hover at.14+var hoverAsked = map[string]int{"sort-by": 17, "flag": 30, "into-string": 45, "variable": 76, "none": 15}15+16+func TestParseHover(t *testing.T) {17+ src := string(recorded(t, "hover.nu"))18+ for name, want := range map[string]string{"sort-by": "sort-by", "flag": "--reverse", "into-string": "into string", "variable": "$n"} {19+ h, ok, err := ParseHover(recorded(t, "hover-"+name+".json"))20+ if err != nil || !ok || src[h.From:h.To] != want {21+ t.Errorf("%s: %+v %v %v", name, h, ok, err)22+ }23+ }24+ if h, ok, err := ParseHover(recorded(t, "hover-variable.json")); !ok || err != nil || h.Text != "int" {25+ t.Errorf("a variable's type: %q", h.Text)26+ }27+ if _, ok, err := ParseHover(recorded(t, "hover-none.json")); ok || err != nil {28+ t.Errorf("no word: %v %v", ok, err)29+ }30+ if _, _, err := ParseHover([]byte(`{"other": 1}`)); !errors.Is(err, ErrOld) {31+ t.Errorf("unreadable answer: %v", err)32+ }33+}34+35+func TestParseHelp(t *testing.T) {36+ h, _, _ := ParseHover(recorded(t, "hover-sort-by.json"))37+ help, ok := ParseHelp(h.Text)38+ if !ok || help.Name != "sort-by" || help.Summary() != "Sort by the given cell path or closure." {39+ t.Fatalf("help %+v", help)40+ }41+ if got := help.Signature(); got != "sort-by <...comparator: cell-path|closure> --reverse --ignore-case --natural --custom" {42+ t.Errorf("signature %q", got)43+ }44+ if got := help.Brief(); got != "sort-by <...comparator> …" {45+ t.Errorf("brief %q", got)46+ }47+ if f, ok := help.Flag("-r"); !ok || f.Long != "--reverse" || f.Desc != "Sort in reverse order." {48+ t.Errorf("-r: %+v", f)49+ }50+ if _, ok := help.Flag("--help"); ok {51+ t.Error("--help is every command's")52+ }53+ if !slices.Equal(help.Types, []string{"list<any> | list<any>", "record | table", "table | table"}) {54+ t.Errorf("types %q", help.Types)55+ }56+ if len(help.Examples) != 6 || help.Examples[0] != (Example{"Sort files by modified date.", "ls | sort-by modified"}) {57+ t.Errorf("examples %q", help.Examples)58+ }59+ h, _, _ = ParseHover(recorded(t, "hover-into-string.json"))60+ help, _ = ParseHelp(h.Text)61+ if got := help.Signature(); got != "into string <...rest: cell-path> --group-digits --decimals <int>" {62+ t.Errorf("signature %q", got)63+ }64+ if help.Name != "into string" || DocsURL(help.Name) != "https://www.nushell.sh/commands/docs/into_string.html" {65+ t.Errorf("docs of %q: %s", help.Name, DocsURL(help.Name))66+ }67+ if _, ok := ParseHelp("int"); ok {68+ t.Error("a variable's type read as help")69+ }70+}71+72+// A command of the script's own, without a description.73+func TestParseHelpOfADef(t *testing.T) {74+ help, ok := ParseHelp("\n### Usage\n```\n greet {flags} <name>\n```\n\n### Flags\n\n `--loud`\\\n `-h`, `--help` - Display the help message for this command\n\n### Parameters\n\n `name: string`\n\n")75+ if !ok || help.Desc != "" || help.Signature() != "greet <name: string> --loud" {76+ t.Errorf("help %+v %q", help, help.Signature())77+ }78+}79+80+// The real nu, when it's the version recorded, says what hover-*.json81+// record.82+func TestNuHover(t *testing.T) {83+ if _, err := exec.LookPath("nu"); err != nil {84+ t.Skip("nu isn't installed")85+ }86+ if v, err := Version(context.Background(), Nu{}); err != nil || v != strings.TrimSpace(string(recorded(t, "version.txt"))) {87+ t.Skipf("nu %s %v isn't the version recorded", v, err)88+ }89+ src := string(recorded(t, "hover.nu"))90+ for name, at := range hoverAsked {91+ got, gotOK, err := HoverAt(context.Background(), Nu{}, src, at)92+ want, wantOK, _ := ParseHover(recorded(t, "hover-"+name+".json"))93+ if err != nil || got != want || gotOK != wantOK {94+ t.Errorf("%s: nu says %+v %v %v, recorded %+v", name, got, gotOK, err, want)95+ }96+ }97+}@@ -0,0 +1 @@
1+{"hover":"flag","span":{"start":29,"end":38}}File mode 100644
@@ -0,0 +1 @@
1+{"hover":"int","span":{"start":75,"end":77}}@@ -298,10 +298,9 @@ func (s *Sheet) pasteMerges(carried []Rect, p pasteLayout) {
298298 if len(carried) == 0 {299299 return300300 }301- next := slices.DeleteFunc(slices.Clone(s.view.merges), func(m Rect) bool {302- _, overlaps := intersectRect(m, p.dst)303- return overlaps304- })301+ // The merges it replaces may reach past it: arrays they blocked302+ // there spill again too.303+ next, span := s.mergesOutside(p.dst)305304 for tr := range p.down {306305 for tc := range p.across {307306 at := Addr{Col: p.dst.From.Col + tc*p.tw, Row: p.dst.From.Row + tr*p.th}@@ -312,7 +311,7 @@ func (s *Sheet) pasteMerges(carried []Rect, p pasteLayout) {
312311 }313312 }314313 }315- s.setMerges(next, p.dst)314+ s.setMerges(next, span)316315 }317316 318317 // moveMerges moves the merges wholly inside src on from to dst on s, as@@ -69,3 +69,21 @@ func TestUnmergeFreesArrayPastTheRange(t *testing.T) {
6969 t.Errorf("H12 once the merge went = %v", v)7070 }7171 }72+73+// A paste carrying merges replaces those it lands over, whole: an array74+// that one blocked past the paste spills again.75+func TestPastedMergeFreesAnArrayPastIt(t *testing.T) {76+ s := New()77+ if err := s.Merge(NewRect(at("A1"), at("A2")), MergeAll); err != nil {78+ t.Fatal(err)79+ }80+ if err := s.Merge(NewRect(at("B14"), at("C14")), MergeAll); err != nil {81+ t.Fatal(err)82+ }83+ s.Set(at("B14"), "=SEQUENCE(2)")84+ wantShown(t, s, map[string]string{"B14": "#REF!"})85+ if _, err := s.Paste(s.Copy(NewRect(at("A1"), at("A2"))), NewRect(at("C13"), at("C14")), false); err != nil {86+ t.Fatal(err)87+ }88+ wantShown(t, s, map[string]string{"B14": "1", "B15": "2"})89+}@@ -26,10 +26,10 @@ import (
2626 // recalculation agreeing with the incremental one);2727 // - recalculates in place, everything at once, to the same values.2828 //29-// go test runs a fixed set of seeds in a second or two; -randedit.seeds29+// go test runs a fixed set of seeds in a few seconds; -randedit.seeds3030 // runs more, and FuzzRandomEdits lets the fuzzer choose the edits.3131 var (32- randSeeds = flag.Int("randedit.seeds", 1000, "seeds TestRandomEdits runs")32+ randSeeds = flag.Int("randedit.seeds", 3000, "seeds TestRandomEdits runs")3333 randSteps = flag.Int("randedit.steps", 40, "edits per seed")3434 )3535 @@ -132,3 +132,59 @@ func (s *Sheet) wakeAllRegions() {
132132 }133133 }134134 }135+136+// table is the cells the region with meta me takes: its table shown or,137+// blocked, the one it needs; none before its rows first arrive.138+func (me *regionMeta) table() (Rect, bool) {139+ switch {140+ case me == nil || !me.has:141+ return Rect{}, false142+ case me.why != "":143+ return me.need, true144+ }145+ return me.written, true146+}147+148+// regionMoved returns the anchors of the arrays over the region's149+// table, before and after it changed, spilling or blocked by a cycle,150+// to compute again: an array reading the table's cells is blocked by151+// it (spillsIntoItself) wherever the table goes, which the cells it152+// reads may not tell it.153+func (s *Sheet) regionMoved(before, after Rect, had bool) []loc {154+ var out []loc155+ for b, sp := range s.spills {156+ if (sp.why == "" || sp.circular) && (had && overlaps(sp.area, before) || overlaps(sp.area, after)) {157+ sp.stale = true158+ out = append(out, loc{s, b})159+ }160+ }161+ return out162+}163+164+// heldByArrays is the region r's table, when an array spilling over it165+// decides whether it shows: shown, or blocked by arrays alone, which it166+// would show without.167+func (s *Sheet) heldByArrays(r Region) (Rect, bool) {168+ me := s.regionMeta[nameKey(r.Name)]169+ t, ok := me.table()170+ if !ok || me.why == "" {171+ return t, ok172+ }173+ if t.To.Row >= MaxRows || t.To.Col >= MaxCols {174+ return Rect{}, false175+ }176+ for a := range s.cells.anyKeysIn(t) {177+ if a == r.At || !s.cells.filledAt(a) {178+ continue179+ }180+ if _, spilled := s.SpillAnchor(a); !spilled || s.cells.derivedAt(a) != slotSpill {181+ return Rect{}, false182+ }183+ }184+ for _, q := range s.regions.list {185+ if q.Name != r.Name && t.Contains(q.At) {186+ return Rect{}, false187+ }188+ }189+ return t, true190+}@@ -101,3 +101,47 @@ func TestPasteIntoARegionsBlankCell(t *testing.T) {
101101 apply(t, s, op)102102 wantShown(t, s, map[string]string{"A1": "#REF!", "A2": "", "B2": "9"})103103 }104+105+// An array that would spill over a region's table whose cells it reads106+// is a circular dependency: spilling, it would block the region and107+// read its #REF!, come out smaller and free it. It shows #REF! and the108+// region its table, whichever came first, and opened again.109+func TestArrayOverRegionItReadsTheCellsOf(t *testing.T) {110+ op := LiveOp{Region: "a", Reset: true, Header: liveRow("x", "y"), Rows: []LiveRow{liveRow("1", "2")}}111+ feeds := map[string]LiveOp{"a": op}112+ want := map[string]string{"I9": "#REF!", "H10": "x", "I11": "2"}113+ for _, arrayFirst := range []bool{false, true} {114+ s := New()115+ sent(t, s, "a", "H10")116+ if !arrayFirst {117+ apply(t, s, op)118+ }119+ s.Set(at("I9"), "=SORT(H9:H11)")120+ if arrayFirst {121+ apply(t, s, op)122+ }123+ feedStale(s.Book(), feeds)124+ if stale := s.Book().StaleOutputs(); len(stale) > 0 {125+ t.Errorf("array first %v: still stale %v", arrayFirst, stale)126+ }127+ wantShown(t, s, want)128+ opened := roundTrip(t, s)129+ feedAll(opened.Book(), feeds)130+ feedStale(opened.Book(), feeds)131+ wantShown(t, opened, want)132+ }133+}134+135+// A region blocked by a formula and an array reading its cells, the formula136+// cleared, is blocked by the array alone, which then finds the cycle.137+func TestRegionFreedButForAnArrayReadingIt(t *testing.T) {138+ op := LiveOp{Region: "a", Reset: true, Header: liveRow("x", "y", "z"), Rows: []LiveRow{liveRow("1", "2", "3")}}139+ s := New()140+ sent(t, s, "a", "F10")141+ s.Set(at("F11"), "=SEQUENCE(1)")142+ s.Set(at("H9"), "=SORT(F6:F10)")143+ apply(t, s, op)144+ s.Set(at("F11"), "")145+ feedStale(s.Book(), map[string]LiveOp{"a": op})146+ wantShown(t, s, map[string]string{"H9": "#REF!", "F10": "x", "H11": "3"})147+}@@ -76,6 +76,9 @@ func (s *Sheet) writeRegions() []loc {
7676 for k, me := range s.regionMeta {7777 if s.regionIndex(k) < 0 {7878 changed = append(changed, s.clearOwned(me, noRect)...)79+ if t, ok := me.table(); ok {80+ changed = append(changed, s.regionMoved(t, noRect, true)...)81+ }7982 delete(s.regionMeta, k)8083 s.wakeAllRegions()8184 for u := range s.wb.nameUsers[nameKey(regionPrefix+k)] {@@ -97,6 +100,9 @@ func (s *Sheet) writeRegions() []loc {
97100 // naming it read #REF! until then, so they're among the cells returned.98101 func (s *Sheet) emptyRegion(r Region, me *regionMeta) []loc {99102 changed := s.clearOwned(me, noRect)103+ if t, ok := me.table(); ok {104+ changed = append(changed, s.regionMoved(t, noRect, true)...)105+ }100106 if me.has {101107 changed = s.regionReaders(r, changed)102108 }@@ -115,6 +121,7 @@ func (s *Sheet) emptyRegion(r Region, me *regionMeta) []loc {
115121 func (s *Sheet) writeTable(r Region, me *regionMeta, header LiveRow, rows []LiveRow, reset bool) []loc {116122 cols, nrows := tableSize(me, header, rows, reset)117123 was, had, shape := me.written, me.has, [2]int{me.rows, me.cols}124+ before, _ := me.table()118125 need := s.tableArea(r, cols, nrows)119126 changed := s.makeRoomAt(r)120127 whole := me.why != "" || me.placed // it may not hold every cell it wrote@@ -160,6 +167,11 @@ func (s *Sheet) writeTable(r Region, me *regionMeta, header LiveRow, rows []Live
160167 for _, c := range changed {161168 s.freedFor(c.a)162169 }170+ // Blocked, what's in its way may have gone but for arrays, which then171+ // find it (heldByArrays).172+ if after, _ := me.table(); !had || after != before || me.why != "" {173+ changed = append(changed, s.regionMoved(before, after, had)...)174+ }163175 if !had || was != me.written || shape != [2]int{me.rows, me.cols} { // moved, or its table changed shape164176 changed = s.regionReaders(r, changed)165177 }@@ -154,6 +154,9 @@ func (s *Sheet) applySpill(a Addr, p pendingSpill) ([]loc, bool) {
154154 changed = s.wb.recheckArrays(loc{s, a})155155 }156156 if why != "" {157+ if !circ && s.wb.blockedMoved(old, area) {158+ changed = append(changed, s.wb.recheckArrays(loc{s, a})...)159+ }157160 changed = append(changed, s.dropSpill(a)...)158161 s.setSpill(a, &spill{area: area, why: why, circular: circ})159162 c.Value = ErrRef@@ -167,9 +170,9 @@ func (s *Sheet) applySpill(a Addr, p pendingSpill) ([]loc, bool) {
167170 changed = append(changed, s.regionsGiveWay(area)...)168171 if old != nil && old.why == "" {169172 changed = append(changed, s.clearSpilled(old.area, area)...)170- if old.area != area {171- changed = append(changed, s.wb.recheckCircular()...)172- }173+ }174+ if old != nil && old.area != area {175+ changed = append(changed, s.wb.recheckCircular()...)173176 }174177 s.setSpill(a, &spill{area: area, arr: p.arr, auto: c.auto})175178 c.Value = p.top@@ -19,7 +19,13 @@ import "slices"
1919 // (regionsGiveWay), so what reads it by name reads #REF! once the array2020 // spills, whatever the array holds: the walk doesn't follow it, or the2121 // array would be blocked or not by whether the region's rows arrived22-// before it spilled.22+// before it spilled. A formula reading the table's cells, though, leads23+// the walk from each of them, those outside area too: spilling empties24+// them, the anchor showing #REF!, which may make the array smaller and25+// free the region, spilling again once it's sent its rows, without26+// end. The table is the one shown or, blocked by arrays alone, the one27+// it needs, so the array is blocked either way (regionMoved has it28+// checked again).2329 func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {2430 if area.From == area.To {2531 return "", nil@@ -30,13 +36,18 @@ func (s *Sheet) spillsIntoItself(a Addr, area Rect) (string, []loc) {
3036 if w.cells(s, area, a, loc{}, true) {3137 return circular(w.origin), nil3238 }39+ for _, r := range s.regions.list {40+ if t, ok := s.heldByArrays(r); ok && overlaps(t, area) && w.cells(s, t, a, loc{}, true) {41+ return "Circular dependency: the array would spill over " + r.Name + "'s table, whose cells its formula reads", nil42+ }43+ }3344 for i := 0; i < len(w.queue); i++ {3445 u := w.queue[i]3546 w.origin, w.parent = w.nodes[i].from, i3647 // An array's cells first, so a cycle through them is found as3748 // one, and the array blocked with it, rather than through the3849 // anchor alone.39- if sp := u.s.spills[u.a]; sp != nil && (sp.why == "" || sp.circular) {50+ if sp := u.s.spills[u.a]; sp != nil && sp.walked() {4051 w.cells(u.s, sp.area, u.a, u, false)4152 }4253 if !w.found {@@ -101,6 +112,25 @@ func (w *Workbook) cycleMoved(circ bool, old *spill) bool {
101112 return false102113 }103114 115+// blockedMoved reports whether the arrays are to be checked again, an116+// array whose spill was old found blocked over area by something other117+// than a cycle: counted as spilling over the cells it needs (walked), it118+// may close or open a cycle through arrays that read them, when it's119+// blocked over other cells, or blocked still, the first time in the120+// recalculation: a formula on the way around a cycle through it may121+// have changed, which the others aren't computed again for, as in122+// cycleMoved.123+func (w *Workbook) blockedMoved(old *spill, area Rect) bool {124+ switch {125+ case old == nil || old.why == "" || old.area != area:126+ return true127+ case !w.spillRechecked:128+ w.spillRechecked = true129+ return true130+ }131+ return false132+}133+104134 // noteCycle counts the array at l found blocked by a cycle, from spilling105135 // or not, in the recalculation.106136 func (w *Workbook) noteCycle(l loc) {@@ -230,13 +260,13 @@ func (w *Workbook) recheckCircular() []loc {
230260 }231261 232262 // forgetSpill drops the anchor at a's spill for good, its formula233-// computing one value or none, returning the cells that changed. When a234-// cycle blocked it, it counted as spilling over the cells it needed, so263+// computing one value or none, returning the cells that changed. When264+// blocked, it counted as spilling over the cells it needed (walked), so235265 // the arrays blocked by a cycle are checked again.236266 func (s *Sheet) forgetSpill(a Addr) []loc {237267 old := s.spills[a]238268 changed := s.dropSpill(a)239- if old != nil && old.circular {269+ if old != nil && old.why != "" {240270 changed = append(changed, s.wb.recheckCircular()...)241271 }242272 return changed@@ -260,3 +290,18 @@ func blockCircular(arrays []loc) []loc {
260290 }261291 return changed262292 }293+294+// walked reports whether spillsIntoItself follows the cells of the295+// array: spilled, or blocked, over the cells it needs, what it would296+// spill over given room. A blocked array whose size follows what it297+// reads may be blocked or free by whether another spills into its298+// inputs, and that other by whether it spills: counted as spilling,299+// the two are blocked together, as when the cycle is found in one go,300+// rather than by the order they were computed in. The cells of an array301+// too big for max-cells aren't walked.302+func (sp *spill) walked() bool {303+ if sp.why == "" || sp.circular {304+ return true305+ }306+ return (sp.area.To.Row-sp.area.From.Row+1)*(sp.area.To.Col-sp.area.From.Col+1) <= MaxCells()307+}@@ -64,3 +64,39 @@ func TestCycleGoneWithAnArray(t *testing.T) {
6464 t.Errorf("C9 opened: %+v", sp)6565 }6666 }67+68+// An array spilling into the inputs of another, blocked by a value but69+// only as wide as it is for the first's spill, and reading what the70+// other needs: counted as spilling, the blocked one closes the cycle,71+// so both are blocked whichever was typed first, as when opened.72+func TestCycleThroughABlockedArray(t *testing.T) {73+ cells := map[string]string{"C9": "1.5", "C13": "=SORT(D19:E20)", "D18": "=SORT(C9:E13)", "E19": "=SEQUENCE(3)"}74+ want := map[string]string{"C13": "#REF!", "D18": "#REF!", "D13": "", "E20": "2"}75+ for _, order := range [][]string{{"C9", "E19", "C13", "D18"}, {"C9", "D18", "C13", "E19"}, {"D18", "C13", "C9", "E19"}} {76+ wantSettled(t, "blocked", order, cells, want)77+ }78+}79+80+// A formula typed last closing a cycle through an array blocked by a81+// value has the arrays checked again, as when the file opens.82+func TestCycleClosedThroughABlockedArray(t *testing.T) {83+ cells := map[string]string{"D9": "=SORT(G15:H19)", "E9": "=COUNTIF(A9:A12,\">2\")", "B16": "=ABS($A$9)+1", "H19": "=SORT(B13:D16)", "H16": "=COUNTIF(H20,\">2\")"}84+ wantSettled(t, "closed", []string{"D9", "E9", "B16", "H19", "H16"}, cells, map[string]string{"D9": "#REF!", "H19": "#REF!"})85+}86+87+// A blocked array that goes, its formula computing one value, frees the88+// arrays whose cycle went through the cells it needed (spill.walked):89+// once Total is defined, B7 reads its own #REF! through G9.90+func TestCycleGoneWithABlockedArray(t *testing.T) {91+ cells := map[string]string{92+ "H9": "=VLOOKUP(G8,G14:G15,1,FALSE)", "E7": "=SORT(F10:G14)", "G9": "=SUM(Total)*2", "B7": "=SORT(G7:H11)",93+ "H5": "=SORT(C4:E8)", "B9": "=VLOOKUP(A13,C14:D18,1,FALSE)", "G18": "=ABS(C14)+1",94+ }95+ s := New()96+ for _, a := range []string{"H9", "E7", "G9", "B7", "H5", "B9"} {97+ s.Set(at(a), cells[a])98+ }99+ s.Book().DefineName("Total", s, NewRect(Addr{}, Addr{Col: 1, Row: 9}))100+ s.Set(at("G18"), cells["G18"])101+ wantShown(t, s, map[string]string{"B7": "#REF!", "H5": ""})102+}@@ -0,0 +1,228 @@
1+// Package cmdhelp is a nushell command's help, F1 on a command in a2+// notebook's cell: a scrollable box over the notebook with what the3+// command does, a link to its page in nushell's docs, its usage, flags,4+// parameters, input and output types and examples, as nu's help has5+// them. It knows the UI only through Host.6+package cmdhelp7+8+import (9+ "strconv"10+ "strings"11+12+ tea "charm.land/bubbletea/v2"13+ "github.com/charmbracelet/x/ansi"14+15+ "github.com/FelineStateMachine/012/internal/nushell"16+ "github.com/FelineStateMachine/012/internal/ui/overlay"17+ "github.com/FelineStateMachine/012/internal/ui/theme"18+)19+20+// Host is what the help needs of the UI.21+type Host interface {22+ Theme() *theme.Theme23+ Size() (width, height int)24+ // Close closes the open overlay.25+ Close()26+ // Code draws a line of nushell highlighted.27+ Code(src string) string28+}29+30+// View is the open help.31+type View struct {32+ h Host33+ help nushell.Help34+ top int35+}36+37+// ID identifies the view's box in mouse events.38+const ID = "cmdhelp"39+40+// maxWidth is the widest the text runs, for reading.41+const maxWidth = 9242+43+// New returns the help of a command, scrolled to the top.44+func New(h Host, help nushell.Help) *View { return &View{h: h, help: help} }45+46+// Top is the first line shown.47+func (s *View) Top() int { return s.top }48+49+func (s *View) Indicator() string { return "HELP" }50+51+// width is the text's width inside the box.52+func (s *View) width() int {53+ w, _ := s.h.Size()54+ return max(min(w-6, maxWidth), 20)55+}56+57+// Lines are the help's lines at the box's width.58+func (s *View) Lines() []string {59+ th := s.h.Theme()60+ w := s.width()61+ hp := s.help62+ var out []string63+ wrap := func(text, indent string) {64+ for line := range strings.SplitSeq(ansi.Wordwrap(text, w-len(indent), ""), "\n") {65+ out = append(out, indent+line)66+ }67+ }68+ heading := func(title string) { out = append(out, "", th.Title.Render(title)) }69+ for _, para := range strings.Split(hp.Desc, "\n\n") {70+ if len(out) > 0 {71+ out = append(out, "")72+ }73+ wrap(strings.Join(strings.Fields(para), " "), "")74+ }75+ if hp.Desc != "" { // a command of the cell's own has no page76+ url := nushell.DocsURL(hp.Name)77+ out = append(out, "", th.Muted.Render("Docs: ")+th.Link.Hyperlink(url).Render(url))78+ }79+ heading("Usage")80+ out = append(out, " "+s.h.Code(hp.Usage))81+ if len(hp.Flags) > 0 {82+ heading("Flags")83+ out = append(out, s.table(flagRows(hp.Flags))...)84+ }85+ if len(hp.Params) > 0 {86+ heading("Parameters")87+ out = append(out, s.table(paramRows(hp.Params))...)88+ }89+ if len(hp.Types) > 0 {90+ heading("Input and output")91+ for _, t := range hp.Types {92+ out = append(out, " "+t)93+ }94+ }95+ if len(hp.Examples) > 0 {96+ heading("Examples")97+ for i, ex := range hp.Examples {98+ if i > 0 {99+ out = append(out, "")100+ }101+ wrap(ex.Desc, " ")102+ for line := range strings.SplitSeq(ex.Code, "\n") {103+ out = append(out, " "+s.h.Code(line))104+ }105+ }106+ }107+ return out108+}109+110+// flagRows are the flags as names and what they do: -r, --reverse.111+func flagRows(flags []nushell.Flag) [][2]string {112+ rows := make([][2]string, len(flags))113+ for i, f := range flags {114+ name := f.Long115+ if f.Short != "" {116+ name = strings.TrimSuffix(f.Short+", "+f.Long, ", ")117+ }118+ if f.Arg != "" {119+ name += " <" + f.Arg + ">"120+ }121+ rows[i] = [2]string{name, f.Desc}122+ }123+ return rows124+}125+126+func paramRows(params []nushell.Param) [][2]string {127+ rows := make([][2]string, len(params))128+ for i, p := range params {129+ rows[i] = [2]string{strings.TrimSuffix(p.Name+": "+p.Type, ": "), p.Desc}130+ }131+ return rows132+}133+134+// table draws names and what they are in two columns, the second135+// wrapped beside the first.136+func (s *View) table(rows [][2]string) []string {137+ th := s.h.Theme()138+ w := s.width()139+ nameW := 0140+ for _, r := range rows {141+ nameW = max(nameW, ansi.StringWidth(r[0]))142+ }143+ nameW = min(nameW, w/2)144+ var out []string145+ for _, r := range rows {146+ name := ansi.Truncate(r[0], nameW, "…")147+ lead := " " + th.Key.Render(name) + strings.Repeat(" ", nameW-ansi.StringWidth(name)+2)148+ pad := strings.Repeat(" ", nameW+4)149+ for i, line := range strings.Split(ansi.Wordwrap(r[1], max(w-nameW-4, 10), ""), "\n") {150+ if i == 0 {151+ out = append(out, lead+line)152+ } else {153+ out = append(out, pad+line)154+ }155+ }156+ }157+ return out158+}159+160+// visible is how many lines fit between the menu bar and the status161+// line.162+func (s *View) visible(total int) int {163+ _, height := s.h.Size()164+ return max(min(total, height-2-2-1), 1)165+}166+167+func (s *View) Layout() []overlay.Box {168+ th := s.h.Theme()169+ width, height := s.h.Size()170+ lines := append([]string{""}, s.Lines()...) // breathing room under the title171+ inner := s.width() + 2172+ n := s.visible(len(lines))173+ s.top = min(max(s.top, 0), max(len(lines)-n, 0))174+ rows := make([]string, n)175+ for i := range rows {176+ line := ""177+ if j := s.top + i; j < len(lines) {178+ line = lines[j]179+ }180+ rows[i] = theme.Cells(th.MenuBar, " "+line, inner)181+ }182+ footer := ""183+ if n < len(lines) {184+ footer = strconv.Itoa(s.top+1) + "-" + strconv.Itoa(s.top+n) + " of " + strconv.Itoa(len(lines))185+ }186+ b := th.Frame(inner, s.help.Name, footer, rows)187+ bw, bh := ansi.StringWidth(b[0]), len(b)188+ return []overlay.Box{{ID: ID, X: (width - bw) / 2, Y: max(overlay.MenuLine+1, (height-1-bh)/2), Lines: b}}189+}190+191+func (s *View) Key(k tea.KeyPressMsg) tea.Cmd {192+ total := len(s.Lines()) + 1193+ page := s.visible(total)194+ switch k.String() {195+ case "up", "k":196+ s.top--197+ case "down", "j":198+ s.top++199+ case "pgup":200+ s.top -= page201+ case "pgdown", "space":202+ s.top += page203+ case "home":204+ s.top = 0205+ case "end":206+ s.top = total207+ case "esc", "enter", "q", "f1":208+ s.h.Close()209+ }210+ s.top = min(max(s.top, 0), max(total-page, 0))211+ return nil212+}213+214+func (s *View) Mouse(e overlay.MouseEvent) tea.Cmd {215+ switch {216+ case e.Kind == overlay.MouseWheel && e.Button == tea.MouseWheelUp:217+ s.top = max(s.top-3, 0)218+ case e.Kind == overlay.MouseWheel && e.Button == tea.MouseWheelDown:219+ s.top += 3 // layout clamps220+ case e.Kind == overlay.MousePress && e.Box != ID:221+ s.h.Close()222+ }223+ return nil224+}225+226+func (s *View) Status() (string, string) {227+ return s.help.Summary(), s.h.Theme().KeyHints("Up/Down", "scroll", "Esc", "close")228+}@@ -0,0 +1,73 @@
1+package cmdhelp2+3+import (4+ "strconv"5+ "strings"6+ "testing"7+8+ tea "charm.land/bubbletea/v2"9+ "github.com/charmbracelet/x/ansi"10+11+ "github.com/FelineStateMachine/012/internal/nushell"12+ "github.com/FelineStateMachine/012/internal/ui/theme"13+)14+15+type fakeHost struct {16+ th theme.Theme17+ w, h int18+ closed bool19+}20+21+func (f *fakeHost) Theme() *theme.Theme { return &f.th }22+func (f *fakeHost) Size() (int, int) { return f.w, f.h }23+func (f *fakeHost) Close() { f.closed = true }24+func (f *fakeHost) Code(src string) string { return src }25+26+var sortBy = nushell.Help{27+ Name: "sort-by", Desc: "Sort by the given cell path or closure.", Usage: "sort-by {flags} <...comparator>",28+ Flags: []nushell.Flag{{Short: "-r", Long: "--reverse", Desc: "Sort in reverse order."},29+ {Short: "-i", Long: "--ignore-case", Desc: "Sort string-based data case-insensitively."}},30+ Params: []nushell.Param{{Name: "...comparator", Type: "oneof<cell-path, closure>", Desc: "The cell path(s) or closure(s) to compare elements by."}},31+ Types: []string{"table | table"},32+ Examples: []nushell.Example{{Desc: "Sort files by modified date.", Code: "ls | sort-by modified"}, {Desc: "Reversed.", Code: "ls | sort-by name -r"}},33+}34+35+func boxText(v *View) []string {36+ lines := v.Layout()[0].Lines37+ out := make([]string, len(lines))38+ for i, l := range lines {39+ out[i] = ansi.Strip(l)40+ }41+ return out42+}43+44+// The help says what the command does, links its docs, and lists its45+// usage, flags, parameters, types and examples, scrolling in a box46+// that fits the screen.47+func TestHelpShowsTheCommand(t *testing.T) {48+ f := &fakeHost{th: theme.New(true), w: 80, h: 16}49+ v := New(f, sortBy)50+ lines := boxText(v)51+ box := strings.Join(lines, "\n")52+ for _, want := range []string{"┌─ sort-by ", "Sort by the given cell path", "Docs: https://www.nushell.sh/commands/docs/sort-by.html", "Usage", "-r, --reverse"} {53+ if !strings.Contains(box, want) {54+ t.Errorf("no %q in\n%s", want, box)55+ }56+ }57+ for _, l := range lines {58+ if w := ansi.StringWidth(l); w > f.w {59+ t.Errorf("a line %d wide: %q", w, l)60+ }61+ }62+ if total := len(v.Lines()) + 1; !strings.Contains(lines[len(lines)-1], "of "+strconv.Itoa(total)) {63+ t.Errorf("the count: %q", lines[len(lines)-1])64+ }65+ v.Key(tea.KeyPressMsg{Code: tea.KeyEnd})66+ if box := strings.Join(boxText(v), "\n"); !strings.Contains(box, "ls | sort-by name -r") {67+ t.Errorf("scrolled to the end\n%s", box)68+ }69+ v.Key(tea.KeyPressMsg{Code: tea.KeyEscape})70+ if !f.closed {71+ t.Error("Esc didn't close")72+ }73+}@@ -7,6 +7,7 @@ import (
77 "github.com/FelineStateMachine/012/internal/sheet"88 "github.com/FelineStateMachine/012/internal/telemetry"99 "github.com/FelineStateMachine/012/internal/ui/choicebar"10+ "github.com/FelineStateMachine/012/internal/ui/cmdhelp"1011 "github.com/FelineStateMachine/012/internal/ui/cmdline"1112 "github.com/FelineStateMachine/012/internal/ui/filterpick"1213 "github.com/FelineStateMachine/012/internal/ui/findbar"@@ -39,6 +40,7 @@ var (
3940 _ sortbar.Host = host{}4041 _ filterpick.Host = host{}4142 _ shortcuts.Host = host{}43+ _ cmdhelp.Host = host{}4244 _ suggest.Host = host{}4345 _ review.Host = host{}4446 )@@ -4,12 +4,16 @@ import (
44 "slices"55 "strings"66 7+ tea "charm.land/bubbletea/v2"8+79 "github.com/FelineStateMachine/012/internal/sheet"10+ "github.com/FelineStateMachine/012/internal/ui/cmdhelp"811 "github.com/FelineStateMachine/012/internal/ui/nbview"912 )1013 1114 // What the code editor knows of nushell: nu itself, asked about the12-// cell being written (nbview.Nu), with the notebook's own words. nu is15+// cell being written (nbview.Nu), with the notebook's own words, and16+// F1's help on the command at the caret. nu is1317 // asked only where a cell could run without asking: not in 012 serve1418 // unless serve-shell allows it, not with shell = off, and not about a1519 // file from another computer until its cells are trusted. Otherwise,@@ -88,3 +92,23 @@ func (m *Model) nbWords(s *sheet.Sheet) []nbview.Word {
8892 }8993 return out9094 }95+96+func init() {97+ register(&command{id: "nb.word_help", macro: macroNever, title: "Help on the command",98+ desc: "Show the whole help of the nushell command or flag at the caret, with its page in nushell's docs",99+ enabled: func(m *Model) bool { v := m.nbView(); return v != nil && v.Editing() },100+ run: func(m *Model) tea.Cmd { return m.nbView().WordHelp() }})101+}102+103+// showWordHelp opens the help of the command at the caret, or the104+// keyboard shortcuts, as F1 anywhere else, when nu has none for it.105+func (m *Model) showWordHelp(msg nbview.HelpMsg) tea.Cmd {106+ if msg.Help == nil {107+ return m.runCommand("help")108+ }109+ m.openOverlay(cmdhelp.New(m.host(), *msg.Help))110+ return nil111+}112+113+// Code draws a line of nushell as a notebook's cells do.114+func (h host) Code(src string) string { return nbview.Highlighted(&h.m.th, src) }@@ -3,9 +3,11 @@ package ui
33 import (44 "bytes"55 "context"6+ "encoding/json"67 "fmt"78 "io"89 "slices"10+ "strconv"911 "strings"1012 "testing"1113 @@ -13,6 +15,8 @@ import (
1315 1416 "github.com/FelineStateMachine/012/internal/nushell"1517 "github.com/FelineStateMachine/012/internal/sheet"18+ "github.com/FelineStateMachine/012/internal/ui/cmdhelp"19+ "github.com/FelineStateMachine/012/internal/ui/shortcuts"1620 )1721 1822 // ideNu is fakeNu answering the code editor's questions too, as nu@@ -22,6 +26,9 @@ type ideNu struct {
2226 *fakeNu2327 ide []string2428 problem string29+ // help is the hover of every command, a command's help as nu30+ // writes it.31+ help string2532 }2633 2734 func (f *ideNu) Run(ctx context.Context, job nushell.Job, script string, stdout io.Writer) error {@@ -33,6 +40,9 @@ func (f *ideNu) Run(ctx context.Context, job nushell.Job, script string, stdout
3340 if i := strings.Index(script, f.problem); job.IDE[0] == "--ide-check" && f.problem != "" && i >= 0 {3441 answer = fmt.Sprintf(`{"type":"diagnostic","severity":"Error","message":"Not a flag.","span":{"start":%d,"end":%d}}`, i, i+len(f.problem))3542 }43+ if job.IDE[0] == "--ide-hover" && f.help != "" {44+ answer = f.hover(script, job.IDE[1])45+ }3646 _, err := io.WriteString(stdout, answer)3747 return err3848 }@@ -92,3 +102,73 @@ func TestNotebookAsksNuAsTrusted(t *testing.T) {
92102 t.Errorf("served without serve-shell: asked %v", got)93103 }94104 }105+106+// hover answers as nu does about the word at byte at of script: a107+// flag's "flag", a command's help.108+func (f *ideNu) hover(script, at string) string {109+ i, _ := strconv.Atoi(at)110+ from, to := i, i111+ for from > 0 && script[from-1] != ' ' && script[from-1] != '\n' {112+ from--113+ }114+ for to < len(script) && script[to] != ' ' {115+ to++116+ }117+ text := f.help118+ switch {119+ case from == to:120+ return ""121+ case script[from] == '-':122+ text = "flag"123+ case script[from] == '$' || script[from] == '|':124+ return ""125+ }126+ b, _ := json.Marshal(map[string]any{"hover": text, "span": map[string]int{"start": from, "end": to}})127+ return string(b)128+}129+130+// sortByHelp is sort-by's hover, as nu 0.116 writes it.131+const sortByHelp = "Sort by the given cell path or closure.\n### Usage\n```\n sort-by {flags} <...comparator>\n```\n\n### Flags\n\n" +132+ " `-h`, `--help` - Display the help message for this command\\\n `-r`, `--reverse` - Sort in reverse order.\\\n" +133+ " `-i`, `--ignore-case` - Sort string-based data case-insensitively.\n\n### Parameters\n\n" +134+ " `...comparator: oneof<cell-path, closure>` - The cell path(s) or closure(s) to compare elements by.\n\n\n" +135+ "### Input/output types\n\n```\n list<any> | list<any>\n table | table\n\n```\n### Example(s)\n```\n```\n" +136+ " Sort files by modified date.\n```\n ls | sort-by modified\n\n"137+138+// The caret resting on a command says its signature on the context139+// line; F1 opens its help, and Esc goes back to the cell being edited.140+// F1 where there's no command opens the shortcuts.141+func TestNotebookWordHelp(t *testing.T) {142+ nu := &ideNu{fakeNu: &fakeNu{out: map[string]string{}}, help: sortByHelp}143+ m := newModel()144+ m.SetShellRunner(nu)145+ m.SetMachine("here")146+ m.nb.still = true147+ m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})148+ run(m, m.runCommand("nb.open"))149+ press(t, m, "b", "<enter>", "ls | sort-by name -r", "<home>", "<right>", "<right>", "<right>", "<right>", "<right>", "<right>")150+ if got := line(m, contextLine); !strings.Contains(got, "sort-by <...comparator: cell-path|closure> --reverse") ||151+ !strings.Contains(got, "Sort by the given cell path or closure.") || !strings.Contains(got, "F1") {152+ t.Fatalf("context line %q", got)153+ }154+ press(t, m, "<end>")155+ if got := line(m, contextLine); !strings.Contains(got, "sort-by --reverse, -r") || !strings.Contains(got, "Sort in reverse order.") {156+ t.Errorf("on the flag: %q", got)157+ }158+ press(t, m, "<f1>")159+ if _, ok := m.overlay.(*cmdhelp.View); !ok {160+ t.Fatalf("F1 opened %T", m.overlay)161+ }162+ if s := screen(m); !strings.Contains(s, "https://www.nushell.sh/commands/docs/sort-by.html") || !strings.Contains(s, "-r, --reverse") ||163+ !strings.Contains(s, "ls | sort-by modified") || !strings.Contains(line(m, 0), "HELP") {164+ t.Errorf("help\n%s", s)165+ }166+ press(t, m, "<esc>")167+ if m.overlay != nil || !m.nbView().Editing() {168+ t.Errorf("Esc: overlay %T, editing %v", m.overlay, m.nbView().Editing())169+ }170+ press(t, m, " ", "<f1>")171+ if _, ok := m.overlay.(*shortcuts.View); !ok {172+ t.Errorf("F1 on no word opened %T", m.overlay)173+ }174+}@@ -268,6 +268,8 @@ func (m *Model) notebookMsg(msg tea.Msg) (tea.Cmd, bool) {
268268 return m.streamPolled(msg), true269269 case nbStreamTickMsg:270270 return m.streamTicked(), true271+ case nbview.HelpMsg:272+ return m.showWordHelp(msg), true271273 }272274 for _, v := range m.nb.views {273275 if cmd, ok := v.Update(msg); ok {@@ -32,6 +32,12 @@ type editor struct {
3232 comp []Completion3333 compSel int3434 stop context.CancelFunc // stops the questions about the text still out35+ // moves counts the caret's moves and the text's changes, so an36+ // answer about a word the caret has left is dropped.37+ moves int38+ hover Hover // what's said of the word at the caret39+ hoverFor string // the text it was said of40+ hoverStop context.CancelFunc3541 }3642 3743 func (e *editor) text() string { return e.area.Text() }@@ -185,7 +191,7 @@ func (v *View) Update(msg tea.Msg) (tea.Cmd, bool) {
185191 }186192 }187193 default:188- return nil, false194+ return v.updateHover(msg)189195 }190196 return nil, true191197 }@@ -197,11 +203,12 @@ func (v *View) changed() tea.Cmd {
197203 v.edit.version++198204 v.edit.comp = nil199205 v.edit.cancel()206+ rest := v.rest()200207 if !v.asksLater() {201- return nil208+ return rest202209 }203210 version := v.edit.version204- return tea.Tick(pause, func(time.Time) tea.Msg { return pausedMsg{view: v, version: version} })211+ return tea.Batch(rest, tea.Tick(pause, func(time.Time) tea.Msg { return pausedMsg{view: v, version: version} }))205212 }206213 207214 // cancel stops the questions about the text being edited that are@@ -211,6 +218,7 @@ func (e *editor) cancel() {
211218 e.stop()212219 e.stop = nil213220 }221+ e.stopHover()214222 }215223 216224 // check asks the highlighter and checker about the text being edited,@@ -0,0 +1,352 @@
1+package nbview2+3+import (4+ "context"5+ "slices"6+ "strings"7+ "time"8+9+ tea "charm.land/bubbletea/v2"10+ "github.com/charmbracelet/x/ansi"11+12+ "github.com/FelineStateMachine/012/internal/notebook"13+ "github.com/FelineStateMachine/012/internal/nuon"14+ "github.com/FelineStateMachine/012/internal/nushell"15+ "github.com/FelineStateMachine/012/internal/ui/lineedit"16+ "github.com/FelineStateMachine/012/internal/ui/theme"17+)18+19+// The word at the caret, on the context line: once the caret rests on20+// a command or a flag, its signature and what it does, as nu's hover21+// says; on a $name the notebook binds, the cell it comes from and the22+// shape of its value, from the notebook's own outputs without asking23+// nu. A problem at the caret wins the line. F1 opens the command's24+// whole help (HelpMsg).25+26+// Hover is what the context line says of a word of a cell's source.27+type Hover struct {28+ From, To int // the word's bytes29+ Text string // sort-by <...comparator: cell-path|closure> --reverse, or $files: table, 12 rows × 4 columns from cell 330+ Desc string // what it does, or ""31+ // Brief is Text where there's no room for it: sort-by <...comparator> ….32+ Brief string33+ // Help is the command's whole help, for F1; nil for a variable.34+ Help *nushell.Help35+}36+37+// Hoverer says what the word at a byte of src is.38+type Hoverer interface {39+ Hover(ctx context.Context, src string, offset int) (Hover, bool)40+}41+42+// HelpMsg asks the UI to show a command's help, or its keyboard43+// shortcuts when Help is nil: F1 on a word nu has no help for.44+type HelpMsg struct{ Help *nushell.Help }45+46+type (47+ // restedMsg is the caret having rested since move moves.48+ restedMsg struct {49+ view *View50+ moves int51+ }52+ // hoveredMsg is the answer about the word at the caret after moves53+ // moves; open says F1 asked, and waits to show its help.54+ hoveredMsg struct {55+ view *View56+ moves int57+ src string58+ hover Hover59+ ok bool60+ open bool61+ }62+)63+64+// rest notes that the caret moved or the text changed: what was asked65+// about the word it was at is stale, and the word it's at is asked66+// about once it rests.67+func (v *View) rest() tea.Cmd {68+ e := &v.edit69+ e.moves++70+ e.stopHover()71+ moves := e.moves72+ return tea.Tick(pause, func(time.Time) tea.Msg { return restedMsg{view: v, moves: moves} })73+}74+75+func (e *editor) stopHover() {76+ if e.hoverStop != nil {77+ e.hoverStop()78+ e.hoverStop = nil79+ }80+}81+82+// updateHover takes the hover's messages.83+func (v *View) updateHover(msg tea.Msg) (tea.Cmd, bool) {84+ switch msg := msg.(type) {85+ case restedMsg:86+ if msg.view != v {87+ return nil, false88+ }89+ if v.edit.on && msg.moves == v.edit.moves {90+ return v.askHover(false), true91+ }92+ case hoveredMsg:93+ if msg.view != v {94+ return nil, false95+ }96+ if !v.edit.on || msg.moves != v.edit.moves {97+ return nil, true98+ }99+ v.edit.hover, v.edit.hoverFor = msg.hover, msg.src100+ if !msg.ok {101+ v.edit.hover = Hover{}102+ }103+ if msg.open {104+ return helpCmd(v.edit.hover.Help), true105+ }106+ default:107+ return nil, false108+ }109+ return nil, true110+}111+112+func helpCmd(h *nushell.Help) tea.Cmd { return func() tea.Msg { return HelpMsg{Help: h} } }113+114+// askHover asks about the word at the caret: a $name of the notebook's115+// at once, anything else in the background.116+func (v *View) askHover(open bool) tea.Cmd {117+ e := &v.edit118+ src, off, moves := e.text(), e.area.Offset(), e.moves119+ at, ok := wordAt(src, off)120+ if !ok || !v.editingCode() {121+ e.hover = Hover{}122+ if open {123+ return helpCmd(nil)124+ }125+ return nil126+ }127+ if h, ok := v.cellHover(src, at); ok {128+ e.hover, e.hoverFor = h, src129+ if open {130+ return helpCmd(nil)131+ }132+ return nil133+ }134+ hv := v.Providers.Hoverer135+ if hv == nil {136+ return helpCmdIf(open)137+ }138+ e.stopHover()139+ ctx, stop := context.WithCancel(context.Background())140+ e.hoverStop = stop141+ return func() tea.Msg {142+ h, ok := hv.Hover(ctx, src, at)143+ if ctx.Err() != nil {144+ return nil145+ }146+ return hoveredMsg{view: v, moves: moves, src: src, hover: h, ok: ok, open: open}147+ }148+}149+150+func helpCmdIf(open bool) tea.Cmd {151+ if open {152+ return helpCmd(nil)153+ }154+ return nil155+}156+157+// editingCode reports whether the cell being edited is code.158+func (v *View) editingCode() bool {159+ for _, c := range v.h.Cells() {160+ if c.ID == v.edit.id {161+ return c.Kind == notebook.Code162+ }163+ }164+ return false165+}166+167+// WordHelp is F1 in edit mode: the help of the command at the caret,168+// asked for if it hasn't been, or the shortcuts when there's none.169+func (v *View) WordHelp() tea.Cmd {170+ if h, ok := v.hovered(); ok && h.Help != nil {171+ return helpCmd(h.Help)172+ }173+ if !v.edit.on {174+ return helpCmd(nil)175+ }176+ return v.askHover(true)177+}178+179+// hovered is what's said of the word at the caret, while the text is180+// what it was said of.181+func (v *View) hovered() (Hover, bool) {182+ e := &v.edit183+ if !e.on || e.hover.Text == "" || e.hoverFor != e.text() {184+ return Hover{}, false185+ }186+ off := e.area.Offset()187+ return e.hover, off >= e.hover.From && off <= e.hover.To188+}189+190+// isWordByte reports whether b is part of a word: not a space, a191+// bracket, a pipe or a quote.192+func isWordByte(b byte) bool { return !strings.ContainsRune(" \t\n|;(){}[],'\"`", rune(b)) }193+194+// wordAt is the byte of src to ask about for the caret at off: the195+// word it's in, or the one it's just after.196+func wordAt(src string, off int) (int, bool) {197+ switch {198+ case off < len(src) && isWordByte(src[off]):199+ return off, true200+ case off > 0 && off <= len(src) && isWordByte(src[off-1]):201+ return off - 1, true202+ }203+ return 0, false204+}205+206+// wordBounds are the bytes of the word holding byte at.207+func wordBounds(src string, at int) (int, int) {208+ from, to := at, at209+ for from > 0 && isWordByte(src[from-1]) {210+ from--211+ }212+ for to < len(src) && isWordByte(src[to]) {213+ to++214+ }215+ return from, to216+}217+218+// varName is the name $name at byte from of src reads, and where it219+// ends: files of $files.name.220+func varName(src string, from int) (string, int) {221+ if from >= len(src) || src[from] != '$' {222+ return "", from223+ }224+ to := from + 1225+ for to < len(src) && (isNameByte(src[to])) {226+ to++227+ }228+ return src[from+1 : to], to229+}230+231+func isNameByte(b byte) bool {232+ return b == '_' || b == '-' || b >= '0' && b <= '9' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= 0x80233+}234+235+// cellHover is what the notebook knows of the $name at byte at: the236+// cell whose output or variable it is, and its value's shape.237+func (v *View) cellHover(src string, at int) (Hover, bool) {238+ from, _ := wordBounds(src, at)239+ name, to := varName(src, from)240+ if name == "" || at >= to {241+ return Hover{}, false242+ }243+ for i, c := range v.h.Cells() {244+ if c.Kind != notebook.Code || c.ID == v.edit.id {245+ continue246+ }247+ var shape string248+ switch {249+ case c.Name() == name:250+ shape = shapeWords(v.shown(c), v.h.Output(c.ID))251+ case slices.Contains(c.Parse().Assigned(), name):252+ shape = varShape(v.h.Output(c.ID), name)253+ default:254+ continue255+ }256+ text := "$" + name + ": " + shape + " from cell " + itoa(i+1)257+ if shape == "" {258+ text = "$" + name + ": from cell " + itoa(i+1) + ", not run yet"259+ }260+ return Hover{From: from, To: to, Text: text}, true261+ }262+ return Hover{}, false263+}264+265+// varShape is the shape of variable name of a cell's last run.266+func varShape(o *notebook.Output, name string) string {267+ if o == nil || o.Vars[name] == nil {268+ return ""269+ }270+ val, err := nuon.Parse(o.Vars[name])271+ if err != nil {272+ return "a value"273+ }274+ sh := shownOf(val)275+ if sh.kind == outValue {276+ return val.Kind.String()277+ }278+ return shapeWords(sh, nil)279+}280+281+// shapeWords is an output's shape, "table, 12 rows × 4 columns", or ""282+// before it has run.283+func shapeWords(sh *shown, o *notebook.Output) string {284+ switch sh.kind {285+ case outTable:286+ return "table, " + more(sh.total, "row") + " × " + more(sh.cols, "column")287+ case outRecord:288+ return "record, " + more(sh.total, "field")289+ case outList:290+ return "list, " + more(sh.total, "item")291+ case outText:292+ if sh.total > 1 {293+ return "string, " + more(sh.total, "line")294+ }295+ return "string"296+ case outError:297+ return "an error"298+ case outValue:299+ if o != nil {300+ if val, err := nuon.Parse(o.NUON); err == nil {301+ return val.Kind.String()302+ }303+ }304+ return "a value"305+ case outNone:306+ if o != nil && o.Count > 0 {307+ return "nothing"308+ }309+ }310+ return ""311+}312+313+// hoverLine draws h in room columns: the signature in the text's own314+// color, then what it does, muted;315+// flags are left out, then the types, then the description cut, before316+// the rest is.317+func hoverLine(th *theme.Theme, h Hover, room int) string {318+ text, desc := h.Text, h.Desc319+ width := func() int {320+ w := ansi.StringWidth(text)321+ if desc != "" {322+ w += 2 + ansi.StringWidth(desc)323+ }324+ return w325+ }326+ for width() > room && desc != "" {327+ i := strings.LastIndex(text, " --")328+ if i < 0 {329+ break330+ }331+ text = strings.TrimSuffix(text[:i], " …") + " …"332+ }333+ if width() > room && h.Brief != "" {334+ text = h.Brief335+ }336+ if desc == "" {337+ return th.Cell.Render(ansi.Truncate(text, max(room, 1), "…"))338+ }339+ out := th.Cell.Render(ansi.Truncate(text, max(room, 1), "…"))340+ if left := room - ansi.StringWidth(text) - 2; left > 3 {341+ out += " " + th.Muted.Render(ansi.Truncate(desc, left, "…"))342+ }343+ return out344+}345+346+// Highlighted draws a line of nushell in its syntax roles, as 012's347+// own highlighter has them: a command's usage and examples in its help.348+func Highlighted(th *theme.Theme, src string) string {349+ buf := []rune(src)350+ roles := rolesOf(src, Tokens{}.Highlight(context.Background(), src))351+ return drawRow(th, buf, lineedit.Row{Start: 0, End: len(buf)}, roles, nil)352+}@@ -0,0 +1,179 @@
1+package nbview2+3+import (4+ "context"5+ "strings"6+ "testing"7+8+ tea "charm.land/bubbletea/v2"9+ "github.com/charmbracelet/x/ansi"10+11+ "github.com/FelineStateMachine/012/internal/notebook"12+)13+14+// 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.17+func 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+}43+44+func 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+}59+60+// settle runs cmd and hands the view the messages it makes, and theirs.61+func settle(v *View, cmd tea.Cmd) []tea.Msg {62+ var out []tea.Msg63+ for _, msg := range drain(cmd) {64+ out = append(out, msg)65+ if next, ok := v.Update(msg); ok {66+ out = append(out, settle(v, next)...)67+ }68+ }69+ return out70+}71+72+// press types keys, then lets the caret rest.73+func press(v *View, keys ...string) {74+ var cmd tea.Cmd75+ for _, k := range keys {76+ cmd, _ = v.Key(key(k))77+ }78+ settle(v, cmd)79+}80+81+// The caret resting on a command shows its signature on the context82+// line, F1 asks for its help, and a problem at the caret wins the line.83+func TestHoverOnTheContextLine(t *testing.T) {84+ f := recordedNu(t)85+ h := newHost(hoverSrc)86+ v := newView(h, 120, 20)87+ v.EditKeys["f1"] = "nb.word_help"88+ _, n := nuFor(f)89+ v.Providers = Providers{Highlighter: n, Checker: n, Hoverer: n}90+ settle(v, v.StartEdit())91+ press(v, "home", "right", "right", "right", "right", "right", "right", "right", "right", "right", "right")92+ left, right := v.ContextLine()93+ if got := ansi.Strip(left); !strings.HasPrefix(got, "sort-by <...comparator: cell-path|closure>") || !strings.Contains(got, "Sort by the given") {94+ t.Errorf("context line %q", got)95+ }96+ if !strings.Contains(ansi.Strip(right), "F1 help") {97+ t.Errorf("keys %q", ansi.Strip(right))98+ }99+ msgs := settle(v, v.WordHelp())100+ if len(msgs) != 1 || msgs[0].(HelpMsg).Help == nil || msgs[0].(HelpMsg).Help.Name != "sort-by" {101+ t.Errorf("F1: %v", msgs)102+ }103+104+ v.edit.diags, v.edit.diagFor = []Diagnostic{{From: 19, To: 26, Msg: "Something's wrong."}}, v.edit.text()105+ if left, _ := v.ContextLine(); ansi.Strip(left) != "Something's wrong." {106+ t.Errorf("the problem gives way: %q", ansi.Strip(left))107+ }108+ v.edit.diags = nil109+110+ cmd, _ := v.Key(key("home")) // the answer about the word left is dropped111+ 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" {112+ t.Error("a stale answer was kept")113+ }114+ settle(v, cmd)115+ if left, _ := v.ContextLine(); strings.Contains(ansi.Strip(left), "sort-by") {116+ t.Errorf("the caret left the word: %q", ansi.Strip(left))117+ }118+}119+120+// F1 on no command asks for the shortcuts.121+func TestWordHelpWithoutAWord(t *testing.T) {122+ h := newHost("ls ")123+ v := newView(h, 80, 20)124+ settle(v, v.StartEdit())125+ msgs := settle(v, v.WordHelp())126+ if len(msgs) != 1 || msgs[0].(HelpMsg).Help != nil {127+ t.Errorf("F1 after a space: %v", msgs)128+ }129+}130+131+// A $name the notebook binds says its cell and its value's shape,132+// without asking nu.133+func TestCellHover(t *testing.T) {134+ h := newHost("files = ls", "x = 4\ny = [1 2 3]", "$files | append $x | append $y | append $gone")135+ h.outs[1] = ¬ebook.Output{Count: 1, NUON: []byte("[{name: a, size: 1, type: file}, {name: b, size: 2, type: dir}]")}136+ h.outs[2] = ¬ebook.Output{Count: 2, NUON: []byte("[1, 2, 3]"), Vars: map[string][]byte{"x": []byte("4")}}137+ v := newView(h, 100, 30)138+ v.Select(2, false)139+ settle(v, v.StartEdit())140+ src := v.edit.text()141+ for word, want := range map[string]string{142+ "$files": "$files: table, 2 rows × 3 columns from cell 1",143+ "$x": "$x: int from cell 2",144+ "$y": "$y: list, 3 items from cell 2",145+ } {146+ v.edit.area.Pos = len([]rune(src[:strings.Index(src, word)+2]))147+ settle(v, v.rest())148+ if left, _ := v.ContextLine(); ansi.Strip(left) != want {149+ t.Errorf("%s: %q", word, ansi.Strip(left))150+ }151+ }152+ h.outs[1] = nil153+ v.edit.area.Pos = 2154+ settle(v, v.rest())155+ if left, _ := v.ContextLine(); ansi.Strip(left) != "$files: from cell 1, not run yet" {156+ t.Errorf("not run: %q", ansi.Strip(left))157+ }158+ v.edit.area.Pos = len([]rune(src)) - 2159+ settle(v, v.rest())160+ if left, _ := v.ContextLine(); ansi.Strip(left) != "" {161+ t.Errorf("a name no cell binds: %q", ansi.Strip(left))162+ }163+}164+165+// A long signature gives up its flags, then its description, to fit.166+func TestHoverLineFits(t *testing.T) {167+ th := newHost().Theme()168+ h := Hover{Text: "sort-by <...comparator: cell-path> --reverse --ignore-case", Brief: "sort-by <...comparator> …", Desc: "Sort by the given cell path or closure."}169+ for room, want := range map[int]string{170+ 200: "sort-by <...comparator: cell-path> --reverse --ignore-case Sort by the given cell path or closure.",171+ 90: "sort-by <...comparator: cell-path> --reverse … Sort by the given cell path or closure.",172+ 60: "sort-by <...comparator> … Sort by the given cell path or c…",173+ 40: "sort-by <...comparator> … Sort by the …",174+ } {175+ if got := ansi.Strip(hoverLine(th, h, room)); got != want || ansi.StringWidth(got) > room {176+ t.Errorf("%d: %q", room, got)177+ }178+ }179+}@@ -219,7 +219,7 @@ func (v *View) editKey(k tea.KeyPressMsg) (tea.Cmd, bool) {
219219 if e.text() != before {220220 return v.changed(), true221221 }222- return nil, true222+ return v.rest(), true223223 }224224 225225 // Paste types text into the cell being edited.@@ -68,7 +68,7 @@ func (v *View) Click(x, y int, shift bool) tea.Cmd {
6868 if v.edit.on && h.cell == v.sel && h.kind == rowSrc && boxed && v.inBox(x) {6969 v.edit.area.Place(v.content(), h.r, x-textX)7070 v.follow()71- return nil71+ return v.rest()7272 }7373 double := !shift && v.lastTap.cell == h.cell && v.lastTap.kind == h.kind && time.Since(v.lastTap.at) < 400*time.Millisecond7474 v.lastTap = tap{at: time.Now(), cell: h.cell, kind: h.kind}@@ -29,7 +29,7 @@ import (
2929 type fakeRunner struct {3030 mu sync.Mutex3131 version string32- answers map[string]string // flag + text asked about → answer32+ answers map[string]string // flags, a line, the text asked about → answer3333 err error // every question fails so3434 block bool // questions wait until they're stopped3535 asked []string@@ -49,7 +49,7 @@ func (f *fakeRunner) Run(ctx context.Context, job nushell.Job, script string, st
4949 <-ctx.Done()5050 return context.Cause(ctx)5151 }52- out, ok := f.answers[job.IDE[0]+script]52+ out, ok := f.answers[asked(job.IDE, script)]5353 if !ok {5454 return &nushell.Error{Msg: "not recorded: " + script}5555 }@@ -57,6 +57,9 @@ func (f *fakeRunner) Run(ctx context.Context, job nushell.Job, script string, st
5757 return err5858 }5959 60+// asked is how fakeRunner files a question.61+func asked(flags []string, script string) string { return strings.Join(flags, " ") + "\n" + script }62+6063 func (f *fakeRunner) questions() []string {6164 f.mu.Lock()6265 defer f.mu.Unlock()@@ -97,15 +100,26 @@ func recordings(t *testing.T) []recording {
97100 }98101 out = append(out, recording{flags: flags, script: script, answer: read(r.answer)})99102 }103+ script := read("hover.nu")104+ for _, at := range hoverAsked {105+ out = append(out, recording{flags: []string{"--ide-hover", fmt.Sprint(at)}, script: script, answer: read(fmt.Sprintf("hover-%d.json", at))})106+ }100107 return out101108 }102109 110+// hoverAsked are the bytes of hover.nu nu's hover is recorded at: the111+// sort-by, -r, into string, --decimals and $n of hoverSrc.112+var hoverAsked = []int{83, 96, 101, 113, 135}113+114+// hoverSrc is the cell hover.nu asks about.115+const hoverSrc = "let n = 4\n$files | sort-by size -r | into string --decimals 2 | append $n"116+103117 // recordedNu is a fake nu answering what testdata/nu records.104118 func recordedNu(t *testing.T) *fakeRunner {105119 t.Helper()106120 f := &fakeRunner{version: "0.116.0\n", answers: map[string]string{}}107121 for _, r := range recordings(t) {108- f.answers[r.flags[0]+r.script] = r.answer122+ f.answers[asked(r.flags, r.script)] = r.answer109123 }110124 return f111125 }@@ -224,6 +238,9 @@ func TestNuFallsBack(t *testing.T) {
224238 }225239 asked := len(f.questions())226240 n.Highlight(context.Background(), src)241+ if _, ok := n.Hover(context.Background(), src, 6); ok {242+ t.Errorf("%s: a hover", name)243+ }227244 if got := n.Complete(context.Background(), "$fi", 3); len(got) != 1 || got[0].Text != "$files" || len(f.questions()) != asked {228245 t.Errorf("%s: completions %v, asked %v", name, got, f.questions())229246 }@@ -234,6 +251,7 @@ func TestNuFallsBack(t *testing.T) {
234251 n.Highlight(context.Background(), src)235252 n.Check(context.Background(), src)236253 n.Complete(context.Background(), src, 2)254+ n.Hover(context.Background(), src, 6)237255 if q := f.questions(); len(q) != 0 {238256 t.Errorf("asked nu while it isn't allowed: %v", q)239257 }@@ -0,0 +1,152 @@
1+package nbview2+3+import (4+ "context"5+ "slices"6+ "strings"7+8+ "github.com/FelineStateMachine/012/internal/notebook"9+ "github.com/FelineStateMachine/012/internal/nushell"10+)11+12+// Hover implements Hoverer: for a command, its signature and help from13+// nu's --ide-hover; for a flag, the flag in its command's help; for a14+// variable, the notebook's word for it ($selection, a linked file), or15+// the type nu gives a variable the cell binds itself.16+func (n *Nu) Hover(ctx context.Context, src string, at int) (Hover, bool) {17+ names, words := n.known()18+ from, to := wordBounds(src, at)19+ if h, ok := wordHover(words, src, from, to); ok {20+ return h, true21+ }22+ p := prepare(src, names)23+ nh, ok := n.hoverAt(ctx, p, from) // the word's start, whichever byte of it the caret is at24+ if !ok {25+ return Hover{}, false26+ }27+ from, to = nh.From-p.at, nh.To-p.at28+ if from < 0 || to > len(src) || from >= to || p.inHead(from, to) {29+ return Hover{}, false30+ }31+ if nh.Text == "flag" {32+ return n.flagHover(ctx, src, p, from, to)33+ }34+ if help, ok := nushell.ParseHelp(nh.Text); ok {35+ return Hover{From: from, To: to, Text: help.Signature(), Brief: help.Brief(), Desc: help.Summary(), Help: &help}, true36+ }37+ word := src[from:to]38+ if strings.HasPrefix(word, "$") && nh.Text != "any" && !strings.ContainsAny(nh.Text, "\n ") {39+ return Hover{From: from, To: to, Text: word + ": " + nh.Text}, true40+ }41+ return Hover{}, false42+}43+44+// hoverAt is nu's hover at byte at of the source p was prepared from.45+func (n *Nu) hoverAt(ctx context.Context, p prepared, at int) (h nushell.Hover, ok bool) {46+ err := n.s.ask(ctx, func(ctx context.Context) (err error) {47+ h, ok, err = nushell.HoverAt(ctx, n.s.Runner, p.text, p.at+at)48+ return err49+ })50+ return h, err == nil && ok51+}52+53+// flagHover is what the help of the command a flag belongs to says of54+// the flag at src[from:to].55+func (n *Nu) flagHover(ctx context.Context, src string, p prepared, from, to int) (Hover, bool) {56+ cmd, ok := commandStart(p.text[p.at:], notebook.Parse(src).Strings, from)57+ if !ok {58+ return Hover{}, false59+ }60+ nh, ok := n.hoverAt(ctx, p, cmd)61+ if !ok {62+ return Hover{}, false63+ }64+ help, ok := nushell.ParseHelp(nh.Text)65+ if !ok {66+ return Hover{}, false67+ }68+ f, ok := help.Flag(src[from:to])69+ if !ok {70+ return Hover{}, false71+ }72+ text := help.Name + " " + f.Long73+ if f.Arg != "" {74+ text += " <" + f.Arg + ">"75+ }76+ if f.Short != "" {77+ text += ", " + f.Short78+ }79+ return Hover{From: from, To: to, Text: strings.TrimSpace(text), Desc: f.Desc, Help: &help}, true80+}81+82+// commandStart is the first byte of the command whose argument begins83+// at byte at of src: after the |, ;, line or bracket opening the84+// pipeline's element it's in, strings in strs left alone, and after a85+// let's =.86+func commandStart(src string, strs [][2]int, at int) (int, bool) {87+ depth, i := 0, at88+scan:89+ for i > 0 {90+ i--91+ if s, ok := stringAround(strs, i); ok {92+ i = s93+ continue94+ }95+ switch src[i] {96+ case ')', '}', ']':97+ depth++98+ case '(', '{', '[':99+ if depth == 0 {100+ i++101+ break scan102+ }103+ depth--104+ case '|', ';', '\n':105+ if depth == 0 {106+ i++107+ break scan108+ }109+ }110+ }111+ for i < at && (src[i] == ' ' || src[i] == '\t' || src[i] == '^') {112+ i++113+ }114+ if _, end := wordBounds(src, i); slices.Contains([]string{"let", "mut", "const"}, src[i:end]) {115+ if eq := strings.IndexByte(src[end:at], '='); eq >= 0 {116+ i = end + eq + 1117+ for i < at && (src[i] == ' ' || src[i] == '\t') {118+ i++119+ }120+ }121+ }122+ return i, i < at123+}124+125+// stringAround is where the string holding byte i starts.126+func stringAround(strs [][2]int, i int) (int, bool) {127+ for _, s := range strs {128+ if i >= s[0] && i < s[1] {129+ return s[0], true130+ }131+ }132+ return 0, false133+}134+135+// wordHover is the notebook's own word for the $name at src[from:to]:136+// $selection, a linked file, a range of a sheet.137+func wordHover(words []Word, src string, from, to int) (Hover, bool) {138+ name, end := varName(src, from)139+ if name == "" {140+ return Hover{}, false141+ }142+ for _, w := range words {143+ switch {144+ case w.Text == "$"+name:145+ return Hover{From: from, To: end, Text: w.Text + ": " + w.Desc}, true146+ case strings.HasSuffix(w.Text, ".") && strings.HasPrefix(src[from:to], w.Text):147+ desc, _, _ := strings.Cut(w.Desc, ":") // a range of a sheet: $sheet.A1:C9148+ return Hover{From: from, To: to, Text: src[from:to] + ": " + desc}, true149+ }150+ }151+ return Hover{}, false152+}@@ -70,6 +70,12 @@ func (v *View) ContextLine() (string, string) {
7070 if d := v.Diagnostic(); d != "" {7171 return th.Warning.Render(d), right7272 }73+ if h, ok := v.hovered(); ok {74+ if h.Help != nil {75+ right = v.hints(v.EditKeys, "nb.word_help", "help")76+ }77+ return hoverLine(th, h, v.width-ansi.StringWidth(right)-6), right78+ }7379 return "", right7480 case v.pending == "d":7581 return th.Hint.Render("d again deletes " + v.selectedWords()), th.KeyHints("Esc", "cancel")@@ -0,0 +1 @@
1+{"hover":"flag","span":{"start":113,"end":123}}@@ -0,0 +1 @@
1+{"hover":"int","span":{"start":135,"end":137}}@@ -0,0 +1 @@
1+{"hover":"flag","span":{"start":96,"end":98}}@@ -129,7 +129,8 @@ func text(v *View) string { return ansi.Strip(strings.Join(v.Lines(), "\n")) }
129129 130130 func key(s string) tea.KeyPressMsg {131131 named := map[string]tea.Key{"enter": {Code: tea.KeyEnter}, "esc": {Code: tea.KeyEscape}, "up": {Code: tea.KeyUp},132- "down": {Code: tea.KeyDown}, "tab": {Code: tea.KeyTab}, "home": {Code: tea.KeyHome}, "end": {Code: tea.KeyEnd}}132+ "down": {Code: tea.KeyDown}, "tab": {Code: tea.KeyTab}, "home": {Code: tea.KeyHome}, "end": {Code: tea.KeyEnd},133+ "left": {Code: tea.KeyLeft}, "right": {Code: tea.KeyRight}}133134 if k, ok := named[s]; ok {134135 return tea.KeyPressMsg(k)135136 }@@ -61,6 +61,7 @@ var nbEditKeys = map[string]string{
6161 "shift+enter": "nb.run_next",6262 "ctrl+enter": "nb.run",6363 "alt+enter": "nb.run_insert",64+ "f1": "nb.word_help",6465 }6566 6667 // nbShortcut is the notebook's key for command id, as shown: its@@ -97,7 +98,7 @@ func nbKeyLabels(id string) []string {
9798 func nbHelpRows(listed map[string]bool) []helpRow {9899 rows := []helpRow{{keys: []string{"Up", "Down", "j", "k"}, action: "Move between cells and outputs"},99100 {keys: []string{"Shift+Up", "Shift+Down", "K", "J"}, action: "Select several cells"}}100- for _, it := range append([]menuItem{{cmd: "nb.edit"}, {cmd: "nb.command_mode"}, {cmd: "nb.add_code"}}, notebookItems...) {101+ for _, it := range append([]menuItem{{cmd: "nb.edit"}, {cmd: "nb.command_mode"}, {cmd: "nb.add_code"}, {cmd: "nb.word_help"}}, notebookItems...) {101102 keys := nbKeyLabels(it.cmd)102103 if it.sep || keys[0] == "" || listed[it.cmd] || commands[it.cmd] == nil {103104 continue@@ -190,7 +191,7 @@ func (m *Model) viewOf(s *sheet.Sheet) *nbview.View {
190191 v = nbview.New(nbHost{m, s})191192 v.Keys, v.EditKeys = nbKeys, nbEditKeys192193 lang := m.nbLang().For()193- v.Providers = nbview.Providers{Highlighter: lang, Completer: lang, Checker: lang}194+ v.Providers = nbview.Providers{Highlighter: lang, Completer: lang, Checker: lang, Hoverer: lang}194195 m.nb.views[s] = v195196 }196197 return v