mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-12 15:53:33 -04:00
Let views receive pasted text in one piece
Editable views receive pasted text as one key press per character, so a newline or a tab in the text triggers the keybinding for Enter or Tab. The commit message panel works around this by checking IsPasting in these keybindings. This is fragile, and it gives the panel no way to look at the pasted text as a whole. Add an OnPaste handler to View. For a view that has one, collect the pasted characters, newlines and tabs while the paste lasts, and pass them to the handler when it ends. Drop all other keys of the paste. They are unlikely to occur in pasted text, and dispatching keys like Esc in the middle of a paste would run them as commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9d2058fbc4
commit
0111d9bd61
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user