1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/gui/controllers/helpers/confirmation_helper.go
Sean 9d68b287db Split commit message panel into commit summary and commit description panel
When we use the one panel for the entire commit message, its tricky to have a keybinding both for adding a newline and submitting.
By having two panels: one for the summary line and one for the description, we allow for 'enter' to submit the message when done from the summary panel,
and 'enter' to add a newline when done from the description panel. Alt-enter, for those who can use that key combo, also works for submitting the message
from the description panel. For those who can't use that key combo, and don't want to remap the keybinding, they can hit tab to go back to the summary panel
and then 'enter' to submit the message.

We have some awkwardness in that both contexts (i.e. panels) need to appear and disappear in tandem and we don't have a great way of handling that concept,
so we just push both contexts one after the other, and likewise remove both contexts when we escape.
2023-04-30 13:19:53 +10:00

350 lines
11 KiB
Go

package helpers
import (
goContext "context"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/mattn/go-runewidth"
)
type ConfirmationHelper struct {
c *HelperCommon
}
func NewConfirmationHelper(c *HelperCommon) *ConfirmationHelper {
return &ConfirmationHelper{
c: c,
}
}
// This file is for the rendering of confirmation panels along with setting and handling associated
// keybindings.
func (self *ConfirmationHelper) wrappedConfirmationFunction(cancel goContext.CancelFunc, function func() error) func() error {
return func() error {
cancel()
if err := self.c.PopContext(); err != nil {
return err
}
if function != nil {
if err := function(); err != nil {
return self.c.Error(err)
}
}
return nil
}
}
func (self *ConfirmationHelper) wrappedPromptConfirmationFunction(cancel goContext.CancelFunc, function func(string) error, getResponse func() string) func() error {
return self.wrappedConfirmationFunction(cancel, func() error {
return function(getResponse())
})
}
func (self *ConfirmationHelper) DeactivateConfirmationPrompt() {
self.c.Mutexes().PopupMutex.Lock()
self.c.State().GetRepoState().SetCurrentPopupOpts(nil)
self.c.Mutexes().PopupMutex.Unlock()
self.c.Views().Confirmation.Visible = false
self.c.Views().Suggestions.Visible = false
self.clearConfirmationViewKeyBindings()
}
func getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += runewidth.StringWidth(line)/width + 1
}
} else {
lineCount = len(lines)
}
return lineCount
}
func (self *ConfirmationHelper) getPopupPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
panelWidth := self.getPopupPanelWidth()
panelHeight := getMessageHeight(wrap, prompt, panelWidth)
return self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return self.getPopupPanelDimensionsAux(panelWidth, contentHeight)
}
func (self *ConfirmationHelper) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
width, height := self.c.GocuiGui().Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (self *ConfirmationHelper) getPopupPanelWidth() int {
width, _ := self.c.GocuiGui().Size()
// we want a minimum width up to a point, then we do it based on ratio.
panelWidth := 4 * width / 7
minWidth := 80
if panelWidth < minWidth {
if width-2 < minWidth {
panelWidth = width - 2
} else {
panelWidth = minWidth
}
}
return panelWidth
}
func (self *ConfirmationHelper) prepareConfirmationPanel(
ctx goContext.Context,
opts types.ConfirmOpts,
) error {
self.c.Views().Confirmation.HasLoader = opts.HasLoader
if opts.HasLoader {
self.c.GocuiGui().StartTicking(ctx)
}
self.c.Views().Confirmation.Title = opts.Title
// for now we do not support wrapping in our editor
self.c.Views().Confirmation.Wrap = !opts.Editable
self.c.Views().Confirmation.FgColor = theme.GocuiDefaultTextColor
self.c.Views().Confirmation.Mask = runeForMask(opts.Mask)
_ = self.c.Views().Confirmation.SetOrigin(0, 0)
suggestionsContext := self.c.Contexts().Suggestions
suggestionsContext.State.FindSuggestions = opts.FindSuggestionsFunc
if opts.FindSuggestionsFunc != nil {
suggestionsView := self.c.Views().Suggestions
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
}
self.ResizeConfirmationPanel()
return nil
}
func runeForMask(mask bool) rune {
if mask {
return '*'
}
return 0
}
func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts types.CreatePopupPanelOpts) error {
self.c.Mutexes().PopupMutex.Lock()
defer self.c.Mutexes().PopupMutex.Unlock()
ctx, cancel := goContext.WithCancel(ctx)
// we don't allow interruptions of non-loader popups in case we get stuck somehow
// e.g. a credentials popup never gets its required user input so a process hangs
// forever.
// The proper solution is to have a queue of popup options
currentPopupOpts := self.c.State().GetRepoState().GetCurrentPopupOpts()
if currentPopupOpts != nil && !currentPopupOpts.HasLoader {
self.c.Log.Error("ignoring create popup panel because a popup panel is already open")
cancel()
return nil
}
// remove any previous keybindings
self.clearConfirmationViewKeyBindings()
err := self.prepareConfirmationPanel(
ctx,
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
HasLoader: opts.HasLoader,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
Mask: opts.Mask,
})
if err != nil {
cancel()
return err
}
confirmationView := self.c.Views().Confirmation
confirmationView.Editable = opts.Editable
if opts.Editable {
textArea := confirmationView.TextArea
textArea.Clear()
textArea.TypeString(opts.Prompt)
self.ResizeConfirmationPanel()
confirmationView.RenderTextArea()
} else {
self.c.ResetViewOrigin(confirmationView)
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt))
}
if err := self.setKeyBindings(cancel, opts); err != nil {
cancel()
return err
}
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
return self.c.PushContext(self.c.Contexts().Confirmation)
}
func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) error {
var onConfirm func() error
if opts.HandleConfirmPrompt != nil {
onConfirm = self.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return self.c.Views().Confirmation.TextArea.GetContent() })
} else {
onConfirm = self.wrappedConfirmationFunction(cancel, opts.HandleConfirm)
}
onSuggestionConfirm := self.wrappedPromptConfirmationFunction(
cancel,
opts.HandleConfirmPrompt,
self.getSelectedSuggestionValue,
)
onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)
self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose
return nil
}
func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
noop := func() error { return nil }
self.c.Contexts().Confirmation.State.OnConfirm = noop
self.c.Contexts().Confirmation.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop
}
func (self *ConfirmationHelper) getSelectedSuggestionValue() string {
selectedSuggestion := self.c.Contexts().Suggestions.GetSelected()
if selectedSuggestion != nil {
return selectedSuggestion.Value
}
return ""
}
func (self *ConfirmationHelper) ResizeConfirmationPanel() {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (self *ConfirmationHelper) ResizeCurrentPopupPanel() error {
c := self.c.CurrentContext()
switch c {
case self.c.Contexts().Menu:
self.resizeMenu()
case self.c.Contexts().Confirmation, self.c.Contexts().Suggestions:
self.resizeConfirmationPanel()
case self.c.Contexts().CommitMessage, self.c.Contexts().CommitDescription:
self.ResizeCommitMessagePanels()
}
return nil
}
func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string) error {
x0, y0, x1, y1 := self.getPopupPanelDimensions(v.Wrap, content)
_, err := self.c.GocuiGui().SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
func (self *ConfirmationHelper) resizeMenu() {
itemCount := self.c.Contexts().Menu.GetList().Len()
offset := 3
panelWidth := self.getPopupPanelWidth()
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
menuBottom := y1 - offset
_, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0)
tooltipTop := menuBottom + 1
tooltipHeight := getMessageHeight(true, self.c.Contexts().Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
}
func (self *ConfirmationHelper) resizeConfirmationPanel() {
suggestionsViewHeight := 0
if self.c.Views().Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (self *ConfirmationHelper) ResizeCommitMessagePanels() {
panelWidth := self.getPopupPanelWidth()
content := self.c.Views().CommitDescription.TextArea.GetContent()
summaryViewHeight := 3
panelHeight := getMessageHeight(false, content, panelWidth)
minHeight := 7
if panelHeight < minHeight {
panelHeight = minHeight
}
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight)
_, _ = self.c.GocuiGui().SetView(self.c.Views().CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0)
_, _ = self.c.GocuiGui().SetView(self.c.Views().CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0)
}
func (self *ConfirmationHelper) IsPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu"
}
func (self *ConfirmationHelper) IsPopupPanelFocused() bool {
return self.IsPopupPanel(self.c.CurrentContext().GetViewName())
}