From db8fea5685c05cc44f870031282d5202c0473118 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 13 Sep 2026 13:54:13 +0200 Subject: [PATCH] Add a menu of the diff's files to jump to one directly n and N step through the files of a multi-file diff one at a time, which is a long way to the far end of a commit touching a hundred files. Bind ctrl+g to a menu of the diff's files, in the order the diff shows them and by the paths the repo knows them by; picking one goes to where its diff begins, exactly where stepping to it with n would have left you. The menu filters as you type, so the file you have in mind is a few characters away however many the commit touches. The key works in the panel the diff belongs to as well as in the diff itself. A commit is read from the commits panel, so being able to jump from there saves focusing the diff and leaving it again for the next commit; the diff scrolls to the file and the focus stays in the panel. It is ctrl+g rather than f because a key that works in every panel has to be free in all of them, and f is fetch in the files panel and fixup in the commits panel. The command applies only while the main view is showing a diff, and is left out of the keybindings menu where it isn't: over a branch's commit log, or while a conflicted file has given the main section over to the merge conflicts view. The diff is read to the end before the menu is built, as searching it does: a file below the part that has been read is in neither the list nor the view. 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. Co-Authored-By: Claude Opus 5 (1M context) --- docs-master/Config.md | 1 + docs-master/keybindings/Keybindings_en.md | 3 + docs-master/keybindings/Keybindings_ja.md | 3 + docs-master/keybindings/Keybindings_ko.md | 3 + docs-master/keybindings/Keybindings_nl.md | 3 + docs-master/keybindings/Keybindings_pl.md | 3 + docs-master/keybindings/Keybindings_pt.md | 3 + docs-master/keybindings/Keybindings_ru.md | 3 + docs-master/keybindings/Keybindings_zh-CN.md | 3 + docs-master/keybindings/Keybindings_zh-TW.md | 3 + pkg/config/user_config.go | 2 + pkg/gui/controllers/global_controller.go | 61 +++++++ .../helpers/diff_line_file_menu.go | 70 ++++++++ .../helpers/diff_line_plain_text.go | 16 +- .../controllers/helpers/diff_line_queries.go | 22 +++ pkg/gui/controllers/main_view_controller.go | 13 ++ pkg/i18n/english.go | 8 + .../main_view/jump_to_a_file_of_the_diff.go | 155 ++++++++++++++++++ .../jump_to_a_file_only_over_a_diff.go | 74 +++++++++ pkg/integration/tests/test_list.go | 2 + schema-master/config.json | 14 ++ 21 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 pkg/gui/controllers/helpers/diff_line_file_menu.go create mode 100644 pkg/integration/tests/main_view/jump_to_a_file_of_the_diff.go create mode 100644 pkg/integration/tests/main_view/jump_to_a_file_only_over_a_diff.go diff --git a/docs-master/Config.md b/docs-master/Config.md index a7633cf33..8925b994c 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -697,6 +697,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 ab0668201..f8013a5d1 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -511,6 +511,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"` @@ -1037,6 +1038,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 b03a98739..ce6fba60a 100644 --- a/pkg/gui/controllers/helpers/diff_line_queries.go +++ b/pkg/gui/controllers/helpers/diff_line_queries.go @@ -345,6 +345,28 @@ func (self *DiffLineHelper) AdjacentFile(view *gocui.View, anchorViewLine int, f return view.ViewLineForBufferLine(target) } +// 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 diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 340a12414..ff314dafc 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,6 +851,10 @@ func (self *MainViewController) placeNavigationTarget(target int, alignTop bool) self.c.Helpers().DiffLine.PlaceNavigationTarget(self.context, target, alignTop) } +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 // configured scroll-off margin, reading more content in first when moving down. The // range anchor is left untouched, so this extends or contracts a range and just moves diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 795e8d289..88eddd9cb 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -416,6 +416,10 @@ type TranslationSet struct { NextHunk string PrevFileInDiff string NextFileInDiff string + JumpToFile string + JumpToFileInDiff string + JumpToFileInDiffTooltip string + OnlyOneFileInDiff string PrevConflict string NextConflict string SelectPrevHunk string @@ -1602,6 +1606,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 161619557..afccd61b2 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -3128,6 +3128,20 @@ ], "default": "0" }, + "jumpToFile": { + "oneOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "default": "\u003cctrl+g\u003e" + }, "nextMatch": { "oneOf": [ {