mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 18:26:03 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e8f3225a6 | ||
|
|
164e65ceee | ||
|
|
61382b8d37 | ||
|
|
d2543bd7ae | ||
|
|
2bf39e0f0c |
@@ -697,6 +697,7 @@ keybinding:
|
||||
- "4"
|
||||
- "5"
|
||||
focusMainView: "0"
|
||||
jumpToFile: <ctrl+g>
|
||||
nextMatch: "n"
|
||||
prevMatch: "N"
|
||||
startSearch: /
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Cancel | |
|
||||
| `` ? `` | Open keybindings menu | |
|
||||
| `` <ctrl+s> `` | 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
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 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
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Commit staged changes. |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | キャンセル | |
|
||||
| `` ? `` | キーバインディングメニューを開く | |
|
||||
| `` <ctrl+s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
|
||||
@@ -203,6 +204,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
@@ -294,6 +296,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 취소 | |
|
||||
| `` ? `` | 매뉴 열기 | |
|
||||
| `` <ctrl+s> `` | 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
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 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
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Annuleren | |
|
||||
| `` ? `` | Open menu | |
|
||||
| `` <ctrl+s> `` | 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
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 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
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Anuluj | |
|
||||
| `` ? `` | Otwórz menu przypisań klawiszy | |
|
||||
| `` <ctrl+s> `` | 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
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 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
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Cancelar | |
|
||||
| `` ? `` | Abrir o menu de atalhos do teclado | |
|
||||
| `` <ctrl+s> `` | 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
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 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
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Submeter mudanças em staging |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Отменить | |
|
||||
| `` ? `` | Открыть меню | |
|
||||
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | 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
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 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
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Сохранить изменения | Commit staged changes. |
|
||||
|
||||
@@ -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. |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 打开菜单 | |
|
||||
| `` <ctrl+s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
|
||||
@@ -281,6 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
@@ -323,6 +325,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | 上一個螢幕模式 | |
|
||||
| `` \| `` | 切換差異渲染器 | 選擇已設定的差異渲染器清單中的下一個渲染器。 |
|
||||
| `` \ `` | 切換差異渲染器(反向) | 選擇已設定的差異渲染器清單中的上一個渲染器。 |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 開啟選單 | |
|
||||
| `` <ctrl+s> `` | 檢視篩選路徑選項 | 檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。 |
|
||||
@@ -70,6 +71,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
@@ -345,6 +347,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | 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. |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
|
||||
@@ -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{"<tab>"},
|
||||
JumpToBlock: []Keybinding{{"1"}, {"2"}, {"3"}, {"4"}, {"5"}},
|
||||
FocusMainView: Keybinding{"0"},
|
||||
JumpToFile: Keybinding{"<ctrl+g>"},
|
||||
NextMatch: Keybinding{"n"},
|
||||
PrevMatch: Keybinding{"N"},
|
||||
StartSearch: Keybinding{"/"},
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -162,11 +162,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)
|
||||
}
|
||||
|
||||
@@ -79,16 +79,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
|
||||
@@ -336,62 +346,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -33,6 +33,21 @@ func (self *DiffLineHelper) MainViewDiffMode() git_commands.DiffMode {
|
||||
return git_commands.DiffModeRendered
|
||||
}
|
||||
|
||||
// DiffRowsCanBePlaced reports whether the rows of the diff the main view is about to be
|
||||
// given can be placed in the file they show. git's own diff describes itself, whether it
|
||||
// is what the user configured or what MainViewDiffMode is about to substitute for a
|
||||
// rendering that can't be acted on; any other rendering says where its rows belong only
|
||||
// if it states records for them.
|
||||
//
|
||||
// It is what anything that means to go from a row back to the file it shows has to ask
|
||||
// first: with neither records nor a diff that describes itself, there is nothing to go
|
||||
// on, and offering the user the way there would be offering nothing.
|
||||
func (self *DiffLineHelper) DiffRowsCanBePlaced() bool {
|
||||
return !self.diffNeedsMetadata() ||
|
||||
self.MainViewDiffMode() == git_commands.DiffModeRaw ||
|
||||
self.diffRendererEmitsMetadata()
|
||||
}
|
||||
|
||||
// RenderFocusedMainViewAgain has the panel beneath the focused main view render its
|
||||
// diff again — which, the main view now holding focus, is git's own diff rather than
|
||||
// the renderer's — and calls place once that is on screen.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// A diff opens with a diffstat naming every file in it, above the diff of each of
|
||||
// them. Here each of those names is made a link to where that file's diff begins, so
|
||||
// that a file of a long diff can be gone to by clicking the line that names it.
|
||||
//
|
||||
// The names are recognized in the output as it is written to the pane, where they cost
|
||||
// next to nothing to find. The diffstat is git's own text whichever renderer the diff
|
||||
// goes through — delta, diff-so-fancy and difftastic all pass it on untouched — and it
|
||||
// comes first, so the scan for it ends with it.
|
||||
|
||||
// DiffStatLinkScheme names a link to a file of the diff the pane is showing, as
|
||||
// lazygit-edit names one that opens a file in the editor. The link is never handed to
|
||||
// the terminal — gocui takes the escape sequence out of the content and gives the URL
|
||||
// back when the cell it covers is clicked — so the path in it needs no escaping.
|
||||
const DiffStatLinkScheme = "lazygit-diff-file://"
|
||||
|
||||
// diffStatEntryPattern matches a line of a diffstat and captures the path it states.
|
||||
// Such a line holds the name of the file, padded out to the width of the longest, then
|
||||
// the number of lines it changes (or "Bin" for a binary file) and the graph of them.
|
||||
//
|
||||
// The name is captured greedily, so that the separator found is the last one on the
|
||||
// line rather than one in a file name that contains " | " itself.
|
||||
var diffStatEntryPattern = regexp.MustCompile(`^ (.*[^ ]) +\| +(?:Bin|\d+)`)
|
||||
|
||||
// DiffStatLinkWriter hands a pane's content on to it, turning the file names in the
|
||||
// diffstat the content opens with into links (see DiffStatLinkScheme).
|
||||
type DiffStatLinkWriter struct {
|
||||
writer io.Writer
|
||||
|
||||
// Whether the diffstat is still to come, is being written now, or is behind us. It
|
||||
// is behind us once a line comes that is no entry of it, or that the diff proper
|
||||
// begins with, and nothing past that is looked at. A name down there names the file
|
||||
// the reader is already in.
|
||||
//
|
||||
// It is atomic because the render is begun on the UI thread while the content of
|
||||
// it arrives on the goroutine reading the command's output.
|
||||
state atomic.Int32
|
||||
}
|
||||
|
||||
type diffStatState int32
|
||||
|
||||
const (
|
||||
diffStatToCome diffStatState = iota
|
||||
inDiffStat
|
||||
diffStatDone
|
||||
)
|
||||
|
||||
func NewDiffStatLinkWriter(writer io.Writer) *DiffStatLinkWriter {
|
||||
return &DiffStatLinkWriter{writer: writer}
|
||||
}
|
||||
|
||||
// BeginRender starts a fresh render, whose own diffstat is the one to look for. It is
|
||||
// called as the render is asked for, before any of it is written.
|
||||
//
|
||||
// linkFiles says whether this render is one whose file names lead anywhere: the pane's
|
||||
// own diff, whose rows can be placed in the files they show. Either is passed through
|
||||
// untouched without it. Content that is no diff of the panel's — a commit log, a
|
||||
// message — has no diffstat in it, and a line of one that happens to read like an entry
|
||||
// of a diffstat names no file to go to. A rendering whose rows nothing can place does
|
||||
// have the files in it, but nothing to find the one a name stands for with.
|
||||
func (self *DiffStatLinkWriter) BeginRender(linkFiles bool) {
|
||||
self.setState(lo.Ternary(linkFiles, diffStatToCome, diffStatDone))
|
||||
}
|
||||
|
||||
func (self *DiffStatLinkWriter) getState() diffStatState {
|
||||
return diffStatState(self.state.Load())
|
||||
}
|
||||
|
||||
func (self *DiffStatLinkWriter) setState(state diffStatState) {
|
||||
self.state.Store(int32(state))
|
||||
}
|
||||
|
||||
func (self *DiffStatLinkWriter) Write(p []byte) (int, error) {
|
||||
linked := self.withFileNameLinked(p)
|
||||
|
||||
written, err := self.writer.Write(linked)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if written < len(linked) {
|
||||
return 0, io.ErrShortWrite
|
||||
}
|
||||
// The caller is owed an answer about what it gave us, not about what we passed on.
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// withFileNameLinked returns the given line of the render with the name in it linked,
|
||||
// where the line is an entry of the diffstat.
|
||||
func (self *DiffStatLinkWriter) withFileNameLinked(line []byte) []byte {
|
||||
state := self.getState()
|
||||
if state == diffStatDone {
|
||||
return line
|
||||
}
|
||||
|
||||
if beginsTheDiffItself(line) {
|
||||
// A diffstat that hasn't come by now isn't coming: the pane is showing a diff
|
||||
// that was asked for without one.
|
||||
self.setState(diffStatDone)
|
||||
return line
|
||||
}
|
||||
|
||||
match := diffStatEntry(line)
|
||||
if match == nil {
|
||||
if state == inDiffStat {
|
||||
self.setState(diffStatDone)
|
||||
}
|
||||
return line
|
||||
}
|
||||
self.setState(inDiffStat)
|
||||
|
||||
start, end := match[2], match[3]
|
||||
// The link states the name as it reads on screen, so that a renderer that colors
|
||||
// the diffstat doesn't put escape sequences into it.
|
||||
name := utils.Decolorise(string(line[start:end]))
|
||||
linked := make([]byte, 0, len(line)+len(name)+32)
|
||||
linked = append(linked, line[:start]...)
|
||||
linked = append(linked, style.PrintHyperlink(string(line[start:end]), DiffStatLinkScheme+name)...)
|
||||
return append(linked, line[end:]...)
|
||||
}
|
||||
|
||||
// diffLineRecordOpener opens an OSC 1717 record, ahead of the version whose fields the
|
||||
// record states (see parseDiffLineMetadata).
|
||||
const diffLineRecordOpener = "\x1b]1717;"
|
||||
|
||||
// beginsTheDiffItself reports whether the line is one of the diff proper rather than
|
||||
// one of the diffstat above it: git's own header for a file, or a line a renderer
|
||||
// states a record about (see statesADiffLine).
|
||||
func beginsTheDiffItself(line []byte) bool {
|
||||
return bytes.HasPrefix(line, []byte("diff --")) || statesADiffLine(line)
|
||||
}
|
||||
|
||||
// diffLineRecordKinds are the kinds of record a renderer states about a line of the
|
||||
// diff itself. A line stating one of them is below the whole diffstat, which is what
|
||||
// makes them the end of the search for it.
|
||||
//
|
||||
// They are listed here rather than read off the parser's table (see
|
||||
// diffLineTypeFromMetadata), which answers a different question: whether a record can be
|
||||
// read at all. A kind the protocol gains for something above the diff — one renderer
|
||||
// stated the commit line — would belong in that table and not in this list, and taking
|
||||
// the table for this would end the search where the diffstat hasn't even begun. Every
|
||||
// kind of the protocol is held against this list by a test.
|
||||
var diffLineRecordKinds = []string{"c", "a", "d", "f", "h"}
|
||||
|
||||
// statesADiffLine reports whether the line carries a record in which a diff renderer
|
||||
// states which line of which file it is rendering. Those records are about the lines of
|
||||
// the diff, and the diffstat comes before all of them.
|
||||
//
|
||||
// The kind the record states has to be one of diffLineRecordKinds. A record of any
|
||||
// other kind says nothing about where the diffstat ends, so the search goes on past it.
|
||||
//
|
||||
// The version the record opens with is passed over rather than read. This asks which
|
||||
// lines a renderer states records for, and the answer holds whichever version of the
|
||||
// protocol it speaks. A record with nothing after the version is the handshake a
|
||||
// renderer announces itself with, which is about no line, so the search goes on past it.
|
||||
func statesADiffLine(line []byte) bool {
|
||||
for rest := line; ; {
|
||||
at := bytes.Index(rest, []byte(diffLineRecordOpener))
|
||||
if at == -1 {
|
||||
return false
|
||||
}
|
||||
rest = rest[at+len(diffLineRecordOpener):]
|
||||
|
||||
// The record reads <version>;<kind>;, and the kind is a single character.
|
||||
digits := 0
|
||||
for digits < len(rest) && rest[digits] >= '0' && rest[digits] <= '9' {
|
||||
digits++
|
||||
}
|
||||
kind := digits + 1
|
||||
if digits == 0 || kind+1 >= len(rest) || rest[digits] != ';' || rest[kind+1] != ';' {
|
||||
continue
|
||||
}
|
||||
if lo.Contains(diffLineRecordKinds, string(rest[kind:kind+1])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// diffStatEntry matches line against diffStatEntryPattern, behind the two checks that
|
||||
// answer for nearly every line of a diff without the pattern being run at all: an entry
|
||||
// of a diffstat is indented by a space, and holds the separator. The indices it returns
|
||||
// are into the whole line, whatever the match was made past.
|
||||
func diffStatEntry(line []byte) []int {
|
||||
start := handshakeEnd(line)
|
||||
rest := line[start:]
|
||||
if len(rest) == 0 || rest[0] != ' ' || bytes.IndexByte(rest, '|') == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
match := diffStatEntryPattern.FindSubmatchIndex(rest)
|
||||
for i := range match {
|
||||
if match[i] >= 0 {
|
||||
match[i] += start
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// handshakeEnd returns where the record a renderer announces itself with ends, for a
|
||||
// line that opens with one, and 0 for every other line.
|
||||
//
|
||||
// A renderer writes the handshake before anything else and with no newline after it,
|
||||
// so it lands at the start of the first line of its output. For a diff with nothing
|
||||
// above its diffstat — the diff of a range of commits, or of a stash — that is the line
|
||||
// naming the first file in it, and the entry begins after the record rather than at the
|
||||
// start of the line.
|
||||
func handshakeEnd(line []byte) int {
|
||||
if !bytes.HasPrefix(line, []byte(diffLineRecordOpener)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
after := len(diffLineRecordOpener)
|
||||
for after < len(line) && line[after] >= '0' && line[after] <= '9' {
|
||||
after++
|
||||
}
|
||||
|
||||
// Either terminator ends a record. One that goes on into a field instead is about
|
||||
// a line of the diff, which is below the whole diffstat and no entry of it.
|
||||
switch {
|
||||
case after < len(line) && line[after] == '\x07':
|
||||
return after + 1
|
||||
case after+1 < len(line) && line[after] == '\x1b' && line[after+1] == '\\':
|
||||
return after + 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// JumpToFileNamedInDiffStat goes to the file of the pane's diff that the given diffstat
|
||||
// entry names, for a click on the link made for that entry. It lands the way picking
|
||||
// the file from the menu of the diff's files does.
|
||||
//
|
||||
// The diff is read to the end first, as it is for that menu. The diffstat is on screen
|
||||
// only while the view is at the top of the diff, so the file clicked is nearly always
|
||||
// below the part of it that has been read.
|
||||
func (self *DiffLineHelper) JumpToFileNamedInDiffStat(pane types.DiffPaneContext, entry string) {
|
||||
manager := self.c.GetViewBufferManagerForView(pane.GetView())
|
||||
if manager == nil {
|
||||
return
|
||||
}
|
||||
manager.ReadToEnd(func() {
|
||||
self.c.OnUIThread(func() error {
|
||||
self.jumpToFileNamedInDiffStat(pane, entry)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *DiffLineHelper) jumpToFileNamedInDiffStat(pane types.DiffPaneContext, entry string) {
|
||||
view := pane.GetView()
|
||||
worktreePath := self.c.Git().RepoPaths.WorktreePath()
|
||||
files := self.FilesInDiff(view)
|
||||
names := lo.Map(files, func(file string, _ int) string {
|
||||
return repoRelativePath(worktreePath, file)
|
||||
})
|
||||
|
||||
index, ok := fileNamedByDiffStatEntry(entry, names)
|
||||
if !ok {
|
||||
self.c.ErrorToast(utils.ResolvePlaceholderString(
|
||||
self.c.Tr.NoFileInDiffNamed, map[string]string{"path": entry}))
|
||||
return
|
||||
}
|
||||
|
||||
if target, ok := self.StartOfFileInDiff(view, files[index]); ok {
|
||||
self.PlaceNavigationTarget(pane, target, true)
|
||||
}
|
||||
}
|
||||
|
||||
// fileNamedByDiffStatEntry returns which of the diff's files a diffstat entry names.
|
||||
//
|
||||
// An entry states the path as the diffstat has room for it rather than as git names
|
||||
// the file. A path too long for the column is cut off on the left behind "...", and a
|
||||
// rename is compacted to the "{old => new}" form. So the name is looked for among the
|
||||
// files the diff turned out to hold, whole and then as the end of one, and is taken
|
||||
// only where it names a single file.
|
||||
func fileNamedByDiffStatEntry(entry string, paths []string) (int, bool) {
|
||||
name := renamedTo(strings.TrimSpace(entry))
|
||||
|
||||
if index, ok := theOneMatching(paths, func(p string) bool { return p == name }); ok {
|
||||
return index, true
|
||||
}
|
||||
|
||||
// Where the diffstat cut the path off, what is left is the end of it. The cut is at
|
||||
// a directory boundary where there is room for one, and inside the file name where
|
||||
// there isn't.
|
||||
tail := strings.TrimPrefix(name, "...")
|
||||
return theOneMatching(paths, func(p string) bool { return strings.HasSuffix(p, tail) })
|
||||
}
|
||||
|
||||
// renamedTo returns the path a diffstat entry for a rename leaves the file at, and the
|
||||
// entry itself for any other one. A rename states both paths, with whatever they have
|
||||
// in common written once: "dir/{old => new}/file", or "old => new" where they share
|
||||
// nothing. The part shared with the old path is gone along with the "{" when the entry
|
||||
// is cut off on the left, which leaves a path to match the end of.
|
||||
func renamedTo(entry string) string {
|
||||
const arrow = " => "
|
||||
at := strings.Index(entry, arrow)
|
||||
if at == -1 {
|
||||
return entry
|
||||
}
|
||||
|
||||
shared := ""
|
||||
if brace := strings.Index(entry[:at], "{"); brace != -1 {
|
||||
shared = entry[:brace]
|
||||
}
|
||||
renamed := entry[at+len(arrow):]
|
||||
if closing := strings.Index(renamed, "}"); closing != -1 {
|
||||
return shared + renamed[:closing] + renamed[closing+1:]
|
||||
}
|
||||
return shared + renamed
|
||||
}
|
||||
|
||||
// theOneMatching returns the index of the one element the predicate holds for, and
|
||||
// false where it holds for none of them or for several.
|
||||
func theOneMatching(paths []string, matches func(string) bool) (int, bool) {
|
||||
found := -1
|
||||
for i, candidate := range paths {
|
||||
if !matches(candidate) {
|
||||
continue
|
||||
}
|
||||
if found != -1 {
|
||||
return 0, false
|
||||
}
|
||||
found = i
|
||||
}
|
||||
return found, found != -1
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// link is the line the writer is expected to produce for a diffstat entry: the space
|
||||
// it is indented by, the name linked, and the rest of the line as it came.
|
||||
func link(name string, rest string) string {
|
||||
return " " + style.PrintHyperlink(name, DiffStatLinkScheme+name) + rest
|
||||
}
|
||||
|
||||
// record is the OSC 1717 record a diff renderer speaking the given version of the
|
||||
// protocol states a line of the given kind with, as it precedes that line in its
|
||||
// output.
|
||||
func record(version string, kind string) string {
|
||||
return fmt.Sprintf("%s%s;%s;;;pkg/gui.go\x1b\\", diffLineRecordOpener, version, kind)
|
||||
}
|
||||
|
||||
// handshake is the record a renderer announces the protocol with: the version it
|
||||
// speaks, and nothing about any line. Renderers end their records with either
|
||||
// terminator, so both turn up.
|
||||
var (
|
||||
handshake = diffLineRecordOpener + "1\x1b\\"
|
||||
handshakeBel = diffLineRecordOpener + "1\x07"
|
||||
)
|
||||
|
||||
func TestDiffStatLinkWriter(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
linkFiles bool
|
||||
lines []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "links the entries of the diffstat, and nothing after it",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
"commit 1234567",
|
||||
"",
|
||||
" A commit message",
|
||||
"",
|
||||
" pkg/gui.go | 12 ++++++------",
|
||||
" dir/other.go | 3 ++-",
|
||||
" 2 files changed, 8 insertions(+), 7 deletions(-)",
|
||||
"",
|
||||
"diff --git a/pkg/gui.go b/pkg/gui.go",
|
||||
" a context line that reads like an entry | 3 ++-",
|
||||
},
|
||||
expected: []string{
|
||||
"commit 1234567",
|
||||
"",
|
||||
" A commit message",
|
||||
"",
|
||||
link("pkg/gui.go", " | 12 ++++++------"),
|
||||
link("dir/other.go", " | 3 ++-"),
|
||||
" 2 files changed, 8 insertions(+), 7 deletions(-)",
|
||||
"",
|
||||
"diff --git a/pkg/gui.go b/pkg/gui.go",
|
||||
" a context line that reads like an entry | 3 ++-",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "links a binary file, a file that changes nothing, and a name with spaces",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
" logo.png | Bin 0 -> 1234 bytes",
|
||||
" script.sh | 0",
|
||||
" my file.txt | 2 +-",
|
||||
},
|
||||
expected: []string{
|
||||
link("logo.png", " | Bin 0 -> 1234 bytes"),
|
||||
link("script.sh", " | 0"),
|
||||
link("my file.txt", " | 2 +-"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stops looking once the diff itself has begun",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
"diff --git a/pkg/gui.go b/pkg/gui.go",
|
||||
" a context line that reads like an entry | 3 ++-",
|
||||
},
|
||||
expected: []string{
|
||||
"diff --git a/pkg/gui.go b/pkg/gui.go",
|
||||
" a context line that reads like an entry | 3 ++-",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stops looking at the first line of the diff a renderer states",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
" pkg/gui.go | 1 +",
|
||||
record("1", "c") + " a context line of the diff",
|
||||
" this/looks/like/a/diff/stat | 2 +",
|
||||
},
|
||||
expected: []string{
|
||||
link("pkg/gui.go", " | 1 +"),
|
||||
record("1", "c") + " a context line of the diff",
|
||||
" this/looks/like/a/diff/stat | 2 +",
|
||||
},
|
||||
},
|
||||
{
|
||||
// A renderer is free to state records about something that is no line of
|
||||
// the diff, and one has stated the commit line above it. The diffstat is
|
||||
// below such a record as much as it is below the handshake.
|
||||
name: "keeps looking past a record of a kind it doesn't know",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
record("1", "C") + "commit 1234567",
|
||||
" pkg/gui.go | 1 +",
|
||||
},
|
||||
expected: []string{
|
||||
record("1", "C") + "commit 1234567",
|
||||
link("pkg/gui.go", " | 1 +"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stops for a record of a protocol version it doesn't read",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
record("7", "f") + "── pkg/gui.go ──",
|
||||
" this/looks/like/a/diff/stat | 2 +",
|
||||
},
|
||||
expected: []string{
|
||||
record("7", "f") + "── pkg/gui.go ──",
|
||||
" this/looks/like/a/diff/stat | 2 +",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keeps looking past the handshake, which states no line",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
handshake,
|
||||
" pkg/gui.go | 1 +",
|
||||
},
|
||||
expected: []string{
|
||||
handshake,
|
||||
link("pkg/gui.go", " | 1 +"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// A diff with nothing above its diffstat. The handshake is written with no
|
||||
// newline after it, so it runs into the entry naming the first file.
|
||||
name: "links an entry the handshake runs into",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
handshake + " pkg/gui.go | 1 +",
|
||||
" dir/other.go | 2 +-",
|
||||
},
|
||||
expected: []string{
|
||||
handshake + link("pkg/gui.go", " | 1 +"),
|
||||
link("dir/other.go", " | 2 +-"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "links an entry a handshake ended with a BEL runs into",
|
||||
linkFiles: true,
|
||||
lines: []string{
|
||||
handshakeBel + " pkg/gui.go | 1 +",
|
||||
},
|
||||
expected: []string{
|
||||
handshakeBel + link("pkg/gui.go", " | 1 +"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "leaves a render whose file names lead nowhere alone",
|
||||
linkFiles: false,
|
||||
lines: []string{
|
||||
" pkg/gui.go | 12 ++++++------",
|
||||
},
|
||||
expected: []string{
|
||||
" pkg/gui.go | 12 ++++++------",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := NewDiffStatLinkWriter(buffer)
|
||||
writer.BeginRender(scenario.linkFiles)
|
||||
|
||||
for _, line := range scenario.lines {
|
||||
written, err := writer.Write([]byte(line + "\n"))
|
||||
assert.NoError(t, err)
|
||||
// The writer answers for what it was given, not for what it passed on.
|
||||
assert.Equal(t, len(line)+1, written)
|
||||
}
|
||||
|
||||
assert.Equal(t, strings.Join(scenario.expected, "\n")+"\n", buffer.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffStatLinkWriterStartsLookingAgainWithEachRender(t *testing.T) {
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := NewDiffStatLinkWriter(buffer)
|
||||
|
||||
for range 2 {
|
||||
buffer.Reset()
|
||||
writer.BeginRender(true)
|
||||
_, _ = writer.Write([]byte(" pkg/gui.go | 1 +\n"))
|
||||
_, _ = writer.Write([]byte(" 1 file changed, 1 insertion(+)\n"))
|
||||
|
||||
assert.Equal(t, link("pkg/gui.go", " | 1 +")+
|
||||
"\n 1 file changed, 1 insertion(+)\n", buffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileNamedByDiffStatEntry(t *testing.T) {
|
||||
paths := []string{
|
||||
"pkg/gui.go",
|
||||
"pkg/integration/tests/main_view/jump_to_a_file_of_the_diff.go",
|
||||
"vendor/github.com/gdamore/tcell/v3/AUTHORS",
|
||||
"pkg/gocui/AUTHORS",
|
||||
"renamed.txt",
|
||||
"a/very/deeply/nested/directory/structure/some_long_file_name.txt",
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
entry string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "a path the diffstat had room for",
|
||||
entry: "pkg/gui.go",
|
||||
expected: "pkg/gui.go",
|
||||
},
|
||||
{
|
||||
name: "a path cut off at a directory boundary",
|
||||
entry: ".../tests/main_view/jump_to_a_file_of_the_diff.go",
|
||||
expected: "pkg/integration/tests/main_view/jump_to_a_file_of_the_diff.go",
|
||||
},
|
||||
{
|
||||
name: "a path cut off inside the file name",
|
||||
entry: "..._long_file_name.txt",
|
||||
expected: "a/very/deeply/nested/directory/structure/some_long_file_name.txt",
|
||||
},
|
||||
{
|
||||
name: "a rename, stated as the part the two paths share",
|
||||
entry: "vendor/github.com/gdamore/tcell/{v2 => v3}/AUTHORS",
|
||||
expected: "vendor/github.com/gdamore/tcell/v3/AUTHORS",
|
||||
},
|
||||
{
|
||||
name: "a rename whose shared part was cut off along with the brace",
|
||||
entry: ".../github.com/jesseduffield => pkg}/gocui/AUTHORS",
|
||||
expected: "pkg/gocui/AUTHORS",
|
||||
},
|
||||
{
|
||||
name: "a rename of paths that share nothing",
|
||||
entry: "original.txt => renamed.txt",
|
||||
expected: "renamed.txt",
|
||||
},
|
||||
{
|
||||
name: "a name of no file of the diff",
|
||||
entry: "pkg/nowhere.go",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "a name several files of the diff end with",
|
||||
entry: "AUTHORS",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
index, ok := fileNamedByDiffStatEntry(scenario.entry, paths)
|
||||
if scenario.expected == "" {
|
||||
assert.False(t, ok)
|
||||
return
|
||||
}
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, scenario.expected, paths[index])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEveryRecordKindIsWeighedAgainstTheDiffStat fails when the protocol gains a kind
|
||||
// of record that nobody has placed relative to the diffstat. Where a line stating that
|
||||
// kind can only come below the diffstat, it ends the search for it and belongs in
|
||||
// diffLineRecordKinds; where it can come above the diff — a record about the commit,
|
||||
// say — it says nothing about where the diffstat ends, and belongs in the list here.
|
||||
//
|
||||
// The parser takes the kind as a field rather than a table, so the kinds it reads are
|
||||
// found by asking it about each character in turn.
|
||||
func TestEveryRecordKindIsWeighedAgainstTheDiffStat(t *testing.T) {
|
||||
kindsAboveTheDiff := []string{}
|
||||
|
||||
printable := lo.RangeFrom(byte(' '), 0x7f-' ')
|
||||
kindsTheParserReads := lo.FilterMap(printable, func(char byte, _ int) (string, bool) {
|
||||
kind := string([]byte{char})
|
||||
_, ok := diffLineTypeFromMetadata(kind)
|
||||
return kind, ok
|
||||
})
|
||||
|
||||
assert.ElementsMatch(t,
|
||||
append(slices.Clone(diffLineRecordKinds), kindsAboveTheDiff...),
|
||||
kindsTheParserReads,
|
||||
"a kind of record has been added to the protocol without being weighed "+
|
||||
"against the diffstat; see this test's comment for where it belongs")
|
||||
}
|
||||
@@ -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,
|
||||
@@ -838,50 +847,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
|
||||
@@ -913,17 +884,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
|
||||
|
||||
+26
-11
@@ -82,6 +82,9 @@ type Gui struct {
|
||||
statusManager *status.StatusManager
|
||||
waitForIntro sync.WaitGroup
|
||||
viewBufferManagerMap map[string]*tasks.ViewBufferManager
|
||||
// holds a mapping of the main section's view names to the writers that link the
|
||||
// files named in the diffstat of what is rendered into them
|
||||
diffStatLinkWriterMap map[string]*helpers.DiffStatLinkWriter
|
||||
// holds a mapping of view names to ptmx's. This is for rendering command outputs
|
||||
// from within a pty. The point of keeping track of them is so that if we re-size
|
||||
// the window, we can tell the pty it needs to resize accordingly.
|
||||
@@ -415,6 +418,17 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
|
||||
return gui.helpers.Files.EditFiles([]string{filepath})
|
||||
}
|
||||
|
||||
if entry, ok := strings.CutPrefix(url, helpers.DiffStatLinkScheme); ok {
|
||||
view, err := gui.g.View(viewname)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if pane := gui.mainContextForView(view); pane != nil {
|
||||
gui.helpers.DiffLine.JumpToFileNamedInDiffStat(pane, entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.os.OpenLink(url); err != nil {
|
||||
return fmt.Errorf(gui.Tr.FailedToOpenURL, url, err)
|
||||
}
|
||||
@@ -775,17 +789,18 @@ func NewGui(
|
||||
test integrationTypes.IntegrationTest,
|
||||
) (*Gui, error) {
|
||||
gui := &Gui{
|
||||
Common: cmn,
|
||||
gitVersion: gitVersion,
|
||||
Config: configurer,
|
||||
Updater: updater,
|
||||
statusManager: status.NewStatusManager(),
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
viewPtmxMap: map[string]oscommands.Pty{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: &utils.Stack[types.RepoLocation]{},
|
||||
RepoStateMap: map[Repo]*GuiRepoState{},
|
||||
GuiLog: []string{},
|
||||
Common: cmn,
|
||||
gitVersion: gitVersion,
|
||||
Config: configurer,
|
||||
Updater: updater,
|
||||
statusManager: status.NewStatusManager(),
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
diffStatLinkWriterMap: map[string]*helpers.DiffStatLinkWriter{},
|
||||
viewPtmxMap: map[string]oscommands.Pty{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: &utils.Stack[types.RepoLocation]{},
|
||||
RepoStateMap: map[Repo]*GuiRepoState{},
|
||||
GuiLog: []string{},
|
||||
|
||||
// initializing this to true for the time being; it will be reset to the
|
||||
// real value after loading the user config:
|
||||
|
||||
@@ -74,6 +74,12 @@ func (gui *Gui) RefreshMainView(opts *types.ViewUpdateOpts, context types.Contex
|
||||
// or a log, and reads as badly cut off at the edge of the pane as it would
|
||||
// anywhere else.
|
||||
view.Wrap = !mainContext.ContentIsDiff() || gui.c.UserConfig().Gui.WrapLinesInDiffView
|
||||
// The files named in the diffstat are linked to where their diff begins, over a
|
||||
// render that has both: the panel's own diff, and rows that can be placed in the
|
||||
// files they show. The writer is told here, on the UI thread, since it is asked
|
||||
// on the one reading the command's output.
|
||||
gui.diffStatLinkWriter(view).BeginRender(
|
||||
mainContext.ContentIsDiff() && gui.helpers.DiffLine.DiffRowsCanBePlaced())
|
||||
}
|
||||
|
||||
if err := gui.runTaskForView(view, opts.Task); err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -150,12 +151,34 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// contentWriter returns what a render of the given view writes its content to: the
|
||||
// view itself, or, for a pane of the main section, the writer that links the files
|
||||
// named in the diffstat on its way there (see DiffStatLinkWriter).
|
||||
func (gui *Gui) contentWriter(view *gocui.View) io.Writer {
|
||||
if gui.mainContextForView(view) == nil {
|
||||
return view
|
||||
}
|
||||
return gui.diffStatLinkWriter(view)
|
||||
}
|
||||
|
||||
// diffStatLinkWriter returns the writer that links the diffstat of the given pane,
|
||||
// making it if the pane hasn't rendered yet. It lasts as long as the view does, and
|
||||
// each render tells it what to make of that render (see DiffStatLinkWriter.BeginRender).
|
||||
func (gui *Gui) diffStatLinkWriter(view *gocui.View) *helpers.DiffStatLinkWriter {
|
||||
writer, ok := gui.diffStatLinkWriterMap[view.Name()]
|
||||
if !ok {
|
||||
writer = helpers.NewDiffStatLinkWriter(view)
|
||||
gui.diffStatLinkWriterMap[view.Name()] = writer
|
||||
}
|
||||
return writer
|
||||
}
|
||||
|
||||
func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
|
||||
manager, ok := gui.viewBufferManagerMap[view.Name()]
|
||||
if !ok {
|
||||
manager = tasks.NewViewBufferManager(
|
||||
gui.Log,
|
||||
view,
|
||||
gui.contentWriter(view),
|
||||
func() {
|
||||
// Called before showing the "loading..." indicator: clear the
|
||||
// displayed buffer so only "loading..." is shown. The actual content
|
||||
|
||||
@@ -416,6 +416,11 @@ type TranslationSet struct {
|
||||
NextHunk string
|
||||
PrevFileInDiff string
|
||||
NextFileInDiff string
|
||||
JumpToFile string
|
||||
JumpToFileInDiff string
|
||||
JumpToFileInDiffTooltip string
|
||||
OnlyOneFileInDiff string
|
||||
NoFileInDiffNamed string
|
||||
PrevConflict string
|
||||
NextConflict string
|
||||
SelectPrevHunk string
|
||||
@@ -1602,6 +1607,11 @@ 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",
|
||||
NoFileInDiffNamed: "This diff has no file named '{{.path}}'",
|
||||
PrevConflict: "Previous conflict",
|
||||
NextConflict: "Next conflict",
|
||||
SelectPrevHunk: "Previous hunk",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ClickAFileInADiffStatThatComesFirst = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Jump to a file by clicking its name in a diffstat the renderer's handshake runs into",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 100,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
// A renderer that announces the protocol and then passes the diff on as it came.
|
||||
// The handshake has no newline after it, so it runs into the first line the
|
||||
// renderer is given — which for the diff of a range of commits is the first
|
||||
// entry of the diffstat, there being no commit above it.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Command: `printf '\033]1717;1\007'; cat`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("aaa.txt", "one\n")
|
||||
shell.Commit("one")
|
||||
shell.CreateFileAndAdd("zzz.txt", "one\n")
|
||||
shell.Commit("two")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
SelectedLine(Contains("two")).
|
||||
Press(keys.Universal.ToggleRangeSelect).
|
||||
SelectNextItem().
|
||||
SelectedLines(
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
)
|
||||
|
||||
// Below the line lazygit writes to say what the diff is of, the diff opens with
|
||||
// the diffstat, and the handshake runs into its first entry. That is the one
|
||||
// clicked here.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("Showing diff for range"),
|
||||
Equals(""),
|
||||
Contains("aaa.txt"),
|
||||
Contains("zzz.txt"),
|
||||
Contains("2 files changed"),
|
||||
).
|
||||
Click(2, 2)
|
||||
|
||||
t.Views().Commits().IsFocused()
|
||||
|
||||
t.Views().Main().
|
||||
TopVisibleLine(Contains("diff --git a/aaa.txt b/aaa.txt"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ClickAFileInTheDiffStat = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Jump to a file of a commit's diff by clicking the line that names it in the diffstat",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 100,
|
||||
Height: 30,
|
||||
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)
|
||||
}
|
||||
// A long file at either end, so that the file jumped to is far below the
|
||||
// diffstat and has a diff under it to scroll past.
|
||||
shell.CreateFileAndAdd("aaa.txt", strings.Join(lines, "\n")+"\n")
|
||||
shell.CreateFileAndAdd("dir/bbb.txt", "one\n")
|
||||
shell.CreateFileAndAdd("zzz.txt", strings.Join(lines, "\n")+"\n")
|
||||
shell.Commit("one")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
SelectedLine(Contains("one"))
|
||||
|
||||
// The click below is at a line of the diffstat, so the diff has to open with
|
||||
// the lines this expects.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("commit"),
|
||||
Contains("Author:"),
|
||||
Contains("Date:"),
|
||||
Equals(""),
|
||||
Contains("one"),
|
||||
Equals("---"),
|
||||
Contains("aaa.txt"),
|
||||
Contains("dir/bbb.txt"),
|
||||
Contains("zzz.txt"),
|
||||
Contains("3 files changed"),
|
||||
).
|
||||
Click(2, 8)
|
||||
|
||||
// The panel keeps the focus, and the diff goes to the file clicked.
|
||||
t.Views().Commits().
|
||||
IsFocused().
|
||||
SelectedLine(Contains("one"))
|
||||
|
||||
t.Views().Main().
|
||||
TopVisibleLine(Contains("diff --git a/zzz.txt b/zzz.txt"))
|
||||
|
||||
t.Views().Commits().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
// With the pane focused there is a selection to move, and the click moves it to
|
||||
// the file, exactly as picking the file from the menu would.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectionIsActive().
|
||||
Press(keys.Universal.GotoTop)
|
||||
|
||||
// The first file's diff begins on screen already, so the view stays where it
|
||||
// is; a jump only scrolls as far as it must once there is a selection to point
|
||||
// at the file with.
|
||||
t.Views().Main().
|
||||
TopVisibleLine(Contains("commit")).
|
||||
Click(2, 6).
|
||||
SelectedLines(
|
||||
Contains("diff --git a/aaa.txt b/aaa.txt"),
|
||||
).
|
||||
TopVisibleLine(Contains("commit")).
|
||||
// The last file's is far below, so that one is scrolled to.
|
||||
Click(2, 8).
|
||||
SelectedLines(
|
||||
Contains("diff --git a/zzz.txt b/zzz.txt"),
|
||||
).
|
||||
TopVisibleLine(Contains("diff --git a/zzz.txt b/zzz.txt"))
|
||||
},
|
||||
})
|
||||
@@ -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()
|
||||
},
|
||||
})
|
||||
@@ -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()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var NoDiffStatLinksUnderAnExternalDiff = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "The files named in the diffstat are not linked under a diff renderer that says nothing about its rows",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 100,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
// An external diff whose output has nothing to say about which line of which
|
||||
// file each row shows. git writes the diffstat itself, so the names are there
|
||||
// to be clicked, but nothing could find the file they name.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Name: "opaque", Type: "extDiff", Command: `sh -c 'echo EXT'`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("aaa.txt", "one\n")
|
||||
shell.CreateFileAndAdd("zzz.txt", "one\n")
|
||||
shell.Commit("one")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
SelectedLine(Contains("one"))
|
||||
|
||||
// The click below is at a line of the diffstat, so the diff has to open with
|
||||
// the lines this expects.
|
||||
t.Views().Main().
|
||||
TopLines(
|
||||
Contains("commit"),
|
||||
Contains("Author:"),
|
||||
Contains("Date:"),
|
||||
Equals(""),
|
||||
Contains("one"),
|
||||
Equals("---"),
|
||||
Contains("aaa.txt"),
|
||||
Contains("zzz.txt"),
|
||||
Contains("2 files changed"),
|
||||
).
|
||||
Click(2, 7)
|
||||
|
||||
// The name is no link, so the click is an ordinary one, which focuses the pane
|
||||
// it lands in. Were it a link, it would have been followed instead, and would
|
||||
// have had to report that it found no such file — the test fails on the toast
|
||||
// that leaves unacknowledged.
|
||||
t.Views().Main().IsFocused()
|
||||
},
|
||||
})
|
||||
@@ -377,6 +377,8 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.BuildPatchWithMixedSelections,
|
||||
main_view.ChangeContextSizeWhileBuildingPatch,
|
||||
main_view.ChangeScreenModeInFocusedDiff,
|
||||
main_view.ClickAFileInADiffStatThatComesFirst,
|
||||
main_view.ClickAFileInTheDiffStat,
|
||||
main_view.ClickSelectsDiffLine,
|
||||
main_view.CommitFromMainView,
|
||||
main_view.CopyRowsThatAreNoDiffLine,
|
||||
@@ -404,6 +406,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,
|
||||
@@ -438,6 +442,7 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.MovePatchToNewCommitBefore,
|
||||
main_view.MovePatchToNewCommitInStackedBranch,
|
||||
main_view.NavigateByHunkAndFile,
|
||||
main_view.NoDiffStatLinksUnderAnExternalDiff,
|
||||
main_view.NoSelectionOverABinaryDiff,
|
||||
main_view.NoSelectionOverACommitLog,
|
||||
main_view.NoSelectionOverAConflictHint,
|
||||
|
||||
@@ -3128,6 +3128,20 @@
|
||||
],
|
||||
"default": "0"
|
||||
},
|
||||
"jumpToFile": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"default": "\u003cctrl+g\u003e"
|
||||
},
|
||||
"nextMatch": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user