mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Select and stage ranges and hunks from the focused main view
Step 1 made the focused main view a staging surface for a single line. This adds the range and hunk selection the staging view has, so you can stage (or unstage) a range or a whole hunk in place — `v` starts a range, shift-up/down extend one, `a` toggles hunk selection — without diving into the staging view. The selection mode (line / range / hunk, plus sticky-range and "the user turned hunk mode on themselves") lives on MainContext rather than the controller, because three places need a pane's mode: the main view controller that drives it, the focus controller that resets it when you focus the view, and togglePanel which sets it on the *other* pane. The selected line and the range anchor stay in the gocui view itself (its cursor and rangeSelectStartY), so the existing native range-select rendering draws the highlight — no new highlight machinery. Hunk bounds come from the diff-line metadata (the isChange run around the cursor, via ChangeBlockBounds), not from a parsed patch, so they work over any conforming rendering. The up/down keys are mode-aware: hunk mode steps hunk to hunk, a non-sticky range collapses on a plain move, a sticky range extends. GetOnStageFocusedMainView now takes the selected view-line range instead of one line; the files handler collects the change lines in the range and applies a single patch. It identifies each change line's patch line by scanning the parsed patch and matching (file line number, deletion?) identities, rather than looking each number up with PatchLineForLineNumber. That lookup can't tell an addition at the very start of a hunk from the deletion above it, nor an addition from the deletion it replaces on a modified line — both of which a range routinely spans. As a side effect this also fixes staging a change on the first line of a file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ee0077a157
commit
4b57cc9efe
|
|
@ -225,6 +225,8 @@ _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> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -335,6 +337,8 @@ _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> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -312,6 +314,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ _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> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -169,6 +171,8 @@ _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> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -233,6 +233,8 @@ _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> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -313,6 +315,8 @@ _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> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ _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> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -208,6 +210,8 @@ _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> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -237,6 +237,8 @@ _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> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -344,6 +346,8 @@ _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> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ _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> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -113,6 +115,8 @@ _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> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -288,6 +288,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -341,6 +343,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ _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> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
@ -370,6 +372,8 @@ _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> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type (
|
|||
onFocusFn = func(types.OnFocusOpts)
|
||||
onFocusLostFn = func(types.OnFocusLostOpts)
|
||||
onClickFocusedMainViewFn = func(mainViewName string, clickedLineIdx int) error
|
||||
onStageFocusedMainViewFn = func(mainViewName string, viewLineIdx int) error
|
||||
onStageFocusedMainViewFn = func(mainViewName string, firstLineIdx int, lastLineIdx int) error
|
||||
)
|
||||
|
||||
var _ types.IBaseContext = &BaseContext{}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,46 @@ import (
|
|||
type MainContext struct {
|
||||
*SimpleContext
|
||||
*SearchTrait
|
||||
|
||||
diffSelect DiffSelectState
|
||||
}
|
||||
|
||||
var _ types.ISearchableContext = (*MainContext)(nil)
|
||||
|
||||
// DiffSelectMode is how the focused main view's diff selection extends from the
|
||||
// cursor: a single line, a range from a fixed anchor, or the change block (hunk)
|
||||
// around the cursor.
|
||||
type DiffSelectMode int
|
||||
|
||||
const (
|
||||
DiffSelectModeLine DiffSelectMode = iota
|
||||
DiffSelectModeRange
|
||||
DiffSelectModeHunk
|
||||
)
|
||||
|
||||
// DiffSelectState holds the *mode* of the focused main view's diff selection. The
|
||||
// selected line and the range anchor themselves live in the gocui view (its
|
||||
// cursor and rangeSelectStartY, i.e. native range select); only the mode lives
|
||||
// here. It's on the context, not the controller, so that the main view controller
|
||||
// (which drives the selection), the focus controller (which resets it on focus),
|
||||
// and togglePanel (which sets it on the other pane) can all reach it via the
|
||||
// context they already hold.
|
||||
type DiffSelectState struct {
|
||||
Mode DiffSelectMode
|
||||
// When a range is sticky, moving the cursor without holding shift extends the
|
||||
// range; otherwise it collapses the range back to a single line.
|
||||
RangeIsSticky bool
|
||||
// Whether the user turned on hunk mode explicitly, as opposed to it being the
|
||||
// configured default; this decides whether escape leaves hunk mode.
|
||||
UserEnabledHunkMode bool
|
||||
}
|
||||
|
||||
// DiffSelectState returns the focused main view's selection mode state, for the
|
||||
// controllers to read and mutate directly.
|
||||
func (self *MainContext) DiffSelectState() *DiffSelectState {
|
||||
return &self.diffSelect
|
||||
}
|
||||
|
||||
func NewMainContext(
|
||||
view *gocui.View,
|
||||
windowName string,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func (self *baseController) GetOnClickFocusedMainView() func(mainViewName string
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *baseController) GetOnStageFocusedMainView() func(mainViewName string, viewLineIdx int) error {
|
||||
func (self *baseController) GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -457,8 +457,8 @@ func (self *FilesController) diffSplitState(node *filetree.FileNode) (split bool
|
|||
return split, mainShowsStaged
|
||||
}
|
||||
|
||||
func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName string, viewLineIdx int) error {
|
||||
return func(mainViewName string, viewLineIdx int) error {
|
||||
func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error {
|
||||
return func(mainViewName string, firstLineIdx int, lastLineIdx int) error {
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
|
|
@ -466,13 +466,13 @@ func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName strin
|
|||
|
||||
node := self.context().GetSelected()
|
||||
if node == nil || !node.IsFile() {
|
||||
// Staging a line of a multi-file (directory) diff is a later step; for
|
||||
// now only single-file diffs are stageable from the focused main view.
|
||||
// Staging from a multi-file (directory) diff is a later step; for now
|
||||
// only single-file diffs are stageable from the focused main view.
|
||||
return nil
|
||||
}
|
||||
|
||||
info, ok := self.c.Helpers().Staging.GetDiffLineInfo(mainViewName, viewLineIdx)
|
||||
if !ok {
|
||||
infos := self.c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx)
|
||||
if len(infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -482,28 +482,57 @@ func (self *FilesController) GetOnStageFocusedMainView() func(mainViewName strin
|
|||
_, mainShowsStaged := self.diffSplitState(node)
|
||||
staged := mainShowsStaged || mainViewName == self.c.Contexts().NormalSecondary.GetViewName()
|
||||
|
||||
return self.stageDiffLine(node.File, info, staged)
|
||||
return self.stageDiffLines(node.File, infos, staged)
|
||||
}
|
||||
}
|
||||
|
||||
// stageDiffLine stages, or when reverse is true unstages, the single diff line
|
||||
// identified by info. It builds a one-line patch from the file's diff and applies
|
||||
// it the same way the staging view does, but resolves the patch line from the
|
||||
// diff-line metadata rather than from a patch-explorer selection. A context or
|
||||
// header line yields an empty patch and is a no-op.
|
||||
func (self *FilesController) stageDiffLine(file *models.File, info types.DiffLineInfo, reverse bool) error {
|
||||
// stageDiffLines stages, or when reverse is true unstages, the diff lines identified
|
||||
// by infos (a single line, a range, or a hunk). It builds one patch from the file's
|
||||
// diff including all the selected change lines and applies it the same way the
|
||||
// staging view does, but identifies the patch lines from the diff-line metadata
|
||||
// rather than from a patch-explorer selection. A selection covering no change lines
|
||||
// yields an empty patch and is a no-op.
|
||||
//
|
||||
// Each selected change line is keyed by its (file line number, deletion?) identity,
|
||||
// and the freshly parsed patch is scanned for the body lines matching those
|
||||
// identities. Scanning the patch and matching identities — rather than looking each
|
||||
// line number up with PatchLineForLineNumber — is what makes a modified line work: a
|
||||
// deletion and the addition replacing it share a position but have distinct
|
||||
// identities, and the line-number lookup can't tell an addition at the start of a
|
||||
// hunk from the deletion above it.
|
||||
func (self *FilesController) stageDiffLines(file *models.File, infos []types.DiffLineInfo, reverse bool) error {
|
||||
parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, true, reverse))
|
||||
|
||||
lineNumber, isDeletion := info.PatchSelectLine()
|
||||
patchLineIdx := parsedPatch.PatchLineForLineNumber(lineNumber)
|
||||
if isDeletion {
|
||||
patchLineIdx = parsedPatch.PatchLineForOldLineNumber(lineNumber)
|
||||
type changeLineKey struct {
|
||||
lineNumber int
|
||||
isDeletion bool
|
||||
}
|
||||
selected := make(map[changeLineKey]bool, len(infos))
|
||||
for _, info := range infos {
|
||||
lineNumber, isDeletion := info.PatchSelectLine()
|
||||
selected[changeLineKey{lineNumber, isDeletion}] = true
|
||||
}
|
||||
|
||||
var patchLineIndices []int
|
||||
for idx, line := range parsedPatch.Lines() {
|
||||
var key changeLineKey
|
||||
switch {
|
||||
case line.IsAddition():
|
||||
key = changeLineKey{parsedPatch.LineNumberOfLine(idx), false}
|
||||
case line.IsDeletion():
|
||||
key = changeLineKey{parsedPatch.OldLineNumberOfLine(idx), true}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if selected[key] {
|
||||
patchLineIndices = append(patchLineIndices, idx)
|
||||
}
|
||||
}
|
||||
|
||||
patchToApply := parsedPatch.
|
||||
Transform(patch.TransformOpts{
|
||||
Reverse: reverse,
|
||||
IncludedLineIndices: []int{patchLineIdx},
|
||||
IncludedLineIndices: patchLineIndices,
|
||||
FileNameOverride: file.GetPath(),
|
||||
}).
|
||||
FormatPlain()
|
||||
|
|
|
|||
|
|
@ -81,6 +81,60 @@ func (self *StagingHelper) FirstChangeLineInView(view *gocui.View) (int, bool) {
|
|||
return 0, false
|
||||
}
|
||||
|
||||
// ChangeBlockBounds returns the view-line range [start, end] of the change block
|
||||
// (lazygit's notion of a hunk; see AdjacentChangeBlock) to select when entering or
|
||||
// moving in hunk mode in view's displayed diff. The block is the one containing
|
||||
// anchorViewLine, or — when that line is context — the first block at or below it
|
||||
// (matching how toggling hunk mode in the staging view snaps to the next change).
|
||||
// ok is false when no change line lies at or below the anchor (e.g. scrolled into
|
||||
// trailing context, or the diff isn't loaded that far yet).
|
||||
func (self *StagingHelper) ChangeBlockBounds(view *gocui.View, anchorViewLine int) (int, int, bool) {
|
||||
anchor, ok := view.BufferLineForViewLine(anchorViewLine)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
resolved := self.resolveDiffLines(view.DiffLineContents())
|
||||
isChange := make([]bool, len(resolved))
|
||||
for i, r := range resolved {
|
||||
isChange[i] = r.ok && r.info.IsChange()
|
||||
}
|
||||
|
||||
// Snap to the first change line at or after the anchor, then expand over the
|
||||
// whole contiguous run of change lines around it.
|
||||
start := anchor
|
||||
for start < len(isChange) && !isChange[start] {
|
||||
start++
|
||||
}
|
||||
if start >= len(isChange) {
|
||||
return 0, 0, false
|
||||
}
|
||||
end := start
|
||||
for start > 0 && isChange[start-1] {
|
||||
start--
|
||||
}
|
||||
for end < len(isChange)-1 && isChange[end+1] {
|
||||
end++
|
||||
}
|
||||
|
||||
startView, ok1 := view.ViewLineForBufferLine(start)
|
||||
endView, ok2 := view.ViewLineForBufferLine(end)
|
||||
if !ok1 || !ok2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
// ViewLineForBufferLine gives the first view line of the block's last buffer
|
||||
// line; extend over any further view lines it wrapped to, so the highlight
|
||||
// covers the whole block.
|
||||
for endView < view.ViewLinesHeight()-1 {
|
||||
next, ok := view.BufferLineForViewLine(endView + 1)
|
||||
if !ok || next != end {
|
||||
break
|
||||
}
|
||||
endView++
|
||||
}
|
||||
return startView, endView, true
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -163,6 +163,35 @@ func (self *StagingHelper) GetDiffLineInfo(windowName string, viewLineIdx int) (
|
|||
return self.GetDiffLineInfoForView(v, viewLineIdx)
|
||||
}
|
||||
|
||||
// ChangeLinesInViewRange resolves every change line (addition/deletion) in the
|
||||
// inclusive view-line range [first, last] of the diff shown in the given window, as
|
||||
// the patch-space identities to stage. It is the range form of GetDiffLineInfo,
|
||||
// behind staging a selection from the focused main view. Context and header lines
|
||||
// are skipped (Transform emits context regardless of the included set, so only
|
||||
// change lines need collecting — see §21.3), and view lines that wrap to the same
|
||||
// buffer line are de-duplicated.
|
||||
func (self *StagingHelper) ChangeLinesInViewRange(windowName string, first int, last int) []types.DiffLineInfo {
|
||||
v, _ := self.c.GocuiGui().View(self.windowHelper.GetViewNameForWindow(windowName))
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resolved := self.resolveDiffLines(v.DiffLineContents())
|
||||
var infos []types.DiffLineInfo
|
||||
lastBufferLine := -1
|
||||
for viewLine := first; viewLine <= last; viewLine++ {
|
||||
bufferLine, ok := v.BufferLineForViewLine(viewLine)
|
||||
if !ok || bufferLine == lastBufferLine {
|
||||
continue
|
||||
}
|
||||
lastBufferLine = bufferLine
|
||||
if bufferLine < len(resolved) && resolved[bufferLine].ok && resolved[bufferLine].info.IsChange() {
|
||||
infos = append(infos, resolved[bufferLine].info)
|
||||
}
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// GetDiffLineInfoForView is GetDiffLineInfo against a specific view rather than
|
||||
// one looked up by window. It is used to read the identity of the line the patch
|
||||
// explorer currently has selected when escaping back to the focused main view,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,24 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
|||
Description: openPullRequestDescription,
|
||||
Tooltip: openPullRequestTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.ToggleSelectHunk),
|
||||
Handler: self.toggleSelectHunk,
|
||||
Description: self.c.Tr.ToggleSelectHunk,
|
||||
DescriptionFunc: func() string {
|
||||
if self.sel().Mode == context.DiffSelectModeHunk {
|
||||
return self.c.Tr.SelectLineByLine
|
||||
}
|
||||
return self.c.Tr.SelectHunk
|
||||
},
|
||||
Tooltip: self.c.Tr.ToggleSelectHunkTooltip,
|
||||
DisplayOnScreen: selectionShown,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.ToggleRangeSelect),
|
||||
Handler: self.toggleRangeSelect,
|
||||
Description: self.c.Tr.ToggleRangeSelect,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.PrevHunk),
|
||||
Handler: self.prevChangeBlock,
|
||||
|
|
@ -126,6 +144,8 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
|||
},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.PrevItem), Handler: self.handlePrevLine},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.NextItem), Handler: self.handleNextLine},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.RangeSelectUp), Handler: self.extendRangeUp, Description: self.c.Tr.RangeSelectUp},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.RangeSelectDown), Handler: self.extendRangeDown, Description: self.c.Tr.RangeSelectDown},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.PrevPage), Handler: self.handlePrevPage, Description: self.c.Tr.PrevPage},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.NextPage), Handler: self.handleNextPage, Description: self.c.Tr.NextPage},
|
||||
{Tag: "navigation", Keys: opts.GetKeys(opts.Config.Universal.GotoTop), Handler: self.handleGotoTop, Description: self.c.Tr.GotoTop},
|
||||
|
|
@ -197,7 +217,7 @@ func (self *MainViewController) togglePanel() error {
|
|||
isDiff := self.isDiffView()
|
||||
self.c.Context().Push(self.otherContext, types.OnFocusOpts{})
|
||||
if isDiff {
|
||||
showInitialDiffSelection(self.c, self.otherContext.GetView())
|
||||
showInitialDiffSelection(self.c, self.otherContext)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -205,8 +225,11 @@ func (self *MainViewController) togglePanel() error {
|
|||
// showInitialDiffSelection turns on the focused main view's selection when entering
|
||||
// a diff view without pointing at a specific line: on the first change line already
|
||||
// visible (so the view doesn't jump), falling back to the current top line when none
|
||||
// is visible (scrolled into trailing context, or not loaded that far yet).
|
||||
func showInitialDiffSelection(c *ControllerCommon, view *gocui.View) {
|
||||
// is visible (scrolled into trailing context, or not loaded that far yet). The
|
||||
// select mode is reset to its default (a single line).
|
||||
func showInitialDiffSelection(c *ControllerCommon, mainContext *context.MainContext) {
|
||||
resetDiffSelectMode(mainContext)
|
||||
view := mainContext.GetView()
|
||||
target, ok := c.Helpers().Staging.FirstChangeLineInView(view)
|
||||
if !ok {
|
||||
target = view.OriginY()
|
||||
|
|
@ -214,6 +237,18 @@ func showInitialDiffSelection(c *ControllerCommon, view *gocui.View) {
|
|||
showSelectionAtLine(view, target, true)
|
||||
}
|
||||
|
||||
// resetDiffSelectMode returns the focused main view to its default select mode — a
|
||||
// single line, no range — used whenever the selection is (re-)established from
|
||||
// scratch (on focus, on a click). The view's range anchor is cleared too so the
|
||||
// next render highlights only the cursor line.
|
||||
func resetDiffSelectMode(mainContext *context.MainContext) {
|
||||
sel := mainContext.DiffSelectState()
|
||||
sel.Mode = context.DiffSelectModeLine
|
||||
sel.RangeIsSticky = false
|
||||
sel.UserEnabledHunkMode = false
|
||||
mainContext.GetView().CancelRangeSelect()
|
||||
}
|
||||
|
||||
func (self *MainViewController) escape() error {
|
||||
self.c.Context().Pop()
|
||||
return nil
|
||||
|
|
@ -225,10 +260,11 @@ func (self *MainViewController) isDiffView() bool {
|
|||
return sidePanelShowsDiff(self.c.Context().NextInStack(self.context))
|
||||
}
|
||||
|
||||
// stageSelectedLine stages (or unstages) the selected diff line, delegating to the
|
||||
// side panel beneath the focused main view since what "stage" means is the panel's
|
||||
// business (the working tree stages; later, commits add to a custom patch). Panels
|
||||
// whose diff isn't stageable register no handler, so this is a no-op there.
|
||||
// stageSelectedLine stages (or unstages) the selected diff line(s) — a single line,
|
||||
// a range, or a hunk — delegating to the side panel beneath the focused main view
|
||||
// since what "stage" means is the panel's business (the working tree stages; later,
|
||||
// commits add to a custom patch). Panels whose diff isn't stageable register no
|
||||
// handler, so this is a no-op there.
|
||||
func (self *MainViewController) stageSelectedLine() error {
|
||||
sidePanelContext := self.c.Context().NextInStack(self.context)
|
||||
if sidePanelContext == nil {
|
||||
|
|
@ -238,7 +274,8 @@ func (self *MainViewController) stageSelectedLine() error {
|
|||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
return handler(self.context.GetViewName(), self.context.GetView().SelectedLineIdx())
|
||||
first, last := self.context.GetView().SelectedLineRange()
|
||||
return handler(self.context.GetViewName(), first, last)
|
||||
}
|
||||
|
||||
func (self *MainViewController) enter() error {
|
||||
|
|
@ -270,114 +307,276 @@ func showSelectionAtLine(view *gocui.View, lineIdx int, scrollIntoView bool) {
|
|||
view.FocusPoint(0, lineIdx, scrollIntoView)
|
||||
}
|
||||
|
||||
// sel returns our pane's diff selection mode state.
|
||||
func (self *MainViewController) sel() *context.DiffSelectState {
|
||||
return self.context.DiffSelectState()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// move it to the target and scroll it into view, like the staging view — re-selecting
|
||||
// the whole block in hunk mode; 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) {
|
||||
v := self.context.GetView()
|
||||
showSelection := v.Highlight
|
||||
anchor := v.OriginY()
|
||||
if showSelection {
|
||||
anchor = v.SelectedLineIdx()
|
||||
if !v.Highlight {
|
||||
if target, ok := find(v, v.OriginY(), forward); ok {
|
||||
v.SetOrigin(0, target)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
target, ok := find(v, anchor, forward)
|
||||
target, ok := find(v, v.SelectedLineIdx(), forward)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if self.sel().Mode == context.DiffSelectModeHunk {
|
||||
self.selectHunkAround(target)
|
||||
} else {
|
||||
// Line mode leaves a single-line selection at the target; an active range
|
||||
// extends to it, since the anchor is untouched.
|
||||
showSelectionAtLine(v, target, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MainViewController) nextChangeBlock() error {
|
||||
self.navigate(self.c.Helpers().Staging.AdjacentChangeBlock, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) prevChangeBlock() error {
|
||||
self.navigate(self.c.Helpers().Staging.AdjacentChangeBlock, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) nextFile() error {
|
||||
self.navigate(self.c.Helpers().Staging.AdjacentFile, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) prevFile() error {
|
||||
self.navigate(self.c.Helpers().Staging.AdjacentFile, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectHunkAround re-selects the whole change block around the given change line,
|
||||
// for hunk mode: the cursor goes to the block's first line and the range anchor to
|
||||
// its last, so the native range highlight spans the block. With no change block
|
||||
// there (the line is trailing context) it falls back to a single-line selection.
|
||||
func (self *MainViewController) selectHunkAround(changeViewLine int) {
|
||||
v := self.context.GetView()
|
||||
start, end, ok := self.c.Helpers().Staging.ChangeBlockBounds(v, changeViewLine)
|
||||
if !ok {
|
||||
self.sel().Mode = context.DiffSelectModeLine
|
||||
v.CancelRangeSelect()
|
||||
showSelectionAtLine(v, changeViewLine, true)
|
||||
return
|
||||
}
|
||||
v.SetRangeSelectStart(end)
|
||||
showSelectionAtLine(v, start, 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 in a range this extends/contracts it; in
|
||||
// line mode it just moves the selected line.
|
||||
func (self *MainViewController) moveCursor(delta int) {
|
||||
v := self.context.GetView()
|
||||
if delta > 0 {
|
||||
if manager := self.c.GetViewBufferManagerForView(v); manager != nil {
|
||||
manager.ReadLines(delta)
|
||||
}
|
||||
}
|
||||
before := v.SelectedLineIdx()
|
||||
after := lo.Clamp(before+delta, 0, v.ViewLinesHeight()-1)
|
||||
if delta == -1 {
|
||||
checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
|
||||
} else if delta == 1 {
|
||||
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
|
||||
}
|
||||
v.FocusPoint(0, after, true)
|
||||
}
|
||||
|
||||
// scroll moves the view by delta lines without a selection, for non-diff main
|
||||
// content where there's nothing to select.
|
||||
func (self *MainViewController) scroll(delta int) {
|
||||
v := self.context.GetView()
|
||||
if delta > 0 {
|
||||
if manager := self.c.GetViewBufferManagerForView(v); manager != nil {
|
||||
manager.ReadLines(delta)
|
||||
}
|
||||
v.ScrollDown(delta)
|
||||
} else {
|
||||
v.ScrollUp(-delta)
|
||||
}
|
||||
}
|
||||
|
||||
// collapseForLineMove drops hunk mode, and a non-sticky range, back to a single-line
|
||||
// selection — what a plain (non-shift, non-hunk-step) move does before moving. A
|
||||
// sticky range is kept so the move extends it.
|
||||
func (self *MainViewController) collapseForLineMove() {
|
||||
sel := self.sel()
|
||||
if sel.Mode == context.DiffSelectModeHunk ||
|
||||
(sel.Mode == context.DiffSelectModeRange && !sel.RangeIsSticky) {
|
||||
sel.Mode = context.DiffSelectModeLine
|
||||
self.context.GetView().CancelRangeSelect()
|
||||
}
|
||||
}
|
||||
|
||||
// adjustSelection moves the selection by delta view lines for the plain up/down and
|
||||
// page keys. In hunk mode a single-line step (delta ±1) jumps to the adjacent hunk; a
|
||||
// larger page step drops out of hunk mode first, like the staging view. A non-sticky
|
||||
// range collapses back to a single line on a plain move. With no selection (non-diff
|
||||
// content) it scrolls.
|
||||
func (self *MainViewController) adjustSelection(delta int) {
|
||||
v := self.context.GetView()
|
||||
if !v.Highlight {
|
||||
self.scroll(delta)
|
||||
return
|
||||
}
|
||||
if self.sel().Mode == context.DiffSelectModeHunk && (delta == 1 || delta == -1) {
|
||||
self.navigate(self.c.Helpers().Staging.AdjacentChangeBlock, delta > 0)
|
||||
return
|
||||
}
|
||||
self.collapseForLineMove()
|
||||
self.moveCursor(delta)
|
||||
}
|
||||
|
||||
// selectAbsoluteLine moves the selection to a specific view line (the top or bottom
|
||||
// of the diff), dropping hunk mode and a non-sticky range like a plain move does.
|
||||
func (self *MainViewController) selectAbsoluteLine(target int) {
|
||||
self.collapseForLineMove()
|
||||
v := self.context.GetView()
|
||||
v.FocusPoint(0, lo.Clamp(target, 0, v.ViewLinesHeight()-1), true)
|
||||
}
|
||||
|
||||
// selectingRange reports whether a range selection is currently active: we're in
|
||||
// range mode and either it's sticky or the anchor and cursor differ (a non-sticky
|
||||
// range that has actually been extended).
|
||||
func (self *MainViewController) selectingRange() bool {
|
||||
if self.sel().Mode != context.DiffSelectModeRange {
|
||||
return false
|
||||
}
|
||||
start, end := self.context.GetView().SelectedLineRange()
|
||||
return self.sel().RangeIsSticky || start != end
|
||||
}
|
||||
|
||||
// toggleSelectHunk switches between selecting the change block (hunk) around the
|
||||
// cursor and a single line, mirroring the staging view's `a`.
|
||||
func (self *MainViewController) toggleSelectHunk() error {
|
||||
v := self.context.GetView()
|
||||
if !v.Highlight {
|
||||
return nil
|
||||
}
|
||||
|
||||
if showSelection {
|
||||
showSelectionAtLine(v, target, true)
|
||||
sel := self.sel()
|
||||
if sel.Mode == context.DiffSelectModeHunk {
|
||||
sel.Mode = context.DiffSelectModeLine
|
||||
v.CancelRangeSelect()
|
||||
} else {
|
||||
v.SetOrigin(0, target)
|
||||
sel.Mode = context.DiffSelectModeHunk
|
||||
sel.UserEnabledHunkMode = true
|
||||
self.selectHunkAround(v.SelectedLineIdx())
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func (self *MainViewController) handleLineChange(delta int) {
|
||||
// toggleRangeSelect starts or cancels a sticky range selection (extended by plain
|
||||
// up/down), mirroring the staging view's `v`.
|
||||
func (self *MainViewController) toggleRangeSelect() error {
|
||||
v := self.context.GetView()
|
||||
if v.Highlight {
|
||||
lineIdxBefore := v.CursorY() + v.OriginY()
|
||||
lineIdxAfter := lo.Clamp(lineIdxBefore+delta, 0, v.ViewLinesHeight()-1)
|
||||
if delta == -1 {
|
||||
checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig(), lineIdxBefore, lineIdxAfter)
|
||||
} else if delta == 1 {
|
||||
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig(), lineIdxBefore, lineIdxAfter)
|
||||
}
|
||||
v.FocusPoint(0, lineIdxAfter, true)
|
||||
} else {
|
||||
if delta < 0 {
|
||||
v.ScrollUp(-delta)
|
||||
} else {
|
||||
v.ScrollDown(delta)
|
||||
self.c.ReadLinesToFillView(v)
|
||||
}
|
||||
if !v.Highlight {
|
||||
return nil
|
||||
}
|
||||
sel := self.sel()
|
||||
if self.selectingRange() {
|
||||
sel.Mode = context.DiffSelectModeLine
|
||||
sel.RangeIsSticky = false
|
||||
v.CancelRangeSelect()
|
||||
} else {
|
||||
sel.Mode = context.DiffSelectModeRange
|
||||
sel.RangeIsSticky = true
|
||||
v.SetRangeSelectStart(v.SelectedLineIdx())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extendRange grows a (non-sticky) range selection by one line in response to
|
||||
// shift+up/down, starting one at the cursor if there isn't one yet. Mirrors the
|
||||
// staging view's range-select keys.
|
||||
func (self *MainViewController) extendRange(forward bool) error {
|
||||
v := self.context.GetView()
|
||||
if !v.Highlight {
|
||||
return nil
|
||||
}
|
||||
sel := self.sel()
|
||||
if !self.selectingRange() {
|
||||
sel.Mode = context.DiffSelectModeRange
|
||||
v.SetRangeSelectStart(v.SelectedLineIdx())
|
||||
}
|
||||
sel.RangeIsSticky = false
|
||||
delta := 1
|
||||
if !forward {
|
||||
delta = -1
|
||||
}
|
||||
self.moveCursor(delta)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) extendRangeUp() error {
|
||||
return self.extendRange(false)
|
||||
}
|
||||
|
||||
func (self *MainViewController) extendRangeDown() error {
|
||||
return self.extendRange(true)
|
||||
}
|
||||
|
||||
func (self *MainViewController) handlePrevLine() error {
|
||||
self.handleLineChange(-1)
|
||||
self.adjustSelection(-1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) handleNextLine() error {
|
||||
self.handleLineChange(1)
|
||||
self.adjustSelection(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) handlePrevPage() error {
|
||||
self.handleLineChange(-self.context.GetViewTrait().PageDelta())
|
||||
self.adjustSelection(-self.context.GetViewTrait().PageDelta())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) handleNextPage() error {
|
||||
self.handleLineChange(self.context.GetViewTrait().PageDelta())
|
||||
self.adjustSelection(self.context.GetViewTrait().PageDelta())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) handleGotoTop() error {
|
||||
v := self.context.GetView()
|
||||
if v.Highlight {
|
||||
v.FocusPoint(0, 0, true)
|
||||
} else {
|
||||
self.handleLineChange(-v.ViewLinesHeight())
|
||||
if !v.Highlight {
|
||||
self.scroll(-v.ViewLinesHeight())
|
||||
return nil
|
||||
}
|
||||
self.selectAbsoluteLine(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) handleGotoBottom() error {
|
||||
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
|
||||
manager.ReadToEnd(func() {
|
||||
self.c.OnUIThread(func() error {
|
||||
v := self.context.GetView()
|
||||
if v.Highlight {
|
||||
v.FocusPoint(0, v.ViewLinesHeight()-1, true)
|
||||
} else {
|
||||
self.handleLineChange(v.ViewLinesHeight())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
manager := self.c.GetViewBufferManagerForView(self.context.GetView())
|
||||
if manager == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.ReadToEnd(func() {
|
||||
self.c.OnUIThread(func() error {
|
||||
v := self.context.GetView()
|
||||
if !v.Highlight {
|
||||
self.scroll(v.ViewLinesHeight())
|
||||
return nil
|
||||
}
|
||||
self.selectAbsoluteLine(v.ViewLinesHeight() - 1)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -537,8 +736,9 @@ func (self *MainViewController) onClickInAlreadyFocusedView(opts gocui.ViewMouse
|
|||
if !self.isDiffView() {
|
||||
return nil
|
||||
}
|
||||
// A click points at a line, so it sets the selection there; a double-click
|
||||
// additionally dives into staging/patch-building for that line.
|
||||
// A click points at a line, so it sets a single-line selection there; a
|
||||
// double-click additionally dives into staging/patch-building for that line.
|
||||
resetDiffSelectMode(self.context)
|
||||
showSelectionAtLine(self.context.GetView(), opts.Y, false)
|
||||
if opts.IsDoubleClick {
|
||||
return self.enterForLine(opts.Y)
|
||||
|
|
@ -555,6 +755,7 @@ func (self *MainViewController) onClickInOtherViewOfMainViewPair(opts gocui.View
|
|||
if !self.isDiffView() {
|
||||
return nil
|
||||
}
|
||||
resetDiffSelectMode(self.context)
|
||||
showSelectionAtLine(self.context.GetView(), opts.Y, false)
|
||||
if opts.IsDoubleClick {
|
||||
return self.enterForLine(opts.Y)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package controllers
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
|
|
@ -77,10 +78,8 @@ func (self *SwitchToFocusedMainViewController) handleFocusMainView() error {
|
|||
return self.focusMainView(self.c.Contexts().Normal, -1)
|
||||
}
|
||||
|
||||
func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext types.Context, clickedLineIdx int) error {
|
||||
if context, ok := mainViewContext.(types.ISearchableContext); ok {
|
||||
context.ClearSearchString()
|
||||
}
|
||||
func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *context.MainContext, clickedLineIdx int) error {
|
||||
mainViewContext.ClearSearchString()
|
||||
self.c.Context().Push(mainViewContext, types.OnFocusOpts{})
|
||||
|
||||
if !sidePanelShowsDiff(self.context) {
|
||||
|
|
@ -89,11 +88,13 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext typ
|
|||
return nil
|
||||
}
|
||||
|
||||
view := mainViewContext.GetView()
|
||||
if clickedLineIdx >= 0 {
|
||||
showSelectionAtLine(view, clickedLineIdx, false)
|
||||
// A click points at a specific line, so select it directly (in the default
|
||||
// single-line mode).
|
||||
resetDiffSelectMode(mainViewContext)
|
||||
showSelectionAtLine(mainViewContext.GetView(), clickedLineIdx, false)
|
||||
} else {
|
||||
showInitialDiffSelection(self.c, view)
|
||||
showInitialDiffSelection(self.c, mainViewContext)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,9 +98,11 @@ type IBaseContext interface {
|
|||
// Likewise for the focused main view: we need this to communicate between a
|
||||
// side panel controller and the focused main view controller.
|
||||
AddOnClickFocusedMainViewFn(func(mainViewName string, clickedLineIdx int) error)
|
||||
// And for staging the selected line directly from the focused main view (space),
|
||||
// delegated to the side panel that owns the diff being shown.
|
||||
AddOnStageFocusedMainViewFn(func(mainViewName string, viewLineIdx int) error)
|
||||
// And for staging the selected line(s) directly from the focused main view
|
||||
// (space), delegated to the side panel that owns the diff being shown. The
|
||||
// inclusive view-line range is the current selection (a single line, a range, or
|
||||
// a hunk).
|
||||
AddOnStageFocusedMainViewFn(func(mainViewName string, firstLineIdx int, lastLineIdx int) error)
|
||||
// Adding on to the above, this is so that a list-specific handler can register
|
||||
// a hook for doing additional click handling
|
||||
AddOnClickFn(func(opts gocui.ViewMouseBindingOpts) error)
|
||||
|
|
@ -333,9 +335,10 @@ type HasKeybindings interface {
|
|||
GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error
|
||||
|
||||
// Implement this in a side-panel controller to stage/unstage (or, later, add to
|
||||
// the custom patch) the selected diff line when the user presses space in the
|
||||
// focused main view. Return nil to do nothing.
|
||||
GetOnStageFocusedMainView() func(mainViewName string, viewLineIdx int) error
|
||||
// the custom patch) the selected diff line(s) when the user presses space in the
|
||||
// focused main view. The inclusive view-line range is the current selection (a
|
||||
// single line, a range, or a hunk). Return nil to do nothing.
|
||||
GetOnStageFocusedMainView() func(mainViewName string, firstLineIdx int, lastLineIdx int) error
|
||||
}
|
||||
|
||||
type IController interface {
|
||||
|
|
|
|||
55
pkg/integration/tests/staging/stage_hunk_from_main_view.go
Normal file
55
pkg/integration/tests/staging/stage_hunk_from_main_view.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package staging
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StageHunkFromMainView = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Select a hunk in the focused main view and stage it, without diving into the staging view",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n")
|
||||
shell.Commit("one")
|
||||
|
||||
// Two separate change blocks, far enough apart to stay distinct hunks.
|
||||
shell.UpdateFile("file1", "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\neight\nNINE\nten\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("file1").IsSelected(),
|
||||
).
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-three"),
|
||||
).
|
||||
// `a` extends the selection to the whole change block around the cursor.
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains("-three"),
|
||||
Contains("+THREE"),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
Tap(func() {
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains("-three"),
|
||||
Contains("+THREE"),
|
||||
)
|
||||
}).
|
||||
// The other block is still unstaged.
|
||||
ContainsLines(
|
||||
Contains("-nine"),
|
||||
Contains("+NINE"),
|
||||
)
|
||||
},
|
||||
})
|
||||
66
pkg/integration/tests/staging/stage_range_from_main_view.go
Normal file
66
pkg/integration/tests/staging/stage_range_from_main_view.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package staging
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StageRangeFromMainView = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Select a range of diff lines in the focused main view and stage it, without diving into the staging view",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFile("file1", "ONE\ntwo\nthree\nfour\nfive\nsix\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("file1").IsSelected(),
|
||||
).
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
// Focusing the main view selects the first change line (a single line).
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-one"),
|
||||
).
|
||||
// Select a range with `v` and extend it down past the modified line and
|
||||
// some context, then stage it.
|
||||
Press(keys.Universal.ToggleRangeSelect).
|
||||
NavigateToLine(Contains("+four")).
|
||||
SelectedLines(
|
||||
Contains("-one"),
|
||||
Contains("+ONE"),
|
||||
Contains(" two"),
|
||||
Contains(" three"),
|
||||
Contains("+four"),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
Tap(func() {
|
||||
// The selected change lines — the deletion, the addition replacing it,
|
||||
// and the added 'four' — are now staged; the context lines came along
|
||||
// but aren't themselves staged.
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains("-one"),
|
||||
Contains("+ONE"),
|
||||
Contains(" two"),
|
||||
Contains(" three"),
|
||||
Contains("+four"),
|
||||
)
|
||||
}).
|
||||
// The unstaged half holds only the additions we didn't select.
|
||||
ContainsLines(
|
||||
Contains("+five"),
|
||||
Contains("+six"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -412,12 +412,14 @@ var tests = []*components.IntegrationTest{
|
|||
staging.Search,
|
||||
staging.SelectNextLineAfterStagingInTwoHunkDiff,
|
||||
staging.SelectNextLineAfterStagingIsolatedAddedLine,
|
||||
staging.StageHunkFromMainView,
|
||||
staging.StageHunks,
|
||||
staging.StageHunksWithRapidKeypresses,
|
||||
staging.StageLines,
|
||||
staging.StagePartialBlockOfChangesFirstLines,
|
||||
staging.StagePartialBlockOfChangesLastLines,
|
||||
staging.StagePartialBlockOfChangesMiddleLines,
|
||||
staging.StageRangeFromMainView,
|
||||
staging.StageRanges,
|
||||
stash.Apply,
|
||||
stash.ApplyPatch,
|
||||
|
|
|
|||
Loading…
Reference in a new issue