cat / 012
clone

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

main
012 / internal / ui / names.go
names.go 6.6K
1package ui23import (4 "strconv"56 tea "charm.land/bubbletea/v2"78 "src.rest/cat/012/internal/sheet"9 "src.rest/cat/012/internal/ui/picker"10 "src.rest/cat/012/internal/ui/theme"11)1213// Named ranges, as Sheets' Data > Named ranges: a picker lists them with14// their ranges, Enter goes to one, F2 renames it or changes its range,15// Ctrl+D deletes it, and the first row adds one for the selection.16// Naming and renaming are asked on the context line; changing the range17// is pointed at with the arrows, like any range prompt.1819func init() {20 register(21 &command{id: "data.named_ranges", macro: macroView, title: "Named ranges", desc: "List the named ranges: go to, add, edit or delete them", run: func(m *Model) tea.Cmd {22 m.openNames(m.selection())23 return nil24 }},25 &command{id: "data.define_name", title: "Define named range", desc: "Name the selected range, to use the name in formulas", run: func(m *Model) tea.Cmd {26 m.defineName(m.selection(), false)27 return nil28 }},29 )30}3132// namesHost is what the named ranges picker acts on. The model33// implements it. The picker stays in package ui: it is a few keys over a34// picker, and what they do (naming, renaming and pointing at a range on35// the context line, going to one) are the model's prompts and36// selection, which open the picker again.37type namesHost interface {38 styles() *theme.Theme39 sheetShown() *sheet.Sheet40 closeOverlay()41 // namesItems are the picker's rows: "Add a range" for sel, then the42 // named ranges.43 namesItems(sel sheet.Rect) []picker.Item44 // editName asks for a new name and range for n, then opens the45 // picker again with sel.46 editName(n sheet.Name, sel sheet.Rect)47 // nameChanged follows a named range changed from the picker: the48 // file is modified, and a macro being recorded notes what it can't49 // replay.50 nameChanged()51}5253// namesPicker is the picker of named ranges with its extra keys.54type namesPicker struct {55 m namesHost // the model, through what the picker needs of it56 *picker.Picker57 sel sheet.Rect // the selection when the picker opened, for "Add a range"58 msg string // feedback on the last action, e.g. a deletion59}6061// openNames opens the picker; sel is what "Add a range" names.62func (m *Model) openNames(sel sheet.Rect) {63 p := m.newPicker("Named ranges", "Type a name", 60, m.namesItems(sel))64 p.Action = "go to"65 m.clearSelection()66 m.openOverlay(&namesPicker{m: m, Picker: p, sel: sel})67}6869func (m *Model) nameChanged() {70 m.syncChanged()71 m.noteUnrecorded()72}7374// namesItems lists "Add a range" and then every named range.75func (m *Model) namesItems(sel sheet.Rect) []picker.Item {76 add := "+ Add a range"77 items := []picker.Item{{78 Title: add, Name: len(add), Detail: sel.String(), Desc: "Name " + sel.String() + ", to use the name in formulas",79 Pick: func() tea.Cmd {80 m.closeOverlay()81 m.defineName(sel, true)82 return nil83 },84 }}85 for _, n := range m.sheet.Names() {86 desc := "Go to " + n.Ref()87 switch users := m.sheet.NameUsers(n.Name); {88 case n.Lost:89 desc = "Its cells were deleted; formulas using it show #REF!"90 case n.Gone():91 desc = "Its sheet was deleted; formulas using it show #REF!"92 case users == 1:93 desc += ", used in 1 formula"94 case users > 1:95 desc += ", used in " + strconv.Itoa(users) + " formulas"96 }97 items = append(items, picker.Item{98 Title: n.Name, Name: len(n.Name), Detail: n.Ref(), Desc: desc, Off: n.Gone(),99 Pick: func() tea.Cmd {100 m.closeOverlay()101 if m.refuseHidden(n.Sheet) {102 return nil103 }104 m.showSheet(n.Sheet)105 m.selectRect(n.Range)106 return nil107 },108 })109 }110 return items111}112113// current is the named range highlighted in the picker, if any.114func (p *namesPicker) current(m namesHost) (sheet.Name, bool) {115 if p.Picker.Sel >= len(p.Shown()) {116 return sheet.Name{}, false117 }118 return m.sheetShown().LookupName(p.Shown()[p.Picker.Sel].Item.Title)119}120121func (p *namesPicker) Key(k tea.KeyPressMsg) tea.Cmd {122 m := p.m123 p.msg = ""124 switch k.String() {125 case "f2":126 if n, ok := p.current(m); ok {127 m.closeOverlay()128 m.editName(n, p.sel)129 }130 return nil131 case "ctrl+d":132 if n, ok := p.current(m); ok {133 m.sheetShown().DeleteName(n.Name)134 m.nameChanged()135 p.msg = "Deleted " + n.Name + "; Ctrl+Z brings it back"136 p.Items = m.namesItems(p.sel)137 sel := p.Picker.Sel138 p.Changed()139 p.Picker.Sel = max(min(sel, len(p.Shown())-1), 0)140 }141 return nil142 }143 return p.Picker.Key(k)144}145146func (p *namesPicker) Status() (string, string) {147 m := p.m148 keys := m.styles().KeyHints("Enter", "go to", "F2", "edit", "Ctrl+D", "delete", "Esc", "close")149 if _, ok := p.current(m); !ok {150 keys = m.styles().KeyHints("Enter", "add", "Esc", "close")151 }152 if p.msg != "" {153 return p.msg, keys154 }155 desc, _ := p.Picker.Status()156 return desc, keys157}158159// defineName asks for a name for r on the context line. From the picker160// (back), the picker opens again afterwards.161func (m *Model) defineName(r sheet.Rect, back bool) {162 m.openText("Name for "+r.String()+":", "", func(m *Model, text string) tea.Cmd {163 if err := m.sheet.DefineName(text, r); err != nil {164 m.fail(err.Error())165 return nil166 }167 m.changed = true168 if back {169 m.noteUnrecorded() // named from the picker, not as the command170 m.openNames(r)171 return nil172 }173 m.selectRect(r)174 m.note = text + " names " + r.String() + ": use it in formulas, e.g. =SUM(" + text + ")"175 return nil176 })177 m.prompt.indicator = "NAME"178}179180// editName asks for a new name, then for the range, pointed at from the181// current one; Enter keeps either as it is. The picker opens again after.182func (m *Model) editName(n sheet.Name, sel sheet.Rect) {183 m.openText("Rename "+n.Name+":", n.Name, func(m *Model, text string) tea.Cmd {184 if err := sheet.ValidName(text); err != nil {185 m.fail(err.Error())186 return nil187 }188 r := n.Range189 if m.refuseHidden(n.Sheet) {190 return nil191 }192 if n.Sheet.Live() {193 m.showSheet(n.Sheet) // the range is pointed at on its own sheet194 }195 m.openRange("Range for "+text+":", func(m *Model, r sheet.Rect) tea.Cmd {196 if err := m.sheet.EditName(n.Name, text, r); err != nil {197 m.fail(err.Error())198 return nil199 }200 m.nameChanged()201 m.openNames(sel)202 return nil203 })204 m.point = pointer{anchor: r.From, at: r.To, anchored: r.From != r.To}205 return nil206 })207 m.prompt.indicator = "NAME"208}209210// namedSelection is the name of the selected range, if it has one, for211// the name box, as in Sheets: a named range's, or a table's.212func (g *grid) namedSelection() (string, bool) {213 r := g.selection()214 for _, n := range g.sheet.Names() {215 if !n.Gone() && n.Sheet == g.sheet && n.Range == r {216 return n.Name, true217 }218 }219 if !g.sheet.HasTables() {220 return "", false221 }222 for _, t := range g.sheet.Tables() {223 if t.Range == r {224 return t.Name, true225 }226 }227 return "", false228}