diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 5f3598c93..fe18e7316 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -224,7 +224,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | Scroll up | | | `` `` | Switch view | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Stage | Toggle selection staged / unstaged. | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | | `` N `` | Go to previous file | | @@ -334,7 +334,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Switch view | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Stage | Toggle selection staged / unstaged. | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index f763599f6..fe4b5ccc2 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -193,7 +193,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 | | `` `` | サイドパネルに戻る | | -| `` `` | Show/hide selection | | +| `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | | `` N `` | Go to previous file | | @@ -311,7 +311,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | 上にスクロール | | | `` `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 | | `` `` | サイドパネルに戻る | | -| `` `` | Show/hide selection | | +| `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index 907d4df20..97229aed7 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -85,7 +85,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | 패널 전환 | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Staged 전환 | 선택한 행을 staged / unstaged | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | | `` N `` | Go to previous file | | @@ -168,7 +168,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | 위로 스크롤 | | | `` `` | 패널 전환 | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Staged 전환 | 선택한 행을 staged / unstaged | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index c172069bc..4b24387d6 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -232,7 +232,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | Scroll omhoog | | | `` `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Toggle staged | Toggle lijnen staged / unstaged | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | | `` N `` | Go to previous file | | @@ -312,7 +312,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Toggle staged | Toggle lijnen staged / unstaged | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index eab96368c..5d2d0dd68 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -100,7 +100,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | | `` N `` | Go to previous file | | @@ -207,7 +207,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | Przewiń w górę | | | `` `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 7eb473481..2e6f87549 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -236,7 +236,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | Rolar para cima | | | `` `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | | `` N `` | Go to previous file | | @@ -343,7 +343,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 83bc6cba0..527ff4cdd 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -75,7 +75,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | | `` N `` | Go to previous file | | @@ -112,7 +112,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | Прокрутить вверх | | | `` `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index b9735b50a..ec174c05c 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -287,7 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) | | `` `` | 退出回到侧边面板 | | -| `` `` | Show/hide selection | | +| `` `` | 切换暂存状态 | 切换行暂存状态 | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | | `` N `` | Go to previous file | | @@ -340,7 +340,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | 向上滚动 | | | `` `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) | | `` `` | 退出回到侧边面板 | | -| `` `` | Show/hide selection | | +| `` `` | 切换暂存状态 | 切换行暂存状态 | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | | `` N `` | Go to previous file | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 6551f92b6..3605b5e91 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -83,7 +83,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` (fn+down) `` | 向上捲動 | | | `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | | `` N `` | Go to previous file | | @@ -369,7 +369,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct |-----|--------|-------------| | `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | -| `` `` | Show/hide selection | | +| `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | | `` N `` | Go to previous file | | diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index 7584b5a12..047ac17c0 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -18,6 +18,7 @@ type BaseContext struct { onDoubleClickFn func() error onClickFn func(opts gocui.ViewMouseBindingOpts) error onClickFocusedMainViewFn onClickFocusedMainViewFn + onStageFocusedMainViewFn onStageFocusedMainViewFn onRenderToMainFn func() onFocusFns []onFocusFn onFocusLostFns []onFocusLostFn @@ -37,6 +38,7 @@ type ( onFocusFn = func(types.OnFocusOpts) onFocusLostFn = func(types.OnFocusLostOpts) onClickFocusedMainViewFn = func(mainViewName string, clickedLineIdx int) error + onStageFocusedMainViewFn = func(mainViewName string, viewLineIdx int) error ) var _ types.IBaseContext = &BaseContext{} @@ -146,6 +148,7 @@ func (self *BaseContext) ClearAllAttachedControllerFunctions() { self.onDoubleClickFn = nil self.onClickFn = nil self.onClickFocusedMainViewFn = nil + self.onStageFocusedMainViewFn = nil self.onRenderToMainFn = nil } @@ -188,6 +191,19 @@ func (self *BaseContext) GetOnClickFocusedMainView() onClickFocusedMainViewFn { return self.onClickFocusedMainViewFn } +func (self *BaseContext) AddOnStageFocusedMainViewFn(fn onStageFocusedMainViewFn) { + if fn != nil { + if self.onStageFocusedMainViewFn != nil { + panic("only one controller is allowed to set an onStageFocusedMainViewFn") + } + self.onStageFocusedMainViewFn = fn + } +} + +func (self *BaseContext) GetOnStageFocusedMainView() onStageFocusedMainViewFn { + return self.onStageFocusedMainViewFn +} + func (self *BaseContext) AddOnRenderToMainFn(fn func()) { if fn != nil { if self.onRenderToMainFn != nil { diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go index f819a2eb4..328e44173 100644 --- a/pkg/gui/context/commit_files_context.go +++ b/pkg/gui/context/commit_files_context.go @@ -19,11 +19,14 @@ type CommitFilesContext struct { } var ( - _ types.IListContext = (*CommitFilesContext)(nil) - _ types.DiffableContext = (*CommitFilesContext)(nil) - _ types.IFilterableContext = (*CommitFilesContext)(nil) + _ types.IListContext = (*CommitFilesContext)(nil) + _ types.DiffableContext = (*CommitFilesContext)(nil) + _ types.IFilterableContext = (*CommitFilesContext)(nil) + _ types.DiffMainViewContext = (*CommitFilesContext)(nil) ) +func (self *CommitFilesContext) IsDiffMainViewContext() {} + func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext { viewModel := filetree.NewCommitFileTreeViewModel( func() []*models.CommitFile { return c.Model().CommitFiles }, diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 4a99259fd..28bbb3409 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -31,11 +31,14 @@ type commitDropIndicator struct { } var ( - _ types.IListContext = (*LocalCommitsContext)(nil) - _ types.DiffableContext = (*LocalCommitsContext)(nil) - _ types.ISearchableContext = (*LocalCommitsContext)(nil) + _ types.IListContext = (*LocalCommitsContext)(nil) + _ types.DiffableContext = (*LocalCommitsContext)(nil) + _ types.ISearchableContext = (*LocalCommitsContext)(nil) + _ types.DiffMainViewContext = (*LocalCommitsContext)(nil) ) +func (self *LocalCommitsContext) IsDiffMainViewContext() {} + func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { dropIndicator := &commitDropIndicator{insertionIndex: -1} viewModel := NewLocalCommitsViewModel( diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index 6358fbbb0..f3544d01e 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -14,10 +14,13 @@ type ReflogCommitsContext struct { } var ( - _ types.IListContext = (*ReflogCommitsContext)(nil) - _ types.DiffableContext = (*ReflogCommitsContext)(nil) + _ types.IListContext = (*ReflogCommitsContext)(nil) + _ types.DiffableContext = (*ReflogCommitsContext)(nil) + _ types.DiffMainViewContext = (*ReflogCommitsContext)(nil) ) +func (self *ReflogCommitsContext) IsDiffMainViewContext() {} + func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { viewModel := NewFilteredListViewModel( func() []*models.Commit { return c.Model().FilteredReflogCommits }, diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index 2014de9f3..50bddf5a2 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -12,10 +12,13 @@ type StashContext struct { } var ( - _ types.IListContext = (*StashContext)(nil) - _ types.DiffableContext = (*StashContext)(nil) + _ types.IListContext = (*StashContext)(nil) + _ types.DiffableContext = (*StashContext)(nil) + _ types.DiffMainViewContext = (*StashContext)(nil) ) +func (self *StashContext) IsDiffMainViewContext() {} + func NewStashContext( c *ContextCommon, ) *StashContext { diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index b0bcee30a..a41daa6a5 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -21,11 +21,14 @@ type SubCommitsContext struct { } var ( - _ types.IListContext = (*SubCommitsContext)(nil) - _ types.DiffableContext = (*SubCommitsContext)(nil) - _ types.ISearchableContext = (*SubCommitsContext)(nil) + _ types.IListContext = (*SubCommitsContext)(nil) + _ types.DiffableContext = (*SubCommitsContext)(nil) + _ types.ISearchableContext = (*SubCommitsContext)(nil) + _ types.DiffMainViewContext = (*SubCommitsContext)(nil) ) +func (self *SubCommitsContext) IsDiffMainViewContext() {} + func NewSubCommitsContext( c *ContextCommon, ) *SubCommitsContext { diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go index d82037e44..51cc169c1 100644 --- a/pkg/gui/context/working_tree_context.go +++ b/pkg/gui/context/working_tree_context.go @@ -15,10 +15,13 @@ type WorkingTreeContext struct { } var ( - _ types.IListContext = (*WorkingTreeContext)(nil) - _ types.IFilterableContext = (*WorkingTreeContext)(nil) + _ types.IListContext = (*WorkingTreeContext)(nil) + _ types.IFilterableContext = (*WorkingTreeContext)(nil) + _ types.DiffMainViewContext = (*WorkingTreeContext)(nil) ) +func (self *WorkingTreeContext) IsDiffMainViewContext() {} + func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext { viewModel := filetree.NewFileTreeViewModel( func() []*models.File { return c.Model().Files }, diff --git a/pkg/gui/controllers/attach.go b/pkg/gui/controllers/attach.go index c9ef5d4b0..bbce20478 100644 --- a/pkg/gui/controllers/attach.go +++ b/pkg/gui/controllers/attach.go @@ -9,6 +9,7 @@ func AttachControllers(context types.Context, controllers ...types.IController) context.AddOnDoubleClickFn(controller.GetOnDoubleClick()) context.AddOnClickFn(controller.GetOnClick()) context.AddOnClickFocusedMainViewFn(controller.GetOnClickFocusedMainView()) + context.AddOnStageFocusedMainViewFn(controller.GetOnStageFocusedMainView()) context.AddOnRenderToMainFn(controller.GetOnRenderToMain()) context.AddOnFocusFn(controller.GetOnFocus()) context.AddOnFocusLostFn(controller.GetOnFocusLost()) diff --git a/pkg/gui/controllers/base_controller.go b/pkg/gui/controllers/base_controller.go index f91f0b4cc..a936af94c 100644 --- a/pkg/gui/controllers/base_controller.go +++ b/pkg/gui/controllers/base_controller.go @@ -23,6 +23,10 @@ func (self *baseController) GetOnClickFocusedMainView() func(mainViewName string return nil } +func (self *baseController) GetOnStageFocusedMainView() func(mainViewName string, viewLineIdx int) error { + return nil +} + func (self *baseController) GetOnClick() func(opts gocui.ViewMouseBindingOpts) error { return nil } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 2e448ae24..f0e2df951 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/filetree" @@ -456,6 +457,72 @@ func (self *FilesController) diffSplitState(node *filetree.FileNode) (split bool return split, mainShowsStaged } +func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName string, viewLineIdx int) error { + return func(mainViewName string, viewLineIdx int) error { + if self.c.UserConfig().Git.DiffContextSize == 0 { + return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage, + self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) + } + + node := self.context().GetSelected() + if node == nil || !node.IsFile() { + // Staging a line of a multi-file (directory) diff is a later step; for + // now only single-file diffs are stageable from the focused main view. + return nil + } + + info, ok := self.c.Helpers().Staging.GetDiffLineInfo(mainViewName, viewLineIdx) + if !ok { + return nil + } + + // The staged diff is shown in the secondary half of a split, and in the main + // half when the file has only staged changes; in those cases space unstages, + // otherwise it stages. + _, mainShowsStaged := self.diffSplitState(node) + staged := mainShowsStaged || mainViewName == self.c.Contexts().NormalSecondary.GetViewName() + + return self.stageDiffLine(node.File, info, staged) + } +} + +// stageDiffLine stages, or when reverse is true unstages, the single diff line +// identified by info. It builds a one-line patch from the file's diff and applies +// it the same way the staging view does, but resolves the patch line from the +// diff-line metadata rather than from a patch-explorer selection. A context or +// header line yields an empty patch and is a no-op. +func (self *FilesController) stageDiffLine(file *models.File, info types.DiffLineInfo, reverse bool) error { + parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, true, reverse)) + + lineNumber, isDeletion := info.PatchSelectLine() + patchLineIdx := parsedPatch.PatchLineForLineNumber(lineNumber) + if isDeletion { + patchLineIdx = parsedPatch.PatchLineForOldLineNumber(lineNumber) + } + + patchToApply := parsedPatch. + Transform(patch.TransformOpts{ + Reverse: reverse, + IncludedLineIndices: []int{patchLineIdx}, + FileNameOverride: file.GetPath(), + }). + FormatPlain() + if patchToApply == "" { + return nil + } + + self.c.LogAction(self.c.Tr.Actions.ApplyPatch) + if err := self.c.Git().Patch.ApplyPatch(patchToApply, git_commands.ApplyPatchOpts{ + Reverse: reverse, + Cached: true, + }); err != nil { + return err + } + + self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}}) + return nil +} + // if we are dealing with a status for which there is no key in this map, // then we won't optimistically render: we'll just let `git status` tell // us what the new status is. diff --git a/pkg/gui/controllers/helpers/diff_line_navigation.go b/pkg/gui/controllers/helpers/diff_line_navigation.go index 5fb3a7015..2f2302fb2 100644 --- a/pkg/gui/controllers/helpers/diff_line_navigation.go +++ b/pkg/gui/controllers/helpers/diff_line_navigation.go @@ -58,6 +58,29 @@ func (self *StagingHelper) AdjacentFile(view *gocui.View, anchorViewLine int, fo return view.ViewLineForBufferLine(target) } +// FirstChangeLineInView returns the view line of the first change line at or below +// the top of the viewport, for placing the initial selection when focusing the main +// view: we select the first change the user can already see rather than jumping to +// the top of the diff (which would be jarring when the view is scrolled down). If +// the top visible line is itself mid-change-block, that line is returned, so the +// selection stays put. ok is false when no change line is loaded at or below the top +// (e.g. scrolled into trailing context, or the diff isn't loaded that far yet), +// leaving the caller to fall back. +func (self *StagingHelper) FirstChangeLineInView(view *gocui.View) (int, bool) { + top, ok := view.BufferLineForViewLine(view.OriginY()) + if !ok { + return 0, false + } + + resolved := self.resolveDiffLines(view.DiffLineContents()) + for i := top; i < len(resolved); i++ { + if resolved[i].ok && resolved[i].info.IsChange() { + return view.ViewLineForBufferLine(i) + } + } + return 0, false +} + // changeBlockStart finds, in a diff whose lines are flagged by isChange, the first // line of the change block adjacent to `from` in the given direction. It is the pure // index arithmetic behind AdjacentChangeBlock, mirroring the staging view's diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 8b1205749..822c1b562 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -38,9 +38,10 @@ func NewMainViewController( } func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { - // When a selection is shown, we surface the bindings that act on it + // A selection is shown whenever the main view holds a diff (see + // sidePanelShowsDiff); we surface the bindings that act on it // (enter to dive into staging, e to edit the selected line, G to open the - // line in the branch's pull request, escape to hide the selection). + // line in the branch's pull request). selectionShown := self.context.GetView().Highlight var enterDescription string @@ -73,9 +74,10 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty }, { Keys: opts.GetKeys(opts.Config.Universal.Select), - Handler: self.toggleSelection, - Description: self.c.Tr.ToggleSelectionInFocusedMainView, - DisplayOnScreen: !selectionShown, + Handler: self.stageSelectedLine, + Description: self.c.Tr.Stage, + Tooltip: self.c.Tr.StageSelectionTooltip, + DisplayOnScreen: selectionShown, }, { Keys: opts.GetKeys(opts.Config.Universal.GoInto), @@ -180,32 +182,57 @@ func (self *MainViewController) GetOnFocus() func(types.OnFocusOpts) { } func (self *MainViewController) togglePanel() error { - if self.otherContext.GetView().Visible { - self.c.Context().Push(self.otherContext, types.OnFocusOpts{}) + if !self.otherContext.GetView().Visible { + return nil } + // Capture diff-view-ness while our context is still the focused main view (so + // NextInStack finds the side panel beneath it), before pushing the other pane. + isDiff := self.isDiffView() + self.c.Context().Push(self.otherContext, types.OnFocusOpts{}) + if isDiff { + showInitialDiffSelection(self.c, self.otherContext.GetView()) + } return nil } -func (self *MainViewController) escape() error { - v := self.context.GetView() - if v.Highlight { - v.Highlight = false - return nil +// showInitialDiffSelection turns on the focused main view's selection when entering +// a diff view without pointing at a specific line: on the first change line already +// visible (so the view doesn't jump), falling back to the current top line when none +// is visible (scrolled into trailing context, or not loaded that far yet). +func showInitialDiffSelection(c *ControllerCommon, view *gocui.View) { + target, ok := c.Helpers().Staging.FirstChangeLineInView(view) + if !ok { + target = view.OriginY() } + showSelectionAtLine(view, target, true) +} + +func (self *MainViewController) escape() error { self.c.Context().Pop() return nil } -func (self *MainViewController) toggleSelection() error { - v := self.context.GetView() - if v.Highlight { - v.Highlight = false +// isDiffView reports whether the focused main view currently shows a diff (so we +// show a selection in it). See sidePanelShowsDiff. +func (self *MainViewController) isDiffView() bool { + return sidePanelShowsDiff(self.c.Context().NextInStack(self.context)) +} + +// stageSelectedLine stages (or unstages) the selected diff line, delegating to the +// side panel beneath the focused main view since what "stage" means is the panel's +// business (the working tree stages; later, commits add to a custom patch). Panels +// whose diff isn't stageable register no handler, so this is a no-op there. +func (self *MainViewController) stageSelectedLine() error { + sidePanelContext := self.c.Context().NextInStack(self.context) + if sidePanelContext == nil { return nil } - // Start the selection in the middle of the visible content. - showSelectionAtLine(v, v.MiddleVisibleLineIdx(), false) - return nil + handler := sidePanelContext.GetOnStageFocusedMainView() + if handler == nil { + return nil + } + return handler(self.context.GetViewName(), self.context.GetView().SelectedLineIdx()) } func (self *MainViewController) enter() error { @@ -279,6 +306,16 @@ func (self *MainViewController) prevFile() error { return self.navigate(self.c.Helpers().Staging.AdjacentFile, false) } +// sidePanelShowsDiff reports whether the given side panel's focused main view +// shows a diff, which is when we show a selection in it (so the user can stage a +// line, edit it, jump by hunk/file, or open it in a PR). Panels whose main view +// shows non-diff content (a branch's commit log, the status dashboard, …) show no +// selection because there's nothing to act on. See types.DiffMainViewContext. +func sidePanelShowsDiff(sidePanel types.Context) bool { + _, ok := sidePanel.(types.DiffMainViewContext) + return ok +} + // focusedMainViewContextForViewName maps a focused main view's view name (as // passed to GetOnClickFocusedMainView) to its context. func focusedMainViewContextForViewName(c *ControllerCommon, viewName string) types.Context { @@ -422,6 +459,9 @@ func githubPullRequestLineURL(prURL string, commitSha string, relativePath strin } func (self *MainViewController) onClickInAlreadyFocusedView(opts gocui.ViewMouseBindingOpts) error { + if !self.isDiffView() { + return nil + } // A click points at a line, so it sets the selection there; a double-click // additionally dives into staging/patch-building for that line. showSelectionAtLine(self.context.GetView(), opts.Y, false) @@ -437,6 +477,9 @@ func (self *MainViewController) editClickedLine(opts gocui.ViewMouseBindingOpts) func (self *MainViewController) onClickInOtherViewOfMainViewPair(opts gocui.ViewMouseBindingOpts) error { self.c.Context().Push(self.context, types.OnFocusOpts{}) + if !self.isDiffView() { + return nil + } showSelectionAtLine(self.context.GetView(), opts.Y, false) if opts.IsDoubleClick { return self.enterForLine(opts.Y) 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 fcafea3b8..21c1a44c1 100644 --- a/pkg/gui/controllers/switch_to_focused_main_view_controller.go +++ b/pkg/gui/controllers/switch_to_focused_main_view_controller.go @@ -69,9 +69,11 @@ func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewM } func (self *SwitchToFocusedMainViewController) handleFocusMainView() error { - // Focusing by keyboard doesn't point at any particular line, so we don't - // show a selection; the user is free to scroll. Clicking does point at a - // line, so it selects it (see focusMainView's clickedLineIdx). + // Focusing by keyboard doesn't point at any particular line: in a diff view + // we start at the first change block (like entering the staging view), in a + // non-diff view we show no selection and the user just scrolls. Clicking does + // point at a line, so it selects that line instead (focusMainView's + // clickedLineIdx). return self.focusMainView(self.c.Contexts().Normal, -1) } @@ -80,8 +82,18 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext typ context.ClearSearchString() } self.c.Context().Push(mainViewContext, types.OnFocusOpts{}) + + if !sidePanelShowsDiff(self.context) { + // Non-diff main content (e.g. a branch's commit log): focus only, no + // selection, since there's nothing to act on. + return nil + } + + view := mainViewContext.GetView() if clickedLineIdx >= 0 { - showSelectionAtLine(mainViewContext.GetView(), clickedLineIdx, false) + showSelectionAtLine(view, clickedLineIdx, false) + } else { + showInitialDiffSelection(self.c, view) } return nil } diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 79c0a9f0a..ec8b7faf9 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -98,6 +98,9 @@ type IBaseContext interface { // Likewise for the focused main view: we need this to communicate between a // side panel controller and the focused main view controller. AddOnClickFocusedMainViewFn(func(mainViewName string, clickedLineIdx int) error) + // And for staging the selected line directly from the focused main view (space), + // delegated to the side panel that owns the diff being shown. + AddOnStageFocusedMainViewFn(func(mainViewName string, viewLineIdx int) error) // Adding on to the above, this is so that a list-specific handler can register // a hook for doing additional click handling AddOnClickFn(func(opts gocui.ViewMouseBindingOpts) error) @@ -174,6 +177,20 @@ type DiffableContext interface { RefForAdjustingLineNumberInDiff() string } +// DiffMainViewContext is implemented by the side panel contexts whose focused +// main view shows a unified diff — files, local commits, sub-commits, reflog, +// stash, and commit files — as opposed to a commit log or other non-diff content +// (branches, tags, status, …). It is distinct from DiffableContext, which is +// about producing a diff between two refs for the diff menu. This is the signal +// for whether to show a selection in the focused main view: a selection is only +// meaningful where there are diff lines to act on (stage, edit, jump by hunk, +// open in a pull request). +type DiffMainViewContext interface { + Context + + IsDiffMainViewContext() +} + type IListContext interface { Context @@ -314,6 +331,11 @@ type HasKeybindings interface { // Implement this in a side-panel controller to get called when there's a click in the main view // that belongs to your panel while the main view is already focused. GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error + + // Implement this in a side-panel controller to stage/unstage (or, later, add to + // the custom patch) the selected diff line when the user presses space in the + // focused main view. Return nil to do nothing. + GetOnStageFocusedMainView() func(mainViewName string, viewLineIdx int) error } type IController interface {