mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-12 15:53:33 -04:00
Improve pasting commit messages (#6210)
When pasting a commit message into the commit message editor (either via the `ctrl+o` menu or with the terminal's Command-V), we now remove the hard line breaks so that the text reflows again when you edit it, like we already do when rewording a message. When pasting with Command-V, we now avoid the extra blank line at the beginning of the description; this wasn't really a problem because it got stripped when committing, but it looked ugly. Along the way, fix a few minor issues when pasting text into various views, e.g. the search prompt or a "New branch name" text field.
This commit is contained in:
@@ -131,7 +131,7 @@ func TestBlockingEvents_PastedKeysReplayedAsPasted(t *testing.T) {
|
||||
g.EndBlockingEvents()
|
||||
runQueuedWork(t, g)
|
||||
assert.Empty(t, *fired)
|
||||
assert.False(t, g.IsPasting)
|
||||
assert.False(t, g.isPasting)
|
||||
}
|
||||
|
||||
func TestBlockingEvents_NestsWithCounter(t *testing.T) {
|
||||
|
||||
+54
-7
@@ -119,6 +119,7 @@ type replayedEvents struct {
|
||||
Resizes chan *TcellResizeEventWrapper
|
||||
MouseEvents chan *TcellMouseEventWrapper
|
||||
FocusEvents chan *TcellFocusEventWrapper
|
||||
PasteEvents chan *TcellPasteEventWrapper
|
||||
}
|
||||
|
||||
type RecordingConfig struct {
|
||||
@@ -187,7 +188,13 @@ type Gui struct {
|
||||
// If Mouse is true then mouse events will be enabled.
|
||||
Mouse bool
|
||||
|
||||
IsPasting bool
|
||||
// Whether a paste is in progress, between the terminal's events for its
|
||||
// start and end
|
||||
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.
|
||||
@@ -305,6 +312,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 +392,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
|
||||
@@ -1275,7 +1290,10 @@ func (g *Gui) handleEvent(ev *GocuiEvent) error {
|
||||
case eventFocus:
|
||||
return g.onFocus(ev)
|
||||
case eventPaste:
|
||||
g.IsPasting = ev.Start
|
||||
g.isPasting = ev.Start
|
||||
if !ev.Start {
|
||||
return g.deliverPaste()
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
@@ -1749,6 +1767,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.
|
||||
@@ -1763,10 +1807,17 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
// in bracketed paste mode. Whatever I'm missing here, converting '\r'
|
||||
// back to '\n' fixes pasting multi-line text from Ghostty, and doesn't
|
||||
// seem harmful for other terminal emulators.
|
||||
if g.IsPasting && ev.Key.Equals(NewKeyStrMod("j", ModCtrl)) {
|
||||
if g.isPasting && ev.Key.Equals(NewKeyStrMod("j", ModCtrl)) {
|
||||
ev.Key = NewKeyName(KeyEnter)
|
||||
}
|
||||
|
||||
if g.isPasting {
|
||||
if 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
|
||||
@@ -2040,10 +2091,6 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||
var globalKb *keybinding
|
||||
var matchingParentViewKb *keybinding
|
||||
|
||||
if g.IsPasting && v != nil && !v.Editable {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we're searching, and we've hit n/N/Esc, we ignore the default keybinding
|
||||
if v != nil && v.IsSearching() {
|
||||
if lo.SomeBy(g.NextSearchMatchKeys, func(k Key) bool { return ev.Key.Equals(k) }) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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)
|
||||
|
||||
enterPressed := false
|
||||
g.SetKeybinding("editor", NewKeyName(KeyEnter), func(*Gui, *View) error {
|
||||
enterPressed = true
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, tev := range []tcell.Event{
|
||||
tcell.NewEventPaste(true),
|
||||
tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModNone),
|
||||
tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone),
|
||||
tcell.NewEventKey(tcell.KeyRune, "b", tcell.ModNone),
|
||||
tcell.NewEventPaste(false),
|
||||
} {
|
||||
ev := gocuiEventFromTcellEvent(tev)
|
||||
assert.NoError(t, g.handleEvent(&ev))
|
||||
}
|
||||
|
||||
assert.Equal(t, "", v.TextArea.GetContent())
|
||||
assert.False(t, enterPressed)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -163,6 +163,13 @@ type View struct {
|
||||
// default.
|
||||
Editor Editor
|
||||
|
||||
// OnPaste, if set, receives pasted text in one piece when the paste ends.
|
||||
// The text consists of the pasted printable characters, newlines and tabs;
|
||||
// other keys in the paste, such as Esc or control characters, are dropped.
|
||||
// Pasting into a view without an OnPaste handler does nothing, so that
|
||||
// pasted text never runs keybindings.
|
||||
OnPaste func(text string) error
|
||||
|
||||
// Overwrite enables or disables the overwrite mode of the view.
|
||||
Overwrite bool
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type CommitDescriptionController struct {
|
||||
@@ -27,7 +26,7 @@ func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOp
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.TogglePanel),
|
||||
Handler: self.handleTogglePanel,
|
||||
Handler: self.switchToCommitMessage,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Return),
|
||||
@@ -86,32 +85,6 @@ func (self *CommitDescriptionController) switchToCommitMessage() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) handleTogglePanel() error {
|
||||
// The default keybinding for this action is "<tab>", which means that we
|
||||
// also get here when pasting multi-line text that contains tabs. In that
|
||||
// case we don't want to toggle the panel, but insert the tab as a character
|
||||
// (somehow, see below).
|
||||
//
|
||||
// Only do this if the TogglePanel command is actually mapped to "<tab>"
|
||||
// (the default). If it's not, we can only hope that it's mapped to some
|
||||
// ctrl key or fn key, which is unlikely to occur in pasted text. And if
|
||||
// they mapped some *other* command to "<tab>", then we're totally out of
|
||||
// luck.
|
||||
if self.c.GocuiGui().IsPasting && lo.Contains(self.c.UserConfig().Keybinding.Universal.TogglePanel, "<tab>") {
|
||||
// Handling tabs in pasted commit messages is not optimal, but hopefully
|
||||
// good enough for now. We simply insert 4 spaces without worrying about
|
||||
// column alignment. This works well enough for leading indentation,
|
||||
// which is common in pasted code snippets.
|
||||
view := self.Context().GetView()
|
||||
for range 4 {
|
||||
view.Editor.Edit(view, gocui.NewKeyRune(' '))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.switchToCommitMessage()
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) close() error {
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
return nil
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type CommitMessageController struct {
|
||||
@@ -49,7 +47,7 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts)
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.TogglePanel),
|
||||
Handler: self.handleTogglePanel,
|
||||
Handler: self.switchToCommitDescription,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.CommitMessage.CommitMenu),
|
||||
@@ -113,32 +111,6 @@ func (self *CommitMessageController) switchToCommitDescription() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) handleTogglePanel() error {
|
||||
// The default keybinding for this action is "<tab>", which means that we
|
||||
// also get here when pasting multi-line text that contains tabs. In that
|
||||
// case we don't want to toggle the panel, but insert the tab as a character
|
||||
// (somehow, see below).
|
||||
//
|
||||
// Only do this if the TogglePanel command is actually mapped to "<tab>"
|
||||
// (the default). If it's not, we can only hope that it's mapped to some
|
||||
// ctrl key or fn key, which is unlikely to occur in pasted text. And if
|
||||
// they mapped some *other* command to "<tab>", then we're totally out of
|
||||
// luck.
|
||||
if self.c.GocuiGui().IsPasting && lo.Contains(self.c.UserConfig().Keybinding.Universal.TogglePanel, "<tab>") {
|
||||
// It is unlikely that a pasted commit message contains a tab in the
|
||||
// subject line, so it shouldn't matter too much how we handle it.
|
||||
// Simply insert 4 spaces instead; all that matters is that we don't
|
||||
// switch to the description panel.
|
||||
view := self.context().GetView()
|
||||
for range 4 {
|
||||
view.Editor.Edit(view, gocui.NewKeyRune(' '))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.switchToCommitDescription()
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) handleCommitIndexChange(value int) error {
|
||||
currentIndex := self.context().GetSelectedIndex()
|
||||
newIndex := currentIndex + value
|
||||
@@ -166,28 +138,12 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
|
||||
}
|
||||
return false, errors.New(self.c.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
if self.c.UserConfig().Git.Commit.AutoWrapCommitMessage {
|
||||
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig().Git.Commit.AutoWrapWidth)
|
||||
}
|
||||
commitMessage = self.c.Helpers().Commits.TryRemoveHardLineBreaksIfAutoWrapping(commitMessage)
|
||||
self.c.Helpers().Commits.SetMessageAndDescriptionInView(commitMessage)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) confirm() error {
|
||||
// The default keybinding for this action is "<enter>", which means that we
|
||||
// also get here when pasting multi-line text that contains newlines. In
|
||||
// that case we don't want to confirm the commit, but switch to the
|
||||
// description panel instead so that the rest of the pasted text goes there.
|
||||
//
|
||||
// Only do this if the SubmitEditorText command is actually mapped to
|
||||
// "<enter>" (the default). If it's not, we can only hope that it's mapped
|
||||
// to some ctrl key or fn key, which is unlikely to occur in pasted text.
|
||||
// And if they mapped some *other* command to "<enter>", then we're totally
|
||||
// out of luck.
|
||||
if self.c.GocuiGui().IsPasting && lo.Contains(self.c.UserConfig().Keybinding.Universal.SubmitEditorText, "<enter>") {
|
||||
return self.switchToCommitDescription()
|
||||
}
|
||||
|
||||
return self.c.Helpers().Commits.HandleCommitConfirm()
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,18 @@ func (self *CommitsHelper) JoinCommitMessageAndUnwrappedDescription() string {
|
||||
return self.getCommitSummary() + "\n" + self.getUnwrappedCommitDescription()
|
||||
}
|
||||
|
||||
func TryRemoveHardLineBreaks(message string, autoWrapWidth int) string {
|
||||
// TryRemoveHardLineBreaksIfAutoWrapping replaces the hard line breaks of a
|
||||
// message with spaces wherever auto-wrapping would break the line at the same
|
||||
// place, so that the paragraphs reflow when the user edits them in the panel.
|
||||
func (self *CommitsHelper) TryRemoveHardLineBreaksIfAutoWrapping(message string) string {
|
||||
commitConfig := self.c.UserConfig().Git.Commit
|
||||
if !commitConfig.AutoWrapCommitMessage {
|
||||
return message
|
||||
}
|
||||
return tryRemoveHardLineBreaks(message, commitConfig.AutoWrapWidth)
|
||||
}
|
||||
|
||||
func tryRemoveHardLineBreaks(message string, autoWrapWidth int) string {
|
||||
lastHardLineStart := 0
|
||||
result := message
|
||||
for i, b := range message {
|
||||
@@ -309,6 +320,46 @@ func (self *CommitsHelper) copyCommitMessageToClipboard() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandlePasteIntoSummary inserts text that the user pasted into the summary
|
||||
// with the terminal's paste command. If the text has several lines, the rest
|
||||
// of them goes into the description.
|
||||
func (self *CommitsHelper) HandlePasteIntoSummary(text string) {
|
||||
summary, rest, hasMoreLines := strings.Cut(text, "\n")
|
||||
|
||||
view := self.c.Views().CommitMessage
|
||||
view.TextArea.TypeString(replaceTabsInPastedText(summary))
|
||||
view.RenderTextArea()
|
||||
self.c.Contexts().CommitMessage.RenderSubtitle()
|
||||
|
||||
if hasMoreLines {
|
||||
self.c.Context().Replace(self.c.Contexts().CommitDescription)
|
||||
self.HandlePasteIntoDescription(rest)
|
||||
}
|
||||
}
|
||||
|
||||
// HandlePasteIntoDescription inserts text that the user pasted into the
|
||||
// description with the terminal's paste command.
|
||||
func (self *CommitsHelper) HandlePasteIntoDescription(text string) {
|
||||
view := self.c.Views().CommitDescription
|
||||
if view.TextArea.GetUnwrappedContent() == "" {
|
||||
// This drops the blank line between subject and body when the user
|
||||
// pastes a whole commit message into the summary
|
||||
text = strings.TrimLeft(text, "\n")
|
||||
}
|
||||
// Remove the hard line breaks of the pasted text on its own, even if it
|
||||
// goes into the middle of a line. Its line breaks come from the layout of
|
||||
// the text that it was copied from, not from the line that it goes into.
|
||||
text = self.TryRemoveHardLineBreaksIfAutoWrapping(replaceTabsInPastedText(text))
|
||||
view.TextArea.TypeString(text)
|
||||
view.RenderTextArea()
|
||||
}
|
||||
|
||||
// Replace each tab with four spaces, without aligning to tab stops. This works
|
||||
// well enough for leading indentation, which is common in pasted code snippets.
|
||||
func replaceTabsInPastedText(text string) string {
|
||||
return strings.ReplaceAll(text, "\t", " ")
|
||||
}
|
||||
|
||||
func stripTrailingWhitespace(message string) string {
|
||||
lines := strings.Split(message, "\n")
|
||||
for i, line := range lines {
|
||||
@@ -331,7 +382,7 @@ func (self *CommitsHelper) pasteCommitMessageFromClipboard() error {
|
||||
Title: self.c.Tr.PasteCommitMessageFromClipboard,
|
||||
Prompt: self.c.Tr.SurePasteCommitMessage,
|
||||
HandleConfirm: func() error {
|
||||
self.SetMessageAndDescriptionInView(message)
|
||||
self.SetMessageAndDescriptionInView(self.TryRemoveHardLineBreaksIfAutoWrapping(message))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestTryRemoveHardLineBreaks(t *testing.T) {
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
actualResult := TryRemoveHardLineBreaks(s.message, s.autoWrapWidth)
|
||||
actualResult := tryRemoveHardLineBreaks(s.message, s.autoWrapWidth)
|
||||
assert.Equal(t, s.expectedResult, actualResult)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,14 +53,6 @@ func (self *ConfirmationHelper) wrappedPromptConfirmationFunction(
|
||||
preserveWhitespace bool,
|
||||
) func() error {
|
||||
return func() error {
|
||||
if self.c.GocuiGui().IsPasting {
|
||||
// The user is pasting multi-line text into a prompt; we don't want to handle the
|
||||
// line feeds as "confirm" keybindings. Simply ignoring them is the best we can do; this
|
||||
// will cause the entire pasted text to appear as a single line in the prompt. Hopefully
|
||||
// the user knows that ctrl-u allows them to delete it again...
|
||||
return nil
|
||||
}
|
||||
|
||||
response := getResponse()
|
||||
if !preserveWhitespace {
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
@@ -884,9 +884,7 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self.c.UserConfig().Git.Commit.AutoWrapCommitMessage {
|
||||
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig().Git.Commit.AutoWrapWidth)
|
||||
}
|
||||
commitMessage = self.c.Helpers().Commits.TryRemoveHardLineBreaksIfAutoWrapping(commitMessage)
|
||||
self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
CommitIndex: commitIdx,
|
||||
@@ -1579,9 +1577,7 @@ func (self *LocalCommitsController) createAmendCommit(commit *models.Commit, inc
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self.c.UserConfig().Git.Commit.AutoWrapCommitMessage {
|
||||
commitMessage = helpers.TryRemoveHardLineBreaks(commitMessage, self.c.UserConfig().Git.Commit.AutoWrapWidth)
|
||||
}
|
||||
commitMessage = self.c.Helpers().Commits.TryRemoveHardLineBreaksIfAutoWrapping(commitMessage)
|
||||
originalSubject, _, _ := strings.Cut(commitMessage, "\n")
|
||||
self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
|
||||
+54
-3
@@ -1,6 +1,8 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
)
|
||||
|
||||
@@ -33,7 +35,19 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool {
|
||||
matched := gui.handleEditorKeypress(v, key, false)
|
||||
|
||||
v.RenderTextArea()
|
||||
gui.onPromptContentChanged(v)
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) pasteIntoPrompt(text string) {
|
||||
v := gui.Views.Prompt
|
||||
if typePastedTextIntoSingleLineView(v, text) {
|
||||
gui.onPromptContentChanged(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onPromptContentChanged(v *gocui.View) {
|
||||
suggestionsContext := gui.State.Contexts.Suggestions
|
||||
// Capture the suggestions function and the input here, on the UI thread; the
|
||||
// main thread rewrites State.FindSuggestions when it (re)creates a prompt
|
||||
@@ -45,8 +59,6 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool {
|
||||
return func() { suggestionsContext.SetSuggestions(suggestions) }
|
||||
})
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) menuFilterEditor(v *gocui.View, key gocui.Key) bool {
|
||||
@@ -68,11 +80,50 @@ func (gui *Gui) menuFilterEditor(v *gocui.View, key gocui.Key) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
gui.onMenuFilterContentChanged(content)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (gui *Gui) pasteIntoMenuFilter(text string) {
|
||||
v := gui.Views.MenuFilter
|
||||
if typePastedTextIntoSingleLineView(v, text) {
|
||||
gui.onMenuFilterContentChanged(v.TextArea.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
// typePastedTextIntoSingleLineView returns false if nothing is left of the text
|
||||
// to type.
|
||||
func typePastedTextIntoSingleLineView(v *gocui.View, text string) bool {
|
||||
text = pastedTextAsSingleLine(text)
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
v.TextArea.TypeString(text)
|
||||
v.RenderTextArea()
|
||||
return true
|
||||
}
|
||||
|
||||
// Views that take a single line of text get pasted text without the newlines at
|
||||
// its start and end, which are often part of a copied line, and with spaces in
|
||||
// place of the newlines and tabs inside it.
|
||||
func pastedTextAsSingleLine(text string) string {
|
||||
text = strings.Trim(text, "\n")
|
||||
return strings.NewReplacer("\n", " ", "\t", " ").Replace(text)
|
||||
}
|
||||
|
||||
func (gui *Gui) onMenuFilterContentChanged(content string) {
|
||||
menuContext := gui.State.Contexts.Menu
|
||||
menuContext.SetFilterStarted(true)
|
||||
gui.helpers.Search.ApplyFilter(menuContext, content)
|
||||
}
|
||||
|
||||
return true
|
||||
func (gui *Gui) pasteIntoSearch(text string) {
|
||||
v := gui.Views.Search
|
||||
if typePastedTextIntoSingleLineView(v, text) {
|
||||
gui.helpers.Search.OnPromptContentChanged(v.TextArea.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key) bool {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPastedTextAsSingleLine(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
text string
|
||||
expected string
|
||||
}{
|
||||
{"single line", "foo bar", "foo bar"},
|
||||
{"newlines at start and end", "\n\nfoo\n", "foo"},
|
||||
{"newlines inside", "foo\nbar\n\nbaz", "foo bar baz"},
|
||||
{"tabs", "\tfoo\tbar", " foo bar"},
|
||||
{"only newlines", "\n\n", ""},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, pastedTextAsSingleLine(s.text))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Search.Editable = true
|
||||
gui.Views.Search.Frame = false
|
||||
gui.Views.Search.Editor = gocui.EditorFunc(gui.searchEditor)
|
||||
gui.Views.Search.OnPaste = func(text string) error {
|
||||
gui.pasteIntoSearch(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, view := range []*gocui.View{gui.Views.Main, gui.Views.Secondary, gui.Views.MergeConflicts} {
|
||||
view.Wrap = true
|
||||
@@ -119,10 +123,18 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.CommitMessage.Visible = false
|
||||
gui.Views.CommitMessage.Editable = true
|
||||
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
gui.Views.CommitMessage.OnPaste = func(text string) error {
|
||||
gui.helpers.Commits.HandlePasteIntoSummary(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.Views.CommitDescription.Visible = false
|
||||
gui.Views.CommitDescription.Editable = true
|
||||
gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor)
|
||||
gui.Views.CommitDescription.OnPaste = func(text string) error {
|
||||
gui.helpers.Commits.HandlePasteIntoDescription(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.Views.Confirmation.Visible = false
|
||||
gui.Views.Confirmation.Wrap = true
|
||||
@@ -132,6 +144,10 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Prompt.Wrap = false // We don't want wrapping in one-line prompts
|
||||
gui.Views.Prompt.Editable = true
|
||||
gui.Views.Prompt.Editor = gocui.EditorFunc(gui.promptEditor)
|
||||
gui.Views.Prompt.OnPaste = func(text string) error {
|
||||
gui.pasteIntoPrompt(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.Views.Suggestions.Visible = false
|
||||
|
||||
@@ -142,6 +158,10 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.MenuFilter.Frame = false
|
||||
gui.Views.MenuFilter.Editable = true
|
||||
gui.Views.MenuFilter.Editor = gocui.EditorFunc(gui.menuFilterEditor)
|
||||
gui.Views.MenuFilter.OnPaste = func(text string) error {
|
||||
gui.pasteIntoMenuFilter(text)
|
||||
return nil
|
||||
}
|
||||
// The filter row belongs to the menu: it shares the menu's focus, and keys
|
||||
// that the input field doesn't take are the menu's to handle.
|
||||
gui.Views.MenuFilterFrame.ParentView = gui.Views.Menu
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -66,6 +66,15 @@ func (self *MenuDriver) Filter(text string) *MenuDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
// pastes the text the way a terminal does it. Only for menus that filter as you
|
||||
// type.
|
||||
func (self *MenuDriver) Paste(text string) *MenuDriver {
|
||||
self.getViewDriver().IsFocused()
|
||||
self.t.paste(text)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuDriver) LineCount(matcher *IntMatcher) *MenuDriver {
|
||||
self.getViewDriver().LineCount(matcher)
|
||||
|
||||
|
||||
@@ -67,10 +67,10 @@ func (self *Popup) CommitMessagePanel() *CommitMessagePanelDriver {
|
||||
return &CommitMessagePanelDriver{t: self.t}
|
||||
}
|
||||
|
||||
func (self *Popup) CommitDescriptionPanel() *CommitMessagePanelDriver {
|
||||
func (self *Popup) CommitDescriptionPanel() *CommitDescriptionPanelDriver {
|
||||
self.inCommitDescriptionPanel()
|
||||
|
||||
return &CommitMessagePanelDriver{t: self.t}
|
||||
return &CommitDescriptionPanelDriver{t: self.t}
|
||||
}
|
||||
|
||||
func (self *Popup) inCommitMessagePanel() {
|
||||
|
||||
@@ -31,6 +31,13 @@ func (self *PromptDriver) Type(value string) *PromptDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
// pastes the text the way a terminal does it
|
||||
func (self *PromptDriver) Paste(text string) *PromptDriver {
|
||||
self.t.paste(text)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *PromptDriver) Clear() *PromptDriver {
|
||||
self.t.press(ClearKey)
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ func (self *SearchDriver) Type(value string) *SearchDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
// pastes the text the way a terminal does it
|
||||
func (self *SearchDriver) Paste(text string) *SearchDriver {
|
||||
self.t.paste(text)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *SearchDriver) Clear() *SearchDriver {
|
||||
self.t.press(ClearKey)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteCommitMessageWithHardLineBreaks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste a commit message whose body was wrapped at the auto-wrap width, and check that the body reflows when editing it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > ../clipboard"
|
||||
config.GetUserConfig().OS.ReadFromClipboardCmd = "cat ../clipboard"
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("subject\n\n" +
|
||||
"The body of this commit was wrapped at the auto-wrap width when it was\n" +
|
||||
"committed, so it contains a hard line break.")
|
||||
shell.CreateFileAndAdd("file", "file content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Press(keys.Commits.CopyCommitAttributeToClipboard)
|
||||
|
||||
t.ExpectPopup().Menu().Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Commit message (subject and body)")).Confirm()
|
||||
|
||||
t.ExpectToast(Equals("Commit message copied to clipboard"))
|
||||
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
OpenCommitMenu()
|
||||
|
||||
t.ExpectPopup().Menu().Title(Equals("Commit Menu")).
|
||||
Select(Contains("Paste commit message from clipboard")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Content(Equals("subject")).
|
||||
SwitchToDescription().
|
||||
GoToBeginning().
|
||||
Type("Now ").
|
||||
Content(Equals(
|
||||
"Now The body of this commit was wrapped at the auto-wrap width when it \n" +
|
||||
"was committed, so it contains a hard line break."))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteIntoCommitDescription = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste text into the commit description with the terminal's paste command",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file", "file content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Type("subject").
|
||||
SwitchToDescription().
|
||||
Type("last line").
|
||||
GoToBeginning().
|
||||
Paste("first line\n\tindented line\n").
|
||||
Content(Equals("first line\n indented line\nlast line"))
|
||||
|
||||
t.ExpectPopup().CommitDescriptionPanel()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteMultiLineCommitMessage = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste a multi-line commit message into the summary with the terminal's paste command",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file", "file content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Paste("subject\n\nbody line 1\nbody line 2").
|
||||
Content(Equals("subject"))
|
||||
|
||||
t.Views().CommitDescription().
|
||||
IsFocused().
|
||||
Content(Equals(
|
||||
"body line 1\nbody line 2"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteMultiLineCommitMessageWithHardLineBreaks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste a commit message whose body was wrapped at the auto-wrap width into the summary with the terminal's paste command, and check that the body reflows when editing it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file", "file content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Paste("subject\n\n" +
|
||||
"The body of this commit was wrapped at the auto-wrap width when it was\n" +
|
||||
"committed, so it contains a hard line break.").
|
||||
Content(Equals("subject"))
|
||||
|
||||
t.ExpectPopup().CommitDescriptionPanel().
|
||||
GoToBeginning().
|
||||
Type("Now ").
|
||||
Content(Equals(
|
||||
"Now The body of this commit was wrapped at the auto-wrap width when it \n" +
|
||||
"was committed, so it contains a hard line break."))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteIntoKeybindingsMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Pasting text that ends with a newline into the keybindings menu filters the menu without running the selected item",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFile("myfile", "myfile")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains(`??`).Contains(`myfile`).IsSelected(),
|
||||
).
|
||||
Press(keys.Universal.OptionMenu)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Keybindings")).
|
||||
Paste("stage all\n")
|
||||
|
||||
t.Views().Files().
|
||||
Lines(
|
||||
Contains(`??`).Contains(`myfile`),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteIntoMenuFilter = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste text into the filter of a menu that filters as you type",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFile("myfile", "myfile")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.OptionMenu)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Keybindings")).
|
||||
Paste("\nIgnore\n").
|
||||
Lines(
|
||||
Contains(`─── Local`),
|
||||
Contains(`Ignore`).IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().MenuFilter().
|
||||
Content(Equals("Ignore"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteIntoSearchPrompt = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste multi-line text into the search prompt",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("one")
|
||||
shell.EmptyCommit("two")
|
||||
shell.EmptyCommit("three")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("three").IsSelected(),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
Press(keys.Universal.StartSearch)
|
||||
|
||||
t.ExpectSearch().
|
||||
Paste("\ntwo\n")
|
||||
|
||||
t.Views().Search().
|
||||
IsFocused().
|
||||
Content(Equals("two"))
|
||||
|
||||
t.ExpectSearch().
|
||||
Confirm()
|
||||
|
||||
t.Views().Search().
|
||||
IsVisible().
|
||||
Content(Contains("matches for 'two' (1 of 1)"))
|
||||
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("three"),
|
||||
Contains("two").IsSelected(),
|
||||
Contains("one"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
package misc
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PasteIntoPrompt = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Paste multi-line text into a prompt",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
Press(keys.Universal.New)
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Contains("New branch name")).
|
||||
Paste("\nmy\tnew\nbranch\n")
|
||||
|
||||
t.Views().Prompt().
|
||||
IsFocused().
|
||||
Content(Equals("my new branch"))
|
||||
},
|
||||
})
|
||||
@@ -185,6 +185,10 @@ var tests = []*components.IntegrationTest{
|
||||
commit.NewBranch,
|
||||
commit.PasteCommitMessage,
|
||||
commit.PasteCommitMessageOverExisting,
|
||||
commit.PasteCommitMessageWithHardLineBreaks,
|
||||
commit.PasteIntoCommitDescription,
|
||||
commit.PasteMultiLineCommitMessage,
|
||||
commit.PasteMultiLineCommitMessageWithHardLineBreaks,
|
||||
commit.PreserveCommitMessage,
|
||||
commit.PreserveCommitMessageWhitespace,
|
||||
commit.ResetAuthor,
|
||||
@@ -333,6 +337,9 @@ var tests = []*components.IntegrationTest{
|
||||
filter_and_search.NestedFilter,
|
||||
filter_and_search.NestedFilterTransient,
|
||||
filter_and_search.NewSearch,
|
||||
filter_and_search.PasteIntoKeybindingsMenu,
|
||||
filter_and_search.PasteIntoMenuFilter,
|
||||
filter_and_search.PasteIntoSearchPrompt,
|
||||
filter_and_search.RerenderTheSearchedMainView,
|
||||
filter_and_search.SearchALongDiff,
|
||||
filter_and_search.SearchStatusAfterARerender,
|
||||
@@ -574,6 +581,7 @@ var tests = []*components.IntegrationTest{
|
||||
misc.DirenvUnloadsOnBlockedEnvrc,
|
||||
misc.FilterRecentRepos,
|
||||
misc.InitialOpen,
|
||||
misc.PasteIntoPrompt,
|
||||
misc.RecentReposBranchColumn,
|
||||
misc.RecentReposColumnWidths,
|
||||
misc.RecentReposOnLaunch,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user