mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Escaping to a focused main view restored the selection by scheduling, on the next UI tick, a ReadToEnd whose callback re-selected the saved line. But ReadToEnd fires its callback synchronously when the manager has no live read channel, and the re-render task triggered by the push creates that channel later, inside its own goroutine (after stopping the previous task). If the UI tick won that race, the restore ran before any content was loaded, FocusPoint no-oped against the unloaded line, and the selection was silently dropped — intermittently, and more often under load. Thread the restore through the task instead: a thenForNextTask hook on the buffer manager, folded into the next cmd/pty task's initial-read Then, mirroring scrollToOriginYForNextTask. It runs once the task has read enough to place the selection, and can't fire before the task exists. The scroll restore already applies at the task's first paint, which precedes the initial read's end, so the origin is in place when the selection is restored. This needs interactive verification (LAZYGIT_SLOW_RENDER + a real pager); see focused-main-view-notes.md §13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
6.2 KiB
Go
183 lines
6.2 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
)
|
|
|
|
type PatchBuildingHelper struct {
|
|
c *HelperCommon
|
|
}
|
|
|
|
func NewPatchBuildingHelper(
|
|
c *HelperCommon,
|
|
) *PatchBuildingHelper {
|
|
return &PatchBuildingHelper{
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
func (self *PatchBuildingHelper) ShowHunkStagingHint() {
|
|
if !self.c.AppState.DidShowHunkStagingHint && self.c.UserConfig().Gui.UseHunkModeInStagingView {
|
|
self.c.AppState.DidShowHunkStagingHint = true
|
|
self.c.SaveAppStateAndLogError()
|
|
|
|
message := fmt.Sprintf(self.c.Tr.HunkStagingHint, self.c.UserConfig().Keybinding.Main.ToggleSelectHunk)
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Prompt: message,
|
|
})
|
|
}
|
|
}
|
|
|
|
// takes us from the patch building panel back to the commit files panel, or to
|
|
// the focused main view if that's where we entered it from
|
|
func (self *PatchBuildingHelper) Escape() {
|
|
EscapeFromPatchExplorer(self.c, self.c.Contexts().CustomPatchBuilder)
|
|
}
|
|
|
|
// EscapeFromPatchExplorer returns from a patch explorer context (staging or
|
|
// patch building). If we entered it from a focused main view, we go back to
|
|
// where we came from (re-rendering the side panel's content into the main view,
|
|
// like the plain escape does), then focus the main view and restore its scroll
|
|
// position and selection. Otherwise we just pop to the side panel.
|
|
func EscapeFromPatchExplorer(c *HelperCommon, context types.IPatchExplorerContext) {
|
|
snapshot := context.GetFocusedMainViewSnapshot()
|
|
if snapshot == nil {
|
|
c.Context().Pop()
|
|
return
|
|
}
|
|
|
|
context.SetFocusedMainViewSnapshot(nil)
|
|
|
|
// Restore the side panel's selection before we render it, so it shows the
|
|
// same content the main view had (diving into staging can change it, e.g.
|
|
// from a directory to a file in the files panel).
|
|
if listContext, ok := snapshot.SidePanel.(types.IListContext); ok && snapshot.SidePanelSelectedLineIdx >= 0 {
|
|
listContext.GetList().SetSelectedLineIdx(snapshot.SidePanelSelectedLineIdx)
|
|
}
|
|
|
|
view := snapshot.MainView.GetView()
|
|
|
|
restore := func() {
|
|
view.FocusPoint(0, snapshot.SelectedLineIdx, false)
|
|
view.Highlight = true
|
|
view.HighlightInactive = false
|
|
}
|
|
|
|
// Ask the upcoming re-render to restore the scroll position and selection.
|
|
// Pushing the side panel re-renders its content into the main view via a
|
|
// cmd/pty task. Until that content is ready, the main view keeps showing the
|
|
// placeholder that CopyContent left in it (the view we're leaving) at its
|
|
// current scroll; the task then scrolls to the saved position as part of the
|
|
// first paint that shows the real content (rather than setting the origin up
|
|
// front, which would jump to the top or show a misplaced placeholder frame).
|
|
// The selection needs the content loaded down to the selected line, so it
|
|
// rides the same task and fires at the end of its initial read. Threading it
|
|
// through the task (rather than a ReadToEnd issued after the pushes) avoids a
|
|
// race: ReadToEnd fires synchronously when the freshly-created task's read
|
|
// channel isn't live yet, which would run FocusPoint before the content is
|
|
// loaded and silently drop the selection.
|
|
manager := c.GetViewBufferManagerForView(view)
|
|
if manager != nil {
|
|
manager.ScrollToOriginYForNextTask(snapshot.OriginY)
|
|
manager.ThenForNextTask(func() {
|
|
c.OnUIThread(func() error {
|
|
restore()
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
// Land on the side panel first (this re-renders the original content into the
|
|
// main view), then focus the main view on top of it.
|
|
c.Context().Push(snapshot.SidePanel, types.OnFocusOpts{})
|
|
c.Context().Push(snapshot.MainView, types.OnFocusOpts{})
|
|
|
|
// Without a buffer manager there is no re-render task to ride, so restore now.
|
|
if manager == nil {
|
|
restore()
|
|
}
|
|
}
|
|
|
|
// kills the custom patch and returns us back to the commit files panel if needed
|
|
func (self *PatchBuildingHelper) Reset() error {
|
|
self.c.Git().Patch.PatchBuilder.Reset()
|
|
|
|
if self.c.Context().CurrentStatic().GetKind() != types.SIDE_CONTEXT {
|
|
self.Escape()
|
|
}
|
|
|
|
self.c.Refresh(types.RefreshOptions{
|
|
Scope: []types.RefreshableView{types.COMMIT_FILES},
|
|
})
|
|
|
|
// refreshing the current context so that the secondary panel is hidden if necessary.
|
|
self.c.PostRefreshUpdate(self.c.Context().Current())
|
|
return nil
|
|
}
|
|
|
|
func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) {
|
|
selectedLineIdx := -1
|
|
selectedRealLineIdx := -1
|
|
if opts.ClickedWindowName == "main" {
|
|
selectedLineIdx = opts.ClickedViewLineIdx
|
|
selectedRealLineIdx = opts.ClickedViewRealLineIdx
|
|
}
|
|
|
|
if !self.c.Git().Patch.PatchBuilder.Active() {
|
|
self.Escape()
|
|
return
|
|
}
|
|
|
|
// get diff from commit file that's currently selected
|
|
file := self.c.Contexts().CommitFiles.GetSelectedFile()
|
|
if file == nil {
|
|
return
|
|
}
|
|
|
|
from, to := self.c.Contexts().CommitFiles.GetFromAndToForDiff()
|
|
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
|
diff, err := self.c.Git().WorkingTree.ShowFileDiff(from, to, reverse, file.Path, file.PreviousPath, true)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(patch.RenderPatchForFileOpts{
|
|
Filename: file.Path,
|
|
PreviousPath: file.PreviousPath,
|
|
Plain: false,
|
|
Reverse: false,
|
|
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
|
|
})
|
|
|
|
context := self.c.Contexts().CustomPatchBuilder
|
|
|
|
oldState := context.GetState()
|
|
|
|
state := patch_exploring.NewState(diff, selectedLineIdx, selectedRealLineIdx, opts.ClickedViewRealLineIsDeletion, context.GetView(), oldState, self.c.UserConfig().Gui.UseHunkModeInStagingView, opts.SelectLineInDefaultMode)
|
|
context.SetState(state)
|
|
if state == nil {
|
|
self.Escape()
|
|
return
|
|
}
|
|
|
|
mainContent := context.GetContentToRender()
|
|
|
|
self.c.Contexts().CustomPatchBuilder.FocusSelection()
|
|
|
|
self.c.RenderToMainViews(types.RefreshMainOpts{
|
|
Pair: self.c.MainViewPairs().PatchBuilding,
|
|
Main: &types.ViewUpdateOpts{
|
|
Task: types.NewRenderStringWithoutScrollTask(mainContent),
|
|
Title: self.c.Tr.Patch,
|
|
},
|
|
Secondary: &types.ViewUpdateOpts{
|
|
Task: types.NewRenderStringWithoutScrollTask(secondaryDiff),
|
|
Title: self.c.Tr.CustomPatch,
|
|
},
|
|
})
|
|
}
|