mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 16:16:28 -04:00
Diving into staging or patch building from a focused main view (by double-clicking a line, or pressing enter on the selected line) always landed on a single-line selection. Entering the same views through the side panel honours the UseHunkModeInStagingView config and selects the whole hunk by default. The two ways in should agree, so that diving in from the main view feels like the established flow. A non-negative line index in NewState was overloaded for two intents: clicking directly on the patch explorer view (where a single-line range is the start of a drag) and diving in from the main view (where we want the default select mode). Distinguish them with SelectLineInDefaultMode on OnFocusOpts: the main-view entry points set it; the click-to-drag path does not. In hunk mode the selection covers the block of changes around the clicked line. A context line has no surrounding changes, so we snap to the next change line (as toggling hunk mode does); the clicked context line itself is then not part of the selection. This is a separate commit only because the branch is a throwaway prototype; in a real history it would be folded into the commit that introduces the focused-main-view enter behavior rather than landing on top of it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
185 lines
6.1 KiB
Go
185 lines
6.1 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()
|
|
|
|
// Ask the upcoming re-render to restore the scroll position. 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. Doing it this way (rather than setting the origin up
|
|
// front) avoids both a jump to the top and a misplaced placeholder frame.
|
|
if manager := c.GetViewBufferManagerForView(view); manager != nil {
|
|
manager.ScrollToOriginYForNextTask(snapshot.OriginY)
|
|
}
|
|
|
|
// 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{})
|
|
|
|
restore := func() {
|
|
view.FocusPoint(0, snapshot.SelectedLineIdx, false)
|
|
view.Highlight = true
|
|
view.HighlightInactive = false
|
|
}
|
|
|
|
// The scroll position is handled by the re-render above, but the selection
|
|
// still needs the content loaded down to the selected line, which happens
|
|
// asynchronously. Wait until the diff has been fully read before restoring
|
|
// it. We do this on the next UI tick, by which point the re-render task is
|
|
// live and ReadToEnd can hook into it.
|
|
c.OnUIThread(func() error {
|
|
manager := c.GetViewBufferManagerForView(view)
|
|
if manager == nil {
|
|
restore()
|
|
return nil
|
|
}
|
|
manager.ReadToEnd(func() {
|
|
c.OnUIThread(func() error {
|
|
restore()
|
|
return nil
|
|
})
|
|
})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// 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, 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,
|
|
},
|
|
})
|
|
}
|