mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 08:06:25 -04:00
When re-rendering content the user was already scrolled into, we want the saved scroll position applied exactly when the real content first paints — not before. Setting the origin up front instead paints it onto whatever placeholder is currently in the view (e.g. the shorter buffer CopyContent left there), which flickers: either a blank frame past the placeholder's end, or a jump to the top when the task resets the origin at startup. Add ViewBufferManager.ScrollToOriginYForNextTask: the next cmd/pty task then (a) does not reset the view to the top at startup even though the command key changed, so the placeholder stays put, (b) sizes its initial read to the saved position so enough content is loaded to fill the view there, and (c) scrolls to it as part of the first refresh, in the same paint that shows the real content. This is the cmd/pty analogue of RenderStringWithScrollTask. No caller sets it yet, so this is behaviour-preserving on its own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 lines
5.8 KiB
Go
187 lines
5.8 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.
|
|
//
|
|
// If targetOriginY is non-nil, the read is sized to that scroll position rather
|
|
// than the view's current one, and the returned LinesToRead carries an
|
|
// ApplyInitialScroll that scrolls the view there at the first refresh. This is
|
|
// used when re-rendering content the user was already scrolled into, so the
|
|
// saved position is applied exactly when the content first paints.
|
|
func (gui *Gui) linesToReadFromCmdTask(v *gocui.View, targetOriginY *int) tasks.LinesToRead {
|
|
height := v.InnerHeight()
|
|
oy := v.OriginY()
|
|
var applyInitialScroll func()
|
|
if targetOriginY != nil {
|
|
oy = *targetOriginY
|
|
applyInitialScroll = func() {
|
|
v.SetOrigin(v.OriginX(), *targetOriginY)
|
|
}
|
|
}
|
|
|
|
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,
|
|
ApplyInitialScroll: applyInitialScroll,
|
|
}
|
|
}
|
|
|
|
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) {
|
|
t := time.Now()
|
|
defer func() {
|
|
gui.Log.Infof("postRefreshUpdate for %s took %s", c.GetKey(), time.Since(t))
|
|
}()
|
|
|
|
c.HandleRender()
|
|
|
|
if gui.currentViewName() == c.GetViewName() {
|
|
c.HandleFocus(types.OnFocusOpts{})
|
|
} 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(false)
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|