diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go deleted file mode 100644 index 8455e7d02..000000000 --- a/pkg/gui/commit_message_panel.go +++ /dev/null @@ -1,38 +0,0 @@ -package gui - -import ( - "strconv" - "strings" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func (gui *Gui) handleCommitMessageFocused() error { - message := utils.ResolvePlaceholderString( - gui.c.Tr.CommitMessageConfirm, - map[string]string{ - "keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return), - "keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm), - "keyBindNewLine": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.AppendNewline), - }, - ) - - gui.RenderCommitLength() - - gui.c.SetViewContent(gui.Views.Options, message) - return nil -} - -func (gui *Gui) RenderCommitLength() { - if !gui.c.UserConfig.Gui.CommitLength.Show { - return - } - - gui.Views.CommitMessage.Subtitle = getBufferLength(gui.Views.CommitMessage) -} - -func getBufferLength(view *gocui.View) string { - return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " " -} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go deleted file mode 100644 index ff13c6fbe..000000000 --- a/pkg/gui/confirmation_panel.go +++ /dev/null @@ -1,316 +0,0 @@ -package gui - -import ( - "context" - "fmt" - "strings" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/theme" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/mattn/go-runewidth" -) - -// This file is for the rendering of confirmation panels along with setting and handling associated -// keybindings. - -func (gui *Gui) wrappedConfirmationFunction(cancel context.CancelFunc, function func() error) func() error { - return func() error { - cancel() - - if err := gui.c.PopContext(); err != nil { - return err - } - - if function != nil { - if err := function(); err != nil { - return gui.c.Error(err) - } - } - - return nil - } -} - -func (gui *Gui) wrappedPromptConfirmationFunction(cancel context.CancelFunc, function func(string) error, getResponse func() string) func() error { - return func() error { - cancel() - - if err := gui.c.PopContext(); err != nil { - return err - } - - if function != nil { - if err := function(getResponse()); err != nil { - return gui.c.Error(err) - } - } - - return nil - } -} - -func (gui *Gui) deactivateConfirmationPrompt() { - gui.Mutexes.PopupMutex.Lock() - gui.State.CurrentPopupOpts = nil - gui.Mutexes.PopupMutex.Unlock() - - gui.Views.Confirmation.Visible = false - gui.Views.Suggestions.Visible = false - - gui.clearConfirmationViewKeyBindings() -} - -func (gui *Gui) 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 (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) { - panelWidth := gui.getConfirmationPanelWidth() - panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) - return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) -} - -func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) { - return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight) -} - -func (gui *Gui) getConfirmationPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) { - width, height := gui.g.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 (gui *Gui) getConfirmationPanelWidth() int { - width, _ := gui.g.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 (gui *Gui) prepareConfirmationPanel( - ctx context.Context, - opts types.ConfirmOpts, -) error { - gui.Views.Confirmation.HasLoader = opts.HasLoader - if opts.HasLoader { - gui.g.StartTicking(ctx) - } - gui.Views.Confirmation.Title = opts.Title - // for now we do not support wrapping in our editor - gui.Views.Confirmation.Wrap = !opts.Editable - gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor - gui.Views.Confirmation.Mask = runeForMask(opts.Mask) - _ = gui.Views.Confirmation.SetOrigin(0, 0) - - gui.findSuggestions = opts.FindSuggestionsFunc - if opts.FindSuggestionsFunc != nil { - suggestionsView := gui.Views.Suggestions - suggestionsView.Wrap = false - suggestionsView.FgColor = theme.GocuiDefaultTextColor - gui.setSuggestions(opts.FindSuggestionsFunc("")) - suggestionsView.Visible = true - suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel) - } - - gui.resizeConfirmationPanel() - return nil -} - -func runeForMask(mask bool) rune { - if mask { - return '*' - } - return 0 -} - -func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPanelOpts) error { - gui.Mutexes.PopupMutex.Lock() - defer gui.Mutexes.PopupMutex.Unlock() - - ctx, cancel := context.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 - if gui.State.CurrentPopupOpts != nil && !gui.State.CurrentPopupOpts.HasLoader { - gui.Log.Error("ignoring create popup panel because a popup panel is already open") - cancel() - return nil - } - - // remove any previous keybindings - gui.clearConfirmationViewKeyBindings() - - err := gui.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 := gui.Views.Confirmation - confirmationView.Editable = opts.Editable - confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor) - - if opts.Editable { - textArea := confirmationView.TextArea - textArea.Clear() - textArea.TypeString(opts.Prompt) - gui.resizeConfirmationPanel() - confirmationView.RenderTextArea() - } else { - gui.c.ResetViewOrigin(confirmationView) - gui.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt)) - } - - if err := gui.setKeyBindings(cancel, opts); err != nil { - cancel() - return err - } - - gui.State.CurrentPopupOpts = &opts - - return gui.c.PushContext(gui.State.Contexts.Confirmation) -} - -func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopupPanelOpts) error { - actions := utils.ResolvePlaceholderString( - gui.c.Tr.CloseConfirm, - map[string]string{ - "keyBindClose": "esc", - "keyBindConfirm": "enter", - }, - ) - - gui.c.SetViewContent(gui.Views.Options, actions) - var onConfirm func() error - if opts.HandleConfirmPrompt != nil { - onConfirm = gui.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) - } else { - onConfirm = gui.wrappedConfirmationFunction(cancel, opts.HandleConfirm) - } - - keybindingConfig := gui.c.UserConfig.Keybinding - onSuggestionConfirm := gui.wrappedPromptConfirmationFunction( - cancel, - opts.HandleConfirmPrompt, - gui.getSelectedSuggestionValue, - ) - - bindings := []*types.Binding{ - { - ViewName: "confirmation", - Key: keybindings.GetKey(keybindingConfig.Universal.Confirm), - Handler: onConfirm, - }, - { - ViewName: "confirmation", - Key: keybindings.GetKey(keybindingConfig.Universal.Return), - Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose), - }, - { - ViewName: "confirmation", - Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), - Handler: func() error { - if len(gui.State.Suggestions) > 0 { - return gui.c.ReplaceContext(gui.State.Contexts.Suggestions) - } - return nil - }, - }, - { - ViewName: "suggestions", - Key: keybindings.GetKey(keybindingConfig.Universal.Confirm), - Handler: onSuggestionConfirm, - }, - { - ViewName: "suggestions", - Key: keybindings.GetKey(keybindingConfig.Universal.Return), - Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose), - }, - { - ViewName: "suggestions", - Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), - Handler: func() error { return gui.c.ReplaceContext(gui.State.Contexts.Confirmation) }, - }, - } - - for _, binding := range bindings { - if err := gui.SetKeybinding(binding); err != nil { - return err - } - } - - return nil -} - -func (gui *Gui) clearConfirmationViewKeyBindings() { - keybindingConfig := gui.c.UserConfig.Keybinding - _ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone) - _ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone) - _ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone) - _ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone) -} - -func (gui *Gui) refreshSuggestions() { - gui.suggestionsAsyncHandler.Do(func() func() { - findSuggestionsFn := gui.findSuggestions - if findSuggestionsFn != nil { - suggestions := gui.findSuggestions(gui.c.GetPromptInput()) - return func() { gui.setSuggestions(suggestions) } - } else { - return func() {} - } - }) -} - -func (gui *Gui) handleAskFocused() error { - keybindingConfig := gui.c.UserConfig.Keybinding - - message := utils.ResolvePlaceholderString( - gui.c.Tr.CloseConfirm, - map[string]string{ - "keyBindClose": keybindings.Label(keybindingConfig.Universal.Return), - "keyBindConfirm": keybindings.Label(keybindingConfig.Universal.Confirm), - }, - ) - - gui.c.SetViewContent(gui.Views.Options, message) - return nil -} diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go new file mode 100644 index 000000000..42e0de5f1 --- /dev/null +++ b/pkg/gui/context/commit_message_context.go @@ -0,0 +1,47 @@ +package context + +import ( + "strconv" + "strings" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CommitMessageContext struct { + *SimpleContext + c *types.HelperCommon +} + +var _ types.Context = (*CommitMessageContext)(nil) + +func NewCommitMessageContext( + c *types.HelperCommon, +) *CommitMessageContext { + return &CommitMessageContext{ + c: c, + SimpleContext: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.PERSISTENT_POPUP, + View: c.Views().CommitMessage, + WindowName: "commitMessage", + Key: COMMIT_MESSAGE_CONTEXT_KEY, + Focusable: true, + HasUncontrolledBounds: true, + }), + ContextCallbackOpts{}, + ), + } +} + +func (self *CommitMessageContext) RenderCommitLength() { + if !self.c.UserConfig.Gui.CommitLength.Show { + return + } + + self.c.Views().CommitMessage.Subtitle = getBufferLength(self.c.Views().CommitMessage) +} + +func getBufferLength(view *gocui.View) string { + return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " " +} diff --git a/pkg/gui/context/confirmation_context.go b/pkg/gui/context/confirmation_context.go new file mode 100644 index 000000000..0224fb0f0 --- /dev/null +++ b/pkg/gui/context/confirmation_context.go @@ -0,0 +1,35 @@ +package context + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ConfirmationContext struct { + *SimpleContext + c *types.HelperCommon + + State ConfirmationContextState +} + +type ConfirmationContextState struct { + OnConfirm func() error + OnClose func() error +} + +var _ types.Context = (*ConfirmationContext)(nil) + +func NewConfirmationContext( + c *types.HelperCommon, +) *ConfirmationContext { + return &ConfirmationContext{ + c: c, + SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ + View: c.Views().Confirmation, + WindowName: "confirmation", + Key: CONFIRMATION_CONTEXT_KEY, + Kind: types.TEMPORARY_POPUP, + Focusable: true, + HasUncontrolledBounds: true, + }), ContextCallbackOpts{}), + } +} diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go index 5a88b4a26..7f1439466 100644 --- a/pkg/gui/context/context.go +++ b/pkg/gui/context/context.go @@ -96,8 +96,8 @@ type ContextTree struct { CustomPatchBuilder *PatchExplorerContext CustomPatchBuilderSecondary types.Context MergeConflicts *MergeConflictsContext - Confirmation types.Context - CommitMessage types.Context + Confirmation *ConfirmationContext + CommitMessage *CommitMessageContext CommandLog types.Context // display contexts diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 8afd7df47..070ec3392 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -2,7 +2,6 @@ package context import ( "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -16,11 +15,7 @@ type MenuContext struct { var _ types.IListContext = (*MenuContext)(nil) func NewMenuContext( - view *gocui.View, - c *types.HelperCommon, - getOptionsMap func() map[string]string, - renderToDescriptionView func(string), ) *MenuContext { viewModel := NewMenuViewModel() @@ -28,11 +23,10 @@ func NewMenuContext( MenuViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Menu, WindowName: "menu", Key: "menu", Kind: types.TEMPORARY_POPUP, - OnGetOptionsMap: getOptionsMap, Focusable: true, HasUncontrolledBounds: true, }), ContextCallbackOpts{}), diff --git a/pkg/gui/context/merge_conflicts_context.go b/pkg/gui/context/merge_conflicts_context.go index 1e765ffe7..4066805bb 100644 --- a/pkg/gui/context/merge_conflicts_context.go +++ b/pkg/gui/context/merge_conflicts_context.go @@ -30,7 +30,6 @@ func NewMergeConflictsContext( opts ContextCallbackOpts, c *types.HelperCommon, - getOptionsMap func() map[string]string, ) *MergeConflictsContext { viewModel := &ConflictsViewModel{ state: mergeconflicts.NewState(), @@ -46,7 +45,6 @@ func NewMergeConflictsContext( View: view, WindowName: "main", Key: MERGE_CONFLICTS_CONTEXT_KEY, - OnGetOptionsMap: getOptionsMap, Focusable: true, HighlightOnFocus: true, }), diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go index 5a3375f33..272784a9c 100644 --- a/pkg/gui/context/patch_explorer_context.go +++ b/pkg/gui/context/patch_explorer_context.go @@ -24,8 +24,6 @@ func NewPatchExplorerContext( windowName string, key types.ContextKey, - onFocus func(types.OnFocusOpts) error, - onFocusLost func(opts types.OnFocusLostOpts) error, getIncludedLineIndices func() []int, c *types.HelperCommon, @@ -43,10 +41,7 @@ func NewPatchExplorerContext( Kind: types.MAIN_CONTEXT, Focusable: true, HighlightOnFocus: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - }), + }), ContextCallbackOpts{}), } } diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 346492e0c..8a28ce514 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -1,31 +1,53 @@ package context import ( - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/tasks" ) type SuggestionsContext struct { *BasicViewModel[*types.Suggestion] *ListContextTrait + + State *SuggestionsContextState +} + +type SuggestionsContextState struct { + Suggestions []*types.Suggestion + OnConfirm func() error + OnClose func() error + AsyncHandler *tasks.AsyncHandler + + // FindSuggestions will take a string that the user has typed into a prompt + // and return a slice of suggestions which match that string. + FindSuggestions func(string) []*types.Suggestion } var _ types.IListContext = (*SuggestionsContext)(nil) func NewSuggestionsContext( - getModel func() []*types.Suggestion, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, - c *types.HelperCommon, ) *SuggestionsContext { + state := &SuggestionsContextState{ + AsyncHandler: tasks.NewAsyncHandler(), + } + getModel := func() []*types.Suggestion { + return state.Suggestions + } + + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetSuggestionListDisplayStrings(state.Suggestions) + } + viewModel := NewBasicViewModel(getModel) return &SuggestionsContext{ + State: state, BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Suggestions, WindowName: "suggestions", Key: SUGGESTIONS_CONTEXT_KEY, Kind: types.PERSISTENT_POPUP, @@ -47,3 +69,22 @@ func (self *SuggestionsContext) GetSelectedItemId() string { return item.Value } + +func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) { + self.State.Suggestions = suggestions + self.SetSelectedLineIdx(0) + self.c.ResetViewOrigin(self.GetView()) + _ = self.HandleRender() +} + +func (self *SuggestionsContext) RefreshSuggestions() { + self.State.AsyncHandler.Do(func() func() { + findSuggestionsFn := self.State.FindSuggestions + if findSuggestionsFn != nil { + suggestions := findSuggestionsFn(self.c.GetPromptInput()) + return func() { self.SetSuggestions(suggestions) } + } else { + return func() {} + } + }) +} diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go index 87887a9e8..994fc8a32 100644 --- a/pkg/gui/context_config.go +++ b/pkg/gui/context_config.go @@ -76,23 +76,6 @@ func (gui *Gui) contextTree() *context.ContextTree { gui.Views.Staging, "main", context.STAGING_MAIN_CONTEXT_KEY, - func(opts types.OnFocusOpts) error { - gui.Views.Staging.Wrap = false - gui.Views.StagingSecondary.Wrap = false - - return gui.helpers.Staging.RefreshStagingPanel(opts) - }, - func(opts types.OnFocusLostOpts) error { - gui.State.Contexts.Staging.SetState(nil) - - if opts.NewContextKey != context.STAGING_SECONDARY_CONTEXT_KEY { - gui.Views.Staging.Wrap = true - gui.Views.StagingSecondary.Wrap = true - _ = gui.State.Contexts.Staging.Render(false) - _ = gui.State.Contexts.StagingSecondary.Render(false) - } - return nil - }, func() []int { return nil }, gui.c, ), @@ -100,23 +83,6 @@ func (gui *Gui) contextTree() *context.ContextTree { gui.Views.StagingSecondary, "secondary", context.STAGING_SECONDARY_CONTEXT_KEY, - func(opts types.OnFocusOpts) error { - gui.Views.Staging.Wrap = false - gui.Views.StagingSecondary.Wrap = false - - return gui.helpers.Staging.RefreshStagingPanel(opts) - }, - func(opts types.OnFocusLostOpts) error { - gui.State.Contexts.StagingSecondary.SetState(nil) - - if opts.NewContextKey != context.STAGING_MAIN_CONTEXT_KEY { - gui.Views.Staging.Wrap = true - gui.Views.StagingSecondary.Wrap = true - _ = gui.State.Contexts.Staging.Render(false) - _ = gui.State.Contexts.StagingSecondary.Render(false) - } - return nil - }, func() []int { return nil }, gui.c, ), @@ -124,21 +90,6 @@ func (gui *Gui) contextTree() *context.ContextTree { gui.Views.PatchBuilding, "main", context.PATCH_BUILDING_MAIN_CONTEXT_KEY, - func(opts types.OnFocusOpts) error { - // no need to change wrap on the secondary view because it can't be interacted with - gui.Views.PatchBuilding.Wrap = false - - return gui.helpers.PatchBuilding.RefreshPatchBuildingPanel(opts) - }, - func(opts types.OnFocusLostOpts) error { - gui.Views.PatchBuilding.Wrap = true - - if gui.git.Patch.PatchBuilder.IsEmpty() { - gui.git.Patch.PatchBuilder.Reset() - } - - return nil - }, func() []int { filename := gui.State.Contexts.CommitFiles.GetSelectedPath() includedLineIndices, err := gui.git.Patch.PatchBuilder.GetFileIncLineIndices(filename) @@ -165,37 +116,9 @@ func (gui *Gui) contextTree() *context.ContextTree { gui.Views.MergeConflicts, context.ContextCallbackOpts{}, gui.c, - func() map[string]string { - // wrapping in a function because contexts are initialized before helpers - return gui.helpers.MergeConflicts.GetMergingOptions() - }, - ), - Confirmation: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.TEMPORARY_POPUP, - View: gui.Views.Confirmation, - WindowName: "confirmation", - Key: context.CONFIRMATION_CONTEXT_KEY, - Focusable: true, - HasUncontrolledBounds: true, - }), - context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(gui.handleAskFocused), - }, - ), - CommitMessage: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.PERSISTENT_POPUP, - View: gui.Views.CommitMessage, - WindowName: "commitMessage", - Key: context.COMMIT_MESSAGE_CONTEXT_KEY, - Focusable: true, - HasUncontrolledBounds: true, - }), - context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused), - }, ), + Confirmation: context.NewConfirmationContext(gui.c), + CommitMessage: context.NewCommitMessageContext(gui.c), Search: context.NewSimpleContext( context.NewBaseContext(context.NewBaseContextOpts{ Kind: types.PERSISTENT_POPUP, @@ -214,12 +137,7 @@ func (gui *Gui) contextTree() *context.ContextTree { Key: context.COMMAND_LOG_CONTEXT_KEY, Focusable: true, }), - context.ContextCallbackOpts{ - OnFocusLost: func(opts types.OnFocusLostOpts) error { - gui.Views.Extras.Autoscroll = true - return nil - }, - }, + context.ContextCallbackOpts{}, ), Options: context.NewDisplayContext(context.OPTIONS_CONTEXT_KEY, gui.Views.Options, "options"), AppStatus: context.NewDisplayContext(context.APP_STATUS_CONTEXT_KEY, gui.Views.AppStatus, "appStatus"), diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 278e85602..21059c1a0 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -24,7 +24,7 @@ func (gui *Gui) resetControllers() { ) rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper) - suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions) + suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.State.Contexts) setCommitMessage := gui.getSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage }) getSavedCommitMessage := func() string { return gui.State.savedCommitMessage @@ -66,6 +66,7 @@ func (gui *Gui) resetControllers() { Window: helpers.NewWindowHelper(helperCommon, viewHelper, gui.State.Contexts), View: viewHelper, Refresh: refreshHelper, + Confirmation: helpers.NewConfirmationHelper(helperCommon, gui.State.Contexts), } gui.CustomCommandsClient = custom_commands.NewClient( @@ -151,6 +152,9 @@ func (gui *Gui) resetControllers() { reflogCommitsController := controllers.NewReflogCommitsController(common, gui.State.Contexts.ReflogCommits) subCommitsController := controllers.NewSubCommitsController(common, gui.State.Contexts.SubCommits) statusController := controllers.NewStatusController(common) + commandLogController := controllers.NewCommandLogController(common) + confirmationController := controllers.NewConfirmationController(common) + suggestionsController := controllers.NewSuggestionsController(common) setSubCommits := func(commits []*models.Commit) { gui.Mutexes.SubCommitsMutex.Lock() @@ -279,6 +283,18 @@ func (gui *Gui) resetControllers() { statusController, ) + controllers.AttachControllers(gui.State.Contexts.CommandLog, + commandLogController, + ) + + controllers.AttachControllers(gui.State.Contexts.Confirmation, + confirmationController, + ) + + controllers.AttachControllers(gui.State.Contexts.Suggestions, + suggestionsController, + ) + controllers.AttachControllers(gui.State.Contexts.Global, syncController, undoController, @@ -303,7 +319,7 @@ func (gui *Gui) getSetTextareaTextFn(getView func() *gocui.View) func(string) { view := getView() view.ClearTextArea() view.TextArea.TypeString(text) - _ = gui.resizePopupPanel(view, view.TextArea.GetContent()) + _ = gui.helpers.Confirmation.ResizePopupPanel(view, view.TextArea.GetContent()) view.RenderTextArea() } } diff --git a/pkg/gui/controllers/command_log_controller.go b/pkg/gui/controllers/command_log_controller.go new file mode 100644 index 000000000..45520edd9 --- /dev/null +++ b/pkg/gui/controllers/command_log_controller.go @@ -0,0 +1,42 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CommandLogController struct { + baseController + *controllerCommon +} + +var _ types.IController = &CommandLogController{} + +func NewCommandLogController( + common *controllerCommon, +) *CommandLogController { + return &CommandLogController{ + baseController: baseController{}, + controllerCommon: common, + } +} + +func (self *CommandLogController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{} + + return bindings +} + +func (self *CommandLogController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.c.Views().Extras.Autoscroll = true + return nil + } +} + +func (self *CommandLogController) Context() types.Context { + return self.context() +} + +func (self *CommandLogController) context() types.Context { + return self.contexts.CommandLog +} diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index e5cdb866d..5db89b0e2 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -31,28 +32,39 @@ func NewCommitMessageController( } } +// TODO: merge that commit panel PR because we're not currently showing how to add a newline as it's +// handled by the editor func rather than by the controller here. func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.SubmitEditorText), - Handler: self.confirm, + Key: opts.GetKey(opts.Config.Universal.SubmitEditorText), + Handler: self.confirm, + Description: self.c.Tr.LcConfirm, }, { - Key: opts.GetKey(opts.Config.Universal.Return), - Handler: self.close, + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: self.close, + Description: self.c.Tr.LcClose, }, } return bindings } +func (self *CommitMessageController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.context().RenderCommitLength() + return nil + } +} + func (self *CommitMessageController) Context() types.Context { return self.context() } // this method is pointless in this context but I'm keeping it consistent // with other contexts so that when generics arrive it's easier to refactor -func (self *CommitMessageController) context() types.Context { +func (self *CommitMessageController) context() *context.CommitMessageContext { return self.contexts.CommitMessage } diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go index c6026d83f..8b0d159da 100644 --- a/pkg/gui/controllers/confirmation_controller.go +++ b/pkg/gui/controllers/confirmation_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -21,14 +22,36 @@ func NewConfirmationController( } func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { - bindings := []*types.Binding{} + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: func() error { return self.context().State.OnConfirm() }, + Description: self.c.Tr.LcConfirm, + Display: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: func() error { return self.context().State.OnClose() }, + Description: self.c.Tr.LcCloseCancel, + Display: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.TogglePanel), + Handler: func() error { + if len(self.contexts.Suggestions.State.Suggestions) > 0 { + return self.c.ReplaceContext(self.contexts.Suggestions) + } + return nil + }, + }, + } return bindings } func (self *ConfirmationController) GetOnFocusLost() func(types.OnFocusLostOpts) error { return func(types.OnFocusLostOpts) error { - deactivateConfirmationPrompt(self.controllerCommon) + self.helpers.Confirmation.DeactivateConfirmationPrompt() return nil } } @@ -37,17 +60,6 @@ func (self *ConfirmationController) Context() types.Context { return self.context() } -func (self *ConfirmationController) context() types.Context { +func (self *ConfirmationController) context() *context.ConfirmationContext { return self.contexts.Confirmation } - -func deactivateConfirmationPrompt(c *controllerCommon) { - c.mutexes.PopupMutex.Lock() - c.c.State().GetRepoState().SetCurrentPopupOpts(nil) - c.mutexes.PopupMutex.Unlock() - - c.c.Views().Confirmation.Visible = false - c.c.Views().Suggestions.Visible = false - - gui.clearConfirmationViewKeyBindings() -} diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go new file mode 100644 index 000000000..04211b7ac --- /dev/null +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -0,0 +1,321 @@ +package helpers + +import ( + goContext "context" + "fmt" + "strings" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/context" + + "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 *types.HelperCommon + contexts *context.ContextTree +} + +func NewConfirmationHelper( + c *types.HelperCommon, + contexts *context.ContextTree, +) *ConfirmationHelper { + return &ConfirmationHelper{ + c: c, + contexts: contexts, + } +} + +// 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) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) { + panelWidth := self.getConfirmationPanelWidth() + panelHeight := getMessageHeight(wrap, prompt, panelWidth) + return self.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) +} + +func (self *ConfirmationHelper) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) { + return self.getConfirmationPanelDimensionsAux(panelWidth, contentHeight) +} + +func (self *ConfirmationHelper) getConfirmationPanelDimensionsAux(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) getConfirmationPanelWidth() 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.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.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.contexts.Confirmation.State.OnConfirm = onConfirm + self.contexts.Confirmation.State.OnClose = onClose + self.contexts.Suggestions.State.OnConfirm = onSuggestionConfirm + self.contexts.Suggestions.State.OnClose = onClose + + return nil +} + +func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() { + noop := func() error { return nil } + self.contexts.Confirmation.State.OnConfirm = noop + self.contexts.Confirmation.State.OnClose = noop + self.contexts.Suggestions.State.OnConfirm = noop + self.contexts.Suggestions.State.OnClose = noop +} + +func (self *ConfirmationHelper) getSelectedSuggestionValue() string { + selectedSuggestion := self.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.getConfirmationPanelWidth() + 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.getConfirmationPanelDimensionsAux(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 { + v := self.c.GocuiGui().CurrentView() + if v == nil { + return nil + } + + if v == self.c.Views().Menu { + self.resizeMenu() + } else if v == self.c.Views().Confirmation || v == self.c.Views().Suggestions { + self.ResizeConfirmationPanel() + } else if self.IsPopupPanel(v.Name()) { + return self.ResizePopupPanel(v, v.Buffer()) + } + + return nil +} + +func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string) error { + x0, y0, x1, y1 := self.getConfirmationPanelDimensions(v.Wrap, content) + _, err := self.c.GocuiGui().SetView(v.Name(), x0, y0, x1, y1, 0) + return err +} + +func (self *ConfirmationHelper) resizeMenu() { + itemCount := self.contexts.Menu.GetList().Len() + offset := 3 + panelWidth := self.getConfirmationPanelWidth() + x0, y0, x1, y1 := self.getConfirmationPanelDimensionsForContentHeight(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.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) IsPopupPanel(viewName string) bool { + return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu" +} + +func (self *ConfirmationHelper) IsPopupPanelFocused() bool { + return self.IsPopupPanel(self.c.CurrentContext().GetViewName()) +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 7d9ce6a30..8c676f4de 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -25,6 +25,7 @@ type Helpers struct { Window *WindowHelper View *ViewHelper Refresh *RefreshHelper + Confirmation *ConfirmationHelper } func NewStubHelpers() *Helpers { @@ -52,5 +53,6 @@ func NewStubHelpers() *Helpers { Window: &WindowHelper{}, View: &ViewHelper{}, Refresh: &RefreshHelper{}, + Confirmation: &ConfirmationHelper{}, } } diff --git a/pkg/gui/controllers/helpers/merge_conflicts_helper.go b/pkg/gui/controllers/helpers/merge_conflicts_helper.go index d83626591..b099a69cd 100644 --- a/pkg/gui/controllers/helpers/merge_conflicts_helper.go +++ b/pkg/gui/controllers/helpers/merge_conflicts_helper.go @@ -1,11 +1,8 @@ package helpers import ( - "fmt" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -27,18 +24,6 @@ func NewMergeConflictsHelper( } } -func (self *MergeConflictsHelper) GetMergingOptions() map[string]string { - keybindingConfig := self.c.UserConfig.Keybinding - - return map[string]string{ - fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): self.c.Tr.LcSelectHunk, - fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock)): self.c.Tr.LcNavigateConflicts, - keybindings.Label(keybindingConfig.Universal.Select): self.c.Tr.LcPickHunk, - keybindings.Label(keybindingConfig.Main.PickBothHunks): self.c.Tr.LcPickAllHunks, - keybindings.Label(keybindingConfig.Universal.Undo): self.c.Tr.LcUndo, - } -} - func (self *MergeConflictsHelper) SetMergeState(path string) (bool, error) { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index 0cc4a642b..53a076f66 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -35,8 +36,8 @@ type ISuggestionsHelper interface { type SuggestionsHelper struct { c *types.HelperCommon - model *types.Model - refreshSuggestionsFn func() + model *types.Model + contexts *context.ContextTree } var _ ISuggestionsHelper = &SuggestionsHelper{} @@ -44,12 +45,12 @@ var _ ISuggestionsHelper = &SuggestionsHelper{} func NewSuggestionsHelper( c *types.HelperCommon, model *types.Model, - refreshSuggestionsFn func(), + contexts *context.ContextTree, ) *SuggestionsHelper { return &SuggestionsHelper{ - c: c, - model: model, - refreshSuggestionsFn: refreshSuggestionsFn, + c: c, + model: model, + contexts: contexts, } } @@ -127,7 +128,7 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type // cache the trie for future use self.model.FilesTrie = trie - self.refreshSuggestionsFn() + self.contexts.Suggestions.RefreshSuggestions() return err }) diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 6b2b6b736..b51d9ebc0 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -28,12 +28,16 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types. Handler: self.press, }, { - Key: opts.GetKey(opts.Config.Universal.Confirm), - Handler: self.press, + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: self.press, + Description: self.c.Tr.LcExecute, + Display: true, }, { - Key: opts.GetKey(opts.Config.Universal.Return), - Handler: self.close, + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: self.close, + Description: self.c.Tr.LcClose, + Display: true, }, } diff --git a/pkg/gui/controllers/merge_conflicts_controller.go b/pkg/gui/controllers/merge_conflicts_controller.go index f4a96c2fb..226ad8756 100644 --- a/pkg/gui/controllers/merge_conflicts_controller.go +++ b/pkg/gui/controllers/merge_conflicts_controller.go @@ -41,21 +41,25 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.PrevBlock), Handler: self.withRenderAndFocus(self.PrevConflict), Description: self.c.Tr.PrevConflict, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.NextBlock), Handler: self.withRenderAndFocus(self.NextConflict), Description: self.c.Tr.NextConflict, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.PrevItem), Handler: self.withRenderAndFocus(self.PrevConflictHunk), Description: self.c.Tr.SelectPrevHunk, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.NextItem), Handler: self.withRenderAndFocus(self.NextConflictHunk), Description: self.c.Tr.SelectNextHunk, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt), @@ -89,6 +93,7 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.Undo), Handler: self.withRenderAndFocus(self.HandleUndo), Description: self.c.Tr.LcUndo, + Display: true, }, { Key: opts.GetKey(opts.Config.Files.OpenMergeTool), @@ -99,11 +104,13 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.Select), Handler: self.withRenderAndFocus(self.HandlePickHunk), Description: self.c.Tr.PickHunk, + Display: true, }, { Key: opts.GetKey(opts.Config.Main.PickBothHunks), Handler: self.withRenderAndFocus(self.HandlePickAllHunks), Description: self.c.Tr.PickAllHunks, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.Return), diff --git a/pkg/gui/controllers/patch_building_controller.go b/pkg/gui/controllers/patch_building_controller.go index 329ae0ea1..8fb29e8b4 100644 --- a/pkg/gui/controllers/patch_building_controller.go +++ b/pkg/gui/controllers/patch_building_controller.go @@ -59,6 +59,27 @@ func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsO return []*gocui.ViewMouseBinding{} } +func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) error { + return func(opts types.OnFocusOpts) error { + // no need to change wrap on the secondary view because it can't be interacted with + self.c.Views().PatchBuilding.Wrap = false + + return self.helpers.PatchBuilding.RefreshPatchBuildingPanel(opts) + } +} + +func (self *PatchBuildingController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(opts types.OnFocusLostOpts) error { + self.c.Views().PatchBuilding.Wrap = true + + if self.git.Patch.PatchBuilder.IsEmpty() { + self.git.Patch.PatchBuilder.Reset() + } + + return nil + } +} + func (self *PatchBuildingController) OpenFile() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() diff --git a/pkg/gui/controllers/staging_controller.go b/pkg/gui/controllers/staging_controller.go index bcd6d5114..c6fb187ab 100644 --- a/pkg/gui/controllers/staging_controller.go +++ b/pkg/gui/controllers/staging_controller.go @@ -99,6 +99,29 @@ func (self *StagingController) GetMouseKeybindings(opts types.KeybindingsOpts) [ return []*gocui.ViewMouseBinding{} } +func (self *StagingController) GetOnFocus() func(types.OnFocusOpts) error { + return func(opts types.OnFocusOpts) error { + self.c.Views().Staging.Wrap = false + self.c.Views().StagingSecondary.Wrap = false + + return self.helpers.Staging.RefreshStagingPanel(opts) + } +} + +func (self *StagingController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(opts types.OnFocusLostOpts) error { + self.context.SetState(nil) + + if opts.NewContextKey != self.otherContext.GetKey() { + self.c.Views().Staging.Wrap = true + self.c.Views().StagingSecondary.Wrap = true + _ = self.contexts.Staging.Render(false) + _ = self.contexts.StagingSecondary.Render(false) + } + return nil + } +} + func (self *StagingController) OpenFile() error { self.context.GetMutex().Lock() defer self.context.GetMutex().Unlock() diff --git a/pkg/gui/controllers/sub_commits_controller.go b/pkg/gui/controllers/sub_commits_controller.go index d17042e47..c8c7a7d1d 100644 --- a/pkg/gui/controllers/sub_commits_controller.go +++ b/pkg/gui/controllers/sub_commits_controller.go @@ -53,8 +53,8 @@ func (self *SubCommitsController) GetOnRenderToMain() func() error { } } -func (self *SubCommitsController) GetOnFocus() func() error { - return func() error { +func (self *SubCommitsController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { context := self.context if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { context.SetLimitCommits(false) diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index aaa4c8647..ca8a488a7 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -22,14 +22,27 @@ func NewSuggestionsController( } func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { - bindings := []*types.Binding{} + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: func() error { return self.context().State.OnConfirm() }, + }, + { + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: func() error { return self.context().State.OnClose() }, + }, + { + Key: opts.GetKey(opts.Config.Universal.TogglePanel), + Handler: func() error { return self.c.ReplaceContext(self.contexts.Confirmation) }, + }, + } return bindings } func (self *SuggestionsController) GetOnFocusLost() func(types.OnFocusLostOpts) error { return func(types.OnFocusLostOpts) error { - deactivateConfirmationPrompt + self.helpers.Confirmation.DeactivateConfirmationPrompt() return nil } } diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index 9e00f8351..4325a56f4 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -71,12 +71,12 @@ func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod g // This function is called again on refresh as part of the general resize popup call, // but we need to call it here so that when we go to render the text area it's not // considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling. - err := gui.resizePopupPanel(v, v.TextArea.GetContent()) + err := gui.helpers.Confirmation.ResizePopupPanel(v, v.TextArea.GetContent()) if err != nil { gui.c.Log.Error(err) } v.RenderTextArea() - gui.RenderCommitLength() + gui.State.Contexts.CommitMessage.RenderCommitLength() return matched } @@ -86,11 +86,12 @@ func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.M v.RenderTextArea() - if gui.findSuggestions != nil { + suggestionsContext := gui.State.Contexts.Suggestions + if suggestionsContext.State.FindSuggestions != nil { input := v.TextArea.GetContent() - gui.suggestionsAsyncHandler.Do(func() func() { - suggestions := gui.findSuggestions(input) - return func() { gui.setSuggestions(suggestions) } + suggestionsContext.State.AsyncHandler.Do(func() func() { + suggestions := suggestionsContext.State.FindSuggestions(input) + return func() { suggestionsContext.SetSuggestions(suggestions) } }) } diff --git a/pkg/gui/filtering.go b/pkg/gui/filtering.go index 2746d0e2b..6d8279992 100644 --- a/pkg/gui/filtering.go +++ b/pkg/gui/filtering.go @@ -17,16 +17,6 @@ func (gui *Gui) validateNotInFilterMode() bool { return true } -func (gui *Gui) outsideFilterMode(f func() error) func() error { - return func() error { - if !gui.validateNotInFilterMode() { - return nil - } - - return f() - } -} - func (gui *Gui) exitFilterMode() error { return gui.clearFiltering() } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 4235f2eb0..4b6a0fb8a 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1,6 +1,7 @@ package gui import ( + goContext "context" "fmt" "io" "os" @@ -94,10 +95,6 @@ type Gui struct { Mutexes types.Mutexes - // findSuggestions will take a string that the user has typed into a prompt - // and return a slice of suggestions which match that string. - findSuggestions func(string) []*types.Suggestion - // when you enter into a submodule we'll append the superproject's path to this array // so that you can return to the superproject RepoPathStack *utils.StringStack @@ -113,8 +110,6 @@ type Gui struct { // the extras window contains things like the command log ShowExtrasWindow bool - suggestionsAsyncHandler *tasks.AsyncHandler - PopupHandler types.IPopupHandler IsNewRepo bool @@ -198,9 +193,6 @@ type GuiRepoState struct { Model *types.Model Modes *types.Modes - // Suggestions will sometimes appear when typing into a prompt - Suggestions []*types.Suggestion - SplitMainPanel bool LimitCommits bool @@ -412,18 +404,17 @@ func NewGui( initialDir string, ) (*Gui, error) { gui := &Gui{ - Common: cmn, - gitVersion: gitVersion, - Config: config, - Updater: updater, - statusManager: &statusManager{}, - viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, - viewPtmxMap: map[string]*os.File{}, - showRecentRepos: showRecentRepos, - RepoPathStack: &utils.StringStack{}, - RepoStateMap: map[Repo]*GuiRepoState{}, - CmdLog: []string{}, - suggestionsAsyncHandler: tasks.NewAsyncHandler(), + Common: cmn, + gitVersion: gitVersion, + Config: config, + Updater: updater, + statusManager: &statusManager{}, + viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, + viewPtmxMap: map[string]*os.File{}, + showRecentRepos: showRecentRepos, + RepoPathStack: &utils.StringStack{}, + RepoStateMap: map[Repo]*GuiRepoState{}, + CmdLog: []string{}, // originally we could only hide the command log permanently via the config // but now we do it via state. So we need to still support the config for the @@ -446,7 +437,9 @@ func NewGui( gui.PopupHandler = popup.NewPopupHandler( cmn, - gui.createPopupPanel, + func(ctx goContext.Context, opts types.CreatePopupPanelOpts) error { + return gui.helpers.Confirmation.CreatePopupPanel(ctx, opts) + }, func() error { return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) }, gui.popContext, gui.currentContext, diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index a90824f9e..e4b8b3981 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -144,3 +144,7 @@ func (self *guiCommon) MainViewPairs() types.MainViewPairs { func (self *guiCommon) State() types.IStateAccessor { return self.gui.stateAccessor } + +func (self *guiCommon) KeybindingsOpts() types.KeybindingsOpts { + return self.gui.keybindingOpts() +} diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 35b6a870d..c409ebdb9 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -50,7 +50,7 @@ func (self *GuiDriver) CurrentContext() types.Context { } func (self *GuiDriver) ContextForView(viewName string) types.Context { - context, ok := self.gui.contextForView(viewName) + context, ok := self.gui.helpers.View.ContextForView(viewName) if !ok { return nil } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 64ca5a190..55dcaa715 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -11,7 +11,17 @@ import ( func (gui *Gui) noPopupPanel(f func() error) func() error { return func() error { - if gui.popupPanelFocused() { + if gui.helpers.Confirmation.IsPopupPanelFocused() { + return nil + } + + return f() + } +} + +func (gui *Gui) outsideFilterMode(f func() error) func() error { + return func() error { + if !gui.validateNotInFilterMode() { return nil } @@ -34,8 +44,7 @@ func (self *Gui) GetCheatsheetKeybindings() []*types.Binding { return bindings } -// renaming receiver to 'self' to aid refactoring. Will probably end up moving all Gui handlers to this pattern eventually. -func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBinding) { +func (self *Gui) keybindingOpts() types.KeybindingsOpts { config := self.c.UserConfig.Keybinding guards := types.KeybindingGuards{ @@ -43,11 +52,18 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi NoPopupPanel: self.noPopupPanel, } - opts := types.KeybindingsOpts{ + return types.KeybindingsOpts{ GetKey: keybindings.GetKey, Config: config, Guards: guards, } +} + +// renaming receiver to 'self' to aid refactoring. Will probably end up moving all Gui handlers to this pattern eventually. +func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBinding) { + config := self.c.UserConfig.Keybinding + + opts := self.c.KeybindingsOpts() bindings := []*types.Binding{ { @@ -486,7 +502,7 @@ func (gui *Gui) SetKeybinding(binding *types.Binding) error { if gocui.IsMouseKey(binding.Key) { handler = func() error { // we ignore click events on views that aren't popup panels, when a popup panel is focused - if gui.popupPanelFocused() && gui.currentViewName() != binding.ViewName { + if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != binding.ViewName { return nil } @@ -502,7 +518,7 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error { baseHandler := binding.Handler newHandler := func(opts gocui.ViewMouseBindingOpts) error { // we ignore click events on views that aren't popup panels, when a popup panel is focused - if gui.popupPanelFocused() && gui.currentViewName() != binding.ViewName { + if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != binding.ViewName { return nil } diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 0b522ec88..96570069c 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -163,7 +163,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { // if you run `lazygit --logs` // this will let you see these branches as prettified json // gui.c.Log.Info(utils.AsJson(gui.State.Model.Branches[0:4])) - return gui.resizeCurrentPopupPanel() + return gui.helpers.Confirmation.ResizeCurrentPopupPanel() } func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { @@ -245,7 +245,7 @@ func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { return func(g *gocui.Gui) error { newView := gui.g.CurrentView() // for now we don't consider losing focus to a popup panel as actually losing focus - if newView != previousView && !gui.isPopupPanel(newView.Name()) { + if newView != previousView && !gui.helpers.Confirmation.IsPopupPanel(newView.Name()) { if err := gui.onViewFocusLost(previousView); err != nil { return err } diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index 25b5a1666..9ce638897 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -14,14 +14,7 @@ import ( ) func (gui *Gui) menuListContext() *context.MenuContext { - return context.NewMenuContext( - gui.Views.Menu, - gui.c, - gui.getMenuOptions, - func(content string) { - gui.Views.Tooltip.SetContent(content) - }, - ) + return context.NewMenuContext(gui.c) } func (gui *Gui) filesListContext() *context.WorkingTreeContext { @@ -237,14 +230,7 @@ func (gui *Gui) submodulesListContext() *context.SubmodulesContext { } func (gui *Gui) suggestionsListContext() *context.SuggestionsContext { - return context.NewSuggestionsContext( - func() []*types.Suggestion { return gui.State.Suggestions }, - gui.Views.Suggestions, - func(startIdx int, length int) [][]string { - return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions) - }, - gui.c, - ) + return context.NewSuggestionsContext(gui.c) } func (gui *Gui) getListContexts() []types.IListContext { diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index bc3d087c3..24d9ff681 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -1,25 +1,12 @@ package gui import ( - "fmt" - - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) getMenuOptions() map[string]string { - keybindingConfig := gui.c.UserConfig.Keybinding - - return map[string]string{ - keybindings.Label(keybindingConfig.Universal.Return): gui.c.Tr.LcClose, - fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcNavigate, - keybindings.Label(keybindingConfig.Universal.Select): gui.c.Tr.LcExecute, - } -} - // note: items option is mutated by this function func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { if !opts.HideCancel { diff --git a/pkg/gui/options_map.go b/pkg/gui/options_map.go index f47025017..617e80354 100644 --- a/pkg/gui/options_map.go +++ b/pkg/gui/options_map.go @@ -2,12 +2,11 @@ package gui import ( "fmt" - "sort" "strings" - "github.com/jesseduffield/generics/maps" "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type OptionsMapMgr struct { @@ -21,36 +20,73 @@ func (gui *Gui) renderContextOptionsMap(c types.Context) { // render the options available for the current context at the bottom of the screen func (self *OptionsMapMgr) renderContextOptionsMap(c types.Context) { - optionsMap := c.GetOptionsMap() - if optionsMap == nil { - optionsMap = self.globalOptionsMap() + bindingsToDisplay := lo.Filter(c.GetKeybindings(self.c.KeybindingsOpts()), func(binding *types.Binding, _ int) bool { + return binding.Display + }) + + var optionsMap []bindingInfo + if len(bindingsToDisplay) == 0 { + optionsMap = self.globalOptions() + } else { + optionsMap = lo.Map(bindingsToDisplay, func(binding *types.Binding, _ int) bindingInfo { + return bindingInfo{ + key: keybindings.LabelFromKey(binding.Key), + description: binding.Description, + } + }) } - self.renderOptions(self.optionsMapToString(optionsMap)) + self.renderOptions(self.formatBindingInfos(optionsMap)) } -func (self *OptionsMapMgr) optionsMapToString(optionsMap map[string]string) string { - options := maps.MapToSlice(optionsMap, func(key string, description string) string { - return key + ": " + description - }) - sort.Strings(options) - return strings.Join(options, ", ") +func (self *OptionsMapMgr) formatBindingInfos(bindingInfos []bindingInfo) string { + return strings.Join( + lo.Map(bindingInfos, func(bindingInfo bindingInfo, _ int) string { + return fmt.Sprintf("%s: %s", bindingInfo.key, bindingInfo.description) + }), + ", ") } func (self *OptionsMapMgr) renderOptions(options string) { self.c.SetViewContent(self.c.Views().Options, options) } -func (self *OptionsMapMgr) globalOptionsMap() map[string]string { +func (self *OptionsMapMgr) globalOptions() []bindingInfo { keybindingConfig := self.c.UserConfig.Keybinding - return map[string]string{ - fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollUpMain), keybindings.Label(keybindingConfig.Universal.ScrollDownMain)): self.c.Tr.LcScroll, - fmt.Sprintf("%s %s %s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock), keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): self.c.Tr.LcNavigate, - keybindings.Label(keybindingConfig.Universal.Return): self.c.Tr.LcCancel, - keybindings.Label(keybindingConfig.Universal.Quit): self.c.Tr.LcQuit, - keybindings.Label(keybindingConfig.Universal.OptionMenuAlt1): self.c.Tr.LcMenu, - fmt.Sprintf("%s-%s", keybindings.Label(keybindingConfig.Universal.JumpToBlock[0]), keybindings.Label(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): self.c.Tr.LcJump, - fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollLeft), keybindings.Label(keybindingConfig.Universal.ScrollRight)): self.c.Tr.LcScrollLeftRight, + return []bindingInfo{ + { + key: fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollUpMain), keybindings.Label(keybindingConfig.Universal.ScrollDownMain)), + description: self.c.Tr.LcScroll, + }, + { + key: fmt.Sprintf("%s %s %s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock), keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)), + description: self.c.Tr.LcNavigate, + }, + { + key: keybindings.Label(keybindingConfig.Universal.Return), + description: self.c.Tr.LcCancel, + }, + { + key: keybindings.Label(keybindingConfig.Universal.Quit), + description: self.c.Tr.LcQuit, + }, + { + key: keybindings.Label(keybindingConfig.Universal.OptionMenuAlt1), + description: self.c.Tr.LcMenu, + }, + { + key: fmt.Sprintf("%s-%s", keybindings.Label(keybindingConfig.Universal.JumpToBlock[0]), keybindings.Label(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])), + description: self.c.Tr.LcJump, + }, + { + key: fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollLeft), keybindings.Label(keybindingConfig.Universal.ScrollRight)), + description: self.c.Tr.LcScrollLeftRight, + }, } } + +type bindingInfo struct { + key string + description string +} diff --git a/pkg/gui/suggestions_panel.go b/pkg/gui/suggestions_panel.go deleted file mode 100644 index 50332079e..000000000 --- a/pkg/gui/suggestions_panel.go +++ /dev/null @@ -1,26 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) getSelectedSuggestionValue() string { - selectedSuggestion := gui.getSelectedSuggestion() - - if selectedSuggestion != nil { - return selectedSuggestion.Value - } - - return "" -} - -func (gui *Gui) getSelectedSuggestion() *types.Suggestion { - return gui.State.Contexts.Suggestions.GetSelected() -} - -func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) { - gui.State.Suggestions = suggestions - gui.State.Contexts.Suggestions.SetSelectedLineIdx(0) - gui.c.ResetViewOrigin(gui.Views.Suggestions) - _ = gui.State.Contexts.Suggestions.HandleRender() -} diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index a92efb801..c8be0692e 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -82,6 +82,8 @@ type IGuiCommon interface { Mutexes() Mutexes State() IStateAccessor + + KeybindingsOpts() KeybindingsOpts } type IPopupHandler interface { diff --git a/pkg/gui/types/keybindings.go b/pkg/gui/types/keybindings.go index c945ce3ab..95978e762 100644 --- a/pkg/gui/types/keybindings.go +++ b/pkg/gui/types/keybindings.go @@ -17,6 +17,12 @@ type Binding struct { Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet OpensMenu bool + // If true, the keybinding will appear at the bottom of the screen. If + // the given view has no bindings with Display: true, the default keybindings + // will be displayed instead. + // TODO: implement this + Display bool + // to be displayed if the keybinding is highlighted from within a menu Tooltip string } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 7e6e6510e..85ec21128 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -62,71 +62,6 @@ func (gui *Gui) currentViewName() string { return currentView.Name() } -func (gui *Gui) resizeCurrentPopupPanel() error { - v := gui.g.CurrentView() - if v == nil { - return nil - } - - if v == gui.Views.Menu { - gui.resizeMenu() - } else if v == gui.Views.Confirmation || v == gui.Views.Suggestions { - gui.resizeConfirmationPanel() - } else if gui.isPopupPanel(v.Name()) { - return gui.resizePopupPanel(v, v.Buffer()) - } - - return nil -} - -func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error { - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content) - _, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0) - return err -} - -func (gui *Gui) resizeMenu() { - itemCount := gui.State.Contexts.Menu.GetList().Len() - offset := 3 - panelWidth := gui.getConfirmationPanelWidth() - x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsForContentHeight(panelWidth, itemCount+offset) - menuBottom := y1 - offset - _, _ = gui.g.SetView(gui.Views.Menu.Name(), x0, y0, x1, menuBottom, 0) - - tooltipTop := menuBottom + 1 - tooltipHeight := gui.getMessageHeight(true, gui.State.Contexts.Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame - _, _ = gui.g.SetView(gui.Views.Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) -} - -func (gui *Gui) resizeConfirmationPanel() { - suggestionsViewHeight := 0 - if gui.Views.Suggestions.Visible { - suggestionsViewHeight = 11 - } - panelWidth := gui.getConfirmationPanelWidth() - prompt := gui.Views.Confirmation.Buffer() - wrap := true - if gui.Views.Confirmation.Editable { - prompt = gui.Views.Confirmation.TextArea.GetContent() - wrap = false - } - panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight - x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) - confirmationViewBottom := y1 - suggestionsViewHeight - _, _ = gui.g.SetView(gui.Views.Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0) - - suggestionsViewTop := confirmationViewBottom + 1 - _, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) -} - -func (gui *Gui) isPopupPanel(viewName string) bool { - return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu" -} - -func (gui *Gui) popupPanelFocused() bool { - return gui.isPopupPanel(gui.currentViewName()) -} - func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error { tabs := gui.viewTabMap()[windowName] if len(tabs) == 0 { diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 65d7d3f3a..2bcb6f905 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -156,6 +156,7 @@ func (gui *Gui) createAllViews() error { gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor) gui.Views.Confirmation.Visible = false + gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor) gui.Views.Suggestions.Visible = false diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index 2d300caf7..1e41ff961 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -94,9 +94,9 @@ func chineseTranslationSet() TranslationSet { LcNewBranch: "新分支", LcDeleteBranch: "删除分支", NoBranchesThisRepo: "此仓库中没有分支", - CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindNewLine}}:新行,{{.keyBindConfirm}}:确认", CommitWithoutMessageErr: "您必须编写提交消息才能进行提交", - CloseConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认", + LcCloseCancel: "关闭", + LcConfirm: "确认", LcClose: "关闭", LcQuit: "退出", LcSquashDown: "向下压缩", @@ -117,8 +117,6 @@ func chineseTranslationSet() TranslationSet { LcAmendToCommit: "用已暂存的更改来修补提交", LcRenameCommitEditor: "使用编辑器重命名提交", Error: "错误", - LcSelectHunk: "切换区块", - LcNavigateConflicts: "浏览冲突", LcPickHunk: "选中区块", LcPickAllHunks: "选中所有区块", LcUndo: "撤销", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index f52908cea..c3b42da38 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -60,9 +60,9 @@ func dutchTranslationSet() TranslationSet { LcNewBranch: "nieuwe branch", LcDeleteBranch: "verwijder branch", NoBranchesThisRepo: "Geen branches voor deze repo", - CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindNewLine}}: Nieuwe lijn, {{.keyBindConfirm}}: Bevestig", CommitWithoutMessageErr: "Je kan geen commit maken zonder commit bericht", - CloseConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig", + LcCloseCancel: "sluiten", + LcConfirm: "bevestig", LcClose: "sluiten", LcQuit: "quit", LcSquashDown: "squash beneden", @@ -83,8 +83,6 @@ func dutchTranslationSet() TranslationSet { LcRenameCommitEditor: "hernoem commit met editor", NoCommitsThisBranch: "Geen commits in deze branch", Error: "Foutmelding", - LcSelectHunk: "selecteer stuk", - LcNavigateConflicts: "navigeer conflicts", LcPickHunk: "kies stuk", LcPickAllHunks: "kies beide stukken", LcUndo: "ongedaan maken", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 4d384da71..34af886a8 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -80,10 +80,10 @@ type TranslationSet struct { LcNewBranch string LcDeleteBranch string NoBranchesThisRepo string - CommitMessageConfirm string CommitWithoutMessageErr string - CloseConfirm string LcClose string + LcCloseCancel string + LcConfirm string LcQuit string LcSquashDown string LcFixupCommit string @@ -107,8 +107,6 @@ type TranslationSet struct { NoCommitsThisBranch string UpdateRefHere string Error string - LcSelectHunk string - LcNavigateConflicts string LcPickHunk string LcPickAllHunks string LcUndo string @@ -750,10 +748,10 @@ func EnglishTranslationSet() TranslationSet { LcNewBranch: "new branch", LcDeleteBranch: "delete branch", NoBranchesThisRepo: "No branches for this repo", - CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: confirm", CommitWithoutMessageErr: "You cannot commit without a commit message", - CloseConfirm: "{{.keyBindClose}}: close/cancel, {{.keyBindConfirm}}: confirm", LcClose: "close", + LcCloseCancel: "close/cancel", + LcConfirm: "confirm", LcQuit: "quit", LcSquashDown: "squash down", LcFixupCommit: "fixup commit", @@ -777,8 +775,6 @@ func EnglishTranslationSet() TranslationSet { SureResetCommitAuthor: "The author field of this commit will be updated to match the configured user. This also renews the author timestamp. Continue?", LcRenameCommitEditor: "reword commit with editor", Error: "Error", - LcSelectHunk: "select hunk", - LcNavigateConflicts: "navigate conflicts", LcPickHunk: "pick hunk", LcPickAllHunks: "pick all hunks", LcUndo: "undo", diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go index 891d737b0..2fd2b850f 100644 --- a/pkg/i18n/japanese.go +++ b/pkg/i18n/japanese.go @@ -85,9 +85,9 @@ func japaneseTranslationSet() TranslationSet { LcNewBranch: "新しいブランチを作成", LcDeleteBranch: "ブランチを削除", NoBranchesThisRepo: "リポジトリにブランチが存在しません", - CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindNewLine}}: 改行, {{.keyBindConfirm}}: 確定", CommitWithoutMessageErr: "コミットメッセージを入力してください", - CloseConfirm: "{{.keyBindClose}}: 閉じる/キャンセル, {{.keyBindConfirm}}: 確認", + LcCloseCancel: "閉じる/キャンセル", + LcConfirm: "確認", LcClose: "閉じる", LcQuit: "終了", // LcSquashDown: "squash down", @@ -108,8 +108,6 @@ func japaneseTranslationSet() TranslationSet { LcAmendToCommit: "ステージされた変更でamendコミット", LcRenameCommitEditor: "エディタでコミットメッセージを編集", Error: "エラー", - LcSelectHunk: "hunkを選択", - LcNavigateConflicts: "コンフリクトを移動", // LcPickHunk: "pick hunk", // LcPickAllHunks: "pick all hunks", LcUndo: "アンドゥ", diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go index 88e7db8f6..3de86121a 100644 --- a/pkg/i18n/korean.go +++ b/pkg/i18n/korean.go @@ -84,9 +84,9 @@ func koreanTranslationSet() TranslationSet { LcNewBranch: "새 브랜치 생성", LcDeleteBranch: "브랜치 삭제", NoBranchesThisRepo: "저장소에 브랜치가 존재하지 않습니다.", - CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindNewLine}}: 개행, {{.keyBindConfirm}}: 확인", CommitWithoutMessageErr: "커밋 메시지를 입력하세요.", - CloseConfirm: "{{.keyBindClose}}: 닫기/취소, {{.keyBindConfirm}}: 확인", + LcCloseCancel: "닫기/취소", + LcConfirm: "확인", LcClose: "닫기", LcQuit: "종료", LcSquashDown: "squash down", @@ -109,8 +109,6 @@ func koreanTranslationSet() TranslationSet { SureResetCommitAuthor: "The author field of this commit will be updated to match the configured user. This also renews the author timestamp. Continue?", LcRenameCommitEditor: "에디터에서 커밋메시지 수정", Error: "오류", - LcSelectHunk: "hunk를 선택", - LcNavigateConflicts: "navigate conflicts", LcPickHunk: "pick hunk", LcPickAllHunks: "pick all hunks", LcUndo: "되돌리기", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 00ed9f38f..fb73dbf3d 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -55,9 +55,9 @@ func polishTranslationSet() TranslationSet { LcNewBranch: "nowa gałąź", LcDeleteBranch: "usuń gałąź", NoBranchesThisRepo: "Brak gałęzi dla tego repozytorium", - CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindNewLine}}: nowa linia, {{.keyBindConfirm}}: potwierdź", CommitWithoutMessageErr: "Nie możesz commitować bez komunikatu", - CloseConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", + LcCloseCancel: "zamknij", + LcConfirm: "potwierdź", LcClose: "zamknij", LcSquashDown: "ściśnij", LcFixupCommit: "napraw commit", @@ -68,8 +68,6 @@ func polishTranslationSet() TranslationSet { LcRewordCommit: "zmień nazwę commita", LcRenameCommitEditor: "zmień nazwę commita w edytorze", Error: "Błąd", - LcSelectHunk: "wybierz kawałek", - LcNavigateConflicts: "nawiguj konflikty", LcPickHunk: "wybierz kawałek", LcPickAllHunks: "wybierz oba kawałki", LcUndo: "cofnij",