diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index a29eb3192..8c811c5d1 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -190,6 +190,10 @@ type Gui struct { IsPasting bool + // The text pasted so far into pasteView, a view with an OnPaste handler + pasteBuffer strings.Builder + pasteView *View + // If InputEsc is true, when ESC sequence is in the buffer and it doesn't // match any known sequence, ESC means KeyEsc. InputEsc bool @@ -1285,6 +1289,9 @@ func (g *Gui) handleEvent(ev *GocuiEvent) error { return g.onFocus(ev) case eventPaste: g.IsPasting = ev.Start + if !ev.Start { + return g.deliverPaste() + } return nil default: return nil @@ -1758,6 +1765,32 @@ func (g *Gui) draw(v *View) { } } +func (g *Gui) collectPastedKey(v *View, key Key) { + if g.pasteView == nil { + g.pasteView = v + } + + switch { + case key.IsPrintable(): + g.pasteBuffer.WriteString(key.Str()) + case key.Equals(NewKeyName(KeyEnter)): + g.pasteBuffer.WriteString("\n") + case key.Equals(NewKeyName(KeyTab)): + g.pasteBuffer.WriteString("\t") + } +} + +func (g *Gui) deliverPaste() error { + v, text := g.pasteView, g.pasteBuffer.String() + g.pasteView = nil + g.pasteBuffer.Reset() + + if v == nil || text == "" { + return nil + } + return v.OnPaste(text) +} + // onKey manages key-press events. A keybinding handler is called when // a key-press or mouse event satisfies a configured keybinding. Furthermore, // currentView's internal buffer is modified if currentView.Editable is true. @@ -1776,6 +1809,11 @@ func (g *Gui) onKey(ev *GocuiEvent) error { ev.Key = NewKeyName(KeyEnter) } + if g.IsPasting && g.currentView != nil && g.currentView.OnPaste != nil { + g.collectPastedKey(g.currentView, ev.Key) + return nil + } + err := g.execKeybindings(g.currentView, ev) if err != nil { return err diff --git a/pkg/gocui/paste_test.go b/pkg/gocui/paste_test.go new file mode 100644 index 000000000..473ca574b --- /dev/null +++ b/pkg/gocui/paste_test.go @@ -0,0 +1,69 @@ +package gocui + +import ( + "testing" + + "github.com/gdamore/tcell/v3" + "github.com/stretchr/testify/assert" +) + +func TestPasteIntoViewWithPasteHandler(t *testing.T) { + g := newTestGui(t) + v, _ := g.SetView("editor", 0, 0, 40, 10, 0) + v.Editable = true + _, err := g.SetCurrentView("editor") + assert.NoError(t, err) + + pasted := []string{} + v.OnPaste = func(text string) error { + pasted = append(pasted, text) + return nil + } + escPressed := false + g.SetKeybinding("editor", NewKeyName(KeyEsc), func(*Gui, *View) error { + escPressed = true + return nil + }) + + for _, tev := range []tcell.Event{ + tcell.NewEventPaste(true), + tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModNone), + tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone), + // Ghostty pastes newlines as ctrl-j + tcell.NewEventKey(tcell.KeyRune, "j", tcell.ModCtrl), + tcell.NewEventKey(tcell.KeyTab, "", tcell.ModNone), + tcell.NewEventKey(tcell.KeyEsc, "", tcell.ModNone), + tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModCtrl), + tcell.NewEventKey(tcell.KeyRune, "b", tcell.ModNone), + tcell.NewEventPaste(false), + } { + ev := gocuiEventFromTcellEvent(tev) + assert.NoError(t, g.handleEvent(&ev)) + } + + assert.Equal(t, []string{"a\n\n\tb"}, pasted) + assert.False(t, escPressed) + assert.Equal(t, "", v.TextArea.GetContent()) + assert.False(t, g.IsPasting) +} + +func TestPasteIntoViewWithoutPasteHandler(t *testing.T) { + g := newTestGui(t) + v, _ := g.SetView("editor", 0, 0, 40, 10, 0) + v.Editable = true + v.Editor = DefaultEditor + _, err := g.SetCurrentView("editor") + assert.NoError(t, err) + + for _, tev := range []tcell.Event{ + tcell.NewEventPaste(true), + tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModNone), + tcell.NewEventKey(tcell.KeyRune, "b", tcell.ModNone), + tcell.NewEventPaste(false), + } { + ev := gocuiEventFromTcellEvent(tev) + assert.NoError(t, g.handleEvent(&ev)) + } + + assert.Equal(t, "ab", v.TextArea.GetContent()) +} diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index f16ed14ec..ae4715a9f 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -163,6 +163,12 @@ type View struct { // default. Editor Editor + // OnPaste, if set, receives pasted text in one piece when the paste ends, + // instead of one key press per character. The text consists of the pasted + // printable characters, newlines and tabs; other keys in the paste, such as + // Esc or control characters, are dropped. + OnPaste func(text string) error + // Overwrite enables or disables the overwrite mode of the view. Overwrite bool