Files
lazygit/pkg/gui/context.go
Stefan Haller 40f6767cfc Avoid deactivating and activating when pushing the current context again
When calling PushContext, do nothing if the context to be pushed is already on
top of the stack. Avoids flicker in certain situations.
2023-03-20 20:14:13 +11:00

402 lines
11 KiB
Go

package gui
import (
"sort"
"strings"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// This file is for the management of contexts. There is a context stack such that
// for example you might start off in the commits context and then open a menu, putting
// you in the menu context. When contexts are activated/deactivated certain things need
// to happen like showing/hiding views and rendering content.
func (gui *Gui) popupViewNames() []string {
popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool {
return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP
})
return slices.Map(popups, func(c types.Context) string {
return c.GetViewName()
})
}
// use replaceContext when you don't want to return to the original context upon
// hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c types.Context) error {
if !c.IsFocusable() {
return nil
}
gui.State.ContextManager.Lock()
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []types.Context{c}
} else {
// replace the last item with the given item
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
}
defer gui.State.ContextManager.Unlock()
return gui.activateContext(c, types.OnFocusOpts{})
}
func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
if !c.IsFocusable() {
return nil
}
contextsToDeactivate, contextToActivate := gui.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate {
if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil {
return err
}
}
if contextToActivate == nil {
return nil
}
return gui.activateContext(contextToActivate, opts)
}
// Adjusts the context stack based on the context that's being pushed and
// returns (contexts to deactivate, context to activate)
func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) {
contextsToDeactivate := []types.Context{}
gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) > 0 &&
c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] {
// Context being pushed is already on top of the stack: nothing to
// deactivate or activate
return contextsToDeactivate, nil
}
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
} else if c.GetKind() == types.SIDE_CONTEXT {
// if we are switching to a side context, remove all other contexts in the stack
contextsToDeactivate = gui.State.ContextManager.ContextStack
gui.State.ContextManager.ContextStack = []types.Context{c}
} else if c.GetKind() == types.MAIN_CONTEXT {
// if we're switching to a main context, remove all other main contexts in the stack
for _, stackContext := range gui.State.ContextManager.ContextStack {
if stackContext.GetKind() == types.MAIN_CONTEXT {
contextsToDeactivate = append(contextsToDeactivate, stackContext)
}
}
gui.State.ContextManager.ContextStack = []types.Context{c}
} else {
topContext := gui.currentContextWithoutLock()
// if we're pushing the same context on, we do nothing.
if topContext.GetKey() != c.GetKey() {
// if top one is a temporary popup, we remove it. Ideally you'd be able to
// escape back to previous temporary popups, but because we're currently reusing
// views for this, you might not be able to get back to where you previously were.
// The exception is when going to the search context e.g. for searching a menu.
if (topContext.GetKind() == types.TEMPORARY_POPUP && c.GetKey() != context.SEARCH_CONTEXT_KEY) ||
// we only ever want one main context on the stack at a time.
(topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) {
contextsToDeactivate = append(contextsToDeactivate, topContext)
_, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack)
}
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
}
}
return contextsToDeactivate, c
}
func (gui *Gui) popContext() error {
gui.State.ContextManager.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 {
// cannot escape from bottommost context
gui.State.ContextManager.Unlock()
return nil
}
var currentContext types.Context
currentContext, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack)
newContext := gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
gui.State.ContextManager.Unlock()
if err := gui.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil {
return err
}
return gui.activateContext(newContext, types.OnFocusOpts{})
}
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
view, _ := gui.g.View(c.GetViewName())
if view != nil && view.IsSearching() {
if err := gui.onSearchEscape(); err != nil {
return err
}
}
// if we are the kind of context that is sent to back upon deactivation, we should do that
if view != nil &&
(c.GetKind() == types.TEMPORARY_POPUP ||
c.GetKind() == types.PERSISTENT_POPUP) {
view.Visible = false
}
if err := c.HandleFocusLost(opts); err != nil {
return err
}
return nil
}
// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed
// if the context's view is set to another context we do nothing.
// if the context's view is the current view we trigger a focus; re-selecting the current item.
func (gui *Gui) postRefreshUpdate(c types.Context) error {
if err := c.HandleRender(); err != nil {
return err
}
if gui.currentViewName() == c.GetViewName() {
if err := c.HandleFocus(types.OnFocusOpts{}); err != nil {
return err
}
}
return nil
}
func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error {
viewName := c.GetViewName()
v, err := gui.g.View(viewName)
if err != nil {
return err
}
gui.setWindowContext(c)
gui.moveToTopOfWindow(c)
if _, err := gui.g.SetCurrentView(viewName); err != nil {
return err
}
desiredTitle := c.Title()
if desiredTitle != "" {
v.Title = desiredTitle
}
v.Visible = true
gui.g.Cursor = v.Editable
// render the options available for the current context at the bottom of the screen
optionsMap := c.GetOptionsMap()
if optionsMap == nil {
optionsMap = gui.globalOptionsMap()
}
gui.renderOptionsMap(optionsMap)
if err := c.HandleFocus(opts); err != nil {
return err
}
return nil
}
func (gui *Gui) 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 (gui *Gui) renderOptionsMap(optionsMap map[string]string) {
_ = gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap))
}
// // currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextManager.ContextStack {
// result += string(context.GetKey()) + "\n"
// }
// return result
// }
func (gui *Gui) currentContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
return gui.currentContextWithoutLock()
}
func (gui *Gui) currentContextWithoutLock() types.Context {
if len(gui.State.ContextManager.ContextStack) == 0 {
return gui.defaultSideContext()
}
return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
}
// the status panel is not yet a list context (and may never be), so this method is not
// quite the same as currentSideContext()
func (gui *Gui) currentSideListContext() types.IListContext {
context := gui.currentSideContext()
listContext, ok := context.(types.IListContext)
if !ok {
return nil
}
return listContext
}
func (gui *Gui) currentSideContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
// on startup the stack can be empty so we'll return an empty string in that case
if len(stack) == 0 {
return gui.defaultSideContext()
}
// find the first context in the stack with the type of types.SIDE_CONTEXT
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() == types.SIDE_CONTEXT {
return context
}
}
return gui.defaultSideContext()
}
// static as opposed to popup
func (gui *Gui) currentStaticContext() types.Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
return gui.currentStaticContextWithoutLock()
}
func (gui *Gui) currentStaticContextWithoutLock() types.Context {
stack := gui.State.ContextManager.ContextStack
if len(stack) == 0 {
return gui.defaultSideContext()
}
// find the first context in the stack without a popup type
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() != types.TEMPORARY_POPUP && context.GetKind() != types.PERSISTENT_POPUP {
return context
}
}
return gui.defaultSideContext()
}
func (gui *Gui) defaultSideContext() types.Context {
if gui.State.Modes.Filtering.Active() {
return gui.State.Contexts.LocalCommits
} else {
return gui.State.Contexts.Files
}
}
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
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 err := gui.onViewFocusLost(previousView); err != nil {
return err
}
previousView = newView
}
return nil
}
}
func (gui *Gui) onViewFocusLost(oldView *gocui.View) error {
if oldView == nil {
return nil
}
oldView.Highlight = false
_ = oldView.SetOriginX(0)
return nil
}
func (gui *Gui) TransientContexts() []types.Context {
return slices.Filter(gui.State.Contexts.Flatten(), func(context types.Context) bool {
return context.IsTransient()
})
}
func (gui *Gui) rerenderView(view *gocui.View) error {
context, ok := gui.contextForView(view.Name())
if !ok {
gui.Log.Errorf("no context found for view %s", view.Name())
return nil
}
return context.HandleRender()
}
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideListContext()
if currentSideContext == nil {
return ""
}
return currentSideContext.GetSelectedItemId()
}
// currently unused
// func (gui *Gui) getCurrentSideView() *gocui.View {
// currentSideContext := gui.currentSideContext()
// if currentSideContext == nil {
// return nil
// }
// view, _ := gui.g.View(currentSideContext.GetViewName())
// return view
// }
// currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextManager.ContextStack {
// result += context.GetViewName() + "\n"
// }
// return result
// }