| 🛟 Updated. 28d5985 k33g 16h ago | 1 | package editor |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | "time" |
| 6 | |
| 7 | "github.com/gdamore/tcell/v2" |
| 8 | ) |
| 9 | |
| 10 | // clickingView returns a view onto text with a clock the test drives, so a |
| 11 | // double click is a matter of what the test says rather than of how fast the |
| 12 | // machine is. |
| 13 | func clickingView(t *testing.T, text string) (*View, tcell.SimulationScreen, *time.Time) { |
| 14 | t.Helper() |
| 15 | |
| 16 | view, screen, _ := newTestView(t, text, 40, 8) |
| 17 | moment := time.Date(2026, 9, 2, 12, 0, 0, 0, time.UTC) |
| 18 | view.SetClock(func() time.Time { return moment }) |
| 19 | return view, screen, &moment |
| 20 | } |
| 21 | |
| 22 | // press sends one button-down at a cell, then the button-up that ends it. |
| 23 | func pressAt(view *View, x, y int) { |
| 24 | view.HandleMouse(tcell.NewEventMouse(x, y, tcell.Button1, tcell.ModNone)) |
| 25 | view.HandleMouse(tcell.NewEventMouse(x, y, tcell.ButtonNone, tcell.ModNone)) |
| 26 | } |
| 27 | |
| 28 | func TestADoubleClickSelectsTheWord(t *testing.T) { |
| 29 | view, _, _ := clickingView(t, "func mainLoop() {}\n") |
| 30 | x := view.gutterWidth() + 8 // inside "mainLoop" |
| 31 | |
| 32 | pressAt(view, x, 0) |
| 33 | pressAt(view, x, 0) |
| 34 | |
| 35 | if got := view.Buffer().SelectedText(); got != "mainLoop" { |
| 36 | t.Errorf("the selection is %q, want %q", got, "mainLoop") |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestASingleClickSelectsNothing(t *testing.T) { |
| 41 | view, _, _ := clickingView(t, "func mainLoop() {}\n") |
| 42 | |
| 43 | pressAt(view, view.gutterWidth()+8, 0) |
| 44 | |
| 45 | if got := view.Buffer().SelectedText(); got != "" { |
| 46 | t.Errorf("one click selected %q", got) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestTwoSlowClicksAreTwoClicks(t *testing.T) { |
| 51 | // Two unrelated clicks in the same place happen all the time while |
| 52 | // reading, and selecting a word each time would be noise. |
| 53 | view, _, moment := clickingView(t, "func mainLoop() {}\n") |
| 54 | x := view.gutterWidth() + 8 |
| 55 | |
| 56 | pressAt(view, x, 0) |
| 57 | *moment = moment.Add(doubleClickWithin + time.Millisecond) |
| 58 | pressAt(view, x, 0) |
| 59 | |
| 60 | if got := view.Buffer().SelectedText(); got != "" { |
| 61 | t.Errorf("two clicks %v apart selected %q", doubleClickWithin, got) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestTwoClicksOnDifferentCellsAreTwoClicks(t *testing.T) { |
| 66 | // A press one column over is a different word, and selecting the first |
| 67 | // because the pointer drifted is worse than selecting nothing. |
| 68 | view, _, _ := clickingView(t, "alpha beta\n") |
| 69 | |
| 70 | pressAt(view, view.gutterWidth()+1, 0) |
| 71 | pressAt(view, view.gutterWidth()+2, 0) |
| 72 | |
| 73 | if got := view.Buffer().SelectedText(); got != "" { |
| 74 | t.Errorf("two clicks on different cells selected %q", got) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func TestADragIsNotASecondClick(t *testing.T) { |
| 79 | // tcell sends Button1 for every event of a drag. Counting those would |
| 80 | // turn a slow drag into a double click halfway through. |
| 81 | view, _, _ := clickingView(t, "alpha beta gamma\n") |
| 82 | x := view.gutterWidth() |
| 83 | |
| 84 | view.HandleMouse(tcell.NewEventMouse(x+1, 0, tcell.Button1, tcell.ModNone)) |
| 85 | for column := 2; column <= 9; column++ { |
| 86 | view.HandleMouse(tcell.NewEventMouse(x+column, 0, tcell.Button1, tcell.ModNone)) |
| 87 | } |
| 88 | view.HandleMouse(tcell.NewEventMouse(x+9, 0, tcell.ButtonNone, tcell.ModNone)) |
| 89 | |
| 90 | if got := view.Buffer().SelectedText(); got != "lpha bet" { |
| 91 | t.Errorf("the drag selected %q, want the characters it crossed", got) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func TestADoubleClickOffAWordSelectsNothing(t *testing.T) { |
| 96 | view, _, _ := clickingView(t, "alpha beta\n") |
| 97 | x := view.gutterWidth() + 5 // the space |
| 98 | |
| 99 | pressAt(view, x, 0) |
| 100 | pressAt(view, x, 0) |
| 101 | |
| 102 | if got := view.Buffer().SelectedText(); got != "" { |
| 103 | t.Errorf("a double click on a space selected %q", got) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | func TestHoldingAfterADoubleClickExtendsFromTheWordsStart(t *testing.T) { |
| 108 | // Not the word-by-word drag a large editor does — the cheap half of it. |
| 109 | // The alternative is the word collapsing back to a single character the |
| 110 | // moment the pointer twitches, which is worse than either. |
| 111 | view, _, _ := clickingView(t, "alpha beta\n") |
| 112 | x := view.gutterWidth() + 1 |
| 113 | |
| 114 | pressAt(view, x, 0) // first click, released |
| 115 | view.HandleMouse(tcell.NewEventMouse(x, 0, tcell.Button1, tcell.ModNone)) // second, and held |
| 116 | view.HandleMouse(tcell.NewEventMouse(x+7, 0, tcell.Button1, tcell.ModNone)) // dragged |
| 117 | view.HandleMouse(tcell.NewEventMouse(x+7, 0, tcell.ButtonNone, tcell.ModNone)) |
| 118 | |
| 119 | if got := view.Buffer().SelectedText(); got != "alpha be" { |
| 120 | t.Errorf("dragging after a double click left %q, want it extended from the word's start", got) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func TestAThirdClickIsCountedButDoesNothingYet(t *testing.T) { |
| 125 | // The counter keeps going so that a triple click can be given a meaning |
| 126 | // later without reworking any of this. Today it selects the word again. |
| 127 | view, _, _ := clickingView(t, "alpha beta\n") |
| 128 | x := view.gutterWidth() + 1 |
| 129 | |
| 130 | pressAt(view, x, 0) |
| 131 | pressAt(view, x, 0) |
| 132 | pressAt(view, x, 0) |
| 133 | |
| 134 | if got := view.lastClick.count; got != 3 { |
| 135 | t.Errorf("the click count is %d, want 3", got) |
| 136 | } |
| 137 | if got := view.Buffer().SelectedText(); got != "alpha" { |
| 138 | t.Errorf("the third click left %q selected", got) |
| 139 | } |
| 140 | } |