Add a jump-to-file menu to the focused main view's diff

n / N step through the files of a multi-file diff one at a time, which
is tedious when the diff spans many files. Add `f`, which pops up a menu
listing every file in the diff — in the order they appear, as
repo-relative paths — so you can jump straight to one.

The menu title and the keybinding description are hard-coded English for
now; this is a prototype.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-06 09:54:04 +02:00
parent 822b3be988
commit 42d8f9697c
11 changed files with 110 additions and 0 deletions

View file

@ -234,6 +234,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Commit staged changes. |
| `` w `` | Commit changes without pre-commit hook | |
@ -316,6 +317,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Commit staged changes. |
| `` w `` | Commit changes without pre-commit hook | |

View file

@ -203,6 +203,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | サイドパネルに戻る | |
| `` c `` | コミット | ステージされた変更をコミットします。 |
| `` w `` | pre-commitフックなしで変更をコミット | |
@ -293,6 +294,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | サイドパネルに戻る | |
| `` c `` | コミット | ステージされた変更をコミットします。 |
| `` w `` | pre-commitフックなしで変更をコミット | |

View file

@ -95,6 +95,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
| `` w `` | Commit changes without pre-commit hook | |
@ -188,6 +189,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
| `` w `` | Commit changes without pre-commit hook | |

View file

@ -242,6 +242,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
@ -316,6 +317,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |

View file

@ -110,6 +110,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
@ -211,6 +212,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |

View file

@ -246,6 +246,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Submeter mudanças em staging |
| `` w `` | Fazer commit de alterações sem pré-commit | |
@ -325,6 +326,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Commit | Submeter mudanças em staging |
| `` w `` | Fazer commit de alterações sem pré-commit | |

View file

@ -85,6 +85,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Сохранить изменения | Commit staged changes. |
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
@ -110,6 +111,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | Exit back to side panel | |
| `` c `` | Сохранить изменения | Commit staged changes. |
| `` w `` | Закоммитить изменения без предварительного хука коммита | |

View file

@ -281,6 +281,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | 退出回到侧边面板 | |
| `` c `` | 提交变更 | 提交暂存文件 |
| `` w `` | 提交变更而无需预先提交钩子 | |
@ -322,6 +323,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | 退出回到侧边面板 | |
| `` c `` | 提交变更 | 提交暂存文件 |
| `` w `` | 提交变更而无需预先提交钩子 | |

View file

@ -70,6 +70,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | 退出回到側邊面板 | |
| `` c `` | 提交變更 | 提交暫存區變更 |
| `` w `` | 沒有預提交 hook 就提交更改 | |
@ -344,6 +345,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 | |
| `` f `` | Jump to file | |
| `` <esc> `` | 退出回到側邊面板 | |
| `` c `` | 提交變更 | 提交暫存區變更 |
| `` w `` | 沒有預提交 hook 就提交更改 | |

View file

@ -455,3 +455,32 @@ func changeBlockStart(isChange []bool, from int, forward bool) (int, bool) {
}
return i, true
}
// DiffFile is a file shown in a (possibly multi-file) diff: its absolute path and the
// view line its section starts at — the row that next/previous-file navigation lands on.
type DiffFile struct {
Path string
FirstViewLine int
}
// FilesInDiff lists the files shown in view's diff, in display order, each paired with
// the view line its section starts at. It is the jump-to-file menu's source: jumping to
// a file goes to its FirstViewLine, computed the same way (backUpOverHeader) that
// AdjacentFile lands on a file, so the menu and n/N agree on where each file begins. A
// file whose start row isn't currently mapped to a view line (not loaded yet) is skipped.
func (self *DiffLineHelper) FilesInDiff(view *gocui.View) []DiffFile {
paths := self.filePaths(view)
var files []DiffFile
prevPath := ""
for i, path := range paths {
if path == "" || path == prevPath {
continue
}
prevPath = path
if viewLine, ok := view.ViewLineForBufferLine(i); ok {
files = append(files, DiffFile{Path: path, FirstViewLine: viewLine})
}
}
return files
}

View file

@ -1,8 +1,10 @@
package controllers
import (
"path/filepath"
"time"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
@ -154,6 +156,13 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.NextFileInDiff),
GetDisabledReason: self.diffSelectionDisabledReason,
},
{
Keys: opts.GetKeys(config.Keybinding{"f"}),
Handler: self.openJumpToFileMenu,
Description: "Jump to file",
DescriptionFunc: self.diffSelectionDescriptionText("Jump to file"),
GetDisabledReason: self.diffSelectionDisabledReason,
},
{
Keys: opts.GetKeys(opts.Config.Universal.Return),
Handler: self.escape,
@ -747,6 +756,60 @@ func (self *MainViewController) placeNavigationTarget(target int) {
self.c.Helpers().DiffLine.ShowSelectionAtLine(v, target, true)
}
// openJumpToFileMenu pops up a menu listing the files in the focused main view's diff, in
// the order they appear, as repo-relative paths; picking one jumps straight to it. It's a
// complement to n / N for a diff that spans many files. A no-op when the main view holds
// no diff (nothing to list).
//
// The diff loads lazily, so we read it to the end first — otherwise a file past the loaded
// portion of a long diff would be missing from the menu (and have no view line to jump to).
// Same as handleGotoBottom.
func (self *MainViewController) openJumpToFileMenu() error {
manager := self.c.GetViewBufferManagerForView(self.context.GetView())
if manager == nil {
return nil
}
manager.ReadToEnd(func() {
self.c.OnUIThread(func() error {
return self.showJumpToFileMenu()
})
})
return nil
}
func (self *MainViewController) showJumpToFileMenu() error {
files := self.c.Helpers().DiffLine.FilesInDiff(self.context.GetView())
if len(files) == 0 {
return nil
}
worktreePath := self.c.Git().RepoPaths.WorktreePath()
menuItems := lo.Map(files, func(file helpers.DiffFile, index int) *types.MenuItem {
label := file.Path
if rel, err := filepath.Rel(worktreePath, file.Path); err == nil {
label = rel
}
firstViewLine := file.FirstViewLine
return &types.MenuItem{
Label: label,
OnPress: func() error { self.jumpToFile(firstViewLine); return nil },
}
})
// TODO: i18n-ize this title
return self.c.Menu(types.CreateMenuOptions{Title: "Jump to file", Items: menuItems, HideCancel: true, FilterAsYouType: true})
}
// jumpToFile moves the focused main view to the given view line exactly the way
// next/previous-file navigation does (see navigate), so jumping from the menu and
// stepping with n / N land identically — scrolling the file to the top with no selection,
// or moving the selection (or hunk selection) to it when one is showing.
func (self *MainViewController) jumpToFile(firstViewLine int) {
self.navigate(func(*gocui.View, int, bool) (int, bool) {
return firstViewLine, true
}, true)
}
// moveCursor moves the selection cursor by delta view lines (negative = up), with the
// configured scroll-off margin, reading more content in first when moving down. The
// range anchor is left untouched, so this extends or contracts a range and just moves