diff --git a/docs/Config.md b/docs/Config.md index 8fd1fd960c0..2b3fd1d449c 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -113,6 +113,9 @@ gui: # paragraphs of markdown text. 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' language: auto diff --git a/pkg/commands/patch/patch.go b/pkg/commands/patch/patch.go index 5e4f9a84665..68c8e609b53 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/config/user_config.go b/pkg/config/user_config.go index d35e35772e9..5458da47674 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -105,6 +105,8 @@ type GuiConfig struct { // makes it much easier to work with diffs that have long lines, e.g. // paragraphs of markdown text. 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' 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. @@ -735,23 +737,24 @@ type IconProperties struct { func GetDefaultConfig() *UserConfig { return &UserConfig{ Gui: GuiConfig{ - ScrollHeight: 2, - ScrollPastBottom: true, - ScrollOffMargin: 2, - ScrollOffBehavior: "margin", - TabWidth: 4, - MouseEvents: true, - SkipDiscardChangeWarning: false, - SkipStashWarning: false, - SidePanelWidth: 0.3333, - ExpandFocusedSidePanel: false, - ExpandedSidePanelWeight: 2, - MainPanelSplitMode: "flexible", - EnlargedSideViewLocation: "left", - WrapLinesInStagingView: true, - Language: "auto", - TimeFormat: "02 Jan 06", - ShortTimeFormat: time.Kitchen, + ScrollHeight: 2, + ScrollPastBottom: true, + ScrollOffMargin: 2, + ScrollOffBehavior: "margin", + TabWidth: 4, + MouseEvents: true, + SkipDiscardChangeWarning: false, + SkipStashWarning: false, + SidePanelWidth: 0.3333, + ExpandFocusedSidePanel: false, + ExpandedSidePanelWeight: 2, + MainPanelSplitMode: "flexible", + EnlargedSideViewLocation: "left", + WrapLinesInStagingView: true, + ShowSelectionInFocusedMainView: false, + Language: "auto", + TimeFormat: "02 Jan 06", + ShortTimeFormat: time.Kitchen, Theme: ThemeConfig{ ActiveBorderColor: []string{"green", "bold"}, SearchingActiveBorderColor: []string{"cyan", "bold"}, diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index b72374392d9..5c1fd05b044 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -228,3 +228,7 @@ func (self *BaseContext) Title() string { func (self *BaseContext) TotalContentHeight() int { return self.view.ViewLinesHeight() } + +func (self *BaseContext) SetHighlightOnFocus(value bool) { + self.highlightOnFocus = value +} diff --git a/pkg/gui/context/main_context.go b/pkg/gui/context/main_context.go index 66babac0362..a0189ef0e9a 100644 --- a/pkg/gui/context/main_context.go +++ b/pkg/gui/context/main_context.go @@ -26,7 +26,7 @@ func NewMainContext( WindowName: windowName, Key: key, Focusable: true, - HighlightOnFocus: false, + HighlightOnFocus: c.UserConfig().Gui.ShowSelectionInFocusedMainView, })), SearchTrait: NewSearchTrait(c), } diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index abbb621551a..78657c69d6b 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, @@ -115,6 +115,7 @@ func (gui *Gui) resetHelpersAndControllers() { AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper), FixupHelper: helpers.NewFixupHelper(helperCommon), Commits: commitsHelper, + CommitFiles: helpers.NewCommitFilesHelper(helperCommon), Snake: helpers.NewSnakeHelper(helperCommon), Diff: diffHelper, Repos: reposHelper, diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 8beeb6ff268..b7f5f72c5e6 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -390,7 +390,7 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm toggle := func() error { return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error { if !self.c.Git().Patch.PatchBuilder.Active() { - if err := self.startPatchBuilder(); err != nil { + if err := self.c.Helpers().CommitFiles.StartPatchBuilder(); err != nil { 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) { self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.DiscardPatch, @@ -451,72 +451,8 @@ func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode) 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 { - return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -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 + return self.c.Helpers().CommitFiles.EnterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1, ClickedViewRealLineIdx: -1}) } // 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 { return func(mainViewName string, clickedLineIdx int) error { + clickedFile, line, ok := self.c.Helpers().Staging.GetFileAndLineForClickedDiffLine(mainViewName, clickedLineIdx) + if !ok { + line = -1 + } + node := self.getSelectedItem() - if node != nil && node.File != nil { - return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx}) + if node == nil { + return 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}) } } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 6df28a523ed..9f1a76873bf 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/commit_files_helper.go b/pkg/gui/controllers/helpers/commit_files_helper.go new file mode 100644 index 00000000000..03cc494f16d --- /dev/null +++ b/pkg/gui/controllers/helpers/commit_files_helper.go @@ -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 +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 1f1050dc974..ddfe0170c26 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -35,6 +35,7 @@ type Helpers struct { AmendHelper *AmendHelper FixupHelper *FixupHelper Commits *CommitsHelper + CommitFiles *CommitFilesHelper Snake *SnakeHelper // lives in context package because our contexts need it to render to main Diff *DiffHelper @@ -73,6 +74,7 @@ func NewStubHelpers() *Helpers { AmendHelper: &AmendHelper{}, FixupHelper: &FixupHelper{}, Commits: &CommitsHelper{}, + CommitFiles: &CommitFilesHelper{}, Snake: &SnakeHelper{}, Diff: &DiffHelper{}, Repos: &ReposHelper{}, diff --git a/pkg/gui/controllers/helpers/patch_building_helper.go b/pkg/gui/controllers/helpers/patch_building_helper.go index 73d0fb608b6..6b41c241afc 100644 --- a/pkg/gui/controllers/helpers/patch_building_helper.go +++ b/pkg/gui/controllers/helpers/patch_building_helper.go @@ -29,7 +29,11 @@ func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) // takes us from the patch building panel back to the commit files panel 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 @@ -53,8 +57,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 +92,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 3d976254171..8ab4ce418e8 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/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index e6b209286ba..289e2e1013d 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -30,6 +30,13 @@ func NewMainViewController( } 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{ { Key: opts.GetKey(opts.Config.Universal.TogglePanel), @@ -43,6 +50,11 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty Handler: self.escape, 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 Key: opts.GetKey(opts.Config.Universal.StartSearch), @@ -79,6 +91,14 @@ func (self *MainViewController) Context() types.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 { if self.otherContext.GetView().Visible { self.otherContext.SetParentContext(self.context.GetParentContext()) @@ -93,7 +113,20 @@ func (self *MainViewController) escape() error { 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 { + if self.context.GetView().Highlight && opts.Y != opts.PreviousY { + return nil + } + parentCtx := self.context.GetParentContext() if parentCtx.GetOnClickFocusedMainView() != nil { return parentCtx.GetOnClickFocusedMainView()(self.context.GetViewName(), opts.Y) diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go index 5b832c80d34..cd225b10a5e 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/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go index fc764ca7edd..f8e7d314831 100644 --- a/pkg/gui/controllers/switch_to_diff_files_controller.go +++ b/pkg/gui/controllers/switch_to_diff_files_controller.go @@ -1,6 +1,9 @@ package controllers import ( + "path/filepath" + + "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -47,6 +50,42 @@ func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOp 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 { return self.context } diff --git a/pkg/gui/controllers/switch_to_focused_main_view_controller.go b/pkg/gui/controllers/switch_to_focused_main_view_controller.go index cb03f5e1516..27d35e90bce 100644 --- a/pkg/gui/controllers/switch_to_focused_main_view_controller.go +++ b/pkg/gui/controllers/switch_to_focused_main_view_controller.go @@ -3,6 +3,7 @@ package controllers import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) // 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 { - return self.focusMainView("main") + return self.focusMainView("main", opts.Y) } func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error { - return self.focusMainView("secondary") + return self.focusMainView("secondary", opts.Y) } 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.SetParentContext(self.context) if context, ok := mainViewContext.(types.ISearchableContext); ok { 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 } diff --git a/pkg/gui/controllers/view_selection_controller.go b/pkg/gui/controllers/view_selection_controller.go index 638c46ba608..2f7f3b17283 100644 --- a/pkg/gui/controllers/view_selection_controller.go +++ b/pkg/gui/controllers/view_selection_controller.go @@ -3,6 +3,7 @@ package controllers import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type ViewSelectionControllerFactory struct { @@ -61,10 +62,21 @@ func (self *ViewSelectionController) handleLineChange(delta int) { } v := self.Context().GetView() - if delta < 0 { - v.ScrollUp(-delta) + if self.context.GetView().Highlight { + 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 { - 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 { v := self.Context().GetView() - self.handleLineChange(-v.ViewLinesHeight()) + if self.context.GetView().Highlight { + v.FocusPoint(0, 0) + } else { + self.handleLineChange(-v.ViewLinesHeight()) + } return nil } @@ -99,7 +115,11 @@ func (self *ViewSelectionController) handleGotoBottom() error { manager.ReadToEnd(func() { self.c.OnUIThread(func() error { 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 }) }) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 90a2a64d48f..d4eb97348e3 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -446,6 +446,11 @@ func (gui *Gui) onUserConfigLoaded() error { 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 // 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 diff --git a/pkg/gui/patch_exploring/state.go b/pkg/gui/patch_exploring/state.go index 73ce0a756b6..7520a0c5a65 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 f76bedeb661..9351fa5831f 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -220,6 +220,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/pkg/i18n/english.go b/pkg/i18n/english.go index a85f7202fdc..8f09c42a73f 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -519,6 +519,7 @@ type TranslationSet struct { EmptyPatchError string EnterCommitFile string EnterCommitFileTooltip string + EnterStaging string ExitCustomPatchBuilder string ExitFocusedMainView string EnterUpstream string @@ -1607,6 +1608,7 @@ func EnglishTranslationSet() *TranslationSet { EmptyPatchError: "Patch is still empty. Add some files or lines to your patch first.", 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.", + EnterStaging: "Enter staging/patch building", ExitCustomPatchBuilder: `Exit custom patch builder`, ExitFocusedMainView: "Exit back to side panel", EnterUpstream: `Enter upstream as ' '`, diff --git a/schema/config.json b/schema/config.json index 96a968e4d10..74e00bee44f 100644 --- a/schema/config.json +++ b/schema/config.json @@ -528,6 +528,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.", "default": true }, + "showSelectionInFocusedMainView": { + "type": "boolean", + "description": "If true, show a selection when the main view is focused.", + "default": false + }, "language": { "type": "string", "enum": [ diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 03d55912a3f..f7266646bb7 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -92,6 +92,12 @@ type ViewMouseBinding struct { type ViewMouseBindingOpts struct { X int // i.e. origin x + cursor x 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 { @@ -1375,6 +1381,8 @@ func (g *Gui) onKey(ev *GocuiEvent) error { newCx = lastCharForLine - v.ox } } + previousX := v.cx + v.ox + previousY := v.cy + v.oy if !IsMouseScrollKey(ev.Key) { v.SetCursor(newCx, newCy) if v.Editable { @@ -1397,7 +1405,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } 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) if err != nil { return err diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 8c95bbe351c..b1ac1521131 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -326,7 +326,11 @@ func (v *View) IsSearching() bool { } 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 { return } @@ -1466,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 {