From cbeca72baea9f4c20ff3ee2aa2ed18e60bcc2138 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 16 Sep 2024 20:38:22 +0200 Subject: [PATCH] Press enter in main view of files/commitFiles to enter staging/patch-building This was already possible, but only when a file was selected, and it woudln't always land on the right line when a pager was used. Now it's also possible to do this for directories, and it jumps to the right line. At the moment this is a hack that relies on delta's hyperlinks, so it only works on lines that have hyperlinks (added and context). The implementation is very hacky for other reasons too (e.g. the addition of the weirdly named ClickedViewRealLineIdx to OnFocusOpts). --- pkg/commands/patch/patch.go | 32 +++++++++++++++++ pkg/gui/controllers.go | 4 +-- .../controllers/commits_files_controller.go | 34 +++++++++++++++--- pkg/gui/controllers/files_controller.go | 33 ++++++++++++++--- .../helpers/patch_building_helper.go | 4 ++- pkg/gui/controllers/helpers/staging_helper.go | 35 ++++++++++++++++--- .../controllers/patch_explorer_controller.go | 10 ++++-- pkg/gui/patch_exploring/state.go | 6 +++- pkg/gui/types/context.go | 3 ++ vendor/github.com/jesseduffield/gocui/view.go | 14 ++++++++ 10 files changed, 155 insertions(+), 20 deletions(-) diff --git a/pkg/commands/patch/patch.go b/pkg/commands/patch/patch.go index 5e4f9a846..68c8e609b 100644 --- a/pkg/commands/patch/patch.go +++ b/pkg/commands/patch/patch.go @@ -104,6 +104,38 @@ func (self *Patch) LineNumberOfLine(idx int) int { return hunk.newStart + offset } +// Takes a line number in the new file and returns the line index in the patch. +// This is the opposite of LineNumberOfLine. +// If the line number is not contained in any of the hunks, it returns the +// closest position. +func (self *Patch) PatchLineForLineNumber(lineNumber int) int { + if len(self.hunks) == 0 { + return len(self.header) + } + + for hunkIdx, hunk := range self.hunks { + if lineNumber <= hunk.newStart { + return self.HunkStartIdx(hunkIdx) + } + + if lineNumber < hunk.newStart+hunk.newLength() { + lines := hunk.bodyLines + offset := lineNumber - hunk.newStart + for i, line := range lines { + if offset == 0 { + return self.HunkStartIdx(hunkIdx) + i + 1 + } + + if line.Kind == ADDITION || line.Kind == CONTEXT { + offset-- + } + } + } + } + + return self.LineCount() - 1 +} + // Returns hunk index containing the line at the given patch line index func (self *Patch) HunkContainingLine(idx int) int { for hunkIdx, hunk := range self.hunks { diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index abbb62155..560e570eb 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -54,8 +54,9 @@ func (gui *Gui) resetHelpersAndControllers() { gpgHelper := helpers.NewGpgHelper(helperCommon) viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts) + windowHelper := helpers.NewWindowHelper(helperCommon, viewHelper) patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon) - stagingHelper := helpers.NewStagingHelper(helperCommon) + stagingHelper := helpers.NewStagingHelper(helperCommon, windowHelper) mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon) searchHelper := helpers.NewSearchHelper(helperCommon) @@ -75,7 +76,6 @@ func (gui *Gui) resetHelpersAndControllers() { rebaseHelper, ) bisectHelper := helpers.NewBisectHelper(helperCommon) - windowHelper := helpers.NewWindowHelper(helperCommon, viewHelper) modeHelper := helpers.NewModeHelper( helperCommon, diffHelper, diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 8beeb6ff2..3e4dd52c4 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -470,7 +470,7 @@ func (self *CommitFilesController) currentFromToReverseForPatchBuilding() (strin } func (self *CommitFilesController) enter(node *filetree.CommitFileNode) error { - return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1}) + return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1, ClickedViewRealLineIdx: -1}) } func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode, opts types.OnFocusOpts) error { @@ -545,11 +545,35 @@ func (self *CommitFilesController) expandAll() error { func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error { return func(mainViewName string, clickedLineIdx int) error { - node := self.getSelectedItem() - if node != nil && node.File != nil { - return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx}) + clickedFile, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(mainViewName, clickedLineIdx) + if !ok { + line = -1 } - return nil + + node := self.getSelectedItem() + if node == nil { + return nil + } + + if !node.IsFile() && ok { + relativePath, err := filepath.Rel(self.c.Git().RepoPaths.RepoPath(), clickedFile) + if err != nil { + return err + } + relativePath = "./" + relativePath + self.context().CommitFileTreeViewModel.ExpandToPath(relativePath) + self.c.PostRefreshUpdate(self.context()) + + idx, ok := self.context().CommitFileTreeViewModel.GetIndexForPath(relativePath) + if ok { + self.context().SetSelectedLineIdx(idx) + self.context().GetViewTrait().FocusPoint( + self.context().ModelIndexToViewIndex(idx)) + node = self.context().GetSelected() + } + } + + return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: line, ClickedViewRealLineIdx: line}) } } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 6df28a523..9f1a76873 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -325,11 +325,34 @@ func (self *FilesController) GetOnClick() func() error { func (self *FilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error { return func(mainViewName string, clickedLineIdx int) error { - node := self.getSelectedItem() - if node != nil && node.File != nil { - return self.EnterFile(types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx}) + clickedFile, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(mainViewName, clickedLineIdx) + if !ok { + line = -1 } - return nil + + node := self.context().GetSelected() + if node == nil { + return nil + } + + if !node.IsFile() && ok { + relativePath, err := filepath.Rel(self.c.Git().RepoPaths.RepoPath(), clickedFile) + if err != nil { + return err + } + relativePath = "./" + relativePath + self.context().FileTreeViewModel.ExpandToPath(relativePath) + self.c.PostRefreshUpdate(self.context()) + + idx, ok := self.context().FileTreeViewModel.GetIndexForPath(relativePath) + if ok { + self.context().SetSelectedLineIdx(idx) + self.context().GetViewTrait().FocusPoint( + self.context().ModelIndexToViewIndex(idx)) + } + } + + return self.EnterFile(types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: line, ClickedViewRealLineIdx: line}) } } @@ -511,7 +534,7 @@ func (self *FilesController) getSelectedFile() *models.File { } func (self *FilesController) enter() error { - return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1}) + return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1, ClickedViewRealLineIdx: -1}) } func (self *FilesController) collapseAll() error { diff --git a/pkg/gui/controllers/helpers/patch_building_helper.go b/pkg/gui/controllers/helpers/patch_building_helper.go index 73d0fb608..931c3347f 100644 --- a/pkg/gui/controllers/helpers/patch_building_helper.go +++ b/pkg/gui/controllers/helpers/patch_building_helper.go @@ -53,8 +53,10 @@ func (self *PatchBuildingHelper) Reset() error { 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() { @@ -86,7 +88,7 @@ func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpt oldState := context.GetState() - state := patch_exploring.NewState(diff, selectedLineIdx, context.GetView(), oldState) + state := patch_exploring.NewState(diff, selectedLineIdx, selectedRealLineIdx, context.GetView(), oldState) context.SetState(state) if state == nil { self.Escape() diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go index 3d9762541..8ab4ce418 100644 --- a/pkg/gui/controllers/helpers/staging_helper.go +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -1,20 +1,26 @@ package helpers import ( + "regexp" + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" ) type StagingHelper struct { - c *HelperCommon + c *HelperCommon + windowHelper *WindowHelper } func NewStagingHelper( c *HelperCommon, + windowHelper *WindowHelper, ) *StagingHelper { return &StagingHelper{ - c: c, + c: c, + windowHelper: windowHelper, } } @@ -30,12 +36,16 @@ func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) { } mainSelectedLineIdx := -1 + mainSelectedRealLineIdx := -1 secondarySelectedLineIdx := -1 + secondarySelectedRealLineIdx := -1 if focusOpts.ClickedViewLineIdx > 0 { if secondaryFocused { secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx + secondarySelectedRealLineIdx = focusOpts.ClickedViewRealLineIdx } else { mainSelectedLineIdx = focusOpts.ClickedViewLineIdx + mainSelectedRealLineIdx = focusOpts.ClickedViewRealLineIdx } } @@ -63,11 +73,11 @@ func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) { secondaryContext.GetMutex().Lock() mainContext.SetState( - patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetView(), mainContext.GetState()), + patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainSelectedRealLineIdx, mainContext.GetView(), mainContext.GetState()), ) secondaryContext.SetState( - patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetView(), secondaryContext.GetState()), + patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondarySelectedRealLineIdx, secondaryContext.GetView(), secondaryContext.GetState()), ) mainState := mainContext.GetState() @@ -124,3 +134,20 @@ func (self *StagingHelper) secondaryStagingFocused() bool { func (self *StagingHelper) mainStagingFocused() bool { return self.c.Context().CurrentStatic().GetKey() == self.c.Contexts().Staging.GetKey() } + +func (self *StagingHelper) GetFileAndLineForClickedDiffLine(windowName string, lineIdx int) (string, int, bool) { + v, _ := self.c.GocuiGui().View(self.windowHelper.GetViewNameForWindow(windowName)) + hyperlink, ok := v.HyperLinkInLine(lineIdx, "lazygit-edit:") + if !ok { + return "", 0, false + } + + re := regexp.MustCompile(`^lazygit-edit://(.+?):(\d+)$`) + matches := re.FindStringSubmatch(hyperlink) + if matches == nil { + return "", 0, false + } + filepath := matches[1] + lineNumber := utils.MustConvertToInt(matches[2]) + return filepath, lineNumber, true +} diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go index 5b832c80d..cd225b10a 100644 --- a/pkg/gui/controllers/patch_explorer_controller.go +++ b/pkg/gui/controllers/patch_explorer_controller.go @@ -163,9 +163,15 @@ func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsO return self.withRenderAndFocus(self.HandleMouseDown)() } + _, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(self.context.GetWindowName(), opts.Y) + if !ok { + line = -1 + } + self.c.Context().Push(self.context, types.OnFocusOpts{ - ClickedWindowName: self.context.GetWindowName(), - ClickedViewLineIdx: opts.Y, + ClickedWindowName: self.context.GetWindowName(), + ClickedViewLineIdx: opts.Y, + ClickedViewRealLineIdx: line, }) return nil diff --git a/pkg/gui/patch_exploring/state.go b/pkg/gui/patch_exploring/state.go index 73ce0a756..7520a0c5a 100644 --- a/pkg/gui/patch_exploring/state.go +++ b/pkg/gui/patch_exploring/state.go @@ -39,7 +39,7 @@ const ( HUNK ) -func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *State) *State { +func NewState(diff string, selectedLineIdx int, selectedRealLineIdx int, view *gocui.View, oldState *State) *State { if oldState != nil && diff == oldState.diff && selectedLineIdx == -1 { // if we're here then we can return the old state. If selectedLineIdx was not -1 // then that would mean we were trying to click and potentially drag a range, which @@ -55,6 +55,10 @@ func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *Stat viewLineIndices, patchLineIndices := wrapPatchLines(diff, view) + if selectedRealLineIdx != -1 { + selectedLineIdx = patch.PatchLineForLineNumber(selectedRealLineIdx) + } + rangeStartLineIdx := 0 if oldState != nil { rangeStartLineIdx = oldState.rangeStartLineIdx diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 1c9759486..7473e8693 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -222,6 +222,9 @@ type IViewTrait interface { type OnFocusOpts struct { ClickedWindowName string ClickedViewLineIdx int + + // If not -1, takes precedence over ClickedViewLineIdx. + ClickedViewRealLineIdx int } type OnFocusLostOpts struct { diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 473c6dbaf..b1ac15211 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -1470,6 +1470,20 @@ func (v *View) Word(x, y int) (string, bool) { return str[nl:nr], true } +func (v *View) HyperLinkInLine(y int, urlScheme string) (string, bool) { + if y < 0 || y >= len(v.viewLines) { + return "", false + } + + for _, c := range v.lines[v.viewLines[y].linesY] { + if strings.HasPrefix(c.hyperlink, urlScheme) { + return c.hyperlink, true + } + } + + return "", false +} + // indexFunc allows to split lines by words taking into account spaces // and 0. func indexFunc(r rune) bool {