diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 8b6179a2d..a29eb3192 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -119,6 +119,7 @@ type replayedEvents struct { Resizes chan *TcellResizeEventWrapper MouseEvents chan *TcellMouseEventWrapper FocusEvents chan *TcellFocusEventWrapper + PasteEvents chan *TcellPasteEventWrapper } type RecordingConfig struct { @@ -305,6 +306,7 @@ func NewGui(opts NewGuiOpts) (*Gui, error) { Resizes: make(chan *TcellResizeEventWrapper), MouseEvents: make(chan *TcellMouseEventWrapper), FocusEvents: make(chan *TcellFocusEventWrapper), + PasteEvents: make(chan *TcellPasteEventWrapper), } } @@ -384,6 +386,13 @@ func (g *Gui) ReplayFocusEvent(ev *TcellFocusEventWrapper) { g.replayedEvents.FocusEvents <- ev } +// ReplayPasteEvent is like ReplayKeyEvent, but for the events that mark the +// start and the end of a paste. +func (g *Gui) ReplayPasteEvent(ev *TcellPasteEventWrapper) { + ev.task = g.NewTask() + g.replayedEvents.PasteEvents <- ev +} + // Busy reports whether any foreground work is in flight, ignoring the event // currently being processed on the main goroutine (see currentTask). Background // routines (auto-fetch etc.) don't count. It's used to decide whether it's safe diff --git a/pkg/gocui/tcell_driver.go b/pkg/gocui/tcell_driver.go index 61a3f0e89..4a3145749 100644 --- a/pkg/gocui/tcell_driver.go +++ b/pkg/gocui/tcell_driver.go @@ -302,6 +302,22 @@ func (wrapper TcellFocusEventWrapper) toTcellEvent() tcell.Event { return tcell.NewEventFocus(wrapper.Focused) } +type TcellPasteEventWrapper struct { + Start bool + + task Task // see GocuiEvent.task +} + +func NewTcellPasteEventWrapper(event *tcell.EventPaste) *TcellPasteEventWrapper { + return &TcellPasteEventWrapper{ + Start: event.Start(), + } +} + +func (wrapper TcellPasteEventWrapper) toTcellEvent() tcell.Event { + return tcell.NewEventPaste(wrapper.Start) +} + // pollEvent get tcell.Event and transform it into gocuiEvent func (g *Gui) pollEvent() GocuiEvent { var tev tcell.Event @@ -319,6 +335,9 @@ func (g *Gui) pollEvent() GocuiEvent { case ev := <-g.replayedEvents.FocusEvents: tev = ev.toTcellEvent() task = ev.task + case ev := <-g.replayedEvents.PasteEvents: + tev = ev.toTcellEvent() + task = ev.task } } else { tev = <-Screen.EventQ() diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index f66bcd020..a9d257072 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -138,6 +138,31 @@ func (self *GuiDriver) FocusIn() { self.waitTillIdle() } +// Paste sends the events that a terminal with bracketed paste mode sends: the +// start of the paste, a key press for each character, and the end of the paste. +func (self *GuiDriver) Paste(text string) { + self.CheckAllToastsAcknowledged() + + self.gui.g.ReplayPasteEvent(gocui.NewTcellPasteEventWrapper(tcell.NewEventPaste(true))) + for _, char := range text { + self.gui.g.ReplayKeyEvent(gocui.NewTcellKeyEventWrapper(keyEventForPastedCharacter(char))) + } + self.gui.g.ReplayPasteEvent(gocui.NewTcellPasteEventWrapper(tcell.NewEventPaste(false))) + + self.waitTillIdle() +} + +func keyEventForPastedCharacter(char rune) *tcell.EventKey { + switch char { + case '\n': + return tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone) + case '\t': + return tcell.NewEventKey(tcell.KeyTab, "", tcell.ModNone) + default: + return tcell.NewEventKey(tcell.KeyRune, string(char), tcell.ModNone) + } +} + func (self *GuiDriver) FocusInAndClick(x, y int) { self.CheckAllToastsAcknowledged() diff --git a/pkg/integration/components/commit_description_panel_driver.go b/pkg/integration/components/commit_description_panel_driver.go index 6281e756b..261759886 100644 --- a/pkg/integration/components/commit_description_panel_driver.go +++ b/pkg/integration/components/commit_description_panel_driver.go @@ -21,6 +21,14 @@ func (self *CommitDescriptionPanelDriver) Type(value string) *CommitDescriptionP return self } +// Paste the text the way a terminal does it, as opposed to pasting it with the +// commit menu +func (self *CommitDescriptionPanelDriver) Paste(text string) *CommitDescriptionPanelDriver { + self.t.paste(text) + + return self +} + func (self *CommitDescriptionPanelDriver) SwitchToSummary() *CommitMessagePanelDriver { self.getViewDriver().PressTab() return &CommitMessagePanelDriver{t: self.t} diff --git a/pkg/integration/components/commit_message_panel_driver.go b/pkg/integration/components/commit_message_panel_driver.go index f24950f13..2d4e25405 100644 --- a/pkg/integration/components/commit_message_panel_driver.go +++ b/pkg/integration/components/commit_message_panel_driver.go @@ -33,6 +33,14 @@ func (self *CommitMessagePanelDriver) Type(value string) *CommitMessagePanelDriv return self } +// Paste the text the way a terminal does it, as opposed to pasting it with the +// commit menu +func (self *CommitMessagePanelDriver) Paste(text string) *CommitMessagePanelDriver { + self.t.paste(text) + + return self +} + func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPanelDriver { self.getViewDriver().PressTab() return &CommitDescriptionPanelDriver{t: self.t} diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index e924a956e..375d37d61 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -136,6 +136,12 @@ func (self *TestDriver) FocusIn() { self.Wait(self.inputDelay) } +func (self *TestDriver) paste(text string) { + self.SetCaption(fmt.Sprintf("Pasting %q", text)) + self.gui.Paste(text) + self.Wait(self.inputDelay) +} + func (self *TestDriver) focusInAndClick(x, y int) { self.SetCaption(fmt.Sprintf("Focusing window and clicking %d, %d", x, y)) self.gui.FocusInAndClick(x, y) diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 7a69c07ad..199f46af7 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -84,6 +84,9 @@ func (self *fakeGuiDriver) OnUIThreadAndWait(f func()) { func (self *fakeGuiDriver) FocusIn() { } +func (self *fakeGuiDriver) Paste(text string) { +} + func (self *fakeGuiDriver) FocusInAndClick(x, y int) { self.clickedCoordinates = append(self.clickedCoordinates, coordinate{x: x, y: y}) } diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 1d24fd6ad..9a40192a1 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -47,6 +47,8 @@ type GuiDriver interface { // Simulate the terminal window regaining focus (which triggers a reload of // changed config files) FocusIn() + // Simulate pasting text in a terminal that supports bracketed paste + Paste(string) // Simulate a terminal dispatching focus-in immediately followed by a click, // without waiting for the focus refresh to finish in between. FocusInAndClick(int, int)