This commit is contained in:
Stefan Haller 2025-05-05 18:54:46 +00:00 committed by GitHub
commit d4847eabc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 417 additions and 117 deletions

View file

@ -113,6 +113,9 @@ gui:
# paragraphs of markdown text. # paragraphs of markdown text.
wrapLinesInStagingView: true wrapLinesInStagingView: true
# If true, show a selection when the main view is focused.
showSelectionInFocusedMainView: false
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru' # One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
language: auto language: auto

View file

@ -104,6 +104,38 @@ func (self *Patch) LineNumberOfLine(idx int) int {
return hunk.newStart + offset 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 // Returns hunk index containing the line at the given patch line index
func (self *Patch) HunkContainingLine(idx int) int { func (self *Patch) HunkContainingLine(idx int) int {
for hunkIdx, hunk := range self.hunks { for hunkIdx, hunk := range self.hunks {

View file

@ -105,6 +105,8 @@ type GuiConfig struct {
// makes it much easier to work with diffs that have long lines, e.g. // makes it much easier to work with diffs that have long lines, e.g.
// paragraphs of markdown text. // paragraphs of markdown text.
WrapLinesInStagingView bool `yaml:"wrapLinesInStagingView"` WrapLinesInStagingView bool `yaml:"wrapLinesInStagingView"`
// If true, show a selection when the main view is focused.
ShowSelectionInFocusedMainView bool `yaml:"showSelectionInFocusedMainView"`
// One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru' // One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
Language string `yaml:"language" jsonschema:"enum=auto,enum=en,enum=zh-TW,enum=zh-CN,enum=pl,enum=nl,enum=ja,enum=ko,enum=ru"` Language string `yaml:"language" jsonschema:"enum=auto,enum=en,enum=zh-TW,enum=zh-CN,enum=pl,enum=nl,enum=ja,enum=ko,enum=ru"`
// Format used when displaying time e.g. commit time. // Format used when displaying time e.g. commit time.
@ -728,23 +730,24 @@ type IconProperties struct {
func GetDefaultConfig() *UserConfig { func GetDefaultConfig() *UserConfig {
return &UserConfig{ return &UserConfig{
Gui: GuiConfig{ Gui: GuiConfig{
ScrollHeight: 2, ScrollHeight: 2,
ScrollPastBottom: true, ScrollPastBottom: true,
ScrollOffMargin: 2, ScrollOffMargin: 2,
ScrollOffBehavior: "margin", ScrollOffBehavior: "margin",
TabWidth: 4, TabWidth: 4,
MouseEvents: true, MouseEvents: true,
SkipDiscardChangeWarning: false, SkipDiscardChangeWarning: false,
SkipStashWarning: false, SkipStashWarning: false,
SidePanelWidth: 0.3333, SidePanelWidth: 0.3333,
ExpandFocusedSidePanel: false, ExpandFocusedSidePanel: false,
ExpandedSidePanelWeight: 2, ExpandedSidePanelWeight: 2,
MainPanelSplitMode: "flexible", MainPanelSplitMode: "flexible",
EnlargedSideViewLocation: "left", EnlargedSideViewLocation: "left",
WrapLinesInStagingView: true, WrapLinesInStagingView: true,
Language: "auto", ShowSelectionInFocusedMainView: false,
TimeFormat: "02 Jan 06", Language: "auto",
ShortTimeFormat: time.Kitchen, TimeFormat: "02 Jan 06",
ShortTimeFormat: time.Kitchen,
Theme: ThemeConfig{ Theme: ThemeConfig{
ActiveBorderColor: []string{"green", "bold"}, ActiveBorderColor: []string{"green", "bold"},
SearchingActiveBorderColor: []string{"cyan", "bold"}, SearchingActiveBorderColor: []string{"cyan", "bold"},

View file

@ -228,3 +228,7 @@ func (self *BaseContext) Title() string {
func (self *BaseContext) TotalContentHeight() int { func (self *BaseContext) TotalContentHeight() int {
return self.view.ViewLinesHeight() return self.view.ViewLinesHeight()
} }
func (self *BaseContext) SetHighlightOnFocus(value bool) {
self.highlightOnFocus = value
}

View file

@ -26,7 +26,7 @@ func NewMainContext(
WindowName: windowName, WindowName: windowName,
Key: key, Key: key,
Focusable: true, Focusable: true,
HighlightOnFocus: false, HighlightOnFocus: c.UserConfig().Gui.ShowSelectionInFocusedMainView,
})), })),
SearchTrait: NewSearchTrait(c), SearchTrait: NewSearchTrait(c),
} }

View file

@ -54,8 +54,9 @@ func (gui *Gui) resetHelpersAndControllers() {
gpgHelper := helpers.NewGpgHelper(helperCommon) gpgHelper := helpers.NewGpgHelper(helperCommon)
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts) viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
windowHelper := helpers.NewWindowHelper(helperCommon, viewHelper)
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon) patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
stagingHelper := helpers.NewStagingHelper(helperCommon) stagingHelper := helpers.NewStagingHelper(helperCommon, windowHelper)
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon) mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
searchHelper := helpers.NewSearchHelper(helperCommon) searchHelper := helpers.NewSearchHelper(helperCommon)
@ -75,7 +76,6 @@ func (gui *Gui) resetHelpersAndControllers() {
rebaseHelper, rebaseHelper,
) )
bisectHelper := helpers.NewBisectHelper(helperCommon) bisectHelper := helpers.NewBisectHelper(helperCommon)
windowHelper := helpers.NewWindowHelper(helperCommon, viewHelper)
modeHelper := helpers.NewModeHelper( modeHelper := helpers.NewModeHelper(
helperCommon, helperCommon,
diffHelper, diffHelper,
@ -115,6 +115,7 @@ func (gui *Gui) resetHelpersAndControllers() {
AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper), AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper),
FixupHelper: helpers.NewFixupHelper(helperCommon), FixupHelper: helpers.NewFixupHelper(helperCommon),
Commits: commitsHelper, Commits: commitsHelper,
CommitFiles: helpers.NewCommitFilesHelper(helperCommon),
Snake: helpers.NewSnakeHelper(helperCommon), Snake: helpers.NewSnakeHelper(helperCommon),
Diff: diffHelper, Diff: diffHelper,
Repos: reposHelper, Repos: reposHelper,

View file

@ -390,7 +390,7 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm
toggle := func() error { toggle := func() error {
return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error {
if !self.c.Git().Patch.PatchBuilder.Active() { if !self.c.Git().Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil { if err := self.c.Helpers().CommitFiles.StartPatchBuilder(); err != nil {
return err return err
} }
} }
@ -429,7 +429,7 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm
}) })
} }
from, to, reverse := self.currentFromToReverseForPatchBuilding() from, to, reverse := self.c.Helpers().CommitFiles.CurrentFromToReverseForPatchBuilding()
if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) { if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) {
self.c.Confirm(types.ConfirmOpts{ self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch, Title: self.c.Tr.DiscardPatch,
@ -451,72 +451,8 @@ func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode)
return self.toggleForPatch([]*filetree.CommitFileNode{root}) return self.toggleForPatch([]*filetree.CommitFileNode{root})
} }
func (self *CommitFilesController) startPatchBuilder() error {
commitFilesContext := self.context()
canRebase := commitFilesContext.GetCanRebase()
from, to, reverse := self.currentFromToReverseForPatchBuilding()
self.c.Git().Patch.PatchBuilder.Start(from, to, reverse, canRebase)
return nil
}
func (self *CommitFilesController) currentFromToReverseForPatchBuilding() (string, string, bool) {
commitFilesContext := self.context()
from, to := commitFilesContext.GetFromAndToForDiff()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
return from, to, reverse
}
func (self *CommitFilesController) enter(node *filetree.CommitFileNode) error { func (self *CommitFilesController) enter(node *filetree.CommitFileNode) error {
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1}) return self.c.Helpers().CommitFiles.EnterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1, ClickedViewRealLineIdx: -1})
}
func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode, opts types.OnFocusOpts) error {
if node.File == nil {
return self.handleToggleCommitFileDirCollapsed(node)
}
if self.c.AppState.DiffContextSize == 0 {
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
}
enterTheFile := func() error {
if !self.c.Git().Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil {
return err
}
}
self.c.Context().Push(self.c.Contexts().CustomPatchBuilder, opts)
return nil
}
from, to, reverse := self.currentFromToReverseForPatchBuilding()
if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
self.c.Git().Patch.PatchBuilder.Reset()
return enterTheFile()
},
})
return nil
}
return enterTheFile()
}
func (self *CommitFilesController) handleToggleCommitFileDirCollapsed(node *filetree.CommitFileNode) error {
self.context().CommitFileTreeViewModel.ToggleCollapsed(node.GetInternalPath())
self.c.PostRefreshUpdate(self.context())
return nil
} }
// NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics // NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics
@ -545,11 +481,35 @@ func (self *CommitFilesController) expandAll() error {
func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error { func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return func(mainViewName string, clickedLineIdx int) error { return func(mainViewName string, clickedLineIdx int) error {
node := self.getSelectedItem() clickedFile, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(mainViewName, clickedLineIdx)
if node != nil && node.File != nil { if !ok {
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx}) 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.c.Helpers().CommitFiles.EnterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: line, ClickedViewRealLineIdx: line})
} }
} }

View file

@ -325,11 +325,34 @@ func (self *FilesController) GetOnClick() func() error {
func (self *FilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error { func (self *FilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return func(mainViewName string, clickedLineIdx int) error { return func(mainViewName string, clickedLineIdx int) error {
node := self.getSelectedItem() clickedFile, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(mainViewName, clickedLineIdx)
if node != nil && node.File != nil { if !ok {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx}) 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 { 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 { func (self *FilesController) collapseAll() error {

View file

@ -0,0 +1,87 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitFilesHelper struct {
c *HelperCommon
}
func NewCommitFilesHelper(c *HelperCommon) *CommitFilesHelper {
return &CommitFilesHelper{
c: c,
}
}
func (self *CommitFilesHelper) EnterCommitFile(node *filetree.CommitFileNode, opts types.OnFocusOpts) error {
if node.File == nil {
self.handleToggleCommitFileDirCollapsed(node)
return nil
}
if self.c.AppState.DiffContextSize == 0 {
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
}
enterTheFile := func() error {
if !self.c.Git().Patch.PatchBuilder.Active() {
if err := self.StartPatchBuilder(); err != nil {
return err
}
}
self.c.Context().Push(self.c.Contexts().CustomPatchBuilder, opts)
return nil
}
from, to, reverse := self.CurrentFromToReverseForPatchBuilding()
if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
self.c.Git().Patch.PatchBuilder.Reset()
return enterTheFile()
},
})
return nil
}
return enterTheFile()
}
func (self *CommitFilesHelper) context() *context.CommitFilesContext {
return self.c.Contexts().CommitFiles
}
func (self *CommitFilesHelper) handleToggleCommitFileDirCollapsed(node *filetree.CommitFileNode) {
self.context().CommitFileTreeViewModel.ToggleCollapsed(node.GetInternalPath())
self.c.PostRefreshUpdate(self.context())
}
func (self *CommitFilesHelper) StartPatchBuilder() error {
commitFilesContext := self.context()
canRebase := commitFilesContext.GetCanRebase()
from, to, reverse := self.CurrentFromToReverseForPatchBuilding()
self.c.Git().Patch.PatchBuilder.Start(from, to, reverse, canRebase)
return nil
}
func (self *CommitFilesHelper) CurrentFromToReverseForPatchBuilding() (string, string, bool) {
commitFilesContext := self.context()
from, to := commitFilesContext.GetFromAndToForDiff()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
return from, to, reverse
}

View file

@ -35,6 +35,7 @@ type Helpers struct {
AmendHelper *AmendHelper AmendHelper *AmendHelper
FixupHelper *FixupHelper FixupHelper *FixupHelper
Commits *CommitsHelper Commits *CommitsHelper
CommitFiles *CommitFilesHelper
Snake *SnakeHelper Snake *SnakeHelper
// lives in context package because our contexts need it to render to main // lives in context package because our contexts need it to render to main
Diff *DiffHelper Diff *DiffHelper
@ -73,6 +74,7 @@ func NewStubHelpers() *Helpers {
AmendHelper: &AmendHelper{}, AmendHelper: &AmendHelper{},
FixupHelper: &FixupHelper{}, FixupHelper: &FixupHelper{},
Commits: &CommitsHelper{}, Commits: &CommitsHelper{},
CommitFiles: &CommitFilesHelper{},
Snake: &SnakeHelper{}, Snake: &SnakeHelper{},
Diff: &DiffHelper{}, Diff: &DiffHelper{},
Repos: &ReposHelper{}, Repos: &ReposHelper{},

View file

@ -29,7 +29,11 @@ func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error)
// takes us from the patch building panel back to the commit files panel // takes us from the patch building panel back to the commit files panel
func (self *PatchBuildingHelper) Escape() { func (self *PatchBuildingHelper) Escape() {
self.c.Context().Pop() if parentCtx := self.c.Contexts().CustomPatchBuilder.GetParentContext(); parentCtx != nil {
self.c.Context().Push(parentCtx, types.OnFocusOpts{})
} else {
self.c.Context().Pop()
}
} }
// kills the custom patch and returns us back to the commit files panel if needed // kills the custom patch and returns us back to the commit files panel if needed
@ -53,8 +57,10 @@ func (self *PatchBuildingHelper) Reset() error {
func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) { func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) {
selectedLineIdx := -1 selectedLineIdx := -1
selectedRealLineIdx := -1
if opts.ClickedWindowName == "main" { if opts.ClickedWindowName == "main" {
selectedLineIdx = opts.ClickedViewLineIdx selectedLineIdx = opts.ClickedViewLineIdx
selectedRealLineIdx = opts.ClickedViewRealLineIdx
} }
if !self.c.Git().Patch.PatchBuilder.Active() { if !self.c.Git().Patch.PatchBuilder.Active() {
@ -86,7 +92,7 @@ func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpt
oldState := context.GetState() 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) context.SetState(state)
if state == nil { if state == nil {
self.Escape() self.Escape()

View file

@ -1,20 +1,26 @@
package helpers package helpers
import ( import (
"regexp"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type StagingHelper struct { type StagingHelper struct {
c *HelperCommon c *HelperCommon
windowHelper *WindowHelper
} }
func NewStagingHelper( func NewStagingHelper(
c *HelperCommon, c *HelperCommon,
windowHelper *WindowHelper,
) *StagingHelper { ) *StagingHelper {
return &StagingHelper{ return &StagingHelper{
c: c, c: c,
windowHelper: windowHelper,
} }
} }
@ -30,12 +36,16 @@ func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) {
} }
mainSelectedLineIdx := -1 mainSelectedLineIdx := -1
mainSelectedRealLineIdx := -1
secondarySelectedLineIdx := -1 secondarySelectedLineIdx := -1
secondarySelectedRealLineIdx := -1
if focusOpts.ClickedViewLineIdx > 0 { if focusOpts.ClickedViewLineIdx > 0 {
if secondaryFocused { if secondaryFocused {
secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx
secondarySelectedRealLineIdx = focusOpts.ClickedViewRealLineIdx
} else { } else {
mainSelectedLineIdx = focusOpts.ClickedViewLineIdx mainSelectedLineIdx = focusOpts.ClickedViewLineIdx
mainSelectedRealLineIdx = focusOpts.ClickedViewRealLineIdx
} }
} }
@ -63,11 +73,11 @@ func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) {
secondaryContext.GetMutex().Lock() secondaryContext.GetMutex().Lock()
mainContext.SetState( mainContext.SetState(
patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetView(), mainContext.GetState()), patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainSelectedRealLineIdx, mainContext.GetView(), mainContext.GetState()),
) )
secondaryContext.SetState( secondaryContext.SetState(
patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetView(), secondaryContext.GetState()), patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondarySelectedRealLineIdx, secondaryContext.GetView(), secondaryContext.GetState()),
) )
mainState := mainContext.GetState() mainState := mainContext.GetState()
@ -124,3 +134,20 @@ func (self *StagingHelper) secondaryStagingFocused() bool {
func (self *StagingHelper) mainStagingFocused() bool { func (self *StagingHelper) mainStagingFocused() bool {
return self.c.Context().CurrentStatic().GetKey() == self.c.Contexts().Staging.GetKey() 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
}

View file

@ -30,6 +30,13 @@ func NewMainViewController(
} }
func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
var goIntoDescription string
// We only want to show the "enter" menu item if the user config is true;
// leaving the description empty causes it to be hidden
if self.c.UserConfig().Gui.ShowSelectionInFocusedMainView {
goIntoDescription = self.c.Tr.EnterStaging
}
return []*types.Binding{ return []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.TogglePanel), Key: opts.GetKey(opts.Config.Universal.TogglePanel),
@ -43,6 +50,11 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
Handler: self.escape, Handler: self.escape,
Description: self.c.Tr.ExitFocusedMainView, Description: self.c.Tr.ExitFocusedMainView,
}, },
{
Key: opts.GetKey(opts.Config.Universal.GoInto),
Handler: self.enter,
Description: goIntoDescription,
},
{ {
// overriding this because we want to read all of the task's output before we start searching // overriding this because we want to read all of the task's output before we start searching
Key: opts.GetKey(opts.Config.Universal.StartSearch), Key: opts.GetKey(opts.Config.Universal.StartSearch),
@ -79,6 +91,14 @@ func (self *MainViewController) Context() types.Context {
return self.context return self.context
} }
func (self *MainViewController) GetOnFocus() func(types.OnFocusOpts) {
return func(opts types.OnFocusOpts) {
if opts.ClickedWindowName != "" {
self.context.GetView().FocusPoint(0, opts.ClickedViewLineIdx)
}
}
}
func (self *MainViewController) togglePanel() error { func (self *MainViewController) togglePanel() error {
if self.otherContext.GetView().Visible { if self.otherContext.GetView().Visible {
self.otherContext.SetParentContext(self.context.GetParentContext()) self.otherContext.SetParentContext(self.context.GetParentContext())
@ -93,7 +113,20 @@ func (self *MainViewController) escape() error {
return nil return nil
} }
func (self *MainViewController) enter() error {
parentCtx := self.context.GetParentContext()
if parentCtx.GetOnClickFocusedMainView() != nil {
return parentCtx.GetOnClickFocusedMainView()(
self.context.GetViewName(), self.context.GetView().SelectedLineIdx())
}
return nil
}
func (self *MainViewController) onClick(opts gocui.ViewMouseBindingOpts) error { func (self *MainViewController) onClick(opts gocui.ViewMouseBindingOpts) error {
if self.context.GetView().Highlight && opts.Y != opts.PreviousY {
return nil
}
parentCtx := self.context.GetParentContext() parentCtx := self.context.GetParentContext()
if parentCtx.GetOnClickFocusedMainView() != nil { if parentCtx.GetOnClickFocusedMainView() != nil {
return parentCtx.GetOnClickFocusedMainView()(self.context.GetViewName(), opts.Y) return parentCtx.GetOnClickFocusedMainView()(self.context.GetViewName(), opts.Y)

View file

@ -163,9 +163,15 @@ func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsO
return self.withRenderAndFocus(self.HandleMouseDown)() 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{ self.c.Context().Push(self.context, types.OnFocusOpts{
ClickedWindowName: self.context.GetWindowName(), ClickedWindowName: self.context.GetWindowName(),
ClickedViewLineIdx: opts.Y, ClickedViewLineIdx: opts.Y,
ClickedViewRealLineIdx: line,
}) })
return nil return nil

View file

@ -1,6 +1,9 @@
package controllers package controllers
import ( import (
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -47,6 +50,42 @@ func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOp
return bindings return bindings
} }
func (self *SwitchToDiffFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return func(mainViewName string, clickedLineIdx int) error {
clickedFile, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(mainViewName, clickedLineIdx)
if !ok {
return nil
}
if err := self.enter(); err != nil {
return err
}
context := self.c.Contexts().CommitFiles
var node *filetree.CommitFileNode
relativePath, err := filepath.Rel(self.c.Git().RepoPaths.RepoPath(), clickedFile)
if err != nil {
return err
}
relativePath = "./" + relativePath
context.CommitFileTreeViewModel.ExpandToPath(relativePath)
self.c.PostRefreshUpdate(context)
idx, ok := context.CommitFileTreeViewModel.GetIndexForPath(relativePath)
if !ok {
return nil
}
context.SetSelectedLineIdx(idx)
context.GetViewTrait().FocusPoint(
context.ModelIndexToViewIndex(idx))
node = context.GetSelected()
self.c.Contexts().CustomPatchBuilder.SetParentContext(self.context)
return self.c.Helpers().CommitFiles.EnterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: line, ClickedViewRealLineIdx: line})
}
}
func (self *SwitchToDiffFilesController) Context() types.Context { func (self *SwitchToDiffFilesController) Context() types.Context {
return self.context return self.context
} }

View file

@ -3,6 +3,7 @@ package controllers
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
) )
// This controller is for all contexts that can focus their main view. // This controller is for all contexts that can focus their main view.
@ -60,23 +61,31 @@ func (self *SwitchToFocusedMainViewController) Context() types.Context {
} }
func (self *SwitchToFocusedMainViewController) onClickMain(opts gocui.ViewMouseBindingOpts) error { func (self *SwitchToFocusedMainViewController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
return self.focusMainView("main") return self.focusMainView("main", opts.Y)
} }
func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error { func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error {
return self.focusMainView("secondary") return self.focusMainView("secondary", opts.Y)
} }
func (self *SwitchToFocusedMainViewController) handleFocusMainView() error { func (self *SwitchToFocusedMainViewController) handleFocusMainView() error {
return self.focusMainView("main") return self.focusMainView("main", -1)
} }
func (self *SwitchToFocusedMainViewController) focusMainView(mainViewName string) error { func (self *SwitchToFocusedMainViewController) focusMainView(mainViewName string, clickedViewLineIdx int) error {
mainViewContext := self.c.Helpers().Window.GetContextForWindow(mainViewName) mainViewContext := self.c.Helpers().Window.GetContextForWindow(mainViewName)
mainViewContext.SetParentContext(self.context) mainViewContext.SetParentContext(self.context)
if context, ok := mainViewContext.(types.ISearchableContext); ok { if context, ok := mainViewContext.(types.ISearchableContext); ok {
context.ClearSearchString() context.ClearSearchString()
} }
self.c.Context().Push(mainViewContext, types.OnFocusOpts{}) onFocusOpts := types.OnFocusOpts{ClickedWindowName: mainViewName}
if clickedViewLineIdx >= 0 {
onFocusOpts.ClickedViewLineIdx = clickedViewLineIdx
} else {
mainView := mainViewContext.GetView()
lineIdx := mainView.OriginY() + mainView.Height()/2
onFocusOpts.ClickedViewLineIdx = lo.Clamp(lineIdx, 0, mainView.LinesHeight()-1)
}
self.c.Context().Push(mainViewContext, onFocusOpts)
return nil return nil
} }

View file

@ -3,6 +3,7 @@ package controllers
import ( import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
) )
type ViewSelectionControllerFactory struct { type ViewSelectionControllerFactory struct {
@ -61,10 +62,21 @@ func (self *ViewSelectionController) handleLineChange(delta int) {
} }
v := self.Context().GetView() v := self.Context().GetView()
if delta < 0 { if self.context.GetView().Highlight {
v.ScrollUp(-delta) lineIdxBefore := v.CursorY() + v.OriginY()
lineIdxAfter := lo.Clamp(lineIdxBefore+delta, 0, v.ViewLinesHeight()-1)
if delta == -1 {
checkScrollUp(self.Context().GetViewTrait(), self.c.UserConfig(), lineIdxBefore, lineIdxAfter)
} else if delta == 1 {
checkScrollDown(self.Context().GetViewTrait(), self.c.UserConfig(), lineIdxBefore, lineIdxAfter)
}
v.FocusPoint(0, lineIdxAfter)
} else { } else {
v.ScrollDown(delta) if delta < 0 {
v.ScrollUp(-delta)
} else {
v.ScrollDown(delta)
}
} }
} }
@ -90,7 +102,11 @@ func (self *ViewSelectionController) handleNextPage() error {
func (self *ViewSelectionController) handleGotoTop() error { func (self *ViewSelectionController) handleGotoTop() error {
v := self.Context().GetView() v := self.Context().GetView()
self.handleLineChange(-v.ViewLinesHeight()) if self.context.GetView().Highlight {
v.FocusPoint(0, 0)
} else {
self.handleLineChange(-v.ViewLinesHeight())
}
return nil return nil
} }
@ -99,7 +115,11 @@ func (self *ViewSelectionController) handleGotoBottom() error {
manager.ReadToEnd(func() { manager.ReadToEnd(func() {
self.c.OnUIThread(func() error { self.c.OnUIThread(func() error {
v := self.Context().GetView() v := self.Context().GetView()
self.handleLineChange(v.ViewLinesHeight()) if self.context.GetView().Highlight {
v.FocusPoint(0, v.ViewLinesHeight()-1)
} else {
self.handleLineChange(v.ViewLinesHeight())
}
return nil return nil
}) })
}) })

View file

@ -446,6 +446,11 @@ func (gui *Gui) onUserConfigLoaded() error {
gui.g.Mouse = userConfig.Gui.MouseEvents gui.g.Mouse = userConfig.Gui.MouseEvents
if gui.State != nil {
gui.Contexts().Normal.SetHighlightOnFocus(userConfig.Gui.ShowSelectionInFocusedMainView)
gui.Contexts().NormalSecondary.SetHighlightOnFocus(userConfig.Gui.ShowSelectionInFocusedMainView)
}
// originally we could only hide the command log permanently via the config // originally we could only hide the command log permanently via the config
// but now we do it via state. So we need to still support the config for the // but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility. We're making use of short circuiting here // sake of backwards compatibility. We're making use of short circuiting here

View file

@ -39,7 +39,7 @@ const (
HUNK 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 oldState != nil && diff == oldState.diff && selectedLineIdx == -1 {
// if we're here then we can return the old state. If selectedLineIdx was not -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 // 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) viewLineIndices, patchLineIndices := wrapPatchLines(diff, view)
if selectedRealLineIdx != -1 {
selectedLineIdx = patch.PatchLineForLineNumber(selectedRealLineIdx)
}
rangeStartLineIdx := 0 rangeStartLineIdx := 0
if oldState != nil { if oldState != nil {
rangeStartLineIdx = oldState.rangeStartLineIdx rangeStartLineIdx = oldState.rangeStartLineIdx

View file

@ -220,6 +220,9 @@ type IViewTrait interface {
type OnFocusOpts struct { type OnFocusOpts struct {
ClickedWindowName string ClickedWindowName string
ClickedViewLineIdx int ClickedViewLineIdx int
// If not -1, takes precedence over ClickedViewLineIdx.
ClickedViewRealLineIdx int
} }
type OnFocusLostOpts struct { type OnFocusLostOpts struct {

View file

@ -519,6 +519,7 @@ type TranslationSet struct {
EmptyPatchError string EmptyPatchError string
EnterCommitFile string EnterCommitFile string
EnterCommitFileTooltip string EnterCommitFileTooltip string
EnterStaging string
ExitCustomPatchBuilder string ExitCustomPatchBuilder string
ExitFocusedMainView string ExitFocusedMainView string
EnterUpstream string EnterUpstream string
@ -1607,6 +1608,7 @@ func EnglishTranslationSet() *TranslationSet {
EmptyPatchError: "Patch is still empty. Add some files or lines to your patch first.", EmptyPatchError: "Patch is still empty. Add some files or lines to your patch first.",
EnterCommitFile: "Enter file / Toggle directory collapsed", EnterCommitFile: "Enter file / Toggle directory collapsed",
EnterCommitFileTooltip: "If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory.", EnterCommitFileTooltip: "If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory.",
EnterStaging: "Enter staging/patch building",
ExitCustomPatchBuilder: `Exit custom patch builder`, ExitCustomPatchBuilder: `Exit custom patch builder`,
ExitFocusedMainView: "Exit back to side panel", ExitFocusedMainView: "Exit back to side panel",
EnterUpstream: `Enter upstream as '<remote> <branchname>'`, EnterUpstream: `Enter upstream as '<remote> <branchname>'`,

View file

@ -527,6 +527,11 @@
"description": "If true, wrap lines in the staging view to the width of the view. This\nmakes it much easier to work with diffs that have long lines, e.g.\nparagraphs of markdown text.", "description": "If true, wrap lines in the staging view to the width of the view. This\nmakes it much easier to work with diffs that have long lines, e.g.\nparagraphs of markdown text.",
"default": true "default": true
}, },
"showSelectionInFocusedMainView": {
"type": "boolean",
"description": "If true, show a selection when the main view is focused.",
"default": false
},
"language": { "language": {
"type": "string", "type": "string",
"enum": [ "enum": [

View file

@ -92,6 +92,12 @@ type ViewMouseBinding struct {
type ViewMouseBindingOpts struct { type ViewMouseBindingOpts struct {
X int // i.e. origin x + cursor x X int // i.e. origin x + cursor x
Y int // i.e. origin y + cursor y Y int // i.e. origin y + cursor y
// the previous cursor right before the click; useful because by the time
// the event is dispatched to handlers, gocui has already set the cursor to
// the new position. This is useful for detecting double clicks.
PreviousX int
PreviousY int
} }
type GuiMutexes struct { type GuiMutexes struct {
@ -1375,6 +1381,8 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
newCx = lastCharForLine - v.ox newCx = lastCharForLine - v.ox
} }
} }
previousX := v.cx + v.ox
previousY := v.cy + v.oy
if !IsMouseScrollKey(ev.Key) { if !IsMouseScrollKey(ev.Key) {
v.SetCursor(newCx, newCy) v.SetCursor(newCx, newCy)
if v.Editable { if v.Editable {
@ -1397,7 +1405,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
} }
if IsMouseKey(ev.Key) { if IsMouseKey(ev.Key) {
opts := ViewMouseBindingOpts{X: newX, Y: newY} opts := ViewMouseBindingOpts{X: newX, Y: newY, PreviousX: previousX, PreviousY: previousY}
matched, err := g.execMouseKeybindings(v, ev, opts) matched, err := g.execMouseKeybindings(v, ev, opts)
if err != nil { if err != nil {
return err return err

View file

@ -326,7 +326,11 @@ func (v *View) IsSearching() bool {
} }
func (v *View) FocusPoint(cx int, cy int) { func (v *View) FocusPoint(cx int, cy int) {
lineCount := len(v.lines) v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.refreshViewLinesIfNeeded()
lineCount := len(v.viewLines)
if cy < 0 || cy > lineCount { if cy < 0 || cy > lineCount {
return return
} }
@ -1466,6 +1470,20 @@ func (v *View) Word(x, y int) (string, bool) {
return str[nl:nr], true 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 // indexFunc allows to split lines by words taking into account spaces
// and 0. // and 0.
func indexFunc(r rune) bool { func indexFunc(r rune) bool {