diff --git a/pkg/gocui/block_events_test.go b/pkg/gocui/block_events_test.go index e95b834ff..15af1452a 100644 --- a/pkg/gocui/block_events_test.go +++ b/pkg/gocui/block_events_test.go @@ -18,7 +18,7 @@ func TestEventWithheldWhileBlocking(t *testing.T) { {"mouse move", GocuiEvent{Type: eventMouseMove}, true}, {"resize", GocuiEvent{Type: eventResize}, false}, {"focus", GocuiEvent{Type: eventFocus}, false}, - {"paste", GocuiEvent{Type: eventPaste}, false}, + {"paste", GocuiEvent{Type: eventPaste}, true}, {"error", GocuiEvent{Type: eventError}, false}, } @@ -86,7 +86,7 @@ func TestBlockingEvents_KeysBufferedAndReplayed(t *testing.T) { runQueuedWork(t, g) assert.Len(t, *fired, 3, "both buffered keys should replay on unblock") - assert.Empty(t, g.bufferedKeyEvents) + assert.Empty(t, g.bufferedInputEvents) } func TestBlockingEvents_KeysArrivingBeforeTheReplayGoBehindIt(t *testing.T) { @@ -111,6 +111,29 @@ func TestBlockingEvents_KeysArrivingBeforeTheReplayGoBehindIt(t *testing.T) { assert.Equal(t, []int{1, 0}, *fired, "the keys should arrive in the order they were pressed") } +func TestBlockingEvents_PastedKeysReplayedAsPasted(t *testing.T) { + g := newTestGui(t) + keyEvent, fired := setupKeyRecorder(t, g) + pasteStart := GocuiEvent{Type: eventPaste, Start: true} + pasteEnd := GocuiEvent{Type: eventPaste, Start: false} + + // Pasting into a view that isn't editable doesn't run its keybindings. + assert.NoError(t, g.handleEvent(&pasteStart)) + assert.NoError(t, g.handleEvent(&keyEvent)) + assert.NoError(t, g.handleEvent(&pasteEnd)) + assert.Empty(t, *fired) + + // The same must hold for a paste that arrives while input is blocked. + g.BeginBlockingEvents() + assert.NoError(t, g.handleEvent(&pasteStart)) + assert.NoError(t, g.handleEvent(&keyEvent)) + assert.NoError(t, g.handleEvent(&pasteEnd)) + g.EndBlockingEvents() + runQueuedWork(t, g) + assert.Empty(t, *fired) + assert.False(t, g.IsPasting) +} + func TestBlockingEvents_NestsWithCounter(t *testing.T) { g := newTestGui(t) keyEvent, fired := setupKeyRecorder(t, g) @@ -136,7 +159,7 @@ func TestBlockingEvents_MouseClicksDroppedNotBuffered(t *testing.T) { g.BeginBlockingEvents() click := GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseLeft)} assert.NoError(t, g.handleEvent(&click)) - assert.Empty(t, g.bufferedKeyEvents, "mouse clicks must be dropped, not buffered") + assert.Empty(t, g.bufferedInputEvents, "mouse clicks must be dropped, not buffered") g.EndBlockingEvents() runQueuedWork(t, g) } diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index afc71a2ed..8b6179a2d 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -242,15 +242,15 @@ type Gui struct { colorSchemeHandler func(DetectedColorScheme) error // blockInputCount, when greater than zero, withholds keyboard input from - // the handlers: key events are buffered into bufferedKeyEvents and replayed - // once the count drops back to zero, while mouse clicks and hover are - // dropped outright. It's a counter so blocking can nest. replayPending says - // that the replay is queued but hasn't run yet, and input is withheld until - // it has. All three fields are only touched on the UI thread. See - // BeginBlockingEvents. - blockInputCount int - bufferedKeyEvents []GocuiEvent - replayPending bool + // the handlers: key and paste events are buffered into bufferedInputEvents + // and replayed once the count drops back to zero, while mouse clicks and + // hover are dropped outright. It's a counter so blocking can nest. + // replayPending says that the replay is queued but hasn't run yet, and + // input is withheld until it has. All three fields are only touched on the + // UI thread. See BeginBlockingEvents. + blockInputCount int + bufferedInputEvents []GocuiEvent + replayPending bool } type NewGuiOpts struct { @@ -929,9 +929,10 @@ func (g *Gui) IsUIThread() bool { // BeginBlockingEvents starts withholding keyboard input from the handlers, so a // long-running operation can't be disrupted by keys the user presses while it // runs. Keys are buffered and replayed once EndBlockingEvents balances this -// call; mouse clicks and hover are dropped for the duration. Scrolling, -// resizing, focus changes and all rendering keep working throughout. It's a -// counter, so blocking nests; every call must be paired with EndBlockingEvents. +// call, together with the start and end of a paste around them; mouse clicks +// and hover are dropped for the duration. Scrolling, resizing, focus changes +// and all rendering keep working throughout. It's a counter, so blocking nests; +// every call must be paired with EndBlockingEvents. // // Must be called on the UI thread. Callers arrange this by beginning the block // synchronously from the keybinding handler, before dispatching the operation @@ -963,18 +964,18 @@ func (g *Gui) EndBlockingEvents() { // before the replay gets its turn would otherwise be handled ahead of the keys // buffered before it. g.replayPending = true - g.Update(func(*Gui) error { return g.replayBufferedKeys() }) + g.Update(func(*Gui) error { return g.replayBufferedInput() }) } -// replayBufferedKeys dispatches the keys withheld while input was blocked, and +// replayBufferedInput dispatches the keys withheld while input was blocked, and // lets input through again. One of their handlers may block input afresh, and // then the keys after it are withheld in their turn, to be replayed when that // block ends. -func (g *Gui) replayBufferedKeys() error { +func (g *Gui) replayBufferedInput() error { g.replayPending = false - buffered := g.bufferedKeyEvents - g.bufferedKeyEvents = nil + buffered := g.bufferedInputEvents + g.bufferedInputEvents = nil for i := range buffered { if err := g.handleEvent(&buffered[i]); err != nil { return err @@ -1251,9 +1252,11 @@ func (g *Gui) processRemainingEvents() (bool, error) { // etc.) func (g *Gui) handleEvent(ev *GocuiEvent) error { if g.withholdingInput() && eventWithheldWhileBlocking(ev) { - if ev.Type == eventKey { - // Buffer keys so they replay against fresh state on unblock. - g.bufferedKeyEvents = append(g.bufferedKeyEvents, *ev) + if ev.Type == eventKey || ev.Type == eventPaste { + // Buffer keys so they replay against fresh state on unblock. Buffer + // the start and end of a paste with them, so that pasted keys are + // still handled as pasted when they replay. + g.bufferedInputEvents = append(g.bufferedInputEvents, *ev) } // Mouse clicks and hover fall through to here without being buffered: // replaying them once the operation has changed the layout underneath @@ -1287,13 +1290,13 @@ func (g *Gui) withholdingInput() bool { } // eventWithheldWhileBlocking reports whether an event must not reach the -// handlers while input is blocked (see BeginBlockingEvents). Key events are -// withheld (buffered for replay); mouse clicks and hover are withheld (dropped). -// Everything else — mouse scrolling, resize, focus, paste, errors — flows +// handlers while input is blocked (see BeginBlockingEvents). Key and paste +// events are withheld (buffered for replay); mouse clicks and hover are withheld +// (dropped). Everything else — mouse scrolling, resize, focus, errors — flows // through as usual. func eventWithheldWhileBlocking(ev *GocuiEvent) bool { switch ev.Type { - case eventKey: + case eventKey, eventPaste: return true case eventMouse: return !IsMouseScrollKey(ev.Key.KeyName()) @@ -1752,8 +1755,6 @@ func (g *Gui) draw(v *View) { func (g *Gui) onKey(ev *GocuiEvent) error { switch ev.Type { case eventKey: - - // newlines. I actually don't quite understand why, because from reading // When pasting text in Ghostty, it sends us '\r' (which is delivered as // ctrl-j by tcell) instead of '\n' for newlines. I actually don't quite // understand why, because from reading Ghostty's source code (e.g.