mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 16:16:28 -04:00
A view drew a selection because something told it to, from four places on three different schedules: a context being focused, a context losing focus, a context being activated over another one, and a list being re-rendered. Whether the flags ended up describing the state of the app depended on which of those had run last, and the last one to run was often none of them: a refresh only re-focuses the view that has the focus, so a list whose contents changed underneath an unfocused panel kept whichever highlight it happened to have. Derive both flags instead, in one place, from the two things they mean: a view shows a selection while its context is on the stack and has something to select, and the context the user is in shows an active one where the ones behind it show inactive ones. Nothing else needs to say anything about highlighting, so nothing else can leave a view saying something untrue about where the focus is. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
180 lines
5.5 KiB
Go
180 lines
5.5 KiB
Go
package gui
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/tasks"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/spkg/bom"
|
|
)
|
|
|
|
func (gui *Gui) resetViewOrigin(v *gocui.View) {
|
|
v.SetCursor(0, 0)
|
|
v.SetOrigin(0, 0)
|
|
}
|
|
|
|
// Returns the number of lines that we should read initially from a cmd task so
|
|
// that the scrollbar has the correct size, along with the number of lines after
|
|
// which the view is filled and we can do a first refresh.
|
|
func (gui *Gui) linesToReadFromCmdTask(v *gocui.View) tasks.LinesToRead {
|
|
height := v.InnerHeight()
|
|
oy := v.OriginY()
|
|
|
|
linesForFirstRefresh := height + oy + 10
|
|
|
|
// We want to read as many lines initially as necessary to let the
|
|
// scrollbar go to its minimum height, so that the scrollbar thumb doesn't
|
|
// change size as you scroll down.
|
|
minScrollbarHeight := 1
|
|
linesToReadForAccurateScrollbar := min(
|
|
// However, cap it at some arbitrary max limit, so that we don't get
|
|
// performance problems for huge monitors or tiny font sizes
|
|
height*(height-1)/minScrollbarHeight+oy, 5000)
|
|
|
|
return tasks.LinesToRead{
|
|
Total: linesToReadForAccurateScrollbar,
|
|
InitialRefreshAfter: linesForFirstRefresh,
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) cleanString(s string) string {
|
|
output := string(bom.Clean([]byte(s)))
|
|
return utils.NormalizeLinefeeds(output)
|
|
}
|
|
|
|
func (gui *Gui) setViewContent(v *gocui.View, s string) {
|
|
v.SetContent(gui.cleanString(s))
|
|
}
|
|
|
|
func (gui *Gui) currentViewName() string {
|
|
currentView := gui.g.CurrentView()
|
|
if currentView == nil {
|
|
return ""
|
|
}
|
|
return currentView.Name()
|
|
}
|
|
|
|
func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error {
|
|
tabs := gui.viewTabMap()[windowName]
|
|
if len(tabs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
viewName := tabs[tabIndex].ViewName
|
|
|
|
context, ok := gui.helpers.View.ContextForView(viewName)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
gui.c.Context().Push(context, types.OnFocusOpts{})
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) handleNextTab() error {
|
|
view := getTabbedView(gui)
|
|
if view == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, context := range gui.State.Contexts.Flatten() {
|
|
if context.GetViewName() == view.Name() {
|
|
return gui.onViewTabClick(
|
|
context.GetWindowName(),
|
|
utils.ModuloWithWrap(view.TabIndex+1, len(view.Tabs)),
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gui *Gui) handlePrevTab() error {
|
|
view := getTabbedView(gui)
|
|
if view == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, context := range gui.State.Contexts.Flatten() {
|
|
if context.GetViewName() == view.Name() {
|
|
return gui.onViewTabClick(
|
|
context.GetWindowName(),
|
|
utils.ModuloWithWrap(view.TabIndex-1, len(view.Tabs)),
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getTabbedView(gui *Gui) *gocui.View {
|
|
// It safe assumption that only static contexts have tabs
|
|
context := gui.c.Context().CurrentStatic()
|
|
view, _ := gui.g.View(context.GetViewName())
|
|
return view
|
|
}
|
|
|
|
func (gui *Gui) render() {
|
|
gui.c.OnUIThread(func() error { return nil })
|
|
}
|
|
|
|
// renderContentOnly triggers a re-render that skips the layout pass and only
|
|
// redraws the views whose content changed (relying on tcell's cell-level dirty
|
|
// tracking to emit just the cells that actually differ). Use it when only a
|
|
// view's content changed, not the window layout.
|
|
func (gui *Gui) renderContentOnly() {
|
|
gui.c.OnUIThreadContentOnly(func() error { 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, opts types.OnFocusOpts) {
|
|
t := time.Now()
|
|
defer func() {
|
|
gui.Log.Infof("postRefreshUpdate for %s took %s", c.GetKey(), time.Since(t))
|
|
}()
|
|
|
|
c.HandleRender()
|
|
|
|
// The render may have given the context its first item, or taken its last one
|
|
// away, which decides whether its view draws a selection at all.
|
|
gui.State.ContextMgr.updateSelectionHighlights()
|
|
|
|
if gui.currentViewName() == c.GetInputViewName() {
|
|
c.HandleFocus(opts)
|
|
} else {
|
|
// The FocusLine call is included in the HandleFocus method which we
|
|
// call for focused views above; but we need to call it here for
|
|
// non-focused views to ensure that an inactive selection is painted
|
|
// correctly, and that integration tests see the up to date selection
|
|
// state.
|
|
c.FocusLine(!opts.KeepScrollPosition)
|
|
if opts.SkipMainViewUpdate {
|
|
return
|
|
}
|
|
|
|
currentCtx := gui.State.ContextMgr.Current()
|
|
if currentCtx.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY || currentCtx.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {
|
|
// Searching can't cope well with the view being updated while it is being searched.
|
|
// We might be able to fix the problems with this, but it doesn't seem easy, so for now
|
|
// just don't rerender the view while searching, on the assumption that users will probably
|
|
// either search or change their data, but not both at the same time.
|
|
if !currentCtx.GetView().IsSearching() {
|
|
sidePanelContext := gui.State.ContextMgr.NextInStack(currentCtx)
|
|
if sidePanelContext != nil && sidePanelContext.GetKey() == c.GetKey() {
|
|
sidePanelContext.HandleRenderToMain()
|
|
}
|
|
}
|
|
} else if c.GetKey() == gui.State.ContextMgr.CurrentStatic().GetKey() {
|
|
// If our view is not the current one, but it is the current static context, then this
|
|
// can only mean that a popup is showing. In that case we want to refresh the main view
|
|
// behind the popup.
|
|
c.HandleRenderToMain()
|
|
}
|
|
}
|
|
}
|