1package ui23import (4 "errors"5 "fmt"6 "unicode/utf8"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/formula"12)1314// Undo and redo, fill, inserting and deleting rows and columns, and F4 in15// formulas. The engine does the work and records each change as one undo16// step; this file maps commands onto the selection and reports back on the17// context line.1819func init() {20 register(21 &command{id: "edit.undo", macro: macroNever, title: "Undo", desc: "Undo the last change", run: (*Model).undo},22 &command{id: "edit.redo", macro: macroNever, title: "Redo", desc: "Redo the last undone change", run: (*Model).redo},23 &command{id: "edit.fill_down", title: "Fill down", desc: "Copy the top row of the selection into the rows below it", edits: (*Model).selection, run: func(m *Model) tea.Cmd {24 return m.fill(m.sheet.FillDown)25 }},26 &command{id: "edit.fill_right", title: "Fill right", desc: "Copy the left column of the selection into the columns right of it", edits: (*Model).selection, run: func(m *Model) tea.Cmd {27 return m.fill(m.sheet.FillRight)28 }},29 &command{id: "insert.row_above", title: "Insert rows above", desc: "Insert as many rows above the selection as it spans", edits: rowsTarget, changes: noCells, keepsSpills: true, run: func(m *Model) tea.Cmd {30 r := m.selection()31 return m.structural(m.sheet.InsertRows(r.From.Row, r.To.Row-r.From.Row+1))32 }},33 &command{id: "insert.row_below", title: "Insert rows below", desc: "Insert as many rows below the selection as it spans", edits: rowsBelowTarget, changes: noCells, keepsSpills: true, run: func(m *Model) tea.Cmd {34 r := m.selection()35 if r.To.Row == sheet.MaxRows-1 {36 return m.structural(sheet.ErrPushedOff)37 }38 return m.structural(m.sheet.InsertRows(r.To.Row+1, r.To.Row-r.From.Row+1))39 }},40 &command{id: "insert.col_left", title: "Insert columns left", desc: "Insert as many columns left of the selection as it spans", edits: colsTarget, changes: noCells, keepsSpills: true, run: func(m *Model) tea.Cmd {41 r := m.selection()42 return m.structural(m.sheet.InsertCols(r.From.Col, r.To.Col-r.From.Col+1))43 }},44 &command{id: "insert.col_right", title: "Insert columns right", desc: "Insert as many columns right of the selection as it spans", edits: colsRightTarget, changes: noCells, keepsSpills: true, run: func(m *Model) tea.Cmd {45 r := m.selection()46 if r.To.Col == sheet.MaxCols-1 {47 return m.structural(sheet.ErrPushedOff)48 }49 return m.structural(m.sheet.InsertCols(r.To.Col+1, r.To.Col-r.From.Col+1))50 }},51 &command{id: "delete.row", title: "Delete rows", desc: "Delete the selected rows; references to them become #REF!", edits: rowsTarget, changes: selectedRowsTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {52 r := m.selection()53 m.sheet.DeleteRows(r.From.Row, r.To.Row-r.From.Row+1)54 return nil55 }},56 &command{id: "delete.col", title: "Delete columns", desc: "Delete the selected columns; references to them become #REF!", edits: colsTarget, changes: selectedColsTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {57 r := m.selection()58 m.sheet.DeleteCols(r.From.Col, r.To.Col-r.From.Col+1)59 return nil60 }},61 // Sheets' Ctrl+Alt+= and Ctrl+Alt+- act on rows, or on columns when62 // whole columns are selected.63 &command{id: "insert.selection", title: "Insert rows or columns", desc: "Insert rows above the selection, or columns left of it when whole columns are selected", run: func(m *Model) tea.Cmd {64 if m.whole == wholeCols {65 return m.runCommand("insert.col_left")66 }67 return m.runCommand("insert.row_above")68 }},69 &command{id: "delete.selection", title: "Delete rows or columns", desc: "Delete the selected rows, or columns when whole columns are selected", run: func(m *Model) tea.Cmd {70 if m.whole == wholeCols {71 return m.runCommand("delete.col")72 }73 return m.runCommand("delete.row")74 }},75 )76 keymap["ctrl+alt+="] = "insert.selection"77 keymap["ctrl+alt+-"] = "delete.selection"78 keymap["ctrl+z"] = "edit.undo"79 keymap["ctrl+y"] = "edit.redo"80 keymap["ctrl+shift+z"] = "edit.redo"81 keymap["ctrl+d"] = "edit.fill_down"82 keymap["ctrl+r"] = "edit.fill_right"83}8485func (m *Model) undo() tea.Cmd {86 if m.undoBlocked(false) {87 return nil88 }89 c, ok := m.sheet.Undo()90 m.afterHistory("Undid", "Nothing to undo", c, ok)91 return nil92}9394func (m *Model) redo() tea.Cmd {95 if m.undoBlocked(true) {96 return nil97 }98 c, ok := m.sheet.Redo()99 m.afterHistory("Redid", "Nothing to redo", c, ok)100 return nil101}102103// afterHistory selects what an undo or redo changed, as Sheets does, and104// says what it was on the context line.105func (m *Model) afterHistory(verb, none string, c sheet.Change, ok bool) {106 if !ok {107 m.note = none108 return109 }110 m.note = verb + ": " + c.Label111 i := m.book().Index(m.sheet)112 if i < 0 {113 i = m.book().Active()114 }115 m.afterSheetsChange(c.Sheet, i)116 if !c.Tabs && !c.Macros {117 m.selectRect(c.Focus)118 }119}120121// selectRect selects r, as whole columns or rows when it spans the sheet.122func (g *grid) selectRect(r sheet.Rect) {123 allRows := r.From.Row == 0 && r.To.Row == sheet.MaxRows-1124 allCols := r.From.Col == 0 && r.To.Col == sheet.MaxCols-1125 switch {126 case allRows && allCols:127 g.selecting, g.whole, g.ext = true, wholeAll, g.cur128 case allRows:129 g.cur.Col = r.From.Col130 g.ext = sheet.Addr{Col: r.To.Col, Row: g.cur.Row}131 g.selecting, g.whole = true, wholeCols132 case allCols:133 g.cur.Row = r.From.Row134 g.ext = sheet.Addr{Col: g.cur.Col, Row: r.To.Row}135 g.selecting, g.whole = true, wholeRows136 default:137 g.cur, g.ext = r.From, r.To138 g.selecting, g.whole = r.From != r.To, wholeNone139 }140}141142func (m *Model) fill(fn func(sheet.Rect) (sheet.Rect, error)) tea.Cmd {143 m.writeChecked("Fill", func() (sheet.Rect, error) { return fn(m.selection()) })144 return nil145}146147// structural reports an insert that couldn't be done.148func (m *Model) structural(err error) tea.Cmd {149 if err != nil {150 m.fail(err.Error())151 }152 return nil153}154155// fillEntry stores the entry being typed in every selected cell, adjusting156// references as if it were copied from the active cell (Ctrl+Enter).157func (m *Model) fillEntry() bool {158 input := m.storedEntry(m.line.Text())159 if err := m.entrySheet().FillEntry(m.selection(), m.cur, input); err != nil {160 m.entryError(err, input)161 return false162 }163 if m.checkEntryFill(m.entrySheet().InvalidIn(m.selection()), input) {164 return false165 }166 m.cancelEntry()167 m.recordEntry(input, true)168 return true169}170171// entryError keeps an entry that can't be stored in EDIT mode, with the172// caret at the problem and the reason on line 3.173func (m *Model) entryError(err error, input string) {174 var pe *sheet.ParseError175 m.mode = modeEdit176 m.entry.hint = sheet.LocalizeError(err, m.locale()).Error()177 if errors.As(err, &pe) {178 m.line.Pos = utf8.RuneCountInString(input[:min(pe.Pos, len(input))])179 }180}181182// toggleAbsolute cycles the reference at the caret through A1, $A$1, A$1183// and $A1, as F4 does in Sheets. A range cycles both corners.184func (m *Model) toggleAbsolute() {185 if buf, pos, ok := formula.CycleRef(m.line.Buf, m.line.Pos); ok {186 m.line.Buf, m.line.Pos = buf, pos187 }188}189190// countCells describes a paste for the context line.191func countCells(r sheet.Rect) string {192 cols, rows := r.To.Col-r.From.Col+1, r.To.Row-r.From.Row+1193 switch {194 case r.AllRows() && r.AllCols():195 return "the whole sheet"196 case r.AllRows() && cols == 1:197 return "1 column"198 case r.AllRows():199 return fmt.Sprintf("%d columns", cols)200 case r.AllCols():201 return rowCount(rows)202 }203 n := cols * rows204 if n == 1 {205 return "1 cell"206 }207 return fmt.Sprintf("%d cells", n)208}