mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Navigate the focused main view by file and hunk
Add file and change-block ("hunk") navigation to the focused main view,
mirroring the staging view's hunk keys: `<left>`/`<right>` jump to the
previous/next hunk and `n`/`N` to the next/previous file. A "hunk" here is
lazygit's notion — a run of consecutive added/deleted lines separated by
context, not a git `@@` section — matching what the staging view jumps
between.
This is a consumer of the diff-line primitive in its forward direction:
resolve each rendered row's patch identity, then scan for the next/previous
change block (by the line type) or file boundary (by the path changing). The
file scan lands on the top of the neighbouring file even when a restructuring
pager leaves the header rows untagged, by backing up over them from the
file's first identifiable row — which is impossible without the per-line
metadata once the pager stops emitting a parseable unified diff. The
boundary arithmetic is pulled out into pure functions and unit-tested.
The anchor is the selected line if a selection is showing, else the top
visible line. With a selection we move it to the target and scroll it into
view, like the staging view; with none we stay in scroll mode, bringing the
target to the top without creating a selection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b6568cf9f
commit
a967fc3099
|
|
@ -225,6 +225,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Main panel (patch building)
|
||||
|
|
@ -331,6 +335,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Stash
|
||||
|
|
|
|||
|
|
@ -194,6 +194,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## タグ
|
||||
|
|
@ -308,6 +312,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## メニュー
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## Stash
|
||||
|
|
@ -165,6 +169,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
|
|
|||
|
|
@ -233,6 +233,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Patch bouwen
|
||||
|
|
@ -309,6 +313,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Staging
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Drzewa pracy
|
||||
|
|
@ -204,6 +208,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Panel główny (scalanie)
|
||||
|
|
|
|||
|
|
@ -237,6 +237,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Painel Principal (preparação)
|
||||
|
|
@ -340,6 +344,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Stash
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Индексирование)
|
||||
|
|
@ -109,6 +113,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Слияние)
|
||||
|
|
|
|||
|
|
@ -288,6 +288,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 正在合并
|
||||
|
|
@ -337,6 +341,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 状态
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 主面板(合併)
|
||||
|
|
@ -366,6 +370,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <space> `` | Show/hide selection | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 狀態
|
||||
|
|
|
|||
165
pkg/gui/controllers/helpers/diff_line_navigation.go
Normal file
165
pkg/gui/controllers/helpers/diff_line_navigation.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
)
|
||||
|
||||
// AdjacentChangeBlock returns the view line to move to for next/previous change-block
|
||||
// navigation in view's displayed diff, starting from anchorViewLine. A "change block"
|
||||
// is lazygit's notion of a hunk — a run of consecutive added/deleted lines separated
|
||||
// by context, of which there may be several within one git @@ hunk — matching what
|
||||
// the staging view's hunk navigation jumps between. forward=true targets the start of
|
||||
// the next block; forward=false the start of the previous one (from mid-block this
|
||||
// skips to the previous block, mirroring State.SelectPreviousHunk). ok is false when
|
||||
// there's no further block, so the caller leaves the view where it is.
|
||||
func (self *StagingHelper) AdjacentChangeBlock(view *gocui.View, anchorViewLine int, forward bool) (int, bool) {
|
||||
contents := view.DiffLineContents()
|
||||
anchor, ok := view.BufferLineForViewLine(anchorViewLine)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
isChange := make([]bool, len(contents))
|
||||
for i := range contents {
|
||||
if info, ok := self.diffLineInfoFromContents(contents, i); ok {
|
||||
isChange[i] = info.IsChange()
|
||||
}
|
||||
}
|
||||
|
||||
target, ok := changeBlockStart(isChange, anchor, forward)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return view.ViewLineForBufferLine(target)
|
||||
}
|
||||
|
||||
// AdjacentFile returns the view line to move to for next/previous file navigation in
|
||||
// view's (possibly multi-file) displayed diff, starting from anchorViewLine: the
|
||||
// first row belonging to the next/previous file, found where the per-row metadata's
|
||||
// file changes. ok is false at the first/last file.
|
||||
func (self *StagingHelper) AdjacentFile(view *gocui.View, anchorViewLine int, forward bool) (int, bool) {
|
||||
contents := view.DiffLineContents()
|
||||
anchor, ok := view.BufferLineForViewLine(anchorViewLine)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
paths := make([]string, len(contents))
|
||||
for i := range contents {
|
||||
if info, ok := self.diffLineInfoFromContents(contents, i); ok {
|
||||
paths[i] = info.Path
|
||||
}
|
||||
}
|
||||
|
||||
target, ok := fileStart(paths, anchor, forward)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return view.ViewLineForBufferLine(target)
|
||||
}
|
||||
|
||||
// changeBlockStart finds, in a diff whose lines are flagged by isChange, the first
|
||||
// line of the change block adjacent to `from` in the given direction. It is the pure
|
||||
// index arithmetic behind AdjacentChangeBlock, mirroring the staging view's
|
||||
// State.SelectNextHunk / SelectPreviousHunk line by line.
|
||||
func changeBlockStart(isChange []bool, from int, forward bool) (int, bool) {
|
||||
if forward {
|
||||
i := from
|
||||
for i < len(isChange) && isChange[i] { // leave the current block
|
||||
i++
|
||||
}
|
||||
for i < len(isChange) && !isChange[i] { // skip the separating context
|
||||
i++
|
||||
}
|
||||
if i < len(isChange) {
|
||||
return i, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
i := from
|
||||
for i >= 0 && isChange[i] { // leave the current block
|
||||
i--
|
||||
}
|
||||
for i >= 0 && !isChange[i] { // skip context, landing on the previous block's last line
|
||||
i--
|
||||
}
|
||||
if i < 0 {
|
||||
return 0, false
|
||||
}
|
||||
for i > 0 && isChange[i-1] { // walk back to that block's first line
|
||||
i--
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
// fileStart finds, in a diff whose lines carry the file path they belong to (empty
|
||||
// for a row no backend could place, e.g. a restructuring pager's file headers), the
|
||||
// top row of the file adjacent to `from` in the given direction. It is the pure
|
||||
// index arithmetic behind AdjacentFile. A file is identified by its path, so we find
|
||||
// where the path changes and then back up over the neighbouring file's unplaced
|
||||
// header rows, landing on its first row — the `diff --git`/`@@` header when the
|
||||
// buffer is parseable, or whatever the pager renders above the file's first tagged
|
||||
// line otherwise.
|
||||
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 backUpOverHeader(paths, i), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Walk back past the current file (its rows and any unplaced rows) to the
|
||||
// previous file's last located row, then back over that whole file to its top.
|
||||
i := from
|
||||
for i >= 0 && (paths[i] == "" || paths[i] == anchorPath) {
|
||||
i--
|
||||
}
|
||||
if i < 0 {
|
||||
return 0, false
|
||||
}
|
||||
prevPath := paths[i]
|
||||
for i > 0 && (paths[i-1] == "" || paths[i-1] == prevPath) {
|
||||
i--
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
// backUpOverHeader moves from a file's first located row up over the unplaced header
|
||||
// rows directly above it, to the file's top. It stops at the previous file's last
|
||||
// located row, so it never crosses into it.
|
||||
func backUpOverHeader(paths []string, firstLocated int) int {
|
||||
i := firstLocated
|
||||
for i > 0 && paths[i-1] == "" {
|
||||
i--
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// anchorFilePath returns the path of the file the anchor sits in: the first row at or
|
||||
// below it that carries a path — the file whose content is at or below the top of the
|
||||
// view — falling back to the nearest above when there's nothing below. Scanning down
|
||||
// first matters because the anchor is often an untagged file-header row whose nearest
|
||||
// tagged row is the *previous* file's content just above it; taking that would make
|
||||
// next-file navigation jump back into the file just left (so a second `n` wouldn't
|
||||
// advance). ok is false when no row carries a path.
|
||||
func anchorFilePath(paths []string, from int) (string, bool) {
|
||||
for i := from; i < len(paths); i++ {
|
||||
if paths[i] != "" {
|
||||
return paths[i], true
|
||||
}
|
||||
}
|
||||
for i := from - 1; i >= 0; i-- {
|
||||
if paths[i] != "" {
|
||||
return paths[i], true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
99
pkg/gui/controllers/helpers/diff_line_navigation_test.go
Normal file
99
pkg/gui/controllers/helpers/diff_line_navigation_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChangeBlockStart(t *testing.T) {
|
||||
// A diff with three change blocks separated by context:
|
||||
// 0 file header 1 hunk header 2 context
|
||||
// 3 + 4 + (block A)
|
||||
// 5 context
|
||||
// 6 - (block B)
|
||||
// 7 context
|
||||
// 8 + (block C)
|
||||
isChange := []bool{false, false, false, true, true, false, true, false, true}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
from int
|
||||
forward bool
|
||||
expected int
|
||||
found bool
|
||||
}{
|
||||
{"forward from a header lands on the first block", 0, true, 3, true},
|
||||
{"forward from separating context lands on the next block", 5, true, 6, true},
|
||||
{"forward from the start of a block skips to the next", 3, true, 6, true},
|
||||
{"forward from inside a block skips the rest of it", 4, true, 6, true},
|
||||
{"forward from the last block finds nothing", 8, true, 0, false},
|
||||
{"backward from a later block lands on the previous one's start", 8, false, 6, true},
|
||||
{"backward from a block start lands on the previous block's start", 6, false, 3, true},
|
||||
{"backward from inside the first block finds nothing", 4, false, 0, false},
|
||||
{"backward from the first block's start finds nothing", 3, false, 0, false},
|
||||
{"backward from context lands on the preceding block's start", 7, false, 6, true},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
got, found := changeBlockStart(isChange, s.from, s.forward)
|
||||
assert.Equal(t, s.found, found)
|
||||
if s.found {
|
||||
assert.Equal(t, s.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileStart(t *testing.T) {
|
||||
// A parseable two-file diff: every row carries its file's path (the headers
|
||||
// included), as the buffer parser reports.
|
||||
parseable := []string{"a", "a", "a", "a", "b", "b", "b", "b"}
|
||||
|
||||
// The same diff as a restructuring pager emits it: only content lines carry the
|
||||
// path; the file/hunk header rows above each file are untagged (empty).
|
||||
tagged := []string{"", "", "a", "a", "", "", "b", "b"}
|
||||
|
||||
// Three such files, to exercise navigating from one file's untagged header to the
|
||||
// next: the row just above b's header is a's content, so the anchor file must be
|
||||
// found by scanning down (b), not up (a) — otherwise next-file would jump back
|
||||
// into b and a second `n` couldn't advance.
|
||||
taggedThree := []string{"", "", "a", "a", "", "", "b", "b", "", "", "c", "c"}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
paths []string
|
||||
from int
|
||||
forward bool
|
||||
expected int
|
||||
found bool
|
||||
}{
|
||||
{"parseable: next file lands on its header", parseable, 1, true, 4, true},
|
||||
{"parseable: next from the last file finds nothing", parseable, 6, true, 0, false},
|
||||
{"parseable: previous file lands on its header", parseable, 5, false, 0, true},
|
||||
{"parseable: previous from the first file finds nothing", parseable, 1, false, 0, false},
|
||||
|
||||
// With only content tagged, both directions still land on the file's top
|
||||
// (the untagged header rows), so navigation feels the same.
|
||||
{"tagged: next file lands on its header, not its first content", tagged, 2, true, 4, true},
|
||||
{"tagged: next from an untagged header still advances", tagged, 0, true, 4, true},
|
||||
{"tagged: previous file lands on its header", tagged, 7, false, 0, true},
|
||||
{"tagged: previous from the first file finds nothing", tagged, 2, false, 0, false},
|
||||
|
||||
// From b's untagged header (row 4), the anchor file is b (below), so next goes
|
||||
// to c and previous goes to a — neither sticks on b.
|
||||
{"tagged: next from a middle file's header advances past it", taggedThree, 4, true, 8, true},
|
||||
{"tagged: previous from a middle file's header lands on the prior file", taggedThree, 4, false, 0, true},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
got, found := fileStart(s.paths, s.from, s.forward)
|
||||
assert.Equal(t, s.found, found)
|
||||
if s.found {
|
||||
assert.Equal(t, s.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"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/types"
|
||||
|
|
@ -94,6 +95,26 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
|||
Description: openPullRequestDescription,
|
||||
Tooltip: openPullRequestTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.PrevHunk),
|
||||
Handler: self.prevChangeBlock,
|
||||
Description: self.c.Tr.PrevHunk,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.NextHunk),
|
||||
Handler: self.nextChangeBlock,
|
||||
Description: self.c.Tr.NextHunk,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(config.Keybinding{"N"}),
|
||||
Handler: self.prevFile,
|
||||
Description: self.c.Tr.PrevFile,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(config.Keybinding{"n"}),
|
||||
Handler: self.nextFile,
|
||||
Description: self.c.Tr.NextFile,
|
||||
},
|
||||
{
|
||||
// overriding this because we want to read all of the task's output before we start searching
|
||||
Keys: opts.GetKeys(opts.Config.Universal.StartSearch),
|
||||
|
|
@ -161,7 +182,7 @@ func (self *MainViewController) toggleSelection() error {
|
|||
return nil
|
||||
}
|
||||
// Start the selection in the middle of the visible area.
|
||||
showSelectionAtLine(v, v.OriginY()+v.InnerHeight()/2)
|
||||
showSelectionAtLine(v, v.OriginY()+v.InnerHeight()/2, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -184,12 +205,56 @@ func (self *MainViewController) enterForLine(lineIdx int) error {
|
|||
}
|
||||
|
||||
// showSelectionAtLine turns on the focused main view's selection and moves it to
|
||||
// the given view line, clamped to the content.
|
||||
func showSelectionAtLine(view *gocui.View, lineIdx int) {
|
||||
// the given view line, clamped to the content. scrollIntoView scrolls the line into
|
||||
// view if it's off-screen (used when navigating to it); a click leaves it false, the
|
||||
// clicked line being visible already.
|
||||
func showSelectionAtLine(view *gocui.View, lineIdx int, scrollIntoView bool) {
|
||||
view.Highlight = true
|
||||
view.HighlightInactive = false
|
||||
lineIdx = lo.Clamp(lineIdx, 0, view.ViewLinesHeight()-1)
|
||||
view.FocusPoint(0, lineIdx, false)
|
||||
view.FocusPoint(0, lineIdx, scrollIntoView)
|
||||
}
|
||||
|
||||
// navigate jumps the focused main view by file or change block (hunk), using find to
|
||||
// locate the target row from the current anchor. The anchor is the selected line if a
|
||||
// selection is showing, otherwise the top visible line. With a selection showing we
|
||||
// move it to the target and scroll it into view, like the staging view; with none we
|
||||
// stay in scroll mode, bringing the target to the top without selecting anything.
|
||||
func (self *MainViewController) navigate(find func(*gocui.View, int, bool) (int, bool), forward bool) error {
|
||||
v := self.context.GetView()
|
||||
showSelection := v.Highlight
|
||||
anchor := v.OriginY()
|
||||
if showSelection {
|
||||
anchor = v.SelectedLineIdx()
|
||||
}
|
||||
|
||||
target, ok := find(v, anchor, forward)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if showSelection {
|
||||
showSelectionAtLine(v, target, true)
|
||||
} else {
|
||||
v.SetOrigin(0, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) nextChangeBlock() error {
|
||||
return self.navigate(self.c.Helpers().Staging.AdjacentChangeBlock, true)
|
||||
}
|
||||
|
||||
func (self *MainViewController) prevChangeBlock() error {
|
||||
return self.navigate(self.c.Helpers().Staging.AdjacentChangeBlock, false)
|
||||
}
|
||||
|
||||
func (self *MainViewController) nextFile() error {
|
||||
return self.navigate(self.c.Helpers().Staging.AdjacentFile, true)
|
||||
}
|
||||
|
||||
func (self *MainViewController) prevFile() error {
|
||||
return self.navigate(self.c.Helpers().Staging.AdjacentFile, false)
|
||||
}
|
||||
|
||||
// focusedMainViewContextForViewName maps a focused main view's view name (as
|
||||
|
|
@ -333,7 +398,7 @@ func githubPullRequestLineURL(prURL string, commitSha string, relativePath strin
|
|||
func (self *MainViewController) onClickInAlreadyFocusedView(opts gocui.ViewMouseBindingOpts) error {
|
||||
// A click points at a line, so it sets the selection there; a double-click
|
||||
// additionally dives into staging/patch-building for that line.
|
||||
showSelectionAtLine(self.context.GetView(), opts.Y)
|
||||
showSelectionAtLine(self.context.GetView(), opts.Y, false)
|
||||
if opts.IsDoubleClick {
|
||||
return self.enterForLine(opts.Y)
|
||||
}
|
||||
|
|
@ -342,7 +407,7 @@ func (self *MainViewController) onClickInAlreadyFocusedView(opts gocui.ViewMouse
|
|||
|
||||
func (self *MainViewController) onClickInOtherViewOfMainViewPair(opts gocui.ViewMouseBindingOpts) error {
|
||||
self.c.Context().Push(self.context, types.OnFocusOpts{})
|
||||
showSelectionAtLine(self.context.GetView(), opts.Y)
|
||||
showSelectionAtLine(self.context.GetView(), opts.Y, false)
|
||||
if opts.IsDoubleClick {
|
||||
return self.enterForLine(opts.Y)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext typ
|
|||
}
|
||||
self.c.Context().Push(mainViewContext, types.OnFocusOpts{})
|
||||
if clickedLineIdx >= 0 {
|
||||
showSelectionAtLine(mainViewContext.GetView(), clickedLineIdx)
|
||||
showSelectionAtLine(mainViewContext.GetView(), clickedLineIdx, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -396,6 +396,8 @@ type TranslationSet struct {
|
|||
AskQuestion string
|
||||
PrevHunk string
|
||||
NextHunk string
|
||||
PrevFile string
|
||||
NextFile string
|
||||
PrevConflict string
|
||||
NextConflict string
|
||||
SelectPrevHunk string
|
||||
|
|
@ -1561,6 +1563,8 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
AskQuestion: "Ask Question",
|
||||
PrevHunk: "Go to previous hunk",
|
||||
NextHunk: "Go to next hunk",
|
||||
PrevFile: "Go to previous file",
|
||||
NextFile: "Go to next file",
|
||||
PrevConflict: "Previous conflict",
|
||||
NextConflict: "Next conflict",
|
||||
SelectPrevHunk: "Previous hunk",
|
||||
|
|
|
|||
Loading…
Reference in a new issue