cat / 012
clone

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

Merge branch 'worktree-agent-a772f6065b4ca6f62'

Dami committed f756416
commit
f7564165d87663a25e07699488dbd5e678c23aa4
parent
5e3e3b5a1594aa67719312358a06279827c20aa2
parent
b19443e48f798d9b888afccdf23152152414044f
author
Dami <dami.etoile@gmail.com>
69 files changed, +2296 -213
MREADME.md3+-
Mdocs/files.md17+++-
Mdocs/formulas.md8+-
Mdocs/keys.md5+-
Mdocs/macros.md15++-
Me2e/import_test.go39++++++++
Ae2e/screensheets_test.go80+++++++++++++++++
Me2e/sheets_test.go55+++++++++++
Ae2e/testdata/screens/autocomplete-sheet-light.html30++++++
Ae2e/testdata/screens/autocomplete-sheet-narrow.html16+++
Ae2e/testdata/screens/autocomplete-sheet-point.html30++++++
Ae2e/testdata/screens/autocomplete-sheet.html30++++++
Ae2e/testdata/screens/import-location-light.html30++++++
Ae2e/testdata/screens/import-location-narrow.html16+++
Ae2e/testdata/screens/import-location.html30++++++
Ae2e/testdata/screens/import-new-sheets.html30++++++
Me2e/testdata/screens/menu-freeze.html10+-
Me2e/testdata/screens/palette-narrow.html2+-
Me2e/testdata/screens/palette-search-light.html2+-
Me2e/testdata/screens/palette-search.html2+-
Me2e/testdata/screens/palette-wide.html2+-
Me2e/testdata/screens/palette.html2+-
Ae2e/testdata/screens/sheets-hidden-picker-light.html30++++++
Ae2e/testdata/screens/sheets-hidden-picker.html30++++++
Ae2e/testdata/screens/sheets-hidden.html30++++++
Ae2e/testdata/screens/sheets-menu-hide.html30++++++
Me2e/testdata/screens/sheets-menu-light.html26+++---
Me2e/testdata/screens/sheets-menu.html26+++---
Minternal/fileio/snapshot.go4+
Minternal/fileio/xlsx.go80++++++++++++++---
Minternal/fileio/xlsx_test.go63+++++++++++++
Minternal/sheet/decimal.go132+++++++++++++++++++++++-----
Minternal/sheet/decimal_test.go17++++
Minternal/sheet/file.go6+
Minternal/sheet/functions_math.go85+++++++++++++-----
Ainternal/sheet/hidden.go84++++++++++++++++++
Ainternal/sheet/hidden_test.go90+++++++++++++++++++
Minternal/sheet/historysheets.go15++-
Ainternal/sheet/insertbook.go117++++++++++++++++++++++++
Ainternal/sheet/insertbook_test.go112+++++++++++++++++++++++
Minternal/sheet/refs.go20++++
Minternal/sheet/sheet.go7+-
Minternal/sheet/workbook.go17+---
Minternal/ui/assist.go36+++++---
Ainternal/ui/assistsheets.go131+++++++++++++++++++++++++++
Ainternal/ui/assistsheets_test.go109+++++++++++++++++++++++
Minternal/ui/entry.go4+-
Minternal/ui/find.go6+-
Minternal/ui/findview.go4+-
Minternal/ui/formula/formula.go21++++-
Minternal/ui/formula/formula_test.go7+
Ainternal/ui/hidden.go113++++++++++++++++++++++++
Ainternal/ui/hidden_test.go144++++++++++++++++++++++++++++++
Ainternal/ui/importplace.go121+++++++++++++++++++++++++
Ainternal/ui/importplace_test.go117++++++++++++++++++++++++
Minternal/ui/macrohostnav.go30++++++
Minternal/ui/macrorec.go3+
Minternal/ui/menubar.go3+-
Minternal/ui/names.go6+
Minternal/ui/palette.go36+++++++-
Minternal/ui/pivotactions.go4+
Minternal/ui/selection.go7+-
Minternal/ui/serve_test.go2+-
Minternal/ui/tabs.go41++++++---
Minternal/ui/tabs_test.go2+-
Minternal/ui/tabstrip.go21+++-
Minternal/ui/trace.go3+
Minternal/ui/transfer.go51++++++-----
Minternal/ui/transfer_test.go12+-
M README.md +2 -1
M docs/files.md +16 -1
@@ -7,10 +7,19 @@ through File > Download. Imports run in the background with a progress
77 bar; Esc cancels. Saving an imported sheet asks whether to save it as a88 `.012` file or download it back in its format.99 10+File > Import asks where the data goes, as Sheets' Import location does11+(a new, empty spreadsheet is simply replaced):12+13+| Location | Does |14+|---|---|15+| Insert new sheet(s) | Adds the file's sheets after the others: every sheet of an `.xlsx`, named after the file for other formats. A name already taken gets a number (`Sales 2`) and the file's formulas follow it; named ranges come along unless their name is taken. One undo step, and the spreadsheet stays the file you're editing |16+| Replace current sheet | Puts the data in place of the sheet shown, keeping its name and position, so formulas and named ranges that read it read the new data. One undo step. Not offered for `.xlsx`, which holds several sheets |17+| Replace spreadsheet | Opens the file instead, as File > Open and the command line do, asking first when there are unsaved changes |18+1019 | Format | Import | Download |1120 |---|---|---|1221 | CSV, TSV | Delimiter (`,` `;` tab `\|`), UTF-8 BOM, UTF-16 and Windows-1252 detected; entries become numbers, dates, currency and percentages as if typed; formulas stay text | Values as shown, as Sheets' Download does |13-| Excel `.xlsx` | Every sheet, opening on the one Excel showed: values, formulas (references between sheets too), workbook named ranges, number formats, bold, italic, underline, strikethrough, alignment, column widths. Formulas 012 can't read (unknown functions) keep their values; Excel pivot tables come in as the values they showed | Every sheet, the same, with formulas in Excel's syntax and their results cached. JEV functions and `#AND#` save as values, and so do pivot tables: Excel gets the results, not a pivot |22+| Excel `.xlsx` | Every sheet, opening on the one Excel showed: values, formulas (references between sheets too), workbook named ranges, number formats, bold, italic, underline, strikethrough, alignment, column widths. Formulas 012 can't read (unknown functions) keep their values; Excel pivot tables come in as the values they showed | Every sheet, the same, with formulas in Excel's syntax and their results cached. JEV functions and `#AND#` save as values, and so do formulas naming a sheet that doesn't exist (their `#REF!`; Excel would refuse the reference) and pivot tables: Excel gets the results, not a pivot. The download's result counts the formulas saved as values, with an example |1423 | SQLite | Pick a table or view, or type a query; a header row names the columns | The sheet shown or the selection as a table, first row as column names; a table of that name is replaced |1524 | Parquet | Every column, with dates and timestamps; lists joined with commas | |1625 | Lotus 1-2-3 `.wk1`, `.wks` | Numbers, labels with their alignment, formats, column widths, formulas translated (references, operators, `@SUM`, `@AVG`, `@IF`, `@ROUND`, `@PMT` and 60 more) or kept as values | |
@@ -84,6 +93,12 @@ arithmetic, the sheet shown when saved) stay at the top:
8493 8594 Older builds refuse version 4 files rather than lose sheets.8695 96+A hidden sheet (Hide sheet on its tab) has `"hidden": true` after its97+name. It needs no version bump: builds without hidden sheets ignore the98+field and show the sheet. A file whose sheets are all hidden opens with99+the first one shown. XLSX downloads write hidden sheets hidden, and100+sheets hidden in Excel import hidden.101+87102 ## Pivot tables88103 89104 A workbook with a pivot table is version 5: version 4 with a `pivot`
M docs/formulas.md +6 -2
@@ -46,7 +46,11 @@ caret on the problem and says what's wrong on the context line, e.g.
4646 - After an operator or `(`, arrow keys (or a click) pick a cell and insert4747 its reference; Shift+arrows (or a drag) pick a range. Keep typing to go on.4848 - While you type a name, suggestions drop down: named ranges first, then49- functions with their arguments. Tab or Enter inserts `NAME(`.49+ the other sheets, then functions with their arguments. Tab or Enter50+ inserts `NAME(`, or a sheet with its `!`, quoted when it needs to be51+ (`=Su` offers `Summary!`; `=Q3` or `='Q3` offers `'Q3 plan'!`). An arrow52+ after it points into that sheet, as clicking its tab does. Hidden53+ sheets aren't offered, though formulas still read them.5054 - Inside a function's parentheses the context line shows its signature with5155 the current argument marked, e.g. `SUMIF(range, criterion, [sum_range])`.5256 - Alt+, and Alt+. trace precedents and dependents: the cells a formula reads
@@ -90,7 +94,7 @@ file and can be undone.
9094 9195 | Computed in decimal | Stays binary |9296 |---|---|93-| `+ - * /` and postfix `%`; `SUM`, `AVERAGE`; `ROUND`, `ROUNDUP`, `ROUNDDOWN`, `TRUNC` | `^`, `SQRT`, `PRODUCT`, `SUMIF`, `SUMPRODUCT`, statistics, finance, dates and everything else |97+| `+ - * /` and postfix `%`; `SUM`, `AVERAGE`, `PRODUCT`, `SUMIF`, `SUMIFS`, `SUMPRODUCT`; `ROUND`, `ROUNDUP`, `ROUNDDOWN`, `TRUNC` | `^`, `SQRT`, statistics, finance, dates and everything else |9498 9599 Values are still stored as doubles. Each decimal step reads its inputs as the96100 shortest decimal that round-trips (what the cell shows), computes exactly
M docs/keys.md +3 -2
@@ -31,7 +31,7 @@ the app uses, so it never drifts from what the keys do.
3131 | Ctrl+Alt+= / Ctrl+Alt+- | Insert rows above / delete the selected rows (columns when whole columns are selected) |3232 | Ctrl+Enter while typing | Enter the same entry in every selected cell |3333 | F4 while typing a formula | Cycle the reference at the caret through A1, $A$1, A$1, $A1 |34-| Typing a function or range name | Suggestions drop down: Up/Down pick, Tab or Enter insert, Esc hides them; inside a function's parentheses the context line shows its arguments with the current one marked |34+| Typing a function, range or sheet name | Suggestions drop down: Up/Down pick, Tab or Enter insert (a sheet as `Summary!`, then arrows point into it), Esc hides them; inside a function's parentheses the context line shows its arguments with the current one marked |3535 | Alt+, / Alt+. | Trace precedents / dependents: highlight the cells a formula reads, or the formulas that read the cell, and jump to the first; press again for the next, Esc to go back (Excel's Ctrl+[ and Ctrl+], which terminals send as Esc) |3636 | Data > Named ranges | Name ranges for formulas (`=SUM(Sales)`): Enter goes to one, F2 renames or repoints it, Ctrl+D deletes it; Data > Define named range names the selection |3737 | Ctrl+Shift+1 ... 6 | Number, time, date, currency, percent, scientific format |
@@ -43,6 +43,7 @@ the app uses, so it never drifts from what the keys do.
4343 | Ctrl+PgDn / Ctrl+PgUp, Alt+Right / Alt+Left | Next / previous sheet; while typing a formula, point into it to insert `Sheet2!A1` |4444 | Shift+F11 | New sheet (also Insert > Sheet) |4545 | Alt+Shift+K | Go to a sheet by name |46+| Right-click a tab > Hide sheet, View > Hidden sheets | Hide a sheet (formulas still read it; next, previous and the tabs skip it), and list the hidden ones to show one again |4647 | Alt+F, Alt+E, Alt+V, Alt+I, Alt+O, Alt+H, F10, click a title | Open a menu (arrows move, Enter runs, Esc closes) |4748 | Ctrl+K, Alt+/, Ctrl+Shift+P | Search the menus: find and run any command |4849 | Right-click, Shift+F10 | Cell, column or row menu |
@@ -125,7 +126,7 @@ bar in terminals that support it (OSC 22).
125126 126127 The sheet tabs at the left of the status line take the mouse too: click a127128 tab to show its sheet, double-click to rename it, right-click for its menu128-(rename, duplicate, delete, move left or right), drag it onto another tab to129+(rename, duplicate, delete, hide, move left or right), drag it onto another tab to129130 move it there, and click `+` to add a sheet. When the tabs don't all fit,130131 `‹` and `›` step through them. While typing a formula, clicking a tab points131132 into that sheet, as in Sheets.
M docs/macros.md +8 -7
@@ -182,9 +182,9 @@ is already in the formulas that use it.
182182 183183 | Function | Does |184184 |---|---|185-| `sheets()` | The sheet names, in tab order |185+| `sheets()` | The sheet names, in tab order, hidden ones included |186186 | `active_sheet()` | The name of the sheet shown |187-| `activate_sheet(name)` | Show a sheet |187+| `activate_sheet(name)` | Show a sheet (not a hidden one: `run("sheet.unhide", answer=name)` shows it again) |188188 | `add_sheet(name=None)` | Add a sheet after the one shown and show it; returns its name |189189 | `move_sheet(position)` | Move the sheet shown to a position, counting from 1 |190190 | `set_width(cols, width)` | Set column widths, 1 to 240: `set_width("B", 14)`, `set_width("B:D", 8)` |
@@ -194,10 +194,11 @@ is already in the formulas that use it.
194194 `run(id, answer=None)` runs any command from the menus or palette by its195195 id, on the selection, as if picked. A command that asks something (a196196 width, a name, a confirmation) needs `answer`: the text you'd type, a range197-for a range question, or the key of a choice (`"enter"`, `"d"`) or its198-label. Commands that open a dialog or picker (Sort range, Insert chart,199-Named ranges) can't run in a script; nor can files, menus, help, undo or200-macros. The ids are those in the recorded scripts; a few:197+for a range question, the key of a choice (`"enter"`, `"d"`) or its198+label, or the item picked from a list (View > Hidden sheets takes the199+sheet's name). Commands that open a dialog or another picker (Sort200+range, Insert chart, Named ranges) can't run in a script; nor can files,201+menus, help, undo or macros. The ids are those in the recorded scripts; a few:201202 202203 | Id | Command |203204 |---|---|
@@ -210,7 +211,7 @@ macros. The ids are those in the recorded scripts; a few:
210211 | `column.width` (answer: the width), `column.reset` | Column widths |211212 | `data.sort_sheet_az`, `data.sort_range_az`, `data.sort_range_za`, `data.filter`, `data.filter_remove` | Sorting and filters |212213 | `data.define_name` (answer: the name) | Name the selection |213-| `sheet.new`, `sheet.duplicate`, `sheet.rename` (answer: the name), `sheet.delete` (answer: `"enter"` when it asks) | Sheets |214+| `sheet.new`, `sheet.duplicate`, `sheet.rename` (answer: the name), `sheet.delete` (answer: `"enter"` when it asks), `sheet.hide`, `sheet.unhide` (answer: the sheet's name) | Sheets |214215 | `view.freeze_rows1`, `view.freeze_cols1`, `view.freeze_rows0`, ... | Frozen panes |215216 216217 Record a macro that uses a command to see its id.
M e2e/import_test.go +39 -0
@@ -189,3 +189,42 @@ func TestImportProgressAndCancel(t *testing.T) {
189189 s.waitFor("Import of big.csv cancelled")190190 s.waitFor("READY")191191 }192+193+// File > Import on a spreadsheet with something in it asks where the194+// file goes: new sheets (every sheet of an .xlsx), undone as one step,195+// or in place of the sheet shown.196+func TestImportLocation(t *testing.T) {197+ dir := t.TempDir()198+ importDir(t, dir)199+ s := start(t, dir)200+ s.keys("Notes", "<enter>")201+ openImportPicker(s)202+ s.keys("q3", "<enter>")203+ s.waitFor("Import location")204+ scr := s.screen()205+ if !strings.Contains(scr, "Insert new sheets") || !strings.Contains(scr, "Replace spreadsheet") || strings.Contains(scr, "Replace current sheet") {206+ t.Errorf("locations for an .xlsx:\n%s", scr)207+ }208+ s.keys("<enter>")209+ s.waitFor("Imported q3.xlsx as Q3 and Q4")210+ if l := s.line(int(s.rows) - 1); !strings.HasPrefix(l, " Sheet1 Q3 Q4 +") {211+ t.Errorf("tabs %q", l)212+ }213+ s.waitFor("$1,200.50")214+ s.keys("<ctrl+z>")215+ s.waitFor("Undid: import q3.xlsx")216+ if l := s.line(int(s.rows) - 1); !strings.HasPrefix(l, " Sheet1 +") {217+ t.Errorf("tabs after undo %q", l)218+ }219+220+ openImportPicker(s)221+ s.keys("tsv", "<enter>")222+ s.waitFor("Import location")223+ s.keys("current", "<enter>")224+ s.waitFor("Imported budget.tsv into Sheet1 (2 rows)")225+ s.waitForBar("A1", "Rent")226+ s.keys("<ctrl+z>")227+ s.waitFor("Undid: import budget.tsv")228+ s.keys("<ctrl+home>")229+ s.waitForBar("A1", "Notes")230+}
A e2e/screensheets_test.go +80 -0
@@ -0,0 +1,80 @@
1+package e2e2+3+import ghostty "go.mitchellh.com/libghostty"4+5+// Golden screens for working across sheets: hiding a sheet and the list6+// of hidden ones, sheet names in formula suggestions, and where File >7+// Import puts a file.8+9+// hideSummary builds the two-sheet budget and hides Summary from its10+// tab's menu.11+func hideSummary(s *session) {12+ summary(s)13+ s.click(ghostty.MouseButtonRight, tabX(s.t, s, "Summary"), int(s.rows)-1)14+ s.waitFor("Hide sheet")15+ s.keys("<down>", "<down>", "<down>", "<enter>")16+ s.waitFor("Hid Summary")17+}18+19+// importLocation opens the import location picker for budget.tsv on a20+// spreadsheet with something in it.21+func importLocation(s *session) {22+ s.keys("Notes", "<enter>")23+ openImportPicker(s)24+ s.keys("tsv", "<enter>")25+ s.waitFor("Import location")26+}27+28+var sheetScreens = []screen{29+ {name: "sheets-menu-hide", setup: func(s *session) {30+ summary(s)31+ s.click(ghostty.MouseButtonRight, tabX(s.t, s, "Summary"), int(s.rows)-1)32+ s.waitFor("Hide sheet")33+ s.keys("<down>", "<down>", "<down>")34+ }},35+ {name: "sheets-hidden", setup: hideSummary},36+ {name: "sheets-hidden-picker", setup: func(s *session) {37+ hideSummary(s)38+ s.keys("<alt+v>", "<down>")39+ s.waitFor("List the hidden sheets")40+ s.keys("<enter>")41+ s.waitFor("1 of 1")42+ }},43+ {name: "autocomplete-sheet", setup: func(s *session) {44+ summary(s)45+ s.keys("<ctrl+pgup>", "<down>", "<down>", "<down>", "=B7/sum")46+ s.waitFor("│ Summary!")47+ }},48+ {name: "autocomplete-sheet-narrow", opts: options{cols: 60, rows: 16}, setup: func(s *session) {49+ summary(s)50+ s.keys("<ctrl+pgup>", "<down>", "<down>", "<down>", "=su")51+ s.waitFor("│ Summary!")52+ }},53+ {name: "autocomplete-sheet-point", setup: func(s *session) {54+ summary(s)55+ s.keys("<down>", "<down>", "<left>", "Rent share", "<tab>", "=sh", "<tab>", "<up>")56+ s.waitFor("POINT")57+ }},58+ {name: "import-location", files: importDir, setup: importLocation},59+ {name: "import-location-narrow", opts: options{cols: 60, rows: 16}, files: importDir, setup: importLocation},60+ {name: "import-new-sheets", files: importDir, setup: func(s *session) {61+ s.keys("Notes", "<enter>")62+ openImportPicker(s)63+ s.keys("q3", "<enter>")64+ s.waitFor("Import location")65+ s.keys("<enter>")66+ s.waitFor("Imported q3.xlsx as")67+ }},68+}69+70+func init() {71+ light := map[string]bool{"sheets-hidden-picker": true, "autocomplete-sheet": true, "import-location": true}72+ for _, sc := range sheetScreens {73+ screens = append(screens, sc)74+ if light[sc.name] {75+ sc.name += "-light"76+ sc.opts.light = true77+ screens = append(screens, sc)78+ }79+ }80+}
M e2e/sheets_test.go +55 -0
@@ -98,3 +98,58 @@ func TestSheets(t *testing.T) {
9898 s.waitForBar("A1", "Yearly total")9999 s.waitFor("24748.8")100100 }101+102+// Hiding a sheet takes it out of the tabs and the file keeps it hidden;103+// View > Hidden sheets shows it again, and undo hides it once more.104+func TestHideSheets(t *testing.T) {105+ dir := t.TempDir()106+ s := start(t, dir, "book.012")107+ hideSummary(s)108+ if l := s.line(int(s.rows) - 1); !strings.HasPrefix(l, " Sheet1 +") {109+ t.Errorf("tabs %q", l)110+ }111+ s.keys("<ctrl+s>")112+ s.eventually("saved", func() bool {113+ data, err := os.ReadFile(filepath.Join(dir, "book.012"))114+ return err == nil && strings.Contains(string(data), "\"name\": \"Summary\",\n \"hidden\": true,")115+ })116+ s.keys("<ctrl+q>")117+ s.waitExit()118+119+ s = start(t, dir, "book.012")120+ s.waitFor("Household budget 2026")121+ if l := s.line(int(s.rows) - 1); !strings.HasPrefix(l, " Sheet1 +") {122+ t.Errorf("tabs after opening %q", l)123+ }124+ s.keys("<alt+v>", "<down>", "<enter>")125+ s.waitFor("Hidden sheets")126+ s.keys("<enter>")127+ s.waitFor("Unhid Summary")128+ s.waitFor("25900.8") // its formulas kept reading Sheet1129+ if l := s.line(int(s.rows) - 1); !strings.HasPrefix(l, " Sheet1 Summary +") {130+ t.Errorf("tabs after unhiding %q", l)131+ }132+ s.keys("<ctrl+z>")133+ s.waitFor("Undid: show sheet Summary")134+ s.waitForBar("A1", "Household budget 2026")135+}136+137+// Typing a sheet's name in a formula offers it; after it, arrows point138+// into that sheet.139+func TestSheetNameSuggestions(t *testing.T) {140+ s := start(t, "")141+ summary(s)142+ s.keys("<ctrl+pgup>", "<f5>", "D1", "<enter>")143+ s.waitForName("D1")144+ s.keys("=su")145+ s.waitFor("│ Summary!")146+ s.keys("<tab>")147+ s.waitFor("=Summary!")148+ s.keys("<down>", "<up>")149+ s.waitFor("POINT")150+ s.waitFor("=Summary!B1")151+ s.keys("<enter>")152+ s.keys("<up>")153+ s.waitForBar("D1", "=Summary!B1")154+ s.waitFor("25900.8")155+}
A e2e/testdata/screens/autocomplete-sheet-light.html +30 -0
@@ -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">ENTER</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B8</span><span style="background:var(--vt-palette-7)"> </span> =B7/sum3+<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)">accept</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Tab</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">accept</span> <span style="color:var(--vt-palette-0)">and</span> <span style="color:var(--vt-palette-0)">go</span> <span style="color:var(--vt-palette-0)">right</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Arrows</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">pick</span> <span style="color:var(--vt-palette-0)">cells</span> <span style="color:var(--vt-palette-0)">after</span> <span style="color:var(--vt-palette-0)">an</span> <span style="color:var(--vt-palette-0)">operator</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)">cancel</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">┌────────────────────────────────────────────────────────────────┐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<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> Househo<span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Summary!</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">A1:B2</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<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> <span style="color:var(--vt-palette-8)">│</span> SUM <span style="color:var(--vt-palette-0)">(value1,</span> <span style="color:var(--vt-palette-0)">[value2,</span> <span style="color:var(--vt-palette-0)">...])</span> <span style="color:var(--vt-palette-8)">│</span>7+<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> Rent <span style="color:var(--vt-palette-8)">│</span> SUMIF <span style="color:var(--vt-palette-0)">(range,</span> <span style="color:var(--vt-palette-0)">criterion,</span> <span style="color:var(--vt-palette-0)">[sum_range])</span> <span style="color:var(--vt-palette-8)">│</span>8+<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> Groceri<span style="color:var(--vt-palette-8)">│</span> SUMIFS <span style="color:var(--vt-palette-0)">(sum_range,</span> <span style="color:var(--vt-palette-0)">criteria_range1,</span> <span style="color:var(--vt-palette-0)">criterion1,</span> <span style="color:var(--vt-palette-0)">[criteri…</span><span style="color:var(--vt-palette-8)">│</span>9+<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> Transit<span style="color:var(--vt-palette-8)">│</span> SUMPRODUCT <span style="color:var(--vt-palette-0)">(array1,</span> <span style="color:var(--vt-palette-0)">[array2,</span> <span style="color:var(--vt-palette-0)">...])</span> <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Savings<span style="color:var(--vt-palette-8)">└────────────────────────────────────────────────────────────────┘</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span> Total 2158.412+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">8</span><span style="background:var(--vt-palette-6)"> </span> <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">=B7/sum</span><span style="background:var(--vt-palette-6)"> </span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+Sheet Summary: A1:B2, 4 cells <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)">Tab</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">insert</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)">hide</span></pre>\ No newline at end of file
A e2e/testdata/screens/autocomplete-sheet-narrow.html +16 -0
@@ -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">ENTER</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B8</span><span style="background:var(--vt-palette-8)"> </span> =su3+<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)">accept</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Tab</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">accept</span> <span style="color:var(--vt-palette-8)">and</span> <span style="color:var(--vt-palette-8)">go</span> <span style="color:var(--vt-palette-8)">right</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Arrows</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">pick</span> <span style="color:var(--vt-palette-8)">cel</span>4+<span style="color:var(--vt-palette-8)">┌──────────────────────────────────────────────────────────┐</span>5+<span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Summary!</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">A1:B2</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<span style="color:var(--vt-palette-8)">│</span> SUBSTITUTE <span style="color:var(--vt-palette-8)">(text,</span> <span style="color:var(--vt-palette-8)">search_for,</span> <span style="color:var(--vt-palette-8)">replace_with,</span> <span style="color:var(--vt-palette-8)">[occurrenc…│</span>7+<span style="color:var(--vt-palette-8)">│</span> SUM <span style="color:var(--vt-palette-8)">(value1,</span> <span style="color:var(--vt-palette-8)">[value2,</span> <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> SUMIF <span style="color:var(--vt-palette-8)">(range,</span> <span style="color:var(--vt-palette-8)">criterion,</span> <span style="color:var(--vt-palette-8)">[sum_range])</span> <span style="color:var(--vt-palette-8)">│</span>9+<span style="color:var(--vt-palette-8)">│</span> SUMIFS <span style="color:var(--vt-palette-8)">(sum_range,</span> <span style="color:var(--vt-palette-8)">criteria_range1,</span> <span style="color:var(--vt-palette-8)">criterion1,</span> <span style="color:var(--vt-palette-8)">[c…│</span>10+<span style="color:var(--vt-palette-8)">│</span> SUMPRODUCT <span style="color:var(--vt-palette-8)">(array1,</span> <span style="color:var(--vt-palette-8)">[array2,</span> <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>12+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">8</span><span style="background:var(--vt-palette-6)"> </span> <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">=su</span><span style="background:var(--vt-palette-6)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+Sheet Summary: A… <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)">Tab</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">insert</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)">hide</span></pre>\ No newline at end of file
A e2e/testdata/screens/autocomplete-sheet-point.html +30 -0
@@ -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">POINT</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Summary!B3</span> =<span style="color:var(--vt-palette-0);background:var(--vt-palette-4)">Sheet1!B4</span>3+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Arrows</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">pick</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Shift+arrows</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">pick</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">range</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)">accept</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)">back</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<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> Household budget 20266+<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>7+<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> Rent 14508+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">4</span><span style="background:var(--vt-palette-6)"> </span> Groceries<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">612.4</span><span style="background:var(--vt-palette-6)"> </span>9+<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> Transit 9610+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> Total 2158.412+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> Summary <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
A e2e/testdata/screens/autocomplete-sheet.html +30 -0
@@ -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">ENTER</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B8</span><span style="background:var(--vt-palette-8)"> </span> =B7/sum3+<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)">accept</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Tab</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">accept</span> <span style="color:var(--vt-palette-8)">and</span> <span style="color:var(--vt-palette-8)">go</span> <span style="color:var(--vt-palette-8)">right</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Arrows</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">pick</span> <span style="color:var(--vt-palette-8)">cells</span> <span style="color:var(--vt-palette-8)">after</span> <span style="color:var(--vt-palette-8)">an</span> <span style="color:var(--vt-palette-8)">operator</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)">cancel</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">┌────────────────────────────────────────────────────────────────┐</span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<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> Househo<span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Summary!</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">A1:B2</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<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> <span style="color:var(--vt-palette-8)">│</span> SUM <span style="color:var(--vt-palette-8)">(value1,</span> <span style="color:var(--vt-palette-8)">[value2,</span> <span style="color:var(--vt-palette-8)">...])</span> <span style="color:var(--vt-palette-8)">│</span>7+<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> Rent <span style="color:var(--vt-palette-8)">│</span> SUMIF <span style="color:var(--vt-palette-8)">(range,</span> <span style="color:var(--vt-palette-8)">criterion,</span> <span style="color:var(--vt-palette-8)">[sum_range])</span> <span style="color:var(--vt-palette-8)">│</span>8+<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> Groceri<span style="color:var(--vt-palette-8)">│</span> SUMIFS <span style="color:var(--vt-palette-8)">(sum_range,</span> <span style="color:var(--vt-palette-8)">criteria_range1,</span> <span style="color:var(--vt-palette-8)">criterion1,</span> <span style="color:var(--vt-palette-8)">[criteri…│</span>9+<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> Transit<span style="color:var(--vt-palette-8)">│</span> SUMPRODUCT <span style="color:var(--vt-palette-8)">(array1,</span> <span style="color:var(--vt-palette-8)">[array2,</span> <span style="color:var(--vt-palette-8)">...])</span> <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings<span style="color: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)">7</span><span style="background:var(--vt-palette-8)"> </span> Total 2158.412+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">8</span><span style="background:var(--vt-palette-6)"> </span> <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">=B7/sum</span><span style="background:var(--vt-palette-6)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+Sheet Summary: A1:B2, 4 cells <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)">Tab</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">insert</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)">hide</span></pre>\ No newline at end of file
A e2e/testdata/screens/import-location-light.html +30 -0
@@ -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">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A2</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Import</span> <span style="font-weight:bold">budget.tsv</span> <span style="color:var(--vt-palette-8)">───────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-0)">Import</span> <span style="color:var(--vt-palette-0)">location</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </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)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<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> Notes <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Insert</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">new</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">after</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">others</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">2</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">│</span> Replace current sheet <span style="color:var(--vt-palette-0)">Sheet1</span> <span style="color:var(--vt-palette-8)">│</span>7+<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> <span style="color:var(--vt-palette-8)">│</span> Replace spreadsheet <span style="color:var(--vt-palette-0)">unsaved</span> <span style="color:var(--vt-palette-0)">changes</span> <span style="color:var(--vt-palette-8)">│</span>8+<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> <span style="color:var(--vt-palette-8)">└──────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">3</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">3</span> <span style="color:var(--vt-palette-8)">─┘</span>9+<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>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</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)">7</span><span style="background:var(--vt-palette-7)"> </span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+Add budget.tsv as a new 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)">import</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
A e2e/testdata/screens/import-location-narrow.html +16 -0
@@ -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">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A2</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Import</span> <span style="font-weight:bold">budget.tsv</span> <span style="color:var(--vt-palette-8)">───────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-8)">Import</span> <span style="color:var(--vt-palette-8)">location</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">├───────────────────────────────────────────────┤</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Insert</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">new</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">after</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">others</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">2</span><span style="color:var(--vt-palette-8)">│</span> Replace current sheet <span style="color:var(--vt-palette-8)">Sheet1</span> <span style="color:var(--vt-palette-8)">│</span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="color:var(--vt-palette-8)">│</span> Replace spreadsheet <span style="color:var(--vt-palette-8)">unsaved</span> <span style="color:var(--vt-palette-8)">changes</span> <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="color:var(--vt-palette-8)">└──────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color: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)">5</span><span style="background:var(--vt-palette-8)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</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)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+Add budget.ts… <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)">import</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
A e2e/testdata/screens/import-location.html +30 -0
@@ -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">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A2</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Import</span> <span style="font-weight:bold">budget.tsv</span> <span style="color:var(--vt-palette-8)">───────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-8)">Import</span> <span style="color:var(--vt-palette-8)">location</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </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)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<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> Notes <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Insert</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">new</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">after</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">others</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">2</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">│</span> Replace current sheet <span style="color:var(--vt-palette-8)">Sheet1</span> <span style="color:var(--vt-palette-8)">│</span>7+<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> <span style="color:var(--vt-palette-8)">│</span> Replace spreadsheet <span style="color:var(--vt-palette-8)">unsaved</span> <span style="color:var(--vt-palette-8)">changes</span> <span style="color:var(--vt-palette-8)">│</span>8+<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> <span style="color:var(--vt-palette-8)">└──────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color: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)">5</span><span style="background:var(--vt-palette-8)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</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)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+Add budget.tsv as a new 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)">import</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
A e2e/testdata/screens/import-new-sheets.html +30 -0
@@ -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">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span> Region3+Imported q3.xlsx as Q3 and Q4 (6 rows)4+<span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Region</span><span style="background:var(--vt-palette-6)"> </span> <span style="font-weight:bold">July</span> <span style="font-weight:bold">August</span> <span style="font-weight:bold">September</span>6+<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> North $1,200.50 $1,310.00 $990.007+<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> South $870.00 $905.25 $1,122.008+<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> West $450.00 $610.00 $702.759+<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> <span style="font-weight:bold">Total</span> $2,520.50 $2,825.25 $2,814.7510+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</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)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Q3</span><span style="background:var(--vt-palette-6)"> </span> Q4 <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
M e2e/testdata/screens/menu-freeze.html +5 -5
@@ -2,11 +2,11 @@
22 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">┌───────────────────────────────┐┌─────────────────────────┐</span>33 <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Freeze</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">›</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">││</span> No rows <span style="color:var(--vt-palette-8)">│</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">├───────────────────────────────┤│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">row</span><span style="background:var(--vt-palette-6)"> </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)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5-<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Item</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span> Command palette <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)">││</span> 2 rows <span style="color:var(--vt-palette-8)">│</span>6-<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> Bolts<span style="color:var(--vt-palette-8)">│</span> Keyboard shortcuts <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)">││</span> Up to current row <span style="color:var(--vt-palette-8)">│</span>7-<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> Nuts <span style="color:var(--vt-palette-8)">└───────────────────────────────┘├─────────────────────────┤</span>8-<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> Washers 1 Fasteners B03 79<span style="color:var(--vt-palette-8)">│</span> No columns <span style="color: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)">5</span><span style="background:var(--vt-palette-8)"> </span> Hinges 1 Hardware B04 26<span style="color:var(--vt-palette-8)">│</span> 1 column <span style="color:var(--vt-palette-8)">│</span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Item</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Hidden</span> <span style="color:var(--vt-palette-8)">sheets</span> <span style="color:var(--vt-palette-8)">││</span> 2 rows <span style="color:var(--vt-palette-8)">│</span>6+<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> Bolts<span style="color:var(--vt-palette-8)">├───────────────────────────────┤│</span> Up to current row <span style="color:var(--vt-palette-8)">│</span>7+<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> Nuts <span style="color:var(--vt-palette-8)">│</span> Command palette <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)">│├─────────────────────────┤</span>8+<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> Washe<span style="color:var(--vt-palette-8)">│</span> Keyboard shortcuts <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)">││</span> No columns <span style="color: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)">5</span><span style="background:var(--vt-palette-8)"> </span> Hinge<span style="color:var(--vt-palette-8)">└───────────────────────────────┘│</span> 1 column <span style="color:var(--vt-palette-8)">│</span>1010 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Brackets Hardware B05 63<span style="color:var(--vt-palette-8)">│</span> 2 columns <span style="color:var(--vt-palette-8)">│</span>1111 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> Screws 1 Fasteners B06 10<span style="color:var(--vt-palette-8)">│</span> Up to current column <span style="color:var(--vt-palette-8)">│</span>1212 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span> Anchors 1 Hardware B07 47<span style="color:var(--vt-palette-8)">└─────────────────────────┘</span>
M e2e/testdata/screens/palette-narrow.html +1 -1
@@ -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)">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>1414 <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-3);font-weight:bold">S</span>ort r<span style="color:var(--vt-palette-3);font-weight:bold">a</span>nge <span style="color:var(--vt-palette-8)">│</span>15-<span style="color:var(--vt-palette-8)">└──────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">30</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">124</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)">30</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">126</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
M e2e/testdata/screens/palette-search-light.html +1 -1
@@ -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> 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>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> 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>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> Insert rows or <span style="color:var(--vt-palette-4);font-weight:bold">col</span>umns <span style="color:var(--vt-palette-0)">Insert</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)">20</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">124</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)">20</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">126</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>
M e2e/testdata/screens/palette-search.html +1 -1
@@ -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> 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>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> 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>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> Insert rows or <span style="color:var(--vt-palette-3);font-weight:bold">col</span>umns <span style="color:var(--vt-palette-8)">Insert</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)">20</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">124</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)">20</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">126</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>
M e2e/testdata/screens/palette-wide.html +1 -1
@@ -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> In<span style="color:var(--vt-palette-3);font-weight:bold">se</span>rt rows be<span style="color:var(--vt-palette-3);font-weight:bold">l</span>ow <span style="color:var(--vt-palette-8)">Insert</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> Re<span style="color:var(--vt-palette-3);font-weight:bold">se</span>t co<span style="color:var(--vt-palette-3);font-weight:bold">l</span>umn width <span style="color:var(--vt-palette-8)">Format</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> In<span style="color:var(--vt-palette-3);font-weight:bold">se</span>rt co<span style="color:var(--vt-palette-3);font-weight:bold">l</span>umns right <span style="color:var(--vt-palette-8)">Insert</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)">22</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">124</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)">22</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">126</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>
M e2e/testdata/screens/palette.html +1 -1
@@ -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> Decimal arithmetic <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Settings</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> Vim keys <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Settings</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> Theme <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Settings</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)">124</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">124</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)">126</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">126</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>
A e2e/testdata/screens/sheets-hidden-picker-light.html +30 -0
@@ -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">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B5</span><span style="background:var(--vt-palette-7)"> </span> 96 <span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Hidden</span> <span style="font-weight:bold">sheets</span> <span style="color:var(--vt-palette-8)">────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-0)">Type</span> <span style="color:var(--vt-palette-0)">a</span> <span style="color:var(--vt-palette-0)">sheet</span> <span style="color:var(--vt-palette-0)">name</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </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)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<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> Household budget 2026 <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Summary</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">A1:B2,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">4</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">cells</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<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> <span style="color:var(--vt-palette-8)">└───────────────────────────────</span> <span style="color:var(--vt-palette-0)">1</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">1</span> <span style="color:var(--vt-palette-8)">─┘</span>7+<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> Rent 14508+<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> Groceries 612.49+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">5</span><span style="background:var(--vt-palette-6)"> </span> Transit <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">96</span><span style="background:var(--vt-palette-6)"> </span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span> Total 2158.412+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+Show Summary again <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)">unhide</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
A e2e/testdata/screens/sheets-hidden-picker.html +30 -0
@@ -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">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B5</span><span style="background:var(--vt-palette-8)"> </span> 96 <span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Hidden</span> <span style="font-weight:bold">sheets</span> <span style="color:var(--vt-palette-8)">────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-8)">Type</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">sheet</span> <span style="color:var(--vt-palette-8)">name</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </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)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<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> Household budget 2026 <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Summary</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">A1:B2,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">4</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">cells</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<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> <span style="color:var(--vt-palette-8)">└───────────────────────────────</span> <span style="color:var(--vt-palette-8)">1</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">1</span> <span style="color:var(--vt-palette-8)">─┘</span>7+<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> Rent 14508+<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> Groceries 612.49+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">5</span><span style="background:var(--vt-palette-6)"> </span> Transit <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">96</span><span style="background:var(--vt-palette-6)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> Total 2158.412+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+Show Summary again <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)">unhide</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
A e2e/testdata/screens/sheets-hidden.html +30 -0
@@ -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">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B5</span><span style="background:var(--vt-palette-8)"> </span> 963+Hid Summary; View &gt; Hidden sheets shows it again4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<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> Household budget 20266+<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>7+<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> Rent 14508+<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> Groceries 612.49+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">5</span><span style="background:var(--vt-palette-6)"> </span> Transit <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">96</span><span style="background:var(--vt-palette-6)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> Total 2158.412+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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>19+<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>20+<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>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<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>26+<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>27+<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>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>29+<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>30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</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
A e2e/testdata/screens/sheets-menu-hide.html +30 -0
@@ -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">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B1</span><span style="background:var(--vt-palette-8)"> </span> =Sheet1!B7*123+4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span> Yearly to<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">25900.8</span><span style="background:var(--vt-palette-6)"> </span>6+<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> Monthly 719.466677+<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>8+<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>9+<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>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</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)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<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>15+<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>16+<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>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>18+<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> <span style="color:var(--vt-palette-8)">│</span> Rename <span style="color:var(--vt-palette-8)">│</span>19+<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> <span style="color:var(--vt-palette-8)">│</span> Duplicate <span style="color:var(--vt-palette-8)">│</span>20+<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> <span style="color:var(--vt-palette-8)">│</span> Delete <span style="color: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)">17</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Hide</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">sheet</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">├───────────────────────────────┤</span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move left <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Move</span> <span style="color:var(--vt-palette-8)">right</span> <span style="color:var(--vt-palette-8)">│</span>25+<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>26+<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> New sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Shift+F11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>27+<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> Go to sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Alt+Shift+K</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)">Hidden</span> <span style="color:var(--vt-palette-8)">sheets</span> <span style="color:var(--vt-palette-8)">│</span>29+<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> <span style="color:var(--vt-palette-8)">└───────────────────────────────┘</span>30+Hide the sheet&#39;s tab; formulas still read it <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)">choose</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
M e2e/testdata/screens/sheets-menu-light.html +13 -13
@@ -14,17 +14,17 @@
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>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>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>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>18-<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>19-<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> <span style="color:var(--vt-palette-8)">┌─────────────────────────────┐</span>20-<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> <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Rename</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>21-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Duplicate <span style="color:var(--vt-palette-8)">│</span>22-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Delete <span style="color:var(--vt-palette-8)">│</span>23-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">├─────────────────────────────┤</span>24-<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move left <span style="color:var(--vt-palette-8)">│</span>25-<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> <span style="color:var(--vt-palette-8)">Move</span> <span style="color:var(--vt-palette-8)">right</span> <span style="color:var(--vt-palette-8)">│</span>26-<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>27-<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> New sheet <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Shift+F11</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> Go to sheet <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Alt+Shift+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>29-<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> <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>18+<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> <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Rename</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>19+<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> <span style="color:var(--vt-palette-8)">│</span> Duplicate <span style="color:var(--vt-palette-8)">│</span>20+<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> <span style="color:var(--vt-palette-8)">│</span> Delete <span style="color:var(--vt-palette-8)">│</span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Hide sheet <span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">├───────────────────────────────┤</span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move left <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Move</span> <span style="color:var(--vt-palette-8)">right</span> <span style="color:var(--vt-palette-8)">│</span>25+<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>26+<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> New sheet <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Shift+F11</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>27+<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> Go to sheet <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Alt+Shift+K</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-8)">Hidden</span> <span style="color:var(--vt-palette-8)">sheets</span> <span style="color:var(--vt-palette-8)">│</span>29+<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> <span style="color:var(--vt-palette-8)">└───────────────────────────────┘</span>3030 Give the sheet a new name; formulas that use it follow <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)">choose</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
M e2e/testdata/screens/sheets-menu.html +13 -13
@@ -14,17 +14,17 @@
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>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)">12</span><span style="background: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>18-<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>19-<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> <span style="color:var(--vt-palette-8)">┌─────────────────────────────┐</span>20-<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> <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Rename</span><span style="background:var(--vt-palette-6)"> </span><span style="color: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)">17</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Duplicate <span style="color:var(--vt-palette-8)">│</span>22-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Delete <span style="color:var(--vt-palette-8)">│</span>23-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">├─────────────────────────────┤</span>24-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move left <span style="color:var(--vt-palette-8)">│</span>25-<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> <span style="color:var(--vt-palette-8)">Move</span> <span style="color:var(--vt-palette-8)">right</span> <span style="color:var(--vt-palette-8)">│</span>26-<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>27-<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> New sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Shift+F11</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> Go to sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Alt+Shift+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>29-<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> <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>18+<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> <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Rename</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>19+<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> <span style="color:var(--vt-palette-8)">│</span> Duplicate <span style="color:var(--vt-palette-8)">│</span>20+<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> <span style="color:var(--vt-palette-8)">│</span> Delete <span style="color: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)">17</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Hide sheet <span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">├───────────────────────────────┤</span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> Move left <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Move</span> <span style="color:var(--vt-palette-8)">right</span> <span style="color:var(--vt-palette-8)">│</span>25+<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>26+<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> New sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Shift+F11</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>27+<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> Go to sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Alt+Shift+K</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)">Hidden</span> <span style="color:var(--vt-palette-8)">sheets</span> <span style="color:var(--vt-palette-8)">│</span>29+<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> <span style="color:var(--vt-palette-8)">└───────────────────────────────┘</span>3030 Give the sheet a new name; formulas that use it follow <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)">choose</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
M internal/fileio/snapshot.go +4 -0
M internal/fileio/xlsx.go +66 -14
@@ -10,6 +10,7 @@ import (
1010 1111 "github.com/xuri/excelize/v2"1212 13+ "github.com/FelineStateMachine/012/internal/formula"1314 "github.com/FelineStateMachine/012/internal/sheet"1415 )1516
@@ -73,6 +74,11 @@ func importXLSX(ctx context.Context, name string, opt Options) (*Result, error)
7374 notes = append(notes, importXLSXNames(x, book)...)7475 active := book.Sheet(clamp(x.GetActiveSheetIndex(), 0, book.Len()-1))7576 book.SetActive(active)77+ for i, ws := range names {78+ if shown, err := x.GetSheetVisible(ws); err == nil && !shown && book.Sheet(i) != active {79+ book.HideSheet(book.Sheet(i)) // hidden in Excel, hidden here80+ }81+ }7682 b.s = active7783 prog.setRows(done)7884 s, notes := b.finish(notes)
@@ -230,7 +236,10 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
230236 if len(sheets) == 0 {231237 sheets = []*Snapshot{snap}232238 }233- w := &xlsxWriter{x: x, styleIDs: map[xlsxStyle]int{}, multi: len(sheets) > 1}239+ w := &xlsxWriter{x: x, styleIDs: map[xlsxStyle]int{}, multi: len(sheets) > 1, known: map[string]bool{}}240+ for _, sn := range sheets {241+ w.known[formula.SheetKey(sn.Name)] = true242+ }234243 res := &ExportResult{}235244 for i, sn := range sheets {236245 ws := sheetName(sn.Name)
@@ -250,6 +259,10 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
250259 res.Rows += rows251260 if sn == snap || snap.Sheets == nil {252261 x.SetActiveSheet(i)262+ } else if sn.Hidden {263+ if err := x.SetSheetVisible(ws, false); err != nil {264+ return nil, err265+ }253266 }254267 }255268 for _, n := range snap.Names {
@@ -261,9 +274,13 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
261274 if err := x.SetCalcProps(&excelize.CalcPropsOptions{FullCalcOnLoad: &yes}); err != nil {262275 return nil, err263276 }264- if w.values > 0 {277+ if w.values.n > 0 {265278 res.Notes = append(res.Notes, fmt.Sprintf("%s with no Excel equivalent saved as values, e.g. %s",266- count(w.values, "formula", "formulas"), w.example))279+ count(w.values.n, "formula", "formulas"), w.values.example))280+ }281+ if w.missing.n > 0 {282+ res.Notes = append(res.Notes, fmt.Sprintf("%s naming a sheet that doesn't exist saved as values, e.g. %s (%s)",283+ count(w.missing.n, "formula", "formulas"), w.missing.example, sheet.QuoteSheet(w.missingSheet)))267284 }268285 return res, writeFile(name, func(w io.Writer) error { return x.Write(w) })269286 }
@@ -272,9 +289,42 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
272289 type xlsxWriter struct {273290 x *excelize.File274291 styleIDs map[xlsxStyle]int275- values int // formulas written as values276- example string // the first of them277- multi bool // examples name their sheet292+ multi bool // examples name their sheet293+294+ // Formulas are written as their values when Excel has no equivalent,295+ // or when they name a sheet the file doesn't have, which Excel would296+ // refuse.297+ values valueCount298+ missing valueCount299+ missingSheet string // the sheet missing's example names300+ known map[string]bool // keys of the sheets written301+}302+303+// valueCount counts formulas written as values, keeping the first one's304+// address as an example.305+type valueCount struct {306+ n int307+ example string308+}309+310+func (v *valueCount) add(multi bool, ws string, a sheet.Addr) {311+ v.n++312+ if v.example == "" {313+ v.example = a.String()314+ if multi {315+ v.example = sheet.QuoteSheet(ws) + "!" + a.String()316+ }317+ }318+}319+320+// unknownSheet returns a sheet c's formula names that isn't written.321+func (w *xlsxWriter) unknownSheet(c SnapCell) (string, bool) {322+ for _, name := range c.Sheets {323+ if !w.known[formula.SheetKey(name)] {324+ return name, true325+ }326+ }327+ return "", false278328 }279329 280330 func (w *xlsxWriter) styleFor(f sheet.Format, st sheet.Style) (int, error) {
@@ -335,7 +385,8 @@ func (w *xlsxWriter) sheet(ws string, snap *Snapshot) (int, error) {
335385 }336386 337387 // cell is the snapshot cell c at a as excelize writes it. A formula with338-// no Excel equivalent is written as its value, and counted.388+// no Excel equivalent, or naming a sheet that isn't written, is written389+// as its value, and counted.339390 func (w *xlsxWriter) cell(ws string, a sheet.Addr, c SnapCell) (excelize.Cell, error) {340391 // A formula's inferred format is written too, so its result shows341392 // the same in Excel.
@@ -350,17 +401,18 @@ func (w *xlsxWriter) cell(ws string, a sheet.Addr, c SnapCell) (excelize.Cell, e
350401 if !c.Formula {351402 return xc, nil352403 }404+ if name, ok := w.unknownSheet(c); ok {405+ if w.missing.n == 0 {406+ w.missingSheet = name407+ }408+ w.missing.add(w.multi, ws, a)409+ return xc, nil410+ }353411 if fx, ok := toExcelFormula(c.Input); ok {354412 xc.Formula = fx355413 return xc, nil356414 }357- w.values++358- if w.example == "" {359- w.example = a.String()360- if w.multi {361- w.example = sheet.QuoteSheet(ws) + "!" + a.String()362- }363- }415+ w.values.add(w.multi, ws, a)364416 return xc, nil365417 }366418
M internal/fileio/xlsx_test.go +63 -0
@@ -328,3 +328,66 @@ func TestXLSXWorkbookRoundTrip(t *testing.T) {
328328 t.Errorf("name Rent: %v %s", ok, n.Ref())329329 }330330 }331+332+// A formula naming a sheet the workbook doesn't have goes out as its333+// value (#REF!), with a note, since Excel would refuse the reference.334+func TestXLSXMissingSheetAsValue(t *testing.T) {335+ src := build(t, map[string]string{"A1": "2", "B1": "=Gone!A1+1", "B2": "=A1*2", "B3": "=SUM('Old plan'!A1:A3, gone!B1)"})336+ name := filepath.Join(t.TempDir(), "missing.xlsx")337+ res, err := Export(context.Background(), name, XLSX, SnapBook(src), ExportOptions{})338+ if err != nil {339+ t.Fatal(err)340+ }341+ want := "2 formulas naming a sheet that doesn't exist saved as values, e.g. B1 (Gone)"342+ if len(res.Notes) != 1 || res.Notes[0] != want {343+ t.Errorf("notes %q, want %q", res.Notes, want)344+ }345+ x, err := excelize.OpenFile(name)346+ if err != nil {347+ t.Fatal(err)348+ }349+ defer x.Close()350+ ws := x.GetSheetName(0)351+ for cell, formula := range map[string]string{"B1": "", "B2": "A1*2", "B3": ""} {352+ if got, _ := x.GetCellFormula(ws, cell); got != formula {353+ t.Errorf("%s formula %q, want %q", cell, got, formula)354+ }355+ }356+ if got, _ := x.GetCellValue(ws, "B1"); got != "#REF!" {357+ t.Errorf("B1 value %q", got)358+ }359+}360+361+// Hidden sheets go out to Excel hidden, and come back hidden.362+func TestXLSXHiddenSheets(t *testing.T) {363+ src := build(t, map[string]string{"A1": "=Data!A1*2"})364+ book := src.Book()365+ data, _ := book.AddSheet("Data", 1)366+ data.Set(addr(t, "A1"), "21")367+ if err := book.HideSheet(data); err != nil {368+ t.Fatal(err)369+ }370+ name := filepath.Join(t.TempDir(), "hidden.xlsx")371+ if _, err := Export(context.Background(), name, XLSX, SnapBook(src), ExportOptions{}); err != nil {372+ t.Fatal(err)373+ }374+ x, err := excelize.OpenFile(name)375+ if err != nil {376+ t.Fatal(err)377+ }378+ if shown, _ := x.GetSheetVisible("Data"); shown {379+ t.Error("Data is visible in Excel")380+ }381+ if shown, _ := x.GetSheetVisible(x.GetSheetName(0)); !shown {382+ t.Error("the first sheet is hidden in Excel")383+ }384+ x.Close()385+ got, err := Import(context.Background(), name, Options{})386+ if err != nil {387+ t.Fatal(err)388+ }389+ gb := got.Sheet.Book()390+ if !gb.Lookup("Data").Hidden() || got.Sheet.Hidden() || shown(got.Sheet, addr(t, "A1")) != "42" {391+ t.Errorf("read back: Data hidden %v, shown %s hidden %v", gb.Lookup("Data").Hidden(), got.Sheet.Name(), got.Sheet.Hidden())392+ }393+}
M internal/sheet/decimal.go +108 -24
@@ -18,7 +18,7 @@ import (
1818 // compute in decimal:1919 //2020 // - the operators + - * / and the postfix %21-// - SUM and AVERAGE21+// - SUM, AVERAGE, PRODUCT, SUMIF, SUMIFS and SUMPRODUCT2222 // - ROUND, ROUNDUP, ROUNDDOWN and TRUNC2323 //2424 // Each step reads its float64 operands as the shortest decimal that
@@ -102,10 +102,54 @@ func decRound(x float64, places int, mode numfmt.Rounding) (Value, bool) {
102102 return fromDec(&z)103103 }104104 105-// decSum adds the numbers SUM would, in decimal, and counts them.106-func decSum(args []Node, get lookup) (sum apd.Decimal, n int, e *Value, ok bool) {107- ok = true108- var x apd.Decimal105+// decAcc accumulates a sum or a product in decimal; ok turns false when106+// a step can't be represented, and the caller falls back to binary.107+type decAcc struct {108+ d, x apd.Decimal109+ ok bool110+}111+112+func newDecAcc(start int64) *decAcc {113+ a := &decAcc{ok: true}114+ a.d.SetInt64(start)115+ return a116+}117+118+func (a *decAcc) add(f float64) { a.step(f, decCtx.Add) }119+func (a *decAcc) mul(f float64) { a.step(f, decCtx.Mul) }120+121+func (a *decAcc) step(f float64, op func(d, x, y *apd.Decimal) (apd.Condition, error)) {122+ if !a.ok {123+ return124+ }125+ if _, err := a.x.SetFloat64(f); err != nil {126+ a.ok = false127+ } else if _, err := op(&a.d, &a.d, &a.x); err != nil {128+ a.ok = false129+ }130+}131+132+// addDec adds a decimal to the sum.133+func (a *decAcc) addDec(d *apd.Decimal) {134+ if !a.ok {135+ return136+ }137+ if _, err := decCtx.Add(&a.d, &a.d, d); err != nil {138+ a.ok = false139+ }140+}141+142+// result is the accumulated value, or false to fall back.143+func (a *decAcc) result() (Value, bool) {144+ if !a.ok {145+ return Value{}, false146+ }147+ return fromDec(&a.d)148+}149+150+// decFold feeds the numbers SUM and PRODUCT take to step and counts151+// them: in ranges only numbers, direct arguments coerced.152+func decFold(args []Node, get lookup, step func(float64)) (n int, e *Value) {109153 e = each(args, get, func(v Value, direct bool) *Value {110154 switch v.Kind {111155 case Error:
@@ -121,49 +165,89 @@ func decSum(args []Node, get lookup) (sum apd.Decimal, n int, e *Value, ok bool)
121165 return err122166 }123167 n++124- if _, err := x.SetFloat64(f); err != nil {125- ok = false126- } else if _, err := decCtx.Add(&sum, &sum, &x); err != nil {127- ok = false128- }168+ step(f)129169 return nil130170 })131- return sum, n, e, ok171+ return n, e172+}173+174+// decTerms builds the decimal twin of a function that visits the terms175+// it adds (SUMIF, SUMIFS).176+func decTerms(terms func([]Node, lookup, func(float64)) *Value) func([]Node, lookup) (Value, bool) {177+ return func(args []Node, get lookup) (Value, bool) {178+ acc := newDecAcc(0)179+ if e := terms(args, get, acc.add); e != nil {180+ return *e, true181+ }182+ return acc.result()183+ }184+}185+186+// decSumProduct is SUMPRODUCT with each entry's product and the sum in187+// decimal.188+func decSumProduct(args []Node, get lookup) (Value, bool) {189+ acc := newDecAcc(0)190+ e := sumProductTerms(args, get, func(fs []float64) {191+ p := newDecAcc(1)192+ for _, f := range fs {193+ p.mul(f)194+ }195+ if !p.ok {196+ acc.ok = false197+ }198+ acc.addDec(&p.d)199+ })200+ if e != nil {201+ return *e, true202+ }203+ return acc.result()132204 }133205 134206 // decEvals are the decimal versions of functions, by name. Each reports135207 // false to fall back to the binary version.136208 var decEvals = map[string]func(args []Node, get lookup) (Value, bool){137209 "SUM": func(args []Node, get lookup) (Value, bool) {138- sum, _, e, ok := decSum(args, get)139- if e != nil {210+ acc := newDecAcc(0)211+ if _, e := decFold(args, get, acc.add); e != nil {140212 return *e, true141213 }142- if !ok {143- return Value{}, false144- }145- return fromDec(&sum)214+ return acc.result()146215 },147216 "AVERAGE": func(args []Node, get lookup) (Value, bool) {148- sum, n, e, ok := decSum(args, get)217+ acc := newDecAcc(0)218+ n, e := decFold(args, get, acc.add)149219 switch {150220 case e != nil:151221 return *e, true152222 case n == 0:153223 return ErrDiv0, true154- case !ok:224+ case !acc.ok:155225 return Value{}, false156226 }157227 var q apd.Decimal158- if _, err := decCtx.Quo(&q, &sum, apd.New(int64(n), 0)); err != nil {228+ if _, err := decCtx.Quo(&q, &acc.d, apd.New(int64(n), 0)); err != nil {159229 return Value{}, false160230 }161231 return fromDec(&q)162232 },163- "ROUND": decRounder(numfmt.HalfUp),164- "ROUNDUP": decRounder(numfmt.Up),165- "ROUNDDOWN": decRounder(numfmt.Down),166- "TRUNC": decRounder(numfmt.Down),233+ "PRODUCT": func(args []Node, get lookup) (Value, bool) {234+ acc := newDecAcc(1)235+ n, e := decFold(args, get, acc.mul)236+ switch {237+ case e != nil:238+ return *e, true239+ case n == 0:240+ return num(0), true241+ }242+ return acc.result()243+ },244+ "SUMIF": decTerms(sumIfTerms),245+ "SUMIFS": decTerms(sumIfsTerms),246+ "SUMPRODUCT": decSumProduct,247+ "ROUND": decRounder(numfmt.HalfUp),248+ "ROUNDUP": decRounder(numfmt.Up),249+ "ROUNDDOWN": decRounder(numfmt.Down),250+ "TRUNC": decRounder(numfmt.Down),167251 }168252 169253 func decRounder(mode numfmt.Rounding) func([]Node, lookup) (Value, bool) {
M internal/sheet/decimal_test.go +17 -0
@@ -69,6 +69,23 @@ func TestDecimalArithmetic(t *testing.T) {
6969 {"average of nothing", "=AVERAGE(A1:A2)", nil, "#DIV/0!", "#DIV/0!"},7070 {"sum ignores text in ranges", "=SUM(A1:A3)", []string{"0.1", "x", "0.2"}, "0.30000000000000004", "0.3"},7171 {"huge sum falls back", "=SUM(1E308, 1E308)", nil, "#NUM!", "#NUM!"},72+ // PRODUCT, SUMIF, SUMIFS and SUMPRODUCT have decimal twins too.73+ {"product", "=PRODUCT(A1:A2)", []string{"1.1", "1.1"}, "1.2100000000000002", "1.21"},74+ {"product of direct", "=PRODUCT(0.07, 3)", nil, "0.21000000000000002", "0.21"},75+ {"product skips text in ranges", "=PRODUCT(A1:A3)", []string{"1.1", "x", "1.1"}, "1.2100000000000002", "1.21"},76+ {"product of nothing", "=PRODUCT(A1:A2)", nil, "0", "0"},77+ {"product of text", `=PRODUCT("a")`, nil, "#VALUE!", "#VALUE!"},78+ {"huge product falls back", "=PRODUCT(1E308, 10)", nil, "#NUM!", "#NUM!"},79+ {"sumif", `=SUMIF(A1:A10, ">0")`, tenCents, "0.9999999999999999", "1"},80+ {"sumif equals", `=SUMIF(A1:A10, ">0")=1`, tenCents, "FALSE", "TRUE"},81+ {"sumif with sum range", `=SUMIF(A1:A3, "<>x", A1:A3)`, []string{"0.1", "x", "0.2"}, "0.30000000000000004", "0.3"},82+ {"sumif error", `=SUMIF(A1:A2, ">0")`, []string{"1", "=1/0"}, "1", "1"},83+ {"sumifs", `=SUMIFS(A1:A10, A1:A10, "0.1")`, tenCents, "0.9999999999999999", "1"},84+ {"sumifs mismatched", `=SUMIFS(A1:A10, A1:A9, ">0")`, tenCents, "#VALUE!", "#VALUE!"},85+ {"sumproduct", "=SUMPRODUCT(A1:A3, A1:A3)", []string{"0.1", "0.2", "0.3"}, "0.14", "0.14"},86+ {"sumproduct of cents", "=SUMPRODUCT(A1:A2, A3:A4)", []string{"0.1", "0.2", "3", "3"}, "0.9000000000000001", "0.9"},87+ {"sumproduct mismatched", "=SUMPRODUCT(A1:A2, A1:A3)", []string{"1", "2", "3"}, "#VALUE!", "#VALUE!"},88+ {"sumproduct error", "=SUMPRODUCT(A1:A2)", []string{"1", "=1/0"}, "#DIV/0!", "#DIV/0!"},7289 }7390 show := func(v Value) string {7491 if v.Kind == Number {
M internal/sheet/file.go +6 -0
M internal/sheet/functions_math.go +60 -25
@@ -134,21 +134,55 @@ func toMultiple(round func(float64) float64) func([]Node, lookup) Value {
134134 }135135 }136136 137+// sumIf, sumIfs and sumProduct add in binary; their decimal twins138+// (decimal.go) visit the same terms and add them in decimal.137139 func sumIf(args []Node, get lookup) Value {140+ total := 0.0141+ if e := sumIfTerms(args, get, func(f float64) { total += f }); e != nil {142+ return *e143+ }144+ return num(total)145+}146+147+func sumIfs(args []Node, get lookup) Value {148+ total := 0.0149+ if e := sumIfsTerms(args, get, func(f float64) { total += f }); e != nil {150+ return *e151+ }152+ return num(total)153+}154+155+func sumProduct(args []Node, get lookup) Value {156+ total := 0.0157+ e := sumProductTerms(args, get, func(fs []float64) {158+ p := 1.0159+ for _, f := range fs {160+ p *= f161+ }162+ total += p163+ })164+ if e != nil {165+ return *e166+ }167+ return num(total)168+}169+170+// sumIfTerms calls add with each number SUMIF sums, or returns the error171+// it gives.172+func sumIfTerms(args []Node, get lookup, add func(float64)) *Value {138173 rng := matrixArg(args[0], get)139174 cv := eval(args[1], get)140175 if cv.Kind == Error {141- return cv176+ return &cv142177 }143178 c := newCriterion(cv)144179 sum := rng145180 if len(args) > 2 {146181 sum = matrixArg(args[2], get).resized(rng.rows, rng.cols, get)147182 if sum.rows != rng.rows || sum.cols != rng.cols {148- return ErrValue183+ return &ErrValue149184 }150185 }151- total := 0.0152186 for i := range rng.size() {153187 if !c.test(rng.at(i)) {154188 continue
@@ -156,24 +190,25 @@ func sumIf(args []Node, get lookup) Value {
156190 v := sum.at(i)157191 switch v.Kind {158192 case Error:159- return v193+ return &v160194 case Number:161- total += v.Num195+ add(v.Num)162196 }163197 }164- return num(total)198+ return nil165199 }166200 167-func sumIfs(args []Node, get lookup) Value {201+// sumIfsTerms calls add with each number SUMIFS sums, or returns the202+// error it gives.203+func sumIfsTerms(args []Node, get lookup, add func(float64)) *Value {168204 sum := matrixArg(args[0], get)169205 rows, cols, mask, err := criteriaMask(args, 1, get)170206 switch {171207 case err != nil:172- return *err208+ return err173209 case rows != sum.rows || cols != sum.cols:174- return ErrValue210+ return &ErrValue175211 }176- total := 0.0177212 for i, ok := range mask {178213 if !ok {179214 continue
@@ -181,39 +216,39 @@ func sumIfs(args []Node, get lookup) Value {
181216 v := sum.at(i)182217 switch v.Kind {183218 case Error:184- return v219+ return &v185220 case Number:186- total += v.Num221+ add(v.Num)187222 }188223 }189- return num(total)224+ return nil190225 }191226 192-// sumProduct multiplies same-sized arrays entry by entry and sums; text193-// and blanks count as 0.194-func sumProduct(args []Node, get lookup) Value {227+// sumProductTerms calls term with the factors of each entry of228+// same-sized arrays (text and blanks count as 0), or returns the error229+// SUMPRODUCT gives. The slice is reused between calls.230+func sumProductTerms(args []Node, get lookup, term func(fs []float64)) *Value {195231 ms := make([]matrix, len(args))196232 for i, a := range args {197233 ms[i] = matrixArg(a, get)198234 if ms[i].rows != ms[0].rows || ms[i].cols != ms[0].cols {199- return ErrValue235+ return &ErrValue200236 }201237 }202- total := 0.0238+ fs := make([]float64, len(ms))203239 for k := range ms[0].size() {204- p := 1.0205- for _, m := range ms {240+ for i, m := range ms {206241 v := m.at(k)207242 switch v.Kind {208243 case Error:209- return v244+ return &v210245 case Number:211- p *= v.Num246+ fs[i] = v.Num212247 default:213- p = 0248+ fs[i] = 0214249 }215250 }216- total += p251+ term(fs)217252 }218- return num(total)253+ return nil219254 }
A internal/sheet/hidden.go +84 -0
@@ -0,0 +1,84 @@
1+package sheet2+3+import (4+ "errors"5+ "slices"6+)7+8+// Hidden sheets, as Sheets' Hide sheet: a hidden sheet keeps its cells9+// and stays in formulas, named ranges and pivot sources; it is left out10+// of the tabs and of moving between sheets, and View > Hidden sheets11+// shows it again. At least one sheet is always visible. Hiding and12+// showing are undo steps that snapshot the sheet list (historysheets.go),13+// and files keep the flag in an optional field older builds ignore.14+15+// Hidden reports whether the sheet is hidden.16+func (s *Sheet) Hidden() bool { return s.tabHidden }17+18+// Visible returns the sheets that aren't hidden, in tab order.19+func (w *Workbook) Visible() []*Sheet {20+ return slices.DeleteFunc(slices.Clone(w.sheets), (*Sheet).Hidden)21+}22+23+// HiddenSheets returns the hidden sheets, in tab order.24+func (w *Workbook) HiddenSheets() []*Sheet {25+ return slices.DeleteFunc(slices.Clone(w.sheets), func(s *Sheet) bool { return !s.tabHidden })26+}27+28+// visibleCount is how many sheets aren't hidden.29+func (w *Workbook) visibleCount() int {30+ n := 031+ for _, s := range w.sheets {32+ if !s.tabHidden {33+ n++34+ }35+ }36+ return n37+}38+39+// errLastVisible is why the last visible sheet can't be hidden or deleted.40+var errLastVisible = errors.New("A spreadsheet needs at least one visible sheet")41+42+// HideSheet hides s, as one undo step. The last visible sheet can't be43+// hidden.44+func (w *Workbook) HideSheet(s *Sheet) error {45+ switch {46+ case !s.live:47+ return errors.New("That sheet was deleted")48+ case s.tabHidden:49+ return nil50+ case w.visibleCount() == 1:51+ return errLastVisible52+ }53+ w.change(s, "hide sheet "+s.name, Rect{}, func() {54+ w.recordSheets()55+ s.tabHidden = true56+ })57+ return nil58+}59+60+// UnhideSheet shows s again, as one undo step.61+func (w *Workbook) UnhideSheet(s *Sheet) error {62+ switch {63+ case !s.live:64+ return errors.New("That sheet was deleted")65+ case !s.tabHidden:66+ return nil67+ }68+ w.change(s, "show sheet "+s.name, Rect{}, func() {69+ w.recordSheets()70+ s.tabHidden = false71+ })72+ return nil73+}74+75+// settleHidden keeps a loaded workbook showable: if every sheet is76+// hidden the first is shown, and a hidden sheet isn't the active one.77+func (w *Workbook) settleHidden() {78+ if w.visibleCount() == 0 {79+ w.sheets[0].tabHidden = false80+ }81+ if w.sheets[w.Active()].tabHidden {82+ w.active = w.Index(w.Visible()[0])83+ }84+}
A internal/sheet/hidden_test.go +90 -0
@@ -0,0 +1,90 @@
1+package sheet2+3+import (4+ "bytes"5+ "strings"6+ "testing"7+)8+9+// Hiding a sheet keeps it in formulas, is one undo step each way, and10+// the last visible sheet can't be hidden or deleted.11+func TestHideSheet(t *testing.T) {12+ w := bookOf(t,13+ page{"Summary", map[string]string{"A1": "=Data!A1*2"}},14+ page{"Data", map[string]string{"A1": "21"}},15+ )16+ data, summary := w.Lookup("Data"), w.Lookup("Summary")17+ if err := w.HideSheet(data); err != nil {18+ t.Fatal(err)19+ }20+ if !data.Hidden() || len(w.Visible()) != 1 || w.HiddenSheets()[0] != data {21+ t.Fatalf("hidden %v, visible %d", data.Hidden(), len(w.Visible()))22+ }23+ if got := show(t, w, "Summary!A1"); got != "42" {24+ t.Errorf("formula reading the hidden sheet = %s", got)25+ }26+ data.Set(at("A1"), "5")27+ if got := show(t, w, "Summary!A1"); got != "10" {28+ t.Errorf("after editing the hidden sheet = %s", got)29+ }30+ if err := w.HideSheet(summary); err != errLastVisible {31+ t.Errorf("hiding the last visible sheet: %v", err)32+ }33+ if err := w.DeleteSheet(summary); err != errLastVisible {34+ t.Errorf("deleting the last visible sheet: %v", err)35+ }36+ c, ok := w.Undo()37+ if !ok || c.Label != "edit A1" {38+ t.Fatalf("undo %+v", c)39+ }40+ c, ok = w.Undo()41+ if !ok || c.Label != "hide sheet Data" || data.Hidden() {42+ t.Fatalf("undo hide: %+v, hidden %v", c, data.Hidden())43+ }44+ w.Redo()45+ if !data.Hidden() {46+ t.Error("redo didn't hide")47+ }48+ if err := w.UnhideSheet(data); err != nil || data.Hidden() {49+ t.Fatalf("unhide: %v", err)50+ }51+ if c, _ := w.Undo(); c.Label != "show sheet Data" || !data.Hidden() {52+ t.Errorf("undo unhide: %+v", c)53+ }54+}55+56+// Hidden sheets are saved in an optional field; a file whose sheets are57+// all hidden, or that was showing a hidden one, opens on a visible one.58+func TestHiddenSheetFile(t *testing.T) {59+ w := bookOf(t,60+ page{"Summary", map[string]string{"A1": "=Data!A1*2"}},61+ page{"Data", map[string]string{"A1": "21"}},62+ )63+ w.HideSheet(w.Lookup("Data"))64+ var b bytes.Buffer65+ if err := w.Write(&b); err != nil {66+ t.Fatal(err)67+ }68+ out := b.String()69+ if !strings.Contains(out, `"version": 4,`) || strings.Count(out, `"hidden": true`) != 1 ||70+ !strings.Contains(out, "\"name\": \"Data\",\n \"hidden\": true,") {71+ t.Fatalf("file:\n%s", out)72+ }73+ r, err := ReadBook(strings.NewReader(out))74+ if err != nil {75+ t.Fatal(err)76+ }77+ if !r.Lookup("Data").Hidden() || r.Lookup("Summary").Hidden() || show(t, r, "Summary!A1") != "42" {78+ t.Errorf("read back: Data hidden %v", r.Lookup("Data").Hidden())79+ }80+81+ all := strings.Replace(out, `"name": "Summary",`, `"name": "Summary", "hidden": true,`, 1)82+ all = strings.Replace(all, `"sheets"`, `"active": 1, "sheets"`, 1)83+ r, err = ReadBook(strings.NewReader(all))84+ if err != nil {85+ t.Fatal(err)86+ }87+ if r.Sheet(0).Hidden() || !r.Sheet(1).Hidden() || r.Active() != 0 {88+ t.Errorf("every sheet hidden: first hidden %v, active %d", r.Sheet(0).Hidden(), r.Active())89+ }90+}
M internal/sheet/historysheets.go +10 -5
A internal/sheet/insertbook.go +117 -0
@@ -0,0 +1,117 @@
1+package sheet2+3+import (4+ "errors"5+ "fmt"6+ "slices"7+)8+9+// Importing into a workbook, as Sheets' Import location: "Insert new10+// sheet(s)" moves every sheet of an imported workbook into this one, and11+// "Replace current sheet" puts the imported sheet in the place of one.12+// The imported workbook is built on its own (fileio) and given up to13+// these; either is one undo step.14+15+// ImportResult says what an import into a workbook did.16+type ImportResult struct {17+ Sheets []*Sheet // the sheets added, in tab order18+ Renamed map[string]string // imported names changed to fit, old to new19+ Names int // named ranges left out, their names taken20+}21+22+// InsertBook moves the sheets of src, an imported workbook, into w after23+// its last sheet, with src's named ranges, as one undo step labelled24+// label. A sheet whose name w already has gets a number ("Sales 2"), and25+// src's formulas follow the new name; a named range whose name w already26+// has is left out. src is used up.27+func (w *Workbook) InsertBook(src *Workbook, label string) (ImportResult, error) {28+ var res ImportResult29+ if src == w || len(src.sheets) == 0 {30+ return res, errors.New("Nothing to import")31+ }32+ sheets := slices.Clone(src.sheets)33+ for _, s := range sheets {34+ if w.Lookup(s.name) == nil {35+ continue36+ }37+ name := freeIn(s.name, w, src)38+ if res.Renamed == nil {39+ res.Renamed = map[string]string{}40+ }41+ res.Renamed[s.name] = name42+ if err := src.RenameSheet(s, name); err != nil {43+ return res, err44+ }45+ }46+ names := src.Names()47+ for _, s := range sheets {48+ src.detach(s)49+ s.wb = w50+ }51+ src.sheets = nil52+ w.change(sheets[0], label, Rect{}, func() {53+ w.recordSheets()54+ for _, s := range sheets {55+ w.insert(s, len(w.sheets))56+ }57+ for _, n := range names {58+ if _, taken := w.LookupName(n.Name); taken {59+ res.Names++60+ continue61+ }62+ w.putName(nameKey(n.Name), &n)63+ }64+ })65+ res.Sheets = sheets66+ return res, nil67+}68+69+// ReplaceSheet puts s, the sheet of an imported workbook, in the place of70+// dst, as one undo step labelled label: it takes dst's name and position,71+// so formulas reading dst read it, and named ranges on dst move to it.72+// dst's cells, charts and the rest go with it; undo brings them back.73+func (w *Workbook) ReplaceSheet(dst, s *Sheet, label string) error {74+ switch {75+ case !dst.live || dst.wb != w:76+ return errors.New("That sheet was deleted")77+ case s.wb == w:78+ return errors.New("That sheet is already in the spreadsheet")79+ }80+ s.wb.detach(s)81+ s.wb.sheets = slices.DeleteFunc(s.wb.sheets, func(t *Sheet) bool { return t == s })82+ s.wb, s.tabHidden = w, dst.tabHidden83+ w.change(dst, label, Rect{}, func() {84+ w.recordSheets()85+ i := w.Index(dst)86+ w.remove(dst)87+ s.name = dst.name88+ w.insert(s, i)89+ for _, n := range w.Names() {90+ if n.Sheet == dst {91+ n.Sheet = s92+ w.putName(nameKey(n.Name), &n)93+ }94+ }95+ })96+ return nil97+}98+99+// freeIn returns base, or base with a number added ("Sales 2"), so that100+// none of books has a sheet of that name, within the length limit.101+func freeIn(base string, books ...*Workbook) string {102+ trim := func(s string, n int) string {103+ if r := []rune(s); len(r) > n {104+ return string(r[:n])105+ }106+ return s107+ }108+ taken := func(name string) bool {109+ return slices.ContainsFunc(books, func(w *Workbook) bool { return w.Lookup(name) != nil })110+ }111+ name := trim(base, maxSheetName)112+ for n := 2; taken(name); n++ {113+ suffix := fmt.Sprintf(" %d", n)114+ name = trim(base, maxSheetName-len(suffix)) + suffix115+ }116+ return name117+}
A internal/sheet/insertbook_test.go +112 -0
@@ -0,0 +1,112 @@
1+package sheet2+3+import (4+ "testing"5+)6+7+// Inserting an imported workbook adds its sheets after the others,8+// renaming the ones whose names are taken (and the formulas that use9+// them), brings its free named ranges, and undoes as one step.10+func TestInsertBook(t *testing.T) {11+ w := bookOf(t,12+ page{"Sales", map[string]string{"A1": "1"}},13+ page{"Notes", map[string]string{"A1": "=Sales!A1+Data!A1"}},14+ )15+ w.DefineName("Taken", w.Sheet(0), NewRect(at("A1"), at("A1")))16+ w.ClearHistory()17+ src := bookOf(t,18+ page{"Sales", map[string]string{"A1": "10", "A2": "=Data!A1*2"}},19+ page{"Data", map[string]string{"A1": "5", "A2": "=Sales!A1+1"}},20+ )21+ src.DefineName("Taken", src.Sheet(1), NewRect(at("A1"), at("A1")))22+ src.DefineName("Fresh", src.Sheet(1), NewRect(at("A1"), at("A1")))23+ src.hidePlain(src.Sheet(1))24+25+ res, err := w.InsertBook(src, "import book.xlsx")26+ if err != nil {27+ t.Fatal(err)28+ }29+ if got := namesOf(w); got != "Sales,Notes,Sales 2,Data" {30+ t.Fatalf("sheets %s", got)31+ }32+ if len(res.Sheets) != 2 || res.Renamed["Sales"] != "Sales 2" || res.Names != 1 {33+ t.Errorf("result %+v", res)34+ }35+ for ref, want := range map[string]string{36+ "Sales 2!A2": "10", // Data!A1*2 on the imported sheets37+ "Data!A2": "11", // Sales!A1+1 now reads Sales 238+ "Data!input:A2": "='Sales 2'!A1+1",39+ "Notes!A1": "6", // Data exists now40+ } {41+ if got := show(t, w, ref); got != want {42+ t.Errorf("%s = %s, want %s", ref, got, want)43+ }44+ }45+ if !w.Lookup("Data").Hidden() {46+ t.Error("Data lost its hidden flag")47+ }48+ if n, ok := w.LookupName("Fresh"); !ok || n.Sheet != w.Lookup("Data") {49+ t.Errorf("Fresh: %v %+v", ok, n)50+ }51+ if n, _ := w.LookupName("Taken"); n.Sheet != w.Sheet(0) {52+ t.Error("an imported name replaced Taken")53+ }54+55+ c, ok := w.Undo()56+ if !ok || c.Label != "import book.xlsx" || namesOf(w) != "Sales,Notes" {57+ t.Fatalf("undo: %+v, sheets %s", c, namesOf(w))58+ }59+ if _, ok := w.LookupName("Fresh"); ok || show(t, w, "Notes!A1") != "#REF!" {60+ t.Errorf("undo left Fresh %v, Notes!A1 %s", ok, show(t, w, "Notes!A1"))61+ }62+ w.Redo()63+ if namesOf(w) != "Sales,Notes,Sales 2,Data" || show(t, w, "Notes!A1") != "6" {64+ t.Errorf("redo: %s", namesOf(w))65+ }66+}67+68+// Replacing a sheet puts the imported one in its place, with its name,69+// so formulas and named ranges reading it read the new data.70+func TestReplaceSheet(t *testing.T) {71+ w := bookOf(t,72+ page{"Sales", map[string]string{"A1": "1", "B1": "old"}},73+ page{"Notes", map[string]string{"A1": "=Sales!A1*2"}},74+ )75+ w.DefineName("First", w.Sheet(0), NewRect(at("A1"), at("A1")))76+ w.Sheet(1).Set(at("A2"), "=First")77+ w.ClearHistory()78+ src := bookOf(t, page{"sales", map[string]string{"A1": "21"}})79+80+ old := w.Sheet(0)81+ if err := w.ReplaceSheet(old, src.Sheet(0), "import sales.csv"); err != nil {82+ t.Fatal(err)83+ }84+ if namesOf(w) != "Sales,Notes" || old.Live() || show(t, w, "Sales!B1") != "" {85+ t.Fatalf("sheets %s", namesOf(w))86+ }87+ if show(t, w, "Notes!A1") != "42" || show(t, w, "Notes!A2") != "21" {88+ t.Errorf("Notes!A1 %s, A2 %s", show(t, w, "Notes!A1"), show(t, w, "Notes!A2"))89+ }90+ c, _ := w.Undo()91+ if c.Label != "import sales.csv" || w.Sheet(0) != old || show(t, w, "Notes!A1") != "2" || show(t, w, "Notes!A2") != "1" {92+ t.Errorf("undo: %+v, Notes!A1 %s", c, show(t, w, "Notes!A1"))93+ }94+ w.Redo()95+ if show(t, w, "Notes!A1") != "42" {96+ t.Errorf("redo: %s", show(t, w, "Notes!A1"))97+ }98+}99+100+func namesOf(w *Workbook) string {101+ var out string102+ for i, s := range w.sheets {103+ if i > 0 {104+ out += ","105+ }106+ out += s.name107+ }108+ return out109+}110+111+// hidePlain hides s outside the history, as a loader would.112+func (w *Workbook) hidePlain(s *Sheet) { s.tabHidden = true }
M internal/sheet/refs.go +20 -0
M internal/sheet/sheet.go +4 -3
M internal/sheet/workbook.go +3 -14
M internal/ui/assist.go +23 -13
@@ -13,9 +13,10 @@ import (
1313 "github.com/FelineStateMachine/012/internal/ui/theme"1414 )1515 16-// Formula assistance, as in Sheets: while a function or range name is17-// being typed, a list of matching functions and named ranges drops down18-// from the formula bar at the caret, and while the caret is inside a16+// Formula assistance, as in Sheets: while a function, range or sheet17+// name is being typed, a list of matching functions, named ranges and18+// other sheets (assistsheets.go) drops down from the formula bar at the19+// caret, and while the caret is inside a1920 // function's parentheses the context line shows its signature with the2021 // current argument marked. The list takes only the keys Sheets gives it2122 // (Up/Down, Tab, Enter, Esc) and only while it shows, so arrows after an
@@ -34,21 +35,24 @@ const assistRows = 8
3435 3536 const assistID = "assist"3637 37-// suggestion is a function or named range offered for the word at the38-// caret.38+// suggestion is a function, named range or sheet offered for the word39+// at the caret.3940 type suggestion struct {40- name string // e.g. "SUM" or "Sales"41- detail string // the signature, or the range a name stands for41+ name string // e.g. "SUM", "Sales" or "'Q3 plan'!"42+ detail string // the signature, the range a name stands for, or a sheet's cells4243 desc string4344 fn bool // a function: accepting it adds "("4445 }4546 46-// suggestions lists the functions and named ranges for word: those47-// starting with it first (names before functions), then, from two48-// letters on, those containing it, such as COUNTIFS for "ifs". A word49-// that is already a cell reference only gets names and functions that50-// start with it.47+// suggestions lists the functions, named ranges and sheets for word:48+// those starting with it first (names, then sheets, then functions),49+// then, from two letters on, those containing it, such as COUNTIFS for50+// "ifs". A word that is already a cell reference only gets those that51+// start with it, and a word in quotes ('Q3) only sheets.5152 func suggestions(sh *sheet.Sheet, word string) []suggestion {53+ if rest, quoted := strings.CutPrefix(word, "'"); quoted {54+ return sheetSuggestions(sh, strings.ToUpper(rest))55+ }5256 w := strings.ToUpper(word)5357 var prefix, inner []suggestion5458 add := func(s suggestion, name string) {
@@ -62,6 +66,9 @@ func suggestions(sh *sheet.Sheet, word string) []suggestion {
6266 for _, n := range sh.Names() {6367 add(suggestion{name: n.Name, detail: n.Ref(), desc: "Named range " + n.Name + ": " + n.Ref()}, strings.ToUpper(n.Name))6468 }69+ for _, s := range otherSheets(sh) {70+ add(sheetSuggestion(s), strings.ToUpper(s.Name()))71+ }6572 for _, f := range sheet.Funcs() {6673 add(suggestion{name: f.Name, detail: f.Name + "(" + f.Args + ")", desc: f.Desc, fn: true}, f.Name)6774 }
@@ -83,7 +90,7 @@ func (a *assist) shown(m *Model) ([]suggestion, int) {
8390 if c.Word == "" {8491 return nil, 08592 }86- list := suggestions(m.sheet, c.Word)93+ list := suggestions(m.entrySheet(), c.Word)8794 if len(list) == 0 || len(list) == 1 && !list[0].fn && strings.EqualFold(list[0].name, c.Word) {8895 return nil, 08996 }
@@ -128,6 +135,9 @@ func (a *assist) accept(m *Model, s suggestion, start int) {
128135 if s.fn && (len(rest) == 0 || rest[0] != '(') {129136 text = append(text, '(')130137 }138+ if strings.HasSuffix(s.name, "!") {139+ rest = trimSheetEnd(rest) // the rest of a sheet name typed before140+ }131141 pos := start + len(text)132142 if s.fn && len(rest) > 0 && rest[0] == '(' {133143 pos++ // into the existing parentheses
A internal/ui/assistsheets.go +131 -0
@@ -0,0 +1,131 @@
1+package ui2+3+import (4+ "slices"5+ "strings"6+7+ "github.com/FelineStateMachine/012/internal/sheet"8+)9+10+// Sheet names in formula suggestions: typing =Su offers Summary! with the11+// functions and named ranges, and ='Q3 offers 'Q3 plan'!, quoted as a12+// reference needs; the sheet the formula is on isn't, as its cells need13+// no name, and neither are hidden sheets. Accepting one inserts the sheet14+// and its "!"; an arrow then points into that sheet, as clicking its tab15+// would, so the reference comes out as Summary!B3.16+17+// sheetSuggestion is the suggestion for sheet s.18+func sheetSuggestion(s *sheet.Sheet) suggestion {19+ detail, desc := "sheet, empty", "Sheet "+s.Name()+", empty"20+ if used, ok := s.UsedRange(); ok {21+ detail = "sheet, " + used.String()22+ desc = "Sheet " + s.Name() + ": " + used.String() + ", " + cellCount(s.Len())23+ }24+ return suggestion{name: sheet.QuoteSheet(s.Name()) + "!", detail: detail, desc: desc}25+}26+27+// otherSheets are the sheets offered in formulas typed on sh: the28+// visible ones but sh, whose cells need no sheet name.29+func otherSheets(sh *sheet.Sheet) []*sheet.Sheet {30+ return slices.DeleteFunc(sh.Book().Visible(), func(s *sheet.Sheet) bool { return s == sh })31+}32+33+// sheetSuggestions lists the other visible sheets for w, a quoted name typed34+// so far in upper case: those starting with it, then from two letters35+// on, those containing it.36+func sheetSuggestions(sh *sheet.Sheet, w string) []suggestion {37+ var prefix, inner []suggestion38+ for _, s := range otherSheets(sh) {39+ name := strings.ToUpper(s.Name())40+ switch {41+ case strings.HasPrefix(name, w):42+ prefix = append(prefix, sheetSuggestion(s))43+ case len(w) >= 2 && strings.Contains(name, w):44+ inner = append(inner, sheetSuggestion(s))45+ }46+ }47+ return append(prefix, inner...)48+}49+50+// trimSheetEnd drops the end of a sheet reference ("!" or "'!") that51+// follows the caret, which an accepted sheet brings with it.52+func trimSheetEnd(rest []rune) []rune {53+ switch {54+ case len(rest) > 0 && rest[0] == '!':55+ return rest[1:]56+ case len(rest) > 1 && rest[0] == '\'' && rest[1] == '!':57+ return rest[2:]58+ }59+ return rest60+}61+62+// sheetBeforeCaret returns the sheet named just before the caret, as in63+// "=SUM(Summary!" or "='Q3 plan'!", and where its name starts.64+func (l *lineEdit) sheetBeforeCaret(book *sheet.Workbook) (*sheet.Sheet, int, bool) {65+ head := l.buf[:l.pos]66+ end := len(head) - 1 // the "!"67+ if end < 2 || head[end] != '!' {68+ return nil, 0, false69+ }70+ start := end71+ if head[end-1] == '\'' {72+ start = -173+ for i := end - 2; i >= 1; i-- {74+ if head[i] != '\'' {75+ continue76+ }77+ if head[i-1] == '\'' {78+ i-- // '' inside a quoted name79+ continue80+ }81+ start = i82+ break83+ }84+ } else {85+ for start > 1 && isNameRune(head[start-1]) {86+ start--87+ }88+ }89+ if start < 1 || start >= end {90+ return nil, 0, false91+ }92+ name, _ := sheet.SplitSheet(string(head[start:]))93+ s := book.Lookup(name)94+ return s, start, s != nil95+}96+97+func isNameRune(r rune) bool {98+ return r == '_' || r == '.' || r >= '0' && r <= '9' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r > 0x7f99+}100+101+// pointAfterSheet starts pointing with key when the caret follows a102+// sheet reference such as "Summary!": on that sheet, the name typed103+// giving way to the reference pointed at (Summary!B3), as if its tab had104+// been clicked. It reports whether it did.105+func (m *Model) pointAfterSheet(key string) bool {106+ if _, extend := extendKey(key); !extend && (!isMoveKey(key) || key == "tab" || key == "shift+tab") {107+ return false108+ }109+ s, start, ok := m.line.sheetBeforeCaret(m.book())110+ if !ok || s.Hidden() {111+ return false112+ }113+ if s == m.sheet && !m.away() {114+ return m.startPoint(key) // its own sheet: the name stays as typed115+ }116+ m.line.buf = append(m.line.buf[:start:start], m.line.buf[m.line.pos:]...)117+ m.line.pos = start118+ if s == m.sheet {119+ return m.startPoint(key)120+ }121+ m.pointInto(s)122+ if m.mode != modePoint {123+ return true124+ }125+ if base, extend := extendKey(key); extend {126+ m.point.anchor, m.point.anchored = m.point.at, true127+ key = base128+ }129+ m.navigate(key, &m.point.at)130+ return true131+}
A internal/ui/assistsheets_test.go +109 -0
@@ -0,0 +1,109 @@
1+package ui2+3+import (4+ "strings"5+ "testing"6+)7+8+// sheetsModel has Sheet1 (shown), Summary with 7 in B3, and Q3 plan with9+// 5 in A2.10+func sheetsModel(t *testing.T) *Model {11+ t.Helper()12+ m := newModel()13+ book := m.book()14+ sum, _ := book.AddSheet("Summary", 1)15+ sum.Set(addr("B3"), "7")16+ plan, _ := book.AddSheet("Q3 plan", 2)17+ plan.Set(addr("A2"), "5")18+ book.ClearHistory()19+ return m20+}21+22+func names(list []suggestion) []string {23+ var out []string24+ for _, s := range list {25+ out = append(out, s.name)26+ }27+ return out28+}29+30+func TestAutocompleteSheets(t *testing.T) {31+ m := sheetsModel(t)32+ press(t, m, "=su")33+ list, _ := m.entry.assist.shown(m)34+ got := names(list)35+ if len(got) < 2 || got[0] != "Summary!" || !strings.HasPrefix(got[1], "SU") {36+ t.Fatalf("suggestions %v", got)37+ }38+ if scr := screen(m); !strings.Contains(scr, "│ Summary! sheet, A1:B3") {39+ t.Errorf("list:\n%s", scr)40+ }41+ if st := status(m); !strings.Contains(st, "Sheet Summary: A1:B3, 1 cell") {42+ t.Errorf("status %q", st)43+ }44+ press(t, m, "<tab>")45+ if m.line.text() != "=Summary!" || m.mode != modeEnter {46+ t.Fatalf("accepted: %q, mode %v", m.line.text(), m.mode)47+ }48+ // Typing the address works as ever.49+ press(t, m, "B3*2", "<enter>")50+ if m.sheet.Value(addr("A1")).Num != 14 {51+ t.Errorf("A1 = %v (%q)", m.sheet.Value(addr("A1")), input(m, "A1"))52+ }53+54+ // A name that needs quotes is offered quoted, from its start or55+ // inside quotes.56+ for _, typed := range []string{"=q3", "='q3 p", "='pl"} {57+ press(t, m, "<esc>", "<esc>", typed)58+ list, start := m.entry.assist.shown(m)59+ if len(list) == 0 || list[0].name != "'Q3 plan'!" || start != 1 {60+ t.Errorf("%s: %v from %d", typed, names(list), start)61+ }62+ }63+ press(t, m, "<tab>")64+ if m.line.text() != "='Q3 plan'!" {65+ t.Fatalf("quoted: %q", m.line.text())66+ }67+ // An arrow then points into the sheet, as clicking its tab would.68+ press(t, m, "<down>")69+ if m.mode != modePoint || m.sheet.Name() != "Q3 plan" || m.pointRef() != "'Q3 plan'!A2" {70+ t.Fatalf("pointing: mode %v on %s, %q", m.mode, m.sheet.Name(), m.pointRef())71+ }72+ press(t, m, "+1", "<enter>")73+ if m.sheet.Name() != "Sheet1" || input(m, "A2") != "='Q3 plan'!A2+1" || m.sheet.Value(addr("A2")).Num != 6 {74+ t.Errorf("stored on %s: %q", m.sheet.Name(), input(m, "A2"))75+ }76+}77+78+// The entry's own sheet isn't offered, and pointing after its name typed79+// keeps the name; hidden sheets aren't offered, and a quote closing the80+// name isn't doubled.81+func TestAutocompleteSheetsEdges(t *testing.T) {82+ m := sheetsModel(t)83+ press(t, m, "=she")84+ if list, _ := m.entry.assist.shown(m); len(list) > 0 && list[0].name == "Sheet1!" {85+ t.Errorf("the formula's own sheet offered: %v", names(list))86+ }87+ press(t, m, "et1!", "<right>")88+ if m.mode != modePoint || m.sheet.Name() != "Sheet1" || m.pointRef() != "B1" {89+ t.Fatalf("own sheet pointing: %v %s %q", m.mode, m.sheet.Name(), m.pointRef())90+ }91+ press(t, m, "<esc>", "<esc>")92+93+ m.book().HideSheet(m.book().Lookup("Summary"))94+ press(t, m, "=summ")95+ if list, _ := m.entry.assist.shown(m); len(list) > 0 && list[0].name == "Summary!" {96+ t.Errorf("hidden sheet offered: %v", names(list))97+ }98+ press(t, m, "<esc>")99+100+ // Accepting inside an existing reference replaces the name only.101+ m.line.set("='Q3 p'!A1")102+ m.line.pos = len("='Q3 p")103+ m.entry.assist.active = true104+ m.mode = modeEnter105+ press(t, m, "<tab>")106+ if m.line.text() != "='Q3 plan'!A1" || m.line.pos != len("='Q3 plan'!") {107+ t.Errorf("replaced: %q at %d", m.line.text(), m.line.pos)108+ }109+}
M internal/ui/entry.go +2 -2
M internal/ui/find.go +3 -3
M internal/ui/findview.go +2 -2
M internal/ui/formula/formula.go +17 -4
@@ -115,8 +115,9 @@ func (s refScan) corners(pos int) ([][2]int, bool) {
115115 }116116 117117 // Caret describes the formula text before the caret: the word being118-// typed, if it may be a function or name, and the innermost function call119-// the caret is in.118+// typed, if it may be a function, name or sheet, and the innermost119+// function call the caret is in. A sheet name being typed in quotes is a120+// word starting with the quote, e.g. "'Q3 p".120121 type Caret struct {121122 Word string122123 WordStart int
@@ -132,7 +133,7 @@ func isWordRune(r rune) bool {
132133 // skipped, parentheses nest, and commas or semicolons at the innermost133134 // level separate arguments.134135 func ScanCaret(buf []rune, pos int) Caret {135- s := caretScan{start: -1}136+ s := caretScan{start: -1, quote: -1}136137 for i := 1; i < pos; i++ { // buf[0] is "=", "+" or "-"137138 s.step(buf, i)138139 }
@@ -140,13 +141,16 @@ func ScanCaret(buf []rune, pos int) Caret {
140141 return Caret{}141142 }142143 var c Caret144+ if s.quote >= 0 {145+ c.Word, c.WordStart = string(buf[s.quote:pos]), s.quote146+ }143147 for i := len(s.stack) - 1; i >= 0; i-- {144148 if s.stack[i].fn != "" {145149 c.Fn, c.Arg = s.stack[i].fn, s.stack[i].arg146150 break147151 }148152 }149- if s.start >= 0 && (pos == len(buf) || !isWordRune(buf[pos])) {153+ if s.quote < 0 && s.start >= 0 && (pos == len(buf) || !isWordRune(buf[pos])) {150154 w := []rune(strings.TrimPrefix(string(buf[s.start:pos]), "@"))151155 if len(w) > 0 && (unicode.IsLetter(w[0]) || w[0] == '_') {152156 c.Word, c.WordStart = string(w), pos-len(w)
@@ -159,6 +163,7 @@ func ScanCaret(buf []rune, pos int) Caret {
159163 type caretScan struct {160164 stack []callFrame // the calls open at this point, innermost last161165 inStr bool166+ quote int // start of a quoted sheet name being read, or -1162167 start int // start of the word being read, or -1163168 prev string // the word just before a space or "(" (SUM ( is allowed)164169 }
@@ -176,6 +181,12 @@ func (s *caretScan) step(buf []rune, i int) {
176181 s.inStr = r != '"'177182 return178183 }184+ if s.quote >= 0 {185+ if r == '\'' {186+ s.quote = -1187+ }188+ return189+ }179190 if isWordRune(r) {180191 if s.start < 0 {181192 s.start = i
@@ -190,6 +201,8 @@ func (s *caretScan) step(buf []rune, i int) {
190201 return // keep prev for "SUM ("191202 case '"':192203 s.inStr = true204+ case '\'':205+ s.quote = i193206 case '(':194207 s.stack = append(s.stack, callFrame{fn: strings.ToUpper(strings.TrimPrefix(s.prev, "@"))})195208 case ')':
M internal/ui/formula/formula_test.go +7 -0
A internal/ui/hidden.go +113 -0
@@ -0,0 +1,113 @@
1+package ui2+3+import (4+ tea "charm.land/bubbletea/v2"5+6+ "github.com/FelineStateMachine/012/internal/sheet"7+)8+9+// Hidden sheets, as in Sheets: the tab menu's Hide sheet takes a sheet10+// out of the tabs, and View > Hidden sheets lists them to show one11+// again. Formulas still read a hidden sheet; moving between sheets, the12+// tab strip, Go to sheet and Find in all sheets skip it, and going to a13+// cell on it (Go to, a named range) says it's hidden rather than show it.14+15+func init() {16+ register(17+ &command{id: "sheet.hide", title: "Hide sheet", desc: "Hide the sheet's tab; formulas still read it",18+ enabled: func(m *Model) bool { return len(m.book().Visible()) > 1 }, run: (*Model).hideSheet},19+ &command{id: "sheet.unhide", title: "Hidden sheets", desc: "List the hidden sheets and show one again",20+ enabled: func(m *Model) bool { return len(m.book().HiddenSheets()) > 0 }, run: func(m *Model) tea.Cmd {21+ m.openHiddenPicker()22+ return nil23+ }},24+ )25+}26+27+// hideSheet hides the sheet shown and shows the next visible one.28+func (m *Model) hideSheet() tea.Cmd {29+ s := m.sheet30+ i := m.book().Index(s)31+ if err := m.book().HideSheet(s); err != nil {32+ m.fail(err.Error())33+ return nil34+ }35+ m.afterSheetsChange(nil, i)36+ m.note = "Hid " + s.Name() + "; View > Hidden sheets shows it again"37+ return nil38+}39+40+// openHiddenPicker lists the hidden sheets; Enter shows the one picked41+// again. A macro records the pick as the command's answer.42+func (m *Model) openHiddenPicker() {43+ var items []pickItem44+ for _, s := range m.book().HiddenSheets() {45+ detail := "empty"46+ if used, ok := s.UsedRange(); ok {47+ detail = used.String() + ", " + cellCount(s.Len())48+ }49+ items = append(items, pickItem{50+ title: s.Name(), name: len(s.Name()), detail: detail, desc: "Show " + s.Name() + " again",51+ pick: func(m *Model) tea.Cmd {52+ m.closeOverlay()53+ if err := m.book().UnhideSheet(s); err != nil {54+ m.fail(err.Error())55+ return nil56+ }57+ m.showSheet(s)58+ m.note = "Unhid " + s.Name()59+ return nil60+ },61+ })62+ }63+ p := newPicker(m, "Hidden sheets", "Type a sheet name", 60, items)64+ p.action = "unhide"65+ p.answers = true66+ m.openOverlay(p)67+}68+69+// refuseHidden says so and reports true when s is a hidden sheet, which70+// isn't shown until it's unhidden.71+func (m *Model) refuseHidden(s *sheet.Sheet) bool {72+ if s == nil || !s.Hidden() {73+ return false74+ }75+ m.fail(hiddenMsg(s))76+ return true77+}78+79+func hiddenMsg(s *sheet.Sheet) string {80+ return s.Name() + " is hidden; View > Hidden sheets shows it again"81+}82+83+// visibleSheets are the sheets with tabs, and the sheet shown if it84+// somehow isn't among them, so the tab strip always has it.85+func (m *Model) visibleSheets() []*sheet.Sheet {86+ if !m.sheet.Hidden() {87+ return m.book().Visible()88+ }89+ var out []*sheet.Sheet90+ for _, s := range m.book().Sheets() {91+ if !s.Hidden() || s == m.sheet {92+ out = append(out, s)93+ }94+ }95+ return out96+}97+98+// nearVisible is the visible sheet at index i of the workbook or the99+// nearest one after it, else before it.100+func (m *Model) nearVisible(i int) *sheet.Sheet {101+ book := m.book()102+ for j := max(i, 0); j < book.Len(); j++ {103+ if s := book.Sheet(j); !s.Hidden() {104+ return s105+ }106+ }107+ for j := min(i, book.Len()-1); j >= 0; j-- {108+ if s := book.Sheet(j); !s.Hidden() {109+ return s110+ }111+ }112+ return book.Sheet(0)113+}
A internal/ui/hidden_test.go +144 -0
@@ -0,0 +1,144 @@
1+package ui2+3+import (4+ "strings"5+ "testing"6+)7+8+// hiddenBook is a model of three sheets, Sheet1 reading Data, showing9+// Data.10+func hiddenBook(t *testing.T) *Model {11+ t.Helper()12+ m := newModel()13+ press(t, m, "=", "D", "a", "t", "a", "!", "A", "1", "*", "2", "<enter>")14+ press(t, m, "<shift+f11>")15+ run(m, m.runCommand("sheet.rename"))16+ m.line.set("Data")17+ press(t, m, "<enter>", "2", "1", "<enter>", "<shift+f11>", "<ctrl+pgup>")18+ if sheetNames(m) != "Sheet1,Data,Sheet3" || m.sheet.Name() != "Data" {19+ t.Fatalf("sheets %s, on %s", sheetNames(m), m.sheet.Name())20+ }21+ return m22+}23+24+func TestHideSheetCommand(t *testing.T) {25+ m := hiddenBook(t)26+ run(m, m.runCommand("sheet.hide"))27+ if m.sheet.Name() != "Sheet3" || !m.book().Lookup("Data").Hidden() {28+ t.Fatalf("after hiding: on %s", m.sheet.Name())29+ }30+ if s := status(m); strings.Contains(s, "Data") || !strings.HasPrefix(s, " Sheet1 Sheet3 +") {31+ t.Errorf("tabs %q", s)32+ }33+ if !strings.Contains(line(m, contextLine), "Hid Data; View > Hidden sheets shows it again") {34+ t.Errorf("context %q", line(m, contextLine))35+ }36+ press(t, m, "<ctrl+pgup>") // skips Data37+ if m.sheet.Name() != "Sheet1" || m.sheet.Value(addr("A1")).Num != 42 {38+ t.Errorf("previous: %s, A1 = %v", m.sheet.Name(), m.sheet.Value(addr("A1")))39+ }40+ press(t, m, "<ctrl+pgdown>")41+ if m.sheet.Name() != "Sheet3" {42+ t.Errorf("next: %s", m.sheet.Name())43+ }44+ // Move left goes past the hidden sheet to the visible neighbour.45+ run(m, m.runCommand("sheet.move_left"))46+ if sheetNames(m) != "Sheet3,Sheet1,Data" {47+ t.Errorf("moved left: %s", sheetNames(m))48+ }49+ run(m, m.runCommand("edit.undo"))50+51+ // The last visible sheet can't be hidden or deleted.52+ run(m, m.runCommand("sheet.hide"))53+ if m.sheet.Name() != "Sheet1" || commands["sheet.hide"].available(m) || commands["sheet.delete"].available(m) {54+ t.Fatalf("one visible sheet left: on %s", m.sheet.Name())55+ }56+ if !strings.HasPrefix(status(m), " Sheet1 +") {57+ t.Errorf("tabs %q", status(m))58+ }59+60+ // Going to a cell on a hidden sheet says it's hidden.61+ run(m, m.runCommand("goto"))62+ m.line.set("Data!A1")63+ press(t, m, "<enter>")64+ if m.sheet.Name() != "Sheet1" || m.mode != modeError || m.errMsg != "Data is hidden; View > Hidden sheets shows it again" {65+ t.Errorf("goto: on %s, %q", m.sheet.Name(), m.errMsg)66+ }67+ press(t, m, "<esc>")68+69+ // Undo shows the sheet again; redo hides it and shows another.70+ press(t, m, "<ctrl+z>")71+ if m.sheet.Name() != "Sheet3" || m.sheet.Hidden() {72+ t.Errorf("undo: on %s", m.sheet.Name())73+ }74+ press(t, m, "<ctrl+y>")75+ if m.sheet.Hidden() || m.sheet.Name() != "Sheet1" {76+ t.Errorf("redo: on %s (hidden %v)", m.sheet.Name(), m.sheet.Hidden())77+ }78+}79+80+func TestHiddenSheetsPicker(t *testing.T) {81+ m := hiddenBook(t)82+ if commands["sheet.unhide"].available(m) {83+ t.Error("Hidden sheets available with none hidden")84+ }85+ run(m, m.runCommand("sheet.hide"))86+ run(m, m.runCommand("sheet.unhide"))87+ p, ok := m.overlay.(*picker)88+ if !ok || p.title != "Hidden sheets" || len(p.items) != 1 || p.items[0].title != "Data" {89+ t.Fatalf("overlay %#v", m.overlay)90+ }91+ if !strings.Contains(screen(m), "Hidden sheets") || !strings.Contains(screen(m), "A1, 1 cell") {92+ t.Errorf("picker:\n%s", screen(m))93+ }94+ press(t, m, "<enter>")95+ if m.overlay != nil || m.sheet.Name() != "Data" || m.sheet.Hidden() {96+ t.Fatalf("unhid: on %s", m.sheet.Name())97+ }98+ if !strings.HasPrefix(status(m), " Sheet1 Data Sheet3") || !strings.Contains(line(m, contextLine), "Unhid Data") {99+ t.Errorf("status %q, context %q", status(m), line(m, contextLine))100+ }101+ press(t, m, "<ctrl+z>")102+ if !m.book().Lookup("Data").Hidden() || m.sheet.Name() == "Data" {103+ t.Errorf("undo unhide: on %s", m.sheet.Name())104+ }105+}106+107+// Hiding is recorded as a command, unhiding with the sheet picked as108+// its answer, and both replay.109+func TestRecordHideAndUnhide(t *testing.T) {110+ m := hiddenBook(t)111+ send(m, nil)112+ run(m, m.runCommand("macro.record"))113+ run(m, m.runCommand("sheet.hide"))114+ run(m, m.runCommand("sheet.unhide"))115+ press(t, m, "<enter>")116+ run(m, m.runCommand("macro.stop"))117+ m.line.set("Toggle")118+ press(t, m, "<enter>")119+ m.line.set("")120+ press(t, m, "<enter>")121+ mc, ok := m.book().Macro("Toggle")122+ if !ok {123+ t.Fatalf("no macro: %q", m.errMsg)124+ }125+ want := "run(\"sheet.hide\")\nrun(\"sheet.unhide\", answer=\"Data\")"126+ if !strings.Contains(mc.Source, want) {127+ t.Errorf("script:\n%s\nwant it to contain\n%s", mc.Source, want)128+ }129+130+ m = hiddenBook(t)131+ script(t, m, `132+run("sheet.hide")133+set("A2", active_sheet())134+run("sheet.unhide", answer="data")135+`)136+ if m.sheet.Name() != "Data" || m.sheet.Hidden() || m.book().Lookup("Sheet3").Value(addr("A2")).Str != "Sheet3" {137+ t.Errorf("replay: on %s, %q", m.sheet.Name(), m.errMsg)138+ }139+ script(t, m, `run("sheet.hide")140+run("sheet.unhide", answer="Nope")`)141+ if !strings.Contains(m.warn, `Hidden sheets asks for one of Data: give run("sheet.unhide", answer=...)`) {142+ t.Errorf("bad answer: %q", m.warn)143+ }144+}
A internal/ui/importplace.go +121 -0
@@ -0,0 +1,121 @@
1+package ui2+3+import (4+ "path/filepath"5+ "strings"6+7+ tea "charm.land/bubbletea/v2"8+9+ "github.com/FelineStateMachine/012/internal/fileio"10+ "github.com/FelineStateMachine/012/internal/sheet"11+)12+13+// Where File > Import puts what it reads, as Sheets' Import location:14+// into new sheets after the others, in place of the sheet shown, or in15+// place of the whole spreadsheet (what File > Open and the command line16+// do). A new, empty spreadsheet is just replaced. Inserting or replacing17+// a sheet is one undo step, and the file stays the one being edited.18+19+// importPlace is where an import goes.20+type importPlace int21+22+const (23+ placeBook importPlace = iota // replace the spreadsheet24+ placeNewSheets // insert new sheets25+ placeSheet // replace the sheet shown26+)27+28+// pristine reports whether the spreadsheet is new and untouched, so an29+// import may simply replace it.30+func (m *Model) pristine() bool {31+ return !m.changed && m.filename == "" && m.xfer.source == "" && m.book().Len() == 1 &&32+ m.sheet.Len() == 0 && len(m.sheet.Charts()) == 033+}34+35+// askImportPlace imports name, asking first where it goes unless the36+// spreadsheet is new and empty.37+func (m *Model) askImportPlace(name string) tea.Cmd {38+ if m.pristine() {39+ return m.startImport(name, fileio.Options{}, placeBook)40+ }41+ k, _ := fileio.KindOf(name)42+ base := filepath.Base(name)43+ insert, as := "Insert new sheet", " as a new sheet"44+ if k.HoldsSheets() {45+ insert, as = "Insert new sheets", " as new sheets"46+ }47+ item := func(title, detail, desc string, run func(m *Model) tea.Cmd) pickItem {48+ return pickItem{title: title, name: len(title), detail: detail, desc: desc, pick: func(m *Model) tea.Cmd {49+ m.closeOverlay()50+ return run(m)51+ }}52+ }53+ items := []pickItem{item(insert, "after the others", "Add "+base+as,54+ func(m *Model) tea.Cmd { return m.startImport(name, fileio.Options{}, placeNewSheets) })}55+ if !k.HoldsSheets() {56+ items = append(items, item("Replace current sheet", m.sheet.Name(), "Put "+base+" in place of "+m.sheet.Name()+", keeping its name",57+ func(m *Model) tea.Cmd { return m.startImport(name, fileio.Options{}, placeSheet) }))58+ }59+ detail := "open it instead"60+ if m.changed {61+ detail = "unsaved changes"62+ }63+ items = append(items, item("Replace spreadsheet", detail, "Open "+base+" instead, as File > Open does",64+ func(m *Model) tea.Cmd { return m.confirmImport(name, fileio.Options{}) }))65+ p := newPicker(m, "Import "+base, "Import location", 60, items)66+ p.action = "import"67+ m.openOverlay(p)68+ return nil69+}70+71+// placeImport puts an imported workbook where the import was asked to go72+// and says what it did, or reports false to replace the spreadsheet.73+func (m *Model) placeImport(msg importedMsg, what string) bool {74+ res := msg.res75+ label := "import " + filepath.Base(msg.name)76+ switch msg.place {77+ case placeNewSheets:78+ ins, err := m.book().InsertBook(res.Sheet.Book(), label)79+ if err != nil {80+ m.fail(err.Error())81+ return true82+ }83+ show := res.Sheet84+ if show.Hidden() {85+ show = ins.Sheets[0]86+ }87+ m.afterSheetsChange(show, m.book().Index(m.sheet))88+ m.note = "Imported " + what + " as " + sheetList(ins.Sheets) + " (" + countRows(res.Rows) + ")"89+ if ins.Names > 0 {90+ m.note += "; " + plural(ins.Names, "1 named range", thousands(ins.Names)+" named ranges") + " left out, their names taken"91+ }92+ case placeSheet:93+ i := m.book().Index(m.sheet)94+ if err := m.book().ReplaceSheet(m.sheet, res.Sheet, label); err != nil {95+ m.fail(err.Error())96+ return true97+ }98+ m.afterSheetsChange(res.Sheet, i)99+ m.note = "Imported " + what + " into " + res.Sheet.Name() + " (" + countRows(res.Rows) + ")"100+ default:101+ return false102+ }103+ m.changed = true104+ if len(res.Notes) > 0 {105+ m.note += "; " + strings.Join(res.Notes, "; ")106+ }107+ return true108+}109+110+// sheetList names sheets for a note: "Sales", "Sales and Q3", or "3111+// sheets, Sales to Q3".112+func sheetList(sheets []*sheet.Sheet) string {113+ switch n := len(sheets); n {114+ case 1:115+ return sheets[0].Name()116+ case 2:117+ return sheets[0].Name() + " and " + sheets[1].Name()118+ default:119+ return thousands(n) + " sheets, " + sheets[0].Name() + " to " + sheets[n-1].Name()120+ }121+}
A internal/ui/importplace_test.go +117 -0
@@ -0,0 +1,117 @@
1+package ui2+3+import (4+ "context"5+ "path/filepath"6+ "strings"7+ "testing"8+9+ "github.com/FelineStateMachine/012/internal/fileio"10+ "github.com/FelineStateMachine/012/internal/sheet"11+)12+13+// importInto runs File > Import on name and picks place in the location14+// picker.15+func importInto(t *testing.T, m *Model, name, place string) {16+ t.Helper()17+ m.runCommand("file.import")18+ press(t, m, name, "<enter>")19+ p, ok := m.overlay.(*picker)20+ if !ok || !strings.HasPrefix(p.title, "Import ") {21+ t.Fatalf("no location picker: %T", m.overlay)22+ }23+ press(t, m, place, "<enter>")24+}25+26+func TestImportLocation(t *testing.T) {27+ t.Chdir(t.TempDir())28+ writeFile(t, "sales.csv", "Region,Total\nNorth,12\n")29+ m := newModel()30+ press(t, m, "=sales!B2*2", "<enter>")31+ m.changed = false32+33+ // The picker offers Sheets' choices, what the current sheet is, and34+ // says what each does.35+ m.runCommand("file.import")36+ press(t, m, "sales.csv", "<enter>")37+ got := strings.Join(pickerTitles(t, m), "\n")38+ want := "Insert new sheet | after the others\nReplace current sheet | Sheet1\nReplace spreadsheet | open it instead"39+ if got != want {40+ t.Errorf("locations:\n%s\nwant\n%s", got, want)41+ }42+ if st := status(m); !strings.Contains(st, "Add sales.csv as a new sheet") {43+ t.Errorf("status %q", st)44+ }45+ press(t, m, "<esc>")46+ if m.overlay != nil || m.book().Len() != 1 {47+ t.Fatalf("Esc imported: %d sheets", m.book().Len())48+ }49+50+ // Insert new sheet: one undo step, the file stays this one.51+ importInto(t, m, "sales.csv", "Insert new sheet")52+ if sheetNames(m) != "Sheet1,sales" || m.sheet.Name() != "sales" || input(m, "A2") != "North" {53+ t.Fatalf("inserted: %s on %s", sheetNames(m), m.sheet.Name())54+ }55+ if line(m, contextLine) != "Imported sales.csv as sales (2 rows)" || !m.changed || m.xfer.source != "" {56+ t.Errorf("context %q changed %v source %q", line(m, contextLine), m.changed, m.xfer.source)57+ }58+ if v := m.book().Sheet(0).Value(addr("A1")); v.Num != 24 {59+ t.Errorf("Sheet1!A1 reading the import = %v", v)60+ }61+ // Again: the name is taken, so the new sheet gets a number.62+ importInto(t, m, "sales.csv", "Insert new sheet")63+ if sheetNames(m) != "Sheet1,sales,sales 2" || m.sheet.Name() != "sales 2" {64+ t.Errorf("second insert: %s", sheetNames(m))65+ }66+ press(t, m, "<ctrl+z>")67+ if sheetNames(m) != "Sheet1,sales" || !strings.Contains(line(m, contextLine), "Undid: import sales.csv") {68+ t.Errorf("undo: %s, %q", sheetNames(m), line(m, contextLine))69+ }70+71+ // Replace current sheet keeps the sheet's name and place.72+ press(t, m, "<ctrl+pgup>")73+ writeFile(t, "sales.csv", "Region,Total\nSouth,50\n")74+ importInto(t, m, "sales.csv", "Replace current sheet")75+ if sheetNames(m) != "Sheet1,sales" || m.sheet.Name() != "Sheet1" || input(m, "A2") != "South" || input(m, "A1") != "Region" {76+ t.Fatalf("replaced: %s on %s, A2 %q", sheetNames(m), m.sheet.Name(), input(m, "A2"))77+ }78+ if line(m, contextLine) != "Imported sales.csv into Sheet1 (2 rows)" {79+ t.Errorf("context %q", line(m, contextLine))80+ }81+ press(t, m, "<ctrl+z>")82+ if m.sheet.Name() != "Sheet1" || input(m, "A1") != "=sales!B2*2" {83+ t.Errorf("undo replace: on %s, A1 %q", m.sheet.Name(), input(m, "A1"))84+ }85+}86+87+// An XLSX workbook inserts every sheet, names made unique, and offers no88+// Replace current sheet.89+func TestImportLocationWorkbook(t *testing.T) {90+ t.Chdir(t.TempDir())91+ src := sheet.New()92+ book := src.Book()93+ book.RenameSheet(src, "Sheet1")94+ src.Set(addr("A1"), "1")95+ q3, _ := book.AddSheet("Q3", 1)96+ q3.Set(addr("A1"), "=Sheet1!A1+1")97+ if _, err := fileio.Export(context.Background(), filepath.Join(".", "book.xlsx"), fileio.XLSX, fileio.SnapBook(src), fileio.ExportOptions{}); err != nil {98+ t.Fatal(err)99+ }100+ m := newModel()101+ press(t, m, "x", "<enter>")102+ m.runCommand("file.import")103+ press(t, m, "book", "<enter>")104+ if got := strings.Join(pickerTitles(t, m), "\n"); got != "Insert new sheets | after the others\nReplace spreadsheet | unsaved changes" {105+ t.Errorf("locations:\n%s", got)106+ }107+ press(t, m, "insert", "<enter>")108+ if sheetNames(m) != "Sheet1,Sheet1 2,Q3" || m.sheet.Name() != "Sheet1 2" {109+ t.Fatalf("sheets %s on %s (%q)", sheetNames(m), m.sheet.Name(), m.errMsg)110+ }111+ if got := m.book().Lookup("Q3"); got.Cell(addr("A1")).Input != "='Sheet1 2'!A1+1" || got.Value(addr("A1")).Num != 2 {112+ t.Errorf("Q3!A1 %q = %v", got.Cell(addr("A1")).Input, got.Value(addr("A1")))113+ }114+ if l := line(m, contextLine); l != "Imported book.xlsx as Sheet1 2 and Q3 (2 rows)" {115+ t.Errorf("context %q", l)116+ }117+}
M internal/ui/macrohostnav.go +30 -0
@@ -22,6 +22,9 @@ func (h scriptHost) Select(ref, active string) error {
2222 if err != nil {2323 return err2424 }25+ if s.Hidden() {26+ return errors.New(hiddenMsg(s))27+ }2528 m.showSheet(s)2629 m.selectRect(r)2730 if active == "" {
@@ -162,6 +165,9 @@ func (h scriptHost) ActivateSheet(name string) error {
162165 if s == nil {163166 return fmt.Errorf("there's no sheet named %s", name)164167 }168+ if s.Hidden() {169+ return errors.New(hiddenMsg(s))170+ }165171 h.m.showSheet(s)166172 return nil167173 }
@@ -289,6 +295,9 @@ func (h scriptHost) answer(c *command, answer *string) error {
289295 // overlays (dialogs, pickers) can't be answered by a script.290296 func (h scriptHost) choose(c *command, answer *string) error {291297 m := h.m298+ if p, ok := m.overlay.(*picker); ok && p.answers {299+ return h.pickAnswer(c, p, answer)300+ }292301 bar, ok := m.overlay.(*choiceBar)293302 if !ok {294303 m.closeOverlay()
@@ -309,3 +318,24 @@ func (h scriptHost) choose(c *command, answer *string) error {
309318 m.closeOverlay()310319 return fmt.Errorf("%s asks %q: give run(%q, answer=...) with one of its keys", c.title, bar.msg, c.id)311320 }321+322+// pickAnswer answers a picker that asks a command's question with the323+// title of one of its items.324+func (h scriptHost) pickAnswer(c *command, p *picker, answer *string) error {325+ m := h.m326+ if answer != nil {327+ if cmd, ok := p.answer(m, *answer); ok {328+ m.macros.cmds = append(m.macros.cmds, cmd)329+ if m.mode == modeError {330+ return h.failure()331+ }332+ return nil333+ }334+ }335+ m.closeOverlay()336+ var titles []string337+ for _, it := range p.items {338+ titles = append(titles, it.title)339+ }340+ return fmt.Errorf("%s asks for one of %s: give run(%q, answer=...)", c.title, strings.Join(titles, ", "), c.id)341+}
M internal/ui/macrorec.go +3 -0
M internal/ui/menubar.go +2 -1
M internal/ui/names.go +6 -0
M internal/ui/palette.go +33 -3
@@ -107,6 +107,11 @@ type picker struct {
107107 // enter, when set, gets the first look at Enter with the search text,108108 // e.g. to take a typed path; it reports whether it handled it.109109 enter func(m *Model, query string) (tea.Cmd, bool)110+111+ // answers marks a picker that is the question of a command that112+ // changes the workbook: a recording keeps the title picked as the113+ // command's answer, and scripts answer it with a title.114+ answers bool110115 }111116 112117 const (
@@ -217,7 +222,7 @@ func (p *picker) key(m *Model, k tea.KeyPressMsg) tea.Cmd {
217222 case "enter":218223 return p.pick(m)219224 case "esc":220- m.closeOverlay()225+ p.close(m)221226 default:222227 before := m.line.text()223228 m.line.key(k)
@@ -237,7 +242,32 @@ func (p *picker) pick(m *Model) tea.Cmd {
237242 if p.sel >= len(p.shown) || p.shown[p.sel].item.off {238243 return nil239244 }240- return p.shown[p.sel].item.pick(m)245+ it := p.shown[p.sel].item246+ cmd := it.pick(m)247+ if p.answers {248+ m.recordAnswer(it.title, false)249+ }250+ return cmd251+}252+253+// close closes the picker without picking, which cancels the question254+// it asks.255+func (p *picker) close(m *Model) {256+ m.closeOverlay()257+ if p.answers {258+ m.recordAnswer("", true)259+ }260+}261+262+// answer picks the item titled title (in any case), as a script answers263+// the picker's question, and reports whether there was one.264+func (p *picker) answer(m *Model, title string) (tea.Cmd, bool) {265+ for i := range p.items {266+ if it := &p.items[i]; strings.EqualFold(it.title, title) && !it.off {267+ return it.pick(m), true268+ }269+ }270+ return nil, false241271 }242272 243273 // Rows of the box: the top border, the search field, a separator, then
@@ -247,7 +277,7 @@ const pickerFirstRow = 3
247277 func (p *picker) mouse(m *Model, e mouseEvent) tea.Cmd {248278 if e.box != pickerID {249279 if e.kind == mousePress {250- m.closeOverlay()280+ p.close(m)251281 }252282 return nil253283 }
M internal/ui/pivotactions.go +4 -0
M internal/ui/selection.go +6 -1
M internal/ui/serve_test.go +1 -1
M internal/ui/tabs.go +27 -14
@@ -52,7 +52,7 @@ func init() {
5252 return nil5353 }},5454 &command{id: "sheet.delete", title: "Delete sheet", desc: "Delete the sheet; formulas that use it show #REF!",55- enabled: func(m *Model) bool { return m.book().Len() > 1 }, run: (*Model).confirmDeleteSheet},55+ enabled: func(m *Model) bool { return len(m.book().Visible()) > 1 }, run: (*Model).confirmDeleteSheet},5656 &command{id: "sheet.move_left", title: "Move sheet left", desc: "Move the sheet's tab one place left", enabled: func(m *Model) bool { return m.sheetAt(-1) != nil },5757 run: func(m *Model) tea.Cmd { return m.moveSheet(-1) }},5858 &command{id: "sheet.move_right", title: "Move sheet right", desc: "Move the sheet's tab one place right", enabled: func(m *Model) bool { return m.sheetAt(1) != nil },
@@ -69,19 +69,27 @@ func init() {
6969 7070 // tabMenu is the menu of a sheet's tab, as in Sheets.7171 var tabMenu = []menuItem{72- {cmd: "sheet.rename", title: "Rename"}, {cmd: "sheet.duplicate", title: "Duplicate"}, {cmd: "sheet.delete", title: "Delete"}, sep,72+ {cmd: "sheet.rename", title: "Rename"}, {cmd: "sheet.duplicate", title: "Duplicate"}, {cmd: "sheet.delete", title: "Delete"}, {cmd: "sheet.hide"}, sep,7373 {cmd: "sheet.move_left", title: "Move left"}, {cmd: "sheet.move_right", title: "Move right"}, sep,74- {cmd: "sheet.new"}, {cmd: "sheet.goto"},74+ {cmd: "sheet.new"}, {cmd: "sheet.goto"}, {cmd: "sheet.unhide"},7575 }7676 77-// sheetAt returns the sheet d tabs from the one shown, or nil past the78-// ends: moving between sheets doesn't wrap around, as in Sheets.77+// sheetAt returns the sheet d tabs from the one shown, skipping hidden78+// sheets, or nil past the ends: moving between sheets doesn't wrap79+// around, as in Sheets.7980 func (g *grid) sheetAt(d int) *sheet.Sheet {80- i := g.book().Index(g.sheet) + d81- if i < 0 || i >= g.book().Len() {82- return nil81+ book, step, n := g.book(), 1, d82+ if d < 0 {83+ step, n = -1, -d84+ }85+ for i := book.Index(g.sheet) + step; i >= 0 && i < book.Len() && n > 0; i += step {86+ if s := book.Sheet(i); !s.Hidden() {87+ if n--; n == 0 {88+ return s89+ }90+ }8391 }84- return g.book().Sheet(i)92+ return nil8593 }8694 8795 func (m *Model) stepSheet(d int) tea.Cmd {
@@ -91,9 +99,12 @@ func (m *Model) stepSheet(d int) tea.Cmd {
9199 return nil92100 }93101 102+// moveSheet moves the sheet shown past its visible neighbour d tabs103+// away (hidden sheets between keep their order).94104 func (m *Model) moveSheet(d int) tea.Cmd {95- i := m.book().Index(m.sheet)96- m.book().MoveSheet(m.sheet, i+d)105+ if s := m.sheetAt(d); s != nil {106+ m.book().MoveSheet(m.sheet, m.book().Index(s))107+ }97108 return nil98109 }99110
@@ -212,16 +223,18 @@ func (m *Model) pointRef() string {
212223 func (m *Model) afterSheetsChange(prefer *sheet.Sheet, index int) {213224 book := m.book()214225 switch {215- case prefer != nil && prefer.Live():226+ case prefer != nil && prefer.Live() && !prefer.Hidden():216227 m.showSheet(prefer)217228 case !m.sheet.Live():218- s := book.Sheet(clamp(index, 0, book.Len()-1))229+ s := m.nearVisible(clamp(index, 0, book.Len()-1))219230 m.sheet = s // the old sheet is gone: nothing to remember of it220231 p := m.tabs.places[s]221232 m.cur, m.top, m.left = p.cur, p.top, p.left222233 m.clearSelection()223234 m.charts.last = -1224235 book.SetActive(s)236+ case m.sheet.Hidden():237+ m.showSheet(m.nearVisible(book.Index(m.sheet)))225238 }226239 }227240
@@ -275,7 +288,7 @@ func (m *Model) openRename() {
275288 func (m *Model) openSheetPicker() {276289 var items []pickItem277290 sel := 0278- for i, s := range m.book().Sheets() {291+ for i, s := range m.visibleSheets() {279292 if s == m.sheet {280293 sel = i281294 }
M internal/ui/tabs_test.go +1 -1
M internal/ui/tabstrip.go +16 -5
@@ -1,6 +1,7 @@
11 package ui22 33 import (4+ "slices"45 "strconv"56 "strings"67
@@ -50,7 +51,8 @@ type tabView struct {
5051 }5152 5253 func (m *Model) tabView() tabView {53- return tabView{sheets: m.book().Sheets(), active: m.book().Index(m.sheet), hover: m.mouse.hover, drag: m.mouse.drag}54+ sheets := m.visibleSheets()55+ return tabView{sheets: sheets, active: slices.Index(sheets, m.sheet), hover: m.mouse.hover, drag: m.mouse.drag}5456 }5557 5658 // tabSpan is a clickable part of the tab strip on the status line.
@@ -213,7 +215,7 @@ func (m *Model) tabPress(h hit, double bool) tea.Cmd {
213215 }214216 return m.runCommand("sheet.new")215217 case hitTab:216- s := m.book().Sheet(h.addr.Col)218+ s := m.tabSheet(h.addr.Col)217219 if m.editing() {218220 m.pointInto(s)219221 return nil
@@ -229,14 +231,23 @@ func (m *Model) tabPress(h hit, double bool) tea.Cmd {
229231 230232 // dropTab moves the dragged sheet to the tab it was released over.231233 func (m *Model) dropTab() {232- if m.mouse.hover.kind == hitTab && m.mouse.hover.addr.Col != m.book().Index(m.sheet) {233- to := m.mouse.hover.addr.Col234+ if m.mouse.hover.kind != hitTab {235+ return236+ }237+ if to := m.book().Index(m.tabSheet(m.mouse.hover.addr.Col)); to != m.book().Index(m.sheet) {234238 m.book().MoveSheet(m.sheet, to)235239 m.note = "Moved " + m.sheet.Name() + " to position " + strconv.Itoa(to+1)236240 m.record(macro.Call("move_sheet", to+1))237241 }238242 }239243 244+// tabSheet is the sheet of tab i, counting the tabs shown (hidden sheets245+// have none).246+func (m *Model) tabSheet(i int) *sheet.Sheet {247+ tabs := m.visibleSheets()248+ return tabs[clamp(i, 0, len(tabs)-1)]249+}250+240251 // tabRightClick shows the tab's sheet and opens its menu.241252 func (m *Model) tabRightClick(h hit, x, y int) {242253 if h.kind != hitTab {
@@ -245,6 +256,6 @@ func (m *Model) tabRightClick(h hit, x, y int) {
245256 }246257 return247258 }248- m.showSheet(m.book().Sheet(h.addr.Col))259+ m.showSheet(m.tabSheet(h.addr.Col))249260 m.showContextMenu(tabMenu, x, y)250261 }
M internal/ui/trace.go +3 -0
M internal/ui/transfer.go +28 -23
@@ -40,14 +40,16 @@ type importJob struct {
4040 name string4141 prog *fileio.Progress4242 cancel context.CancelFunc43+ place importPlace // where the result goes; see importplace.go4344 }4445 4546 type importedMsg struct {46- id int47- name string48- opt fileio.Options49- res *fileio.Result50- err error47+ id int48+ name string49+ opt fileio.Options50+ place importPlace51+ res *fileio.Result52+ err error5153 }5254 5355 type importTickMsg struct{ id int }
@@ -57,7 +59,7 @@ const importTick = 100 * time.Millisecond
5759 5860 func init() {5961 register(&command{id: "file.import", macro: macroNever, title: "Import",60- desc: "Import a " + importNouns() + " file, replacing this sheet",62+ desc: "Import a " + importNouns() + " file as new sheets, or in place of this sheet or spreadsheet",6163 run: (*Model).openImport})6264 }6365
@@ -83,7 +85,7 @@ func (m *Model) startupCmd() tea.Cmd {
8385 return nil8486 }8587 m.xfer.startup = ""86- return m.startImport(name, fileio.Options{})88+ return m.startImport(name, fileio.Options{}, placeBook)8789 }8890 8991 // importable lists the files in the directory names are relative to
@@ -130,7 +132,7 @@ func (m *Model) openImport() tea.Cmd {
130132 items = append(items, pickItem{title: name, name: len(name), detail: detail, desc: desc,131133 pick: func(m *Model) tea.Cmd {132134 m.closeOverlay()133- return m.confirmImport(name, fileio.Options{})135+ return m.askImportPlace(name)134136 }})135137 }136138 p := newPicker(m, "Import", "Type to filter, or a path", 72, items)
@@ -145,7 +147,7 @@ func (m *Model) openImport() tea.Cmd {
145147 m.fail(fmt.Sprintf("Can't import %s: 012 imports %s", query, importExts()))146148 return nil, true147149 }148- return m.confirmImport(query, fileio.Options{}), true150+ return m.askImportPlace(query), true149151 }150152 m.openOverlay(p)151153 return nil
@@ -177,13 +179,13 @@ func fileSize(n int64) string {
177179 // unsaved changes.178180 func (m *Model) confirmImport(name string, opt fileio.Options) tea.Cmd {179181 if !m.changed {180- return m.startImport(name, opt)182+ return m.startImport(name, opt, placeBook)181183 }182184 m.openOverlay(&choiceBar{183- msg: "Importing replaces this sheet, which has unsaved changes.",185+ msg: "Importing replaces this spreadsheet, which has unsaved changes.",184186 warn: true,185187 choices: []choice{186- {key: "enter", label: "Import", run: func(m *Model) tea.Cmd { return m.startImport(name, opt) }},188+ {key: "enter", label: "Import", run: func(m *Model) tea.Cmd { return m.startImport(name, opt, placeBook) }},187189 {key: "esc", label: "Cancel", run: func(*Model) tea.Cmd { return nil }},188190 },189191 })
@@ -191,30 +193,30 @@ func (m *Model) confirmImport(name string, opt fileio.Options) tea.Cmd {
191193 }192194 193195 // startImport reads name in the background.194-func (m *Model) startImport(name string, opt fileio.Options) tea.Cmd {196+func (m *Model) startImport(name string, opt fileio.Options, place importPlace) tea.Cmd {195197 m.note = ""196198 path, ok := m.path("import", name)197199 if !ok {198200 return nil199201 }200- return m.xfer.start(name, path, opt)202+ return m.xfer.start(name, path, opt, place)201203 }202204 203205 // start reads name, at path on disk, in the background, replacing any204206 // import running.205-func (x *transfer) start(name, path string, opt fileio.Options) tea.Cmd {207+func (x *transfer) start(name, path string, opt fileio.Options, place importPlace) tea.Cmd {206208 if x.job != nil {207209 x.job.cancel()208210 }209211 ctx, cancel := context.WithCancel(context.Background())210212 x.lastID++211- job := &importJob{id: x.lastID, name: name, prog: fileio.NewProgress(), cancel: cancel}213+ job := &importJob{id: x.lastID, name: name, prog: fileio.NewProgress(), cancel: cancel, place: place}212214 opt.Progress = job.prog213215 x.job = job214216 return tea.Batch(215217 func() tea.Msg {216218 res, err := fileio.Import(ctx, path, opt)217- return importedMsg{id: job.id, name: name, opt: opt, res: res, err: err}219+ return importedMsg{id: job.id, name: name, opt: opt, place: place, res: res, err: err}218220 },219221 tickImport(job.id),220222 )
@@ -310,14 +312,12 @@ func (m *Model) handleImported(msg importedMsg) tea.Cmd {
310312 m.note = "Import of " + filepath.Base(msg.name) + " cancelled"311313 return nil312314 case errors.As(msg.err, &need):313- m.openTablePicker(msg.name, need.Tables)315+ m.openTablePicker(msg.name, need.Tables, msg.place)314316 return nil315317 case msg.err != nil:316318 m.fail(fmt.Sprintf("Couldn't import %s: %v", filepath.Base(msg.name), msg.err))317319 return nil318320 }319- m.reset(msg.res.Sheet, "")320- m.xfer.source, m.xfer.kind = msg.name, msg.res.Kind321321 what := filepath.Base(msg.name)322322 switch {323323 case msg.opt.Table != "":
@@ -325,6 +325,11 @@ func (m *Model) handleImported(msg importedMsg) tea.Cmd {
325325 case msg.opt.Query != "":326326 what = "the query from " + what327327 }328+ if m.placeImport(msg, what) {329+ return m.term.notify("Imported " + what)330+ }331+ m.reset(msg.res.Sheet, "")332+ m.xfer.source, m.xfer.kind = msg.name, msg.res.Kind328333 m.note = "Imported " + what + " (" + countRows(msg.res.Rows) + ")"329334 if len(msg.res.Notes) > 0 {330335 m.note += "; " + strings.Join(msg.res.Notes, "; ")
@@ -335,7 +340,7 @@ func (m *Model) handleImported(msg importedMsg) tea.Cmd {
335340 336341 // openTablePicker asks which table of a SQLite database to import, or337342 // for a query.338-func (m *Model) openTablePicker(name string, tables []fileio.TableInfo) {343+func (m *Model) openTablePicker(name string, tables []fileio.TableInfo, place importPlace) {339344 var items []pickItem340345 for _, t := range tables {341346 detail := countRows(t.Rows) + ", " + plural(len(t.Cols), "1 column", fmt.Sprintf("%d columns", len(t.Cols)))
@@ -346,7 +351,7 @@ func (m *Model) openTablePicker(name string, tables []fileio.TableInfo) {
346351 items = append(items, pickItem{title: t.Name, name: len(t.Name), detail: detail, desc: desc,347352 pick: func(m *Model) tea.Cmd {348353 m.closeOverlay()349- return m.startImport(name, fileio.Options{Table: t.Name})354+ return m.startImport(name, fileio.Options{Table: t.Name}, place)350355 }})351356 }352357 first := "table"
@@ -361,7 +366,7 @@ func (m *Model) openTablePicker(name string, tables []fileio.TableInfo) {
361366 if text == "" {362367 return nil363368 }364- return m.startImport(name, fileio.Options{Query: text})369+ return m.startImport(name, fileio.Options{Query: text}, place)365370 })366371 m.prompt.indicator = "SQL"367372 return nil
M internal/ui/transfer_test.go +6 -6
@@ -64,7 +64,7 @@ func TestImportPicker(t *testing.T) {
6464 6565 // A typed path imports a file that isn't listed.6666 m.runCommand("file.import")67- press(t, m, "sub/deep.tsv", "<enter>")67+ press(t, m, "sub/deep.tsv", "<enter>", "Replace spreadsheet", "<enter>")6868 if input(m, "B1") != "b" || m.xfer.source != "sub/deep.tsv" {6969 t.Errorf("typed path: B1 %q source %q err %q", input(m, "B1"), m.xfer.source, m.errMsg)7070 }
@@ -76,7 +76,7 @@ func TestImportPicker(t *testing.T) {
7676 }7777 press(t, m, "<esc>")7878 m.runCommand("file.import")79- press(t, m, "old", "<enter>")79+ press(t, m, "old", "<enter>", "Replace spreadsheet", "<enter>")8080 if m.mode != modeError || m.errMsg != "Couldn't import Old.WK1: not a Lotus 1-2-3 worksheet: the file is empty" &&8181 !strings.Contains(m.errMsg, "Couldn't import Old.WK1: not a Lotus 1-2-3 worksheet") {8282 t.Errorf("wk1: %v %q", m.mode, m.errMsg)
@@ -89,8 +89,8 @@ func TestImportAsksBeforeReplacingChanges(t *testing.T) {
8989 m := newModel()9090 press(t, m, "keep me", "<enter>")9191 m.runCommand("file.import")92- press(t, m, "<enter>")93- if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "Importing replaces this sheet, which has unsaved changes.") {92+ press(t, m, "<enter>", "Replace spreadsheet", "<enter>")93+ if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(line(m, contextLine), "Importing replaces this spreadsheet, which has unsaved changes.") {9494 t.Fatalf("no warning: %q", line(m, contextLine))9595 }9696 press(t, m, "<esc>")
@@ -98,7 +98,7 @@ func TestImportAsksBeforeReplacingChanges(t *testing.T) {
9898 t.Errorf("Esc imported anyway")9999 }100100 m.runCommand("file.import")101- press(t, m, "<enter>", "<enter>")101+ press(t, m, "<enter>", "Replace spreadsheet", "<enter>", "<enter>")102102 if input(m, "A1") != "1" {103103 t.Errorf("Enter didn't import: %q", input(m, "A1"))104104 }
@@ -162,7 +162,7 @@ func TestSQLiteTablePicker(t *testing.T) {
162162 }163163 164164 m.runCommand("file.import")165- press(t, m, "<enter>", "query", "<enter>")165+ press(t, m, "<enter>", "Replace spreadsheet", "<enter>", "query", "<enter>")166166 if m.mode != modePrompt || m.line.text() != "SELECT * FROM orders" {167167 t.Fatalf("query prompt %v %q", m.mode, m.line.text())168168 }