Ctrl+arrows, Option+arrows (alt+b/f) and Alt+arrows outside a cell move by word; Ctrl+Backspace (ctrl+h without the kitty protocol), Option+Backspace and Ctrl+W delete the word before the caret, Alt+D and Ctrl+Delete the one after. The notebook-running screen waits for its settled state.
- commit
- b56c15256fbf7f989dbe955e0e56090a5ef9c090
- parent
- e95283ba98c1144d525666304f7291f249e11bd9
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.7 of 3, Jev's estimate
| M | docs/ | 4 | + |
| M | e2e/ | 3 | +- |
| M | internal/ | 6 | ++ |
| M | internal/ | 3 | + |
| A | internal/ | 98 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 75 | +++++++++++++++++++++++ |
| A | internal/ | 20 | ++++++ |
@@ -35,6 +35,9 @@ mouse and the vim keymap added.
3535 | Ctrl+Enter | Enter the same entry in every selected cell |3636 | Arrows, Shift+arrows | While typing a formula, after an operator: point at a cell, or a range |3737 | F4 | While typing a formula: cycle the reference at the caret through `A1`, `$A$1`, `A$1`, `$A1` |38+| Ctrl+Left, Ctrl+Right (Option+arrows on macOS; Alt+arrows outside a cell) | While editing any text: move by word; in ENTER mode Ctrl+arrows commit and jump, as arrows do |39+| Ctrl+Backspace, Option+Backspace, Ctrl+W; Alt+D, Ctrl+Delete | While editing any text: delete the word before the caret; the word after it |40+| Ctrl+U, Cmd+Backspace | While editing any text: delete to the start of the line |3841 | Up, Down; Tab, Enter | While suggestions for a function, range or sheet name show: pick one; insert it (a sheet as `Summary!`, then arrows point into it). Esc hides them |3942 4043 ## Selecting@@ -152,6 +155,7 @@ on a sheet, `!` starts an entry as any character does.
152155 | Shift+Enter, Ctrl+Enter, Alt+Enter | Run, as in command mode |153156 | Enter, Tab | A new line; complete the word at the caret |154157 | Up, Down, Home, End, Ctrl+A, Ctrl+E | Move by the lines on screen; the start, the end of the line |158+| Word keys | As [anywhere text is edited](#entering-data); Ctrl+U deletes to the start of the line, not the cell |155159 156160 | Key | On an output full-screen |157161 |---|---|@@ -60,7 +60,8 @@ var notebookScreens = []screen{
6060 {name: "notebook-running", setup: func(s *session) {6161 filesNotebook(s)6262 s.keys("<enter>", "sleep 10min", "<esc>", "b", "<enter>", "{name: 1}", "<esc>", "<f9>")63- s.waitFor("waiting")63+ // F9 queues every cell; the first two finish, then sleep runs.64+ s.waitFor("running 1, 1 waiting")6465 }},6566 {name: "notebook-stale", setup: func(s *session) {6667 filesNotebook(s)@@ -196,9 +196,15 @@ func (a *Area) Key(k tea.KeyPressMsg, width int) bool {
196196 indent := a.indent()197197 a.insert('\n')198198 a.insert(indent...)199+ case "ctrl+u", "super+backspace":200+ a.DeleteBack(a.lineStart())199201 case "left", "right", "backspace", "delete":200202 a.Line.Key(k)201203 default:204+ if IsWordKey(k.String()) {205+ a.Line.Key(k)206+ break207+ }202208 text := Typed(k)203209 if text == "" {204210 return false@@ -57,6 +57,9 @@ func (l *Line) Insert(text string) {
5757 5858 // Key applies a line-editing Key.5959 func (l *Line) Key(k tea.KeyPressMsg) {60+ if l.wordKey(k.String()) {61+ return62+ }6063 switch k.String() {6164 case "left":6265 l.Pos = max(l.Pos-1, 0)@@ -0,0 +1,98 @@
1+package lineedit2+3+import (4+ "slices"5+ "unicode"6+)7+8+// Word keys, by the names terminals send them under. Option+arrows on9+// macOS arrive as alt+b and alt+f in most terminals, Ctrl+Backspace as10+// ctrl+h without the kitty keyboard protocol, and Cmd+Backspace as11+// ctrl+u.12+var (13+ wordLeftKeys = []string{"ctrl+left", "alt+left", "alt+b"}14+ wordRightKeys = []string{"ctrl+right", "alt+right", "alt+f"}15+ wordBackKeys = []string{"ctrl+backspace", "alt+backspace", "ctrl+h", "ctrl+w"}16+ wordDelKeys = []string{"ctrl+delete", "alt+delete", "alt+d"}17+ lineBackKeys = []string{"ctrl+u", "super+backspace"}18+)19+20+// IsWordKey reports whether key moves or deletes by word or to the start21+// of the line, so a field can pass it on to Key.22+func IsWordKey(key string) bool {23+ for _, keys := range [][]string{wordLeftKeys, wordRightKeys, wordBackKeys, wordDelKeys, lineBackKeys} {24+ if slices.Contains(keys, key) {25+ return true26+ }27+ }28+ return false29+}30+31+// wordKey applies a word key and reports whether key was one.32+func (l *Line) wordKey(key string) bool {33+ switch {34+ case slices.Contains(wordLeftKeys, key):35+ l.Pos = l.wordStart(l.Pos)36+ case slices.Contains(wordRightKeys, key):37+ l.Pos = l.wordEnd(l.Pos)38+ case slices.Contains(wordBackKeys, key):39+ l.DeleteBack(l.wordStart(l.Pos))40+ case slices.Contains(wordDelKeys, key):41+ l.Buf = slices.Delete(l.Buf, l.Pos, l.wordEnd(l.Pos))42+ case slices.Contains(lineBackKeys, key):43+ l.DeleteBack(0)44+ default:45+ return false46+ }47+ return true48+}49+50+// DeleteBack deletes from index from up to the caret.51+func (l *Line) DeleteBack(from int) {52+ l.Buf = slices.Delete(l.Buf, from, l.Pos)53+ l.Pos = from54+}55+56+// A word is a run of letters, digits and underscores, or a run of other57+// symbols, so in a formula Ctrl+Backspace takes B2 from SUM(A1:B2 and58+// then the colon. Spaces and line breaks around a word go with it.59+func wordClass(r rune) int {60+ switch {61+ case unicode.IsSpace(r):62+ return 063+ case r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r):64+ return 165+ }66+ return 267+}68+69+// wordStart is the start of the word before i, skipping spaces first.70+func (l *Line) wordStart(i int) int {71+ for i > 0 && wordClass(l.Buf[i-1]) == 0 {72+ i--73+ }74+ if i == 0 {75+ return 076+ }77+ c := wordClass(l.Buf[i-1])78+ for i > 0 && wordClass(l.Buf[i-1]) == c {79+ i--80+ }81+ return i82+}83+84+// wordEnd is the end of the word after i, skipping spaces first.85+func (l *Line) wordEnd(i int) int {86+ n := len(l.Buf)87+ for i < n && wordClass(l.Buf[i]) == 0 {88+ i++89+ }90+ if i == n {91+ return n92+ }93+ c := wordClass(l.Buf[i])94+ for i < n && wordClass(l.Buf[i]) == c {95+ i++96+ }97+ return i98+}@@ -0,0 +1,75 @@
1+package lineedit2+3+import (4+ "testing"5+6+ tea "charm.land/bubbletea/v2"7+)8+9+var (10+ ctrlLeft = tea.KeyPressMsg{Code: tea.KeyLeft, Mod: tea.ModCtrl}11+ altRight = tea.KeyPressMsg{Code: tea.KeyRight, Mod: tea.ModAlt}12+ ctrlBackspace = tea.KeyPressMsg{Code: tea.KeyBackspace, Mod: tea.ModCtrl}13+ legacyCtrlBS = tea.KeyPressMsg{Code: 'h', Mod: tea.ModCtrl}14+ optBackspace = tea.KeyPressMsg{Code: tea.KeyBackspace, Mod: tea.ModAlt}15+ altD = tea.KeyPressMsg{Code: 'd', Mod: tea.ModAlt}16+ ctrlU = tea.KeyPressMsg{Code: 'u', Mod: tea.ModCtrl}17+)18+19+func TestWordMoves(t *testing.T) {20+ var l Line21+ l.Set("=SUM(A1:B2) + total")22+ for _, want := range []int{14, 12, 10, 8, 7, 5, 4, 1, 0, 0} {23+ l.Key(ctrlLeft)24+ if l.Pos != want {25+ t.Fatalf("ctrl+left to %d, want %d", l.Pos, want)26+ }27+ }28+ for _, want := range []int{1, 4, 5, 7, 8, 10, 11, 13, 19, 19} {29+ l.Key(altRight)30+ if l.Pos != want {31+ t.Fatalf("alt+right to %d, want %d", l.Pos, want)32+ }33+ }34+}35+36+func TestWordDeletes(t *testing.T) {37+ var l Line38+ l.Set("=SUM(A1:B2")39+ for _, step := range []struct {40+ k tea.KeyPressMsg41+ want string42+ }{43+ {ctrlBackspace, "=SUM(A1:"},44+ {legacyCtrlBS, "=SUM(A1"},45+ {optBackspace, "=SUM("},46+ } {47+ l.Key(step.k)48+ if l.Text() != step.want {49+ t.Fatalf("%s left %q, want %q", step.k, l.Text(), step.want)50+ }51+ }52+ l.Set("hello big world")53+ l.Pos = 654+ l.Key(altD)55+ if l.Text() != "hello world" || l.Pos != 6 {56+ t.Errorf("alt+d left %q at %d", l.Text(), l.Pos)57+ }58+ l.Key(ctrlU)59+ if l.Text() != " world" || l.Pos != 0 {60+ t.Errorf("ctrl+u left %q at %d", l.Text(), l.Pos)61+ }62+}63+64+func TestAreaWordKeys(t *testing.T) {65+ a := &Area{}66+ a.Set("ls\n| where size > 1kb")67+ a.Key(ctrlBackspace, 40)68+ if a.Text() != "ls\n| where size > " {69+ t.Fatalf("ctrl+backspace left %q", a.Text())70+ }71+ a.Key(ctrlU, 40)72+ if a.Text() != "ls\n" {73+ t.Errorf("ctrl+u took more than its line: %q", a.Text())74+ }75+}@@ -0,0 +1,20 @@
1+package ui2+3+import "testing"4+5+// Word keys edit the entry by word in ENTER and EDIT.6+func TestEntryWordKeys(t *testing.T) {7+ m := newModel()8+ press(t, m, "=SUM(A1:B2", "<ctrl+backspace>")9+ if got := m.line.Text(); got != "=SUM(A1:" {10+ t.Fatalf("ctrl+backspace left %q", got)11+ }12+ press(t, m, "<alt+backspace>", "<f2>", "<ctrl+left>")13+ if m.mode != modeEdit || m.line.Pos != 5 || m.cur != addr("A1") {14+ t.Errorf("ctrl+left in EDIT: mode %v, caret %d, cell %v", m.mode, m.line.Pos, m.cur)15+ }16+ press(t, m, "<ctrl+u>")17+ if got := m.line.Text(); got != "A1" {18+ t.Errorf("ctrl+u left %q", got)19+ }20+}