diff --git a/docs-master/Config.md b/docs-master/Config.md index ee1c7ff43..d35455c84 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -709,6 +709,7 @@ keybinding: - "4" - "5" focusMainView: "0" + jumpToFile: nextMatch: "n" prevMatch: "N" startSearch: / diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 2aac3e182..992f34dc9 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | Prev screen mode | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | Cancel | | | `` ? `` | Open keybindings menu | | | `` `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. | @@ -234,6 +235,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Go to next hunk | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit | Commit staged changes. | @@ -317,6 +319,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Go to next hunk | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit | Commit staged changes. | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index d273dbd2e..33f8a59b3 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | 前の画面モード | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | キャンセル | | | `` ? `` | キーバインディングメニューを開く | | | `` `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 | @@ -203,6 +204,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 次のハンクに移動 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | サイドパネルに戻る | | | `` c `` | コミット | ステージされた変更をコミットします。 | @@ -294,6 +296,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 次のハンクに移動 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | サイドパネルに戻る | | | `` c `` | コミット | ステージされた変更をコミットします。 | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index 4c28bfaa6..88a272d5b 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | 이전 스크린 모드 | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | 취소 | | | `` ? `` | 매뉴 열기 | | | `` `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. | @@ -95,6 +96,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 다음 hunk를 선택 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. | @@ -189,6 +191,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 다음 hunk를 선택 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 6fb5291a9..df0d2fa25 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | Vorige scherm modus | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | Annuleren | | | `` ? `` | Open menu | | | `` `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. | @@ -242,6 +243,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Selecteer de volgende hunk | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit veranderingen | Commit gestagede wijzigingen. | @@ -317,6 +319,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Selecteer de volgende hunk | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit veranderingen | Commit gestagede wijzigingen. | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 46117e5fb..940a6181e 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | Poprzedni tryb ekranu | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | Anuluj | | | `` ? `` | Otwórz menu przypisań klawiszy | | | `` `` | Pokaż opcje filtrowania | Pokaż opcje filtrowania dziennika commitów, tak aby pokazywane były tylko commity pasujące do filtra. | @@ -110,6 +111,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Idź do następnego fragmentu | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit | Zatwierdź zmiany zatwierdzone. | @@ -212,6 +214,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Idź do następnego fragmentu | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit | Zatwierdź zmiany zatwierdzone. | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 704ccebf4..5ac753a74 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | Modo de tela anterior | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | Cancelar | | | `` ? `` | Abrir o menu de atalhos do teclado | | | `` `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. | @@ -246,6 +247,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Ir para o próximo trecho | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit | Submeter mudanças em staging | @@ -326,6 +328,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Ir para o próximo trecho | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Commit | Submeter mudanças em staging | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 33f7da705..9726e8012 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | Предыдущий режим экрана | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | Отменить | | | `` ? `` | Открыть меню | | | `` `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. | @@ -85,6 +86,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Выбрать следующую часть | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Сохранить изменения | Commit staged changes. | @@ -111,6 +113,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | Выбрать следующую часть | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | Exit back to side panel | | | `` c `` | Сохранить изменения | Commit staged changes. | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 9b5afed6e..946b61812 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | 上一屏模式 | | | `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. | | `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | 取消 | | | `` ? `` | 打开菜单 | | | `` `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 | @@ -281,6 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 选择下一个区块 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | 退出回到侧边面板 | | | `` c `` | 提交变更 | 提交暂存文件 | @@ -323,6 +325,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 选择下一个区块 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | 退出回到侧边面板 | | | `` c `` | 提交变更 | 提交暂存文件 | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index bee7ce31f..af5625138 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` _ `` | 上一個螢幕模式 | | | `` \| `` | 切換差異渲染器 | 選擇已設定的差異渲染器清單中的下一個渲染器。 | | `` \ `` | 切換差異渲染器(反向) | 選擇已設定的差異渲染器清單中的上一個渲染器。 | +| `` `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. | | `` `` | 取消 | | | `` ? `` | 開啟選單 | | | `` `` | 檢視篩選路徑選項 | 檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。 | @@ -70,6 +71,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 選擇下一段 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | 退出回到側邊面板 | | | `` c `` | 提交變更 | 提交暫存區變更 | @@ -345,6 +347,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` , l `` | 選擇下一段 | | | `` N `` | Go to previous file | | | `` n `` | Go to next file | | +| `` `` | Jump to file | | | `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. | | `` `` | 退出回到側邊面板 | | | `` c `` | 提交變更 | 提交暫存區變更 | diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 20b6b39f7..eedde41e0 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -516,6 +516,7 @@ type KeybindingUniversalConfig struct { PrevBlockAlt2 Keybinding `yaml:"prevBlock-alt2"` JumpToBlock []Keybinding `yaml:"jumpToBlock"` FocusMainView Keybinding `yaml:"focusMainView"` + JumpToFile Keybinding `yaml:"jumpToFile"` NextMatch Keybinding `yaml:"nextMatch"` PrevMatch Keybinding `yaml:"prevMatch"` StartSearch Keybinding `yaml:"startSearch"` @@ -1043,6 +1044,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { NextBlockAlt2: Keybinding{""}, JumpToBlock: []Keybinding{{"1"}, {"2"}, {"3"}, {"4"}, {"5"}}, FocusMainView: Keybinding{"0"}, + JumpToFile: Keybinding{""}, NextMatch: Keybinding{"n"}, PrevMatch: Keybinding{"N"}, StartSearch: Keybinding{"/"}, diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index f4de89567..a51b70fc5 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -3,6 +3,7 @@ package controllers import ( "strconv" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -74,6 +75,14 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type Description: self.c.Tr.CycleDiffRenderersReverse, Tooltip: self.c.Tr.CycleDiffRenderersReverseTooltip, }, + { + Keys: opts.GetKeys(opts.Config.Universal.JumpToFile), + Handler: opts.Guards.NoPopupPanel(self.jumpToFileInDiff), + Description: self.c.Tr.JumpToFileInDiff, + DescriptionFunc: self.jumpToFileInDiffDescription, + Tooltip: self.c.Tr.JumpToFileInDiffTooltip, + OpensMenu: true, + }, { Keys: opts.GetKeys(opts.Config.Universal.Return), Handler: self.escape, @@ -206,6 +215,58 @@ func (self *GlobalController) canCycleDiffRenderers() *types.DisabledReason { return nil } +// jumpToFileInDiff offers the files of the diff the main section is showing in a menu, +// and scrolls that pane to the file picked. The panel the user is in keeps the focus; +// they are reading the diff from there, and the next commit or file to read is picked +// there too. +func (self *GlobalController) jumpToFileInDiff() error { + pane := self.diffPane() + if pane == nil { + return nil + } + + return self.c.Helpers().DiffLine.OpenJumpToFileMenu(pane, self.c.Tr.JumpToFileInDiff) +} + +// jumpToFileInDiffDescription qualifies the command's description so that it is listed +// only where it applies. A command with no description is left out of the keybindings +// menu. +// +// It doesn't apply where the main section is showing content that is no diff of the +// panel's — a branch's commit log, the status dashboard, a message. Nor does it while +// the focus is in one of the panes, which bind the key themselves; the menu would +// otherwise offer it twice there, once for the pane and once among the global keys. +// +// The static Description stays as it is: the cheatsheets are generated from that, and +// they document what a key does rather than when it applies. +func (self *GlobalController) jumpToFileInDiffDescription() string { + _, focusIsInAPane := self.c.Context().Current().(*context.MainContext) + if focusIsInAPane || self.diffPane() == nil { + return "" + } + return self.c.Tr.JumpToFileInDiff +} + +// diffPane returns the pane of the main section showing the diff of the panel the user +// is in, and nil when neither of them is showing one. A pane is cleared as it is +// emptied, so a pane that says it is showing a diff is showing one. Its window also has +// to be showing the pane. Resolving a conflicted file puts the merge conflicts view +// there instead, and the pane behind it goes on holding the diff it last rendered. +// +// Where both panes show a diff — the unstaged and staged sides of a file — the answer +// is the upper one, the pane the keys for scrolling the section act on. +func (self *GlobalController) diffPane() *context.MainContext { + for _, pane := range []*context.MainContext{ + self.c.Contexts().Normal, self.c.Contexts().NormalSecondary, + } { + onScreen := self.c.Helpers().Window.GetContextForWindow(pane.GetWindowName()) == pane + if onScreen && pane.ContentIsDiff() { + return pane + } + } + return nil +} + func (self *GlobalController) createOptionsMenu() error { return (&OptionsMenuAction{c: self.c}).Call() } diff --git a/pkg/gui/controllers/helpers/diff_line_file_menu.go b/pkg/gui/controllers/helpers/diff_line_file_menu.go new file mode 100644 index 000000000..a2e4c30bb --- /dev/null +++ b/pkg/gui/controllers/helpers/diff_line_file_menu.go @@ -0,0 +1,70 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" +) + +// OpenJumpToFileMenu offers the files of the diff the given pane is showing in a menu, +// so that one of them can be gone to directly rather than by stepping through the diff +// a file at a time. Picking one goes to where that file's diff begins, the way stepping +// to it with next-file does: the selection moves there in a focused pane, and an +// unfocused one scrolls the file to the top. +// +// The diff is read to the end before the menu is built: a file below the part of it that +// has been read so far is in neither the list nor the view, and reaching the far end of +// a long diff is what the menu is for. +func (self *DiffLineHelper) OpenJumpToFileMenu(pane types.DiffPaneContext, title string) error { + manager := self.c.GetViewBufferManagerForView(pane.GetView()) + if manager == nil { + return nil + } + manager.ReadToEnd(func() { + self.c.OnUIThread(func() error { return self.showJumpToFileMenu(pane, title) }) + }) + return nil +} + +// showJumpToFileMenu offers the diff's files by the paths git names them by. Each item +// names its file rather than the row that file begins at, so that a diff re-rendered +// while the menu is up is jumped into at the row the file begins at now. +// +// A menu offering the one file of a single-file diff would be a menu with nothing to +// choose, so it says what it found instead. It says it here rather than as the key's +// disabled reason because how many files there are is only known once the diff has been +// read to the end, which is too much to do for every keypress that asks whether a key +// applies. +func (self *DiffLineHelper) showJumpToFileMenu(pane types.DiffPaneContext, title string) error { + view := pane.GetView() + files := self.FilesInDiff(view) + if len(files) == 0 { + return nil + } + if len(files) == 1 { + self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + self.c.Tr.OnlyOneFileInDiff) + return nil + } + + worktreePath := self.c.Git().RepoPaths.WorktreePath() + menuItems := lo.Map(files, func(path string, _ int) *types.MenuItem { + label := repoRelativePath(worktreePath, path) + if label == "" { + label = path + } + return &types.MenuItem{ + Label: label, + OnPress: func() error { + if target, ok := self.StartOfFileInDiff(view, path); ok { + self.PlaceNavigationTarget(pane, target, true) + } + return nil + }, + } + }) + + return self.c.Menu(types.CreateMenuOptions{ + Title: title, + Items: menuItems, + FilterAsYouType: true, + }) +} diff --git a/pkg/gui/controllers/helpers/diff_line_plain_text.go b/pkg/gui/controllers/helpers/diff_line_plain_text.go index f76c4f0a7..6b53d94af 100644 --- a/pkg/gui/controllers/helpers/diff_line_plain_text.go +++ b/pkg/gui/controllers/helpers/diff_line_plain_text.go @@ -154,11 +154,19 @@ func (self *selectedDiffLines) covers(info types.DiffLineInfo) bool { func repoRelativePaths(worktreePath string, paths []string) []string { relPaths := make([]string, 0, len(paths)) for _, path := range paths { - relPath, err := filepath.Rel(worktreePath, path) - if err != nil || strings.HasPrefix(relPath, "..") { - continue + if relPath := repoRelativePath(worktreePath, path); relPath != "" { + relPaths = append(relPaths, relPath) } - relPaths = append(relPaths, filepath.ToSlash(relPath)) } return relPaths } + +// repoRelativePath turns one such path into the repo-relative one, and is "" for a path +// that lies outside the worktree. +func repoRelativePath(worktreePath string, path string) string { + relPath, err := filepath.Rel(worktreePath, path) + if err != nil || strings.HasPrefix(relPath, "..") { + return "" + } + return filepath.ToSlash(relPath) +} diff --git a/pkg/gui/controllers/helpers/diff_line_queries.go b/pkg/gui/controllers/helpers/diff_line_queries.go index 8409b093c..ce6fba60a 100644 --- a/pkg/gui/controllers/helpers/diff_line_queries.go +++ b/pkg/gui/controllers/helpers/diff_line_queries.go @@ -78,16 +78,26 @@ func (self *DiffLineHelper) ChangeLineOrdinals( // while the diff's text names the trees where an ordinary diff has git's a/ and b/ // prefixes and so needs nothing. func (self *DiffLineHelper) inRepoTerms(view *gocui.View, infos []types.DiffLineInfo) []types.DiffLineInfo { + toRepoTerms := self.repoTermsMapper(view) + return lo.Map(infos, func(info types.DiffLineInfo, _ int) types.DiffLineInfo { + info.Path = toRepoTerms(info.Path) + return info + }) +} + +// repoTermsMapper returns how a path recovered from view is brought into the repo's +// terms, for the callers that have a path rather than a whole identity to bring. The +// mapping is worked out once, per view rather than per path. +func (self *DiffLineHelper) repoTermsMapper(view *gocui.View) func(string) string { if !self.ShowsCustomPatch(view) { - return infos + return func(path string) string { return path } } worktreePath := self.c.Git().RepoPaths.WorktreePath() treesDir := self.c.Git().Patch.PatchBuilder.TempDir() - return lo.Map(infos, func(info types.DiffLineInfo, _ int) types.DiffLineInfo { - info.Path = repoPathOfTreePath(info.Path, treesDir, worktreePath) - return info - }) + return func(path string) string { + return repoPathOfTreePath(path, treesDir, worktreePath) + } } // repoPathOfTreePath maps a path under one of the trees the custom patch was materialized @@ -335,62 +345,98 @@ func (self *DiffLineHelper) AdjacentFile(view *gocui.View, anchorViewLine int, f return view.ViewLineForBufferLine(target) } -// filePaths resolves view's rendered diff to the path each buffer line belongs to, -// empty for a row whose identity couldn't be recovered. +// FilesInDiff lists the files of view's (possibly multi-file) rendered diff, in the +// order it shows them, by the paths of the repo's files. It is what a menu offering to +// jump between them is built from; jumping to one of them goes to StartOfFileInDiff. +func (self *DiffLineHelper) FilesInDiff(view *gocui.View) []string { + return lo.Map(fileStarts(self.filePaths(view)), + func(start diffFileStart, _ int) string { return start.path }) +} + +// StartOfFileInDiff returns the view line the given file's section of view's rendered +// diff begins at. That is the row file navigation lands on, so jumping to a file from a +// menu and stepping to it with next-file land in the same place. ok is false for a file +// the diff doesn't show, e.g. because it was re-rendered since the file was listed. +func (self *DiffLineHelper) StartOfFileInDiff(view *gocui.View, path string) (int, bool) { + start, ok := lo.Find(fileStarts(self.filePaths(view)), func(start diffFileStart) bool { + return start.path == path + }) + if !ok { + return 0, false + } + return view.ViewLineForBufferLine(start.row) +} + +// filePaths resolves view's rendered diff to the path each buffer line belongs to, in +// the repo's terms, and empty for a row whose identity couldn't be recovered. Naming the +// files the way the rest of the queries name them means a row of the custom patch's +// preview belongs to the repo's file rather than to the copy of it in the tree the patch +// was materialized into, so that both halves of a change belong to the same file however +// the diff renderer states them. func (self *DiffLineHelper) filePaths(view *gocui.View) []string { resolved := self.resolveDiffLines(view.DiffLineContents()) + toRepoTerms := self.repoTermsMapper(view) paths := make([]string, len(resolved)) for i, row := range resolved { if row.ok { - paths[i] = row.info.Path + paths[i] = toRepoTerms(row.info.Path) } } return paths } -// fileStart finds, in a diff whose lines carry the file path they belong to (empty for -// a row no backend could place), the first located row of the file adjacent to `from` -// in the given direction — the row file navigation lands on. It is the pure index -// arithmetic behind AdjacentFile. +// diffFileStart is where one file of a diff begins: the path of the file, and the row +// of the diff its section starts at. +type diffFileStart struct { + path string + row int +} + +// fileStarts finds, in a diff whose lines carry the file path they belong to (empty for +// a row no backend could place), where each file of it begins, in the order the diff +// shows them. // -// A file is identified by its path, so we look for where the path changes, skipping -// rows that carry none: those are the blank separator rows between files, or the -// header rows of a diff renderer that doesn't state which file its headers belong to. -// So the landing row is the file's header wherever the source says so — a parseable -// buffer, or a renderer that tags its headers — and the file's first content line -// otherwise, which is an accepted degradation. +// A file is identified by its path, and the rows showing it are consecutive, so a path +// differing from the one before it begins a file. Rows carrying no path are passed over: +// those are the blank separator rows between files, or the header rows of a diff +// renderer that doesn't state which file its headers belong to. So a file begins at its +// header wherever the source says so (a parseable buffer, or a renderer that tags its +// headers), and at its first content line otherwise, which is an accepted degradation. +func fileStarts(paths []string) []diffFileStart { + starts := []diffFileStart{} + previousPath := "" + for row, path := range paths { + if path == "" || path == previousPath { + continue + } + previousPath = path + starts = append(starts, diffFileStart{path: path, row: row}) + } + return starts +} + +// fileStart returns where the file adjacent to `from` in the given direction begins — +// the row file navigation lands on. It is the pure index arithmetic behind AdjacentFile. +// ok is false at the first or last file of the diff. func fileStart(paths []string, from int, forward bool) (int, bool) { anchorPath, ok := anchorFilePath(paths, from) if !ok { return 0, false } - if forward { - for i := from; i < len(paths); i++ { - if paths[i] != "" && paths[i] != anchorPath { - return i, true - } - } + starts := fileStarts(paths) + _, anchor, ok := lo.FindIndexOf(starts, func(start diffFileStart) bool { + return start.path == anchorPath + }) + if !ok { return 0, false } - // Walk back past the current file (its rows and any unlocated ones) to the previous - // file's last located row, then back over that whole file, landing on its first. - i := from - for i >= 0 && (paths[i] == "" || paths[i] == anchorPath) { - i-- - } - if i < 0 { + target := anchor + lo.Ternary(forward, 1, -1) + if target < 0 || target >= len(starts) { return 0, false } - prevPath := paths[i] - for i > 0 && (paths[i-1] == "" || paths[i-1] == prevPath) { - i-- - } - for paths[i] != prevPath { - i++ - } - return i, true + return starts[target].row, true } // anchorFilePath returns the path of the file the anchor sits in: the first row at or diff --git a/pkg/gui/controllers/helpers/diff_line_queries_test.go b/pkg/gui/controllers/helpers/diff_line_queries_test.go index 47769ad98..25d7a5433 100644 --- a/pkg/gui/controllers/helpers/diff_line_queries_test.go +++ b/pkg/gui/controllers/helpers/diff_line_queries_test.go @@ -48,6 +48,41 @@ func TestChangeBlockStart(t *testing.T) { } } +func TestFileStarts(t *testing.T) { + scenarios := []struct { + name string + paths []string + expected []diffFileStart + }{ + { + name: "a parseable diff begins each file at its header", + paths: []string{"a", "a", "a", "b", "b"}, + expected: []diffFileStart{{path: "a", row: 0}, {path: "b", row: 3}}, + }, + { + name: "a diff whose headers carry no path begins each file at its first content line", + paths: []string{"", "", "a", "a", "", "", "b", "b"}, + expected: []diffFileStart{{path: "a", row: 2}, {path: "b", row: 6}}, + }, + { + name: "an unlocated row within a file doesn't begin another one", + paths: []string{"a", "", "a", "b"}, + expected: []diffFileStart{{path: "a", row: 0}, {path: "b", row: 3}}, + }, + { + name: "a diff with no located rows shows no files", + paths: []string{"", ""}, + expected: []diffFileStart{}, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + assert.Equal(t, s.expected, fileStarts(s.paths)) + }) + } +} + func TestFileStart(t *testing.T) { // A parseable two-file diff: every row carries its file's path, headers included, // as the buffer parser reports it. diff --git a/pkg/gui/controllers/helpers/diff_line_selection.go b/pkg/gui/controllers/helpers/diff_line_selection.go index 4b53d6613..e80147a1e 100644 --- a/pkg/gui/controllers/helpers/diff_line_selection.go +++ b/pkg/gui/controllers/helpers/diff_line_selection.go @@ -7,9 +7,9 @@ import ( "github.com/samber/lo" ) -// Putting a selection in the focused main view: where it starts out, and how it is -// widened to a whole change block. Both are answered from what the view is showing, -// as recovered by the queries next door. +// Putting a selection in the focused main view: where it starts out, where a jump +// leaves it, and how it is widened to a whole change block. All three are answered +// from what the view is showing, as recovered by the queries next door. // EstablishSelection turns on the focused main view's selection once the view has // been focused. clickedViewLine is the view line a click pointed at, or -1 for @@ -63,6 +63,66 @@ func (self *DiffLineHelper) EstablishSelection(mainContext *context.MainContext, self.ShowSelectionAtLine(view, target, false) } +// PlaceNavigationTarget moves the pane's selection to the row a jump found, bringing +// it on screen if it isn't already. With no selection to move — a pane that isn't +// focused, or one showing a diff with nothing selectable in it — the row goes to the +// top of the view instead, that being all a jump can do there. +// +// alignTop asks for the target to become the view's top line, so that everything that +// begins there is on screen. It only applies to a target the view has to scroll to: a +// jump to something already on screen leaves the view alone, there being nothing to +// gain from moving what the user is looking at. In hunk mode what ends up selected is +// the first change block at or below the target, which a large context size can put +// further down than a screenful; the selection is then scrolled into view as any other +// jump's is, and the alignment gives way to that. +func (self *DiffLineHelper) PlaceNavigationTarget( + pane types.DiffPaneContext, target int, alignTop bool, +) { + view := pane.GetView() + if !view.Highlight { + view.SetOrigin(0, target) + return + } + if alignTop { + self.scrollTargetToTop(pane, target) + } + // Jumping to another block or file moves the cursor without shift held, so a + // range that grows only while shift is held collapses rather than stretching all + // the way to the target. A sticky range stretches instead; this is the point of + // being sticky. + self.CollapseNonStickyRange(pane) + if pane.DiffSelectState().Mode == types.DiffSelectModeHunk { + self.SelectChangeBlock(pane, target, true) + return + } + // Line mode leaves a single-line selection at the target; an active range extends + // to it, the anchor being untouched. + self.ShowSelectionAtLine(view, target, true) +} + +// scrollTargetToTop scrolls the given row of the diff to the top of the view, leaving +// the view where it is when that row is on screen already. The last screenful of the +// diff is as far as it goes, so that the view doesn't scroll past the end of what it is +// showing. +func (self *DiffLineHelper) scrollTargetToTop(pane types.DiffPaneContext, target int) { + view := pane.GetView() + originY, height := pane.GetViewTrait().ViewPortYBounds() + if target >= originY && target < originY+height { + return + } + view.SetOriginY(min(target, max(0, view.ViewLinesHeight()-height))) +} + +// CollapseNonStickyRange drops a range that only grows while shift is held back to a +// single line at the cursor. +func (self *DiffLineHelper) CollapseNonStickyRange(pane types.DiffPaneContext) { + sel := pane.DiffSelectState() + if sel.Mode == types.DiffSelectModeRange && !sel.RangeIsSticky { + sel.Mode = types.DiffSelectModeLine + pane.GetView().CancelRangeSelect() + } +} + // changeToSelectOnScreen returns the change line keyboard focus establishes the // selection on. In hunk mode that is the first block that begins on screen, so that // the block being offered up is one the user can see the extent of, falling back to a diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index f6528c27d..71aee5888 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -159,6 +159,15 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.NextFileInDiff), GetDisabledReason: self.diffSelectionDisabledReason, }, + { + Keys: opts.GetKeys(opts.Config.Universal.JumpToFile), + Handler: self.openJumpToFileMenu, + Description: self.c.Tr.JumpToFile, + DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.JumpToFile), + GetDisabledReason: self.diffSelectionDisabledReason, + OpensMenu: true, + DisplayOnScreen: true, + }, { Keys: opts.GetKeys(opts.Config.Commits.OpenPullRequestInBrowser), Handler: self.openPullRequestAtSelectedLine, @@ -842,50 +851,12 @@ func (self *MainViewController) prevFile() error { return nil } -// placeNavigationTarget moves the selection to the row a jump found, bringing it on -// screen if it isn't already. -// -// alignTop asks for the target to become the view's top line, so that everything that -// begins there is on screen. It only applies to a target the view has to scroll to: a -// jump to something already on screen leaves the view alone, there being nothing to -// gain from moving what the user is looking at. In hunk mode what ends up selected is -// the first change block at or below the target, which a large context size can put -// further down than a screenful; the selection is then scrolled into view as any other -// jump's is, and the alignment gives way to that. func (self *MainViewController) placeNavigationTarget(target int, alignTop bool) { - v := self.context.GetView() - if !v.Highlight { - v.SetOrigin(0, target) - return - } - if alignTop { - self.scrollTargetToTop(target) - } - // Jumping to another block or file moves the cursor without shift held, so a - // range that grows only while shift is held collapses rather than stretching all - // the way to the target. A sticky range stretches instead; this is the point of - // being sticky. - self.collapseNonStickyRange() - if self.diffSelectState().Mode == types.DiffSelectModeHunk { - self.selectHunkAround(target, true) - return - } - // Line mode leaves a single-line selection at the target; an active range extends - // to it, the anchor being untouched. - self.c.Helpers().DiffLine.ShowSelectionAtLine(v, target, true) + self.c.Helpers().DiffLine.PlaceNavigationTarget(self.context, target, alignTop) } -// scrollTargetToTop scrolls the given row of the diff to the top of the view, leaving -// the view where it is when that row is on screen already. The last screenful of the -// diff is as far as it goes, so that the view doesn't scroll past the end of what it is -// showing. -func (self *MainViewController) scrollTargetToTop(target int) { - view := self.context.GetView() - originY, height := self.context.GetViewTrait().ViewPortYBounds() - if target >= originY && target < originY+height { - return - } - view.SetOriginY(min(target, max(0, view.ViewLinesHeight()-height))) +func (self *MainViewController) openJumpToFileMenu() error { + return self.c.Helpers().DiffLine.OpenJumpToFileMenu(self.context, self.c.Tr.JumpToFile) } // moveCursor moves the selection cursor by delta view lines (negative = up), with the @@ -917,17 +888,7 @@ func (self *MainViewController) collapseForLineMove() { self.context.GetView().CancelRangeSelect() return } - self.collapseNonStickyRange() -} - -// collapseNonStickyRange drops a range that only grows while shift is held back to a -// single line at the cursor. -func (self *MainViewController) collapseNonStickyRange() { - sel := self.diffSelectState() - if sel.Mode == types.DiffSelectModeRange && !sel.RangeIsSticky { - sel.Mode = types.DiffSelectModeLine - self.context.GetView().CancelRangeSelect() - } + self.c.Helpers().DiffLine.CollapseNonStickyRange(self.context) } // adjustSelection moves the selection by delta view lines, for the plain up/down and diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index c6ada57f4..4aee3fb42 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -417,6 +417,10 @@ type TranslationSet struct { NextHunk string PrevFileInDiff string NextFileInDiff string + JumpToFile string + JumpToFileInDiff string + JumpToFileInDiffTooltip string + OnlyOneFileInDiff string PrevConflict string NextConflict string SelectPrevHunk string @@ -1604,6 +1608,10 @@ func EnglishTranslationSet() *TranslationSet { NextHunk: "Go to next hunk", PrevFileInDiff: "Go to previous file", NextFileInDiff: "Go to next file", + JumpToFile: "Jump to file", + JumpToFileInDiff: "Jump to file in diff", + JumpToFileInDiffTooltip: "Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel.", + OnlyOneFileInDiff: "There is only one file in this diff", PrevConflict: "Previous conflict", NextConflict: "Next conflict", SelectPrevHunk: "Previous hunk", diff --git a/pkg/integration/tests/main_view/jump_to_a_file_of_the_diff.go b/pkg/integration/tests/main_view/jump_to_a_file_of_the_diff.go new file mode 100644 index 000000000..d36959b16 --- /dev/null +++ b/pkg/integration/tests/main_view/jump_to_a_file_of_the_diff.go @@ -0,0 +1,155 @@ +package main_view + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var JumpToAFileOfTheDiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Jump to a file of a commit's diff by picking it from a menu of the diff's files", + ExtraCmdArgs: []string{}, + Skip: false, + // A short terminal, so that the first file's diff is longer than the part of the + // diff that has been read when the menu asks which files there are. + Width: 100, + Height: 20, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.UseHunkModeInDiffView = false + }, + SetupRepo: func(shell *Shell) { + lines := make([]string, 600) + for i := range lines { + lines[i] = fmt.Sprintf("line%03d", i+1) + } + shell.CreateFileAndAdd("aaa.txt", strings.Join(lines, "\n")+"\n") + shell.CreateFileAndAdd("ccc.txt", "one\n") + shell.CreateFileAndAdd("dir/bbb.txt", "one\n") + // Another long one at the end, so that the file jumped to below has a diff + // under it to scroll past and ends up at the top of the view. + shell.CreateFileAndAdd("zzz.txt", strings.Join(lines, "\n")+"\n") + shell.Commit("one") + + shell.UpdateFileAndAdd("ccc.txt", "two\n") + shell.Commit("two") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // A menu offering the one file of a single-file diff would be a menu with + // nothing to choose, so it says what it found instead. + t.Views().Commits(). + Focus(). + SelectedLine(Contains("two")). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectionIsActive(). + Press(keys.Universal.JumpToFile) + + t.ExpectToast(Contains("There is only one file in this diff")) + + t.Views().Main(). + IsFocused(). + Press(keys.Universal.Return) + + t.Views().Commits(). + IsFocused(). + SelectNextItem(). + SelectedLine(Contains("one")). + Press(keys.Universal.FocusMainView) + + // Every file of the diff is offered, in the order the diff shows them and by + // the path the repo knows them by, including the ones below the part of the + // diff that has been read. + t.Views().Main(). + IsFocused(). + SelectionIsActive(). + Press(keys.Universal.JumpToFile) + + t.ExpectPopup().Menu(). + Title(Equals("Jump to file")). + Lines( + Equals("aaa.txt"), + Equals("ccc.txt"), + Equals("dir/bbb.txt"), + Equals("zzz.txt"), + Equals("Cancel"), + ). + Select(Equals("dir/bbb.txt")). + Confirm() + + // The file lands where stepping to it with next-file would leave it: selected, + // and at the top of the view. + t.Views().Main(). + IsFocused(). + TopVisibleLine(Contains("diff --git a/dir/bbb.txt b/dir/bbb.txt")). + SelectedLines( + Contains("diff --git a/dir/bbb.txt b/dir/bbb.txt"), + ). + Press(keys.Universal.JumpToFile) + + // The menu filters as you type, which is the point of it for a diff of many + // files. + t.ExpectPopup().Menu(). + Title(Equals("Jump to file")). + Filter("ccc"). + Lines( + Equals("ccc.txt"), + ). + Confirm() + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("diff --git a/ccc.txt b/ccc.txt"), + ). + Press(keys.Universal.Return) + + // The panel the diff belongs to offers the same menu, so that a file can be + // jumped to without focusing the diff and having to come back out of it. + t.Views().Commits(). + IsFocused(). + SelectedLine(Contains("one")). + Press(keys.Universal.JumpToFile) + + t.ExpectPopup().Menu(). + Title(Equals("Jump to file in diff")). + Select(Equals("zzz.txt")). + Confirm() + + // Only the diff moves: it goes to the top of the file, while the panel keeps + // the focus and its selection. + t.Views().Commits(). + IsFocused(). + SelectedLine(Contains("one")) + + t.Views().Main(). + TopVisibleLine(Contains("diff --git a/zzz.txt b/zzz.txt")) + + // A panel showing something else in the main view — a branch's commit log — + // doesn't offer the menu at all, rather than offering it disabled. + t.Views().Branches(). + Focus(). + Press(keys.Universal.OptionMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Tap(func() { + t.Views().Menu().Content(DoesNotContain("Jump to file")) + }). + Cancel() + + t.Views().Commits(). + Focus(). + Press(keys.Universal.OptionMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Tap(func() { + t.Views().Menu().Content(Contains("Jump to file in diff")) + }). + Cancel() + }, +}) diff --git a/pkg/integration/tests/main_view/jump_to_a_file_only_over_a_diff.go b/pkg/integration/tests/main_view/jump_to_a_file_only_over_a_diff.go new file mode 100644 index 000000000..73a72d889 --- /dev/null +++ b/pkg/integration/tests/main_view/jump_to_a_file_only_over_a_diff.go @@ -0,0 +1,74 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var JumpToAFileOnlyOverADiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The menu of the diff's files is offered only while the main view is showing a diff", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shared.CreateMergeConflictFiles(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // A commit shows its diff in the main view, so the menu is offered over it. + t.Views().Commits(). + Focus(). + NavigateToLine(Contains("original")). + Press(keys.Universal.OptionMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Tap(func() { + t.Views().Menu().Content(Contains("Jump to file in diff")) + }). + Cancel() + + // The pane binds the same key itself, so the global one isn't offered on top of + // the pane's while the pane has the focus. + t.Views().Commits(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + Press(keys.Universal.OptionMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Tap(func() { + t.Views().Menu(). + Content(Contains("Jump to file")). + Content(DoesNotContain("Jump to file in diff")) + }). + Cancel() + + t.Views().Main(). + IsFocused(). + Press(keys.Universal.Return) + + // Working through a conflicted file gives the main section over to the merge + // conflicts view, which is no diff to jump around in. The pane behind it goes + // on holding the diff it last rendered, so it is the view on screen that + // decides. + t.Views().Files(). + Focus(). + NavigateToLine(Contains("UU file1")). + PressEnter() + + t.Views().MergeConflicts(). + IsFocused(). + Press(keys.Universal.OptionMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Keybindings")). + Tap(func() { + t.Views().Menu().Content(DoesNotContain("Jump to file")) + }). + Cancel() + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 0e27c9bbd..dad72fd68 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -405,6 +405,8 @@ var tests = []*components.IntegrationTest{ main_view.FocusLeavesAnAlwaysSplitEmptyPane, main_view.FocusReturnsWhenSplitCollapses, main_view.HideSelectionWhenChangesVanish, + main_view.JumpToAFileOfTheDiff, + main_view.JumpToAFileOnlyOverADiff, main_view.KeepAWrappedLineCoveredAcrossARerender, main_view.KeepBothHalvesOfAChangeSelected, main_view.KeepPositionByTheVisibleEndOfASelection, diff --git a/schema-master/config.json b/schema-master/config.json index b9fe29991..2607715b2 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -3138,6 +3138,20 @@ ], "default": "0" }, + "jumpToFile": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": "\u003cctrl+g\u003e" + }, "nextMatch": { "oneOf": [ {