package editor import ( "testing" "time" "github.com/gdamore/tcell/v2" ) // clickingView returns a view onto text with a clock the test drives, so a // double click is a matter of what the test says rather than of how fast the // machine is. func clickingView(t *testing.T, text string) (*View, tcell.SimulationScreen, *time.Time) { t.Helper() view, screen, _ := newTestView(t, text, 40, 8) moment := time.Date(2026, 9, 2, 12, 0, 0, 0, time.UTC) view.SetClock(func() time.Time { return moment }) return view, screen, &moment } // press sends one button-down at a cell, then the button-up that ends it. func pressAt(view *View, x, y int) { view.HandleMouse(tcell.NewEventMouse(x, y, tcell.Button1, tcell.ModNone)) view.HandleMouse(tcell.NewEventMouse(x, y, tcell.ButtonNone, tcell.ModNone)) } func TestADoubleClickSelectsTheWord(t *testing.T) { view, _, _ := clickingView(t, "func mainLoop() {}\n") x := view.gutterWidth() + 8 // inside "mainLoop" pressAt(view, x, 0) pressAt(view, x, 0) if got := view.Buffer().SelectedText(); got != "mainLoop" { t.Errorf("the selection is %q, want %q", got, "mainLoop") } } func TestASingleClickSelectsNothing(t *testing.T) { view, _, _ := clickingView(t, "func mainLoop() {}\n") pressAt(view, view.gutterWidth()+8, 0) if got := view.Buffer().SelectedText(); got != "" { t.Errorf("one click selected %q", got) } } func TestTwoSlowClicksAreTwoClicks(t *testing.T) { // Two unrelated clicks in the same place happen all the time while // reading, and selecting a word each time would be noise. view, _, moment := clickingView(t, "func mainLoop() {}\n") x := view.gutterWidth() + 8 pressAt(view, x, 0) *moment = moment.Add(doubleClickWithin + time.Millisecond) pressAt(view, x, 0) if got := view.Buffer().SelectedText(); got != "" { t.Errorf("two clicks %v apart selected %q", doubleClickWithin, got) } } func TestTwoClicksOnDifferentCellsAreTwoClicks(t *testing.T) { // A press one column over is a different word, and selecting the first // because the pointer drifted is worse than selecting nothing. view, _, _ := clickingView(t, "alpha beta\n") pressAt(view, view.gutterWidth()+1, 0) pressAt(view, view.gutterWidth()+2, 0) if got := view.Buffer().SelectedText(); got != "" { t.Errorf("two clicks on different cells selected %q", got) } } func TestADragIsNotASecondClick(t *testing.T) { // tcell sends Button1 for every event of a drag. Counting those would // turn a slow drag into a double click halfway through. view, _, _ := clickingView(t, "alpha beta gamma\n") x := view.gutterWidth() view.HandleMouse(tcell.NewEventMouse(x+1, 0, tcell.Button1, tcell.ModNone)) for column := 2; column <= 9; column++ { view.HandleMouse(tcell.NewEventMouse(x+column, 0, tcell.Button1, tcell.ModNone)) } view.HandleMouse(tcell.NewEventMouse(x+9, 0, tcell.ButtonNone, tcell.ModNone)) if got := view.Buffer().SelectedText(); got != "lpha bet" { t.Errorf("the drag selected %q, want the characters it crossed", got) } } func TestADoubleClickOffAWordSelectsNothing(t *testing.T) { view, _, _ := clickingView(t, "alpha beta\n") x := view.gutterWidth() + 5 // the space pressAt(view, x, 0) pressAt(view, x, 0) if got := view.Buffer().SelectedText(); got != "" { t.Errorf("a double click on a space selected %q", got) } } func TestHoldingAfterADoubleClickExtendsFromTheWordsStart(t *testing.T) { // Not the word-by-word drag a large editor does — the cheap half of it. // The alternative is the word collapsing back to a single character the // moment the pointer twitches, which is worse than either. view, _, _ := clickingView(t, "alpha beta\n") x := view.gutterWidth() + 1 pressAt(view, x, 0) // first click, released view.HandleMouse(tcell.NewEventMouse(x, 0, tcell.Button1, tcell.ModNone)) // second, and held view.HandleMouse(tcell.NewEventMouse(x+7, 0, tcell.Button1, tcell.ModNone)) // dragged view.HandleMouse(tcell.NewEventMouse(x+7, 0, tcell.ButtonNone, tcell.ModNone)) if got := view.Buffer().SelectedText(); got != "alpha be" { t.Errorf("dragging after a double click left %q, want it extended from the word's start", got) } } func TestAThirdClickIsCountedButDoesNothingYet(t *testing.T) { // The counter keeps going so that a triple click can be given a meaning // later without reworking any of this. Today it selects the word again. view, _, _ := clickingView(t, "alpha beta\n") x := view.gutterWidth() + 1 pressAt(view, x, 0) pressAt(view, x, 0) pressAt(view, x, 0) if got := view.lastClick.count; got != 3 { t.Errorf("the click count is %d, want 3", got) } if got := view.Buffer().SelectedText(); got != "alpha" { t.Errorf("the third click left %q selected", got) } }