mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 02:35:39 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33e53a7579 | ||
|
|
2c233d52d4 | ||
|
|
55752f8ba7 | ||
|
|
5f032e372a | ||
|
|
636832e045 | ||
|
|
2236c8d8d9 |
@@ -226,6 +226,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -338,6 +339,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -195,6 +195,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -315,6 +316,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -87,6 +87,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -172,6 +173,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -234,6 +234,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -316,6 +317,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -102,6 +102,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -211,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -238,6 +238,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -347,6 +348,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -77,6 +77,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -116,6 +117,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -289,6 +289,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -344,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
@@ -78,6 +78,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
@@ -366,6 +367,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
|
||||
+3
-3
@@ -1484,9 +1484,9 @@ func (v *View) CopyContent(from *View) {
|
||||
|
||||
// A background task may be streaming output into the source view's buffer
|
||||
// via Write, so read it under its own lock. The source is always a
|
||||
// different view than the destination (see the sole caller,
|
||||
// moveMainContextToTop), and no other code holds two view write locks at
|
||||
// once, so this can't deadlock.
|
||||
// different view than the destination — its callers hand content from one
|
||||
// view to another — and no other code holds two view write locks at once, so
|
||||
// this can't deadlock.
|
||||
from.writeMutex.Lock()
|
||||
defer from.writeMutex.Unlock()
|
||||
|
||||
|
||||
@@ -13,14 +13,15 @@ type BaseContext struct {
|
||||
windowName string
|
||||
onGetOptionsMap func() map[string]string
|
||||
|
||||
keybindingsFns []types.KeybindingsFn
|
||||
mouseKeybindingsFns []types.MouseKeybindingsFn
|
||||
onDoubleClickFn func() error
|
||||
onClickFn func(opts gocui.ViewMouseBindingOpts) error
|
||||
onRenderToMainFn func()
|
||||
onFocusFns []onFocusFn
|
||||
onFocusLostFns []onFocusLostFn
|
||||
onQuitFns []func()
|
||||
keybindingsFns []types.KeybindingsFn
|
||||
mouseKeybindingsFns []types.MouseKeybindingsFn
|
||||
onDoubleClickFn func() error
|
||||
onClickFn func(opts gocui.ViewMouseBindingOpts) error
|
||||
focusedMainViewDiffSource types.FocusedMainViewDiffSource
|
||||
onRenderToMainFn func()
|
||||
onFocusFns []onFocusFn
|
||||
onFocusLostFns []onFocusLostFn
|
||||
onQuitFns []func()
|
||||
|
||||
focusable bool
|
||||
transient bool
|
||||
@@ -159,6 +160,7 @@ func (self *BaseContext) ClearAllAttachedControllerFunctions() {
|
||||
self.onQuitFns = nil
|
||||
self.onDoubleClickFn = nil
|
||||
self.onClickFn = nil
|
||||
self.focusedMainViewDiffSource = nil
|
||||
self.onRenderToMainFn = nil
|
||||
}
|
||||
|
||||
@@ -180,6 +182,15 @@ func (self *BaseContext) AddOnClickFn(fn func(opts gocui.ViewMouseBindingOpts) e
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BaseContext) AddFocusedMainViewDiffSource(source types.FocusedMainViewDiffSource) {
|
||||
if source != nil {
|
||||
if self.focusedMainViewDiffSource != nil {
|
||||
panic("only one controller is allowed to set the focused main view diff source")
|
||||
}
|
||||
self.focusedMainViewDiffSource = source
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BaseContext) GetOnDoubleClick() func() error {
|
||||
return self.onDoubleClickFn
|
||||
}
|
||||
@@ -188,6 +199,10 @@ func (self *BaseContext) GetOnClick() func(opts gocui.ViewMouseBindingOpts) erro
|
||||
return self.onClickFn
|
||||
}
|
||||
|
||||
func (self *BaseContext) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self.focusedMainViewDiffSource
|
||||
}
|
||||
|
||||
func (self *BaseContext) AddOnRenderToMainFn(fn func()) {
|
||||
if fn != nil {
|
||||
if self.onRenderToMainFn != nil {
|
||||
|
||||
@@ -85,10 +85,16 @@ func (self *CommitFilesContext) RefForAdjustingLineNumberInDiff() string {
|
||||
}
|
||||
|
||||
func (self *CommitFilesContext) GetFromAndToForDiff() (string, string) {
|
||||
if refs := self.GetRefRange(); refs != nil {
|
||||
return refs.From.ParentRefName(), refs.To.RefName()
|
||||
return FromAndToForDiff(self.GetRef(), self.GetRefRange())
|
||||
}
|
||||
|
||||
// FromAndToForDiff gives the two ends to diff for a ref, or for a range of them: a
|
||||
// range runs from the parent of its first ref to its last, a single ref from its own
|
||||
// parent to itself.
|
||||
func FromAndToForDiff(ref models.Ref, refRange *types.RefRange) (string, string) {
|
||||
if refRange != nil {
|
||||
return refRange.From.ParentRefName(), refRange.To.RefName()
|
||||
}
|
||||
ref := self.GetRef()
|
||||
return ref.ParentRefName(), ref.RefName()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ func AttachControllers(context types.Context, controllers ...types.IController)
|
||||
context.AddMouseKeybindingsFn(controller.GetMouseKeybindings)
|
||||
context.AddOnDoubleClickFn(controller.GetOnDoubleClick())
|
||||
context.AddOnClickFn(controller.GetOnClick())
|
||||
context.AddFocusedMainViewDiffSource(controller.GetFocusedMainViewDiffSource())
|
||||
context.AddOnRenderToMainFn(controller.GetOnRenderToMain())
|
||||
context.AddOnFocusFn(controller.GetOnFocus())
|
||||
context.AddOnFocusLostFn(controller.GetOnFocusLost())
|
||||
|
||||
@@ -23,6 +23,10 @@ func (self *baseController) GetOnClick() func(opts gocui.ViewMouseBindingOpts) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *baseController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *baseController) GetOnRenderToMain() func() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -191,6 +191,18 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self
|
||||
}
|
||||
|
||||
// PlainDiff hands out the commit's diff for the given files. Both panes show the same
|
||||
// diff here — the secondary one shows the custom patch built from it, which is not a
|
||||
// diff of the commit — so which pane asks makes no difference.
|
||||
func (self *CommitFilesController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(from, to, paths)
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) copyDiffToClipboard(paths []string, toastMessage string) error {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
|
||||
@@ -21,6 +21,9 @@ type FilesController struct {
|
||||
baseController
|
||||
*ListControllerTrait[*filetree.FileNode]
|
||||
c *ControllerCommon
|
||||
|
||||
// what this panel offers on the diff it shows in the focused main view
|
||||
diffActions *WorkingTreeDiffActions
|
||||
}
|
||||
|
||||
var _ types.IController = &FilesController{}
|
||||
@@ -36,6 +39,7 @@ func NewFilesController(
|
||||
c.Contexts().Files.GetSelected,
|
||||
c.Contexts().Files.GetSelectedItems,
|
||||
),
|
||||
diffActions: NewWorkingTreeDiffActions(c),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,42 +370,44 @@ func (self *FilesController) renderNonTextualConflict(node *filetree.FileNode) {
|
||||
func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) {
|
||||
self.c.Helpers().MergeConflicts.ResetMergeState()
|
||||
|
||||
split := self.c.UserConfig().Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
|
||||
mainShowsStaged := !split && node.GetHasStagedChanges()
|
||||
// The unstaged side of a file's diff is shown in the main pane and the staged side
|
||||
// in the secondary one, each only where there is a side to show — so a side is
|
||||
// always in the same place, whatever the file happens to have. A file with nothing
|
||||
// unstaged therefore shows its staged changes in the secondary pane, which then has
|
||||
// the whole section to itself. Configured to always split, both panes are shown
|
||||
// whether or not there is anything on either side.
|
||||
alwaysSplit := self.c.UserConfig().Gui.SplitDiff == "always"
|
||||
showStaged := node.GetHasStagedChanges() || alwaysSplit
|
||||
showUnstaged := node.GetHasUnstagedChanges() || alwaysSplit || !showStaged
|
||||
|
||||
paths := self.pathsForDiff(node)
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, paths)
|
||||
title := self.c.Tr.UnstagedChanges
|
||||
if mainShowsStaged {
|
||||
title = self.c.Tr.StagedChanges
|
||||
}
|
||||
refreshOpts := types.RefreshMainOpts{
|
||||
Pair: self.c.MainViewPairs().Normal,
|
||||
Main: &types.ViewUpdateOpts{
|
||||
refreshOpts := types.RefreshMainOpts{Pair: self.c.MainViewPairs().Normal}
|
||||
|
||||
if showUnstaged {
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, false, paths)
|
||||
refreshOpts.Main = &types.ViewUpdateOpts{
|
||||
Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()),
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Title: title,
|
||||
},
|
||||
}
|
||||
|
||||
if split {
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, paths)
|
||||
|
||||
title := self.c.Tr.StagedChanges
|
||||
if mainShowsStaged {
|
||||
title = self.c.Tr.UnstagedChanges
|
||||
Title: self.c.Tr.UnstagedChanges,
|
||||
}
|
||||
}
|
||||
|
||||
if showStaged {
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, paths)
|
||||
refreshOpts.Secondary = &types.ViewUpdateOpts{
|
||||
Title: title,
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()),
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Title: self.c.Tr.StagedChanges,
|
||||
}
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(refreshOpts)
|
||||
}
|
||||
|
||||
func (self *FilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self.diffActions
|
||||
}
|
||||
|
||||
func (self *FilesController) GetOnDoubleClick() func() error {
|
||||
return self.withItemGraceful(func(node *filetree.FileNode) error {
|
||||
return self.press([]*filetree.FileNode{node})
|
||||
|
||||
@@ -83,6 +83,19 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
|
||||
return types.NewRunDiffRendererTask(cmdObj.GetCmd())
|
||||
}
|
||||
|
||||
// PlainDiffBetweenRefs returns the diff of the given files between two refs as git
|
||||
// writes it, without colour or a diff renderer's involvement — what a panel showing
|
||||
// a commit's diff hands out as the diff behind its rendering (see
|
||||
// types.FocusedMainViewDiffSource). It honours diffing mode, so that the diff is of
|
||||
// the same two ends the main view is showing.
|
||||
func (self *DiffHelper) PlainDiffBetweenRefs(from string, to string, paths []string) string {
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
// An error means there is no diff to be had, which for our purposes is the same
|
||||
// as an empty one.
|
||||
diff, _ := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, true).RunWithOutput()
|
||||
return diff
|
||||
}
|
||||
|
||||
func (self *DiffHelper) FilterPathsForCommit(commit *models.Commit) []string {
|
||||
filterPath := self.c.Modes().Filtering.GetPath()
|
||||
if filterPath != "" {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// Reading a rendering back to the diff it came from. What a diff view shows is a diff
|
||||
// renderer's picture of a diff, and a picture is not what you want on your clipboard,
|
||||
// or in a patch — so the lines of interest are located by identity in the diff itself,
|
||||
// which the panel that rendered it hands out (types.FocusedMainViewDiffSource).
|
||||
|
||||
// PlainDiffOfSelection returns the text of the rows selected in view, in two parts.
|
||||
//
|
||||
// aboveDiff is what the selection covers above the diff, as it reads on screen; see
|
||||
// textAboveDiff. fromDiff is the diff behind the rest: per file the selection touches,
|
||||
// the run of diff lines from the first of its selected lines to the last, with the
|
||||
// files in the order the selection meets them.
|
||||
//
|
||||
// A run, rather than the matched lines alone, so that what comes out reads as a diff:
|
||||
// the lines between two selected ones come along even when the rendering didn't show
|
||||
// them (difftastic leaves out whitespace-only changes) or showed them in another order
|
||||
// (a side-by-side rendering groups the deletions of a hunk before its additions).
|
||||
//
|
||||
// Headers are selected lines like any other. A hunk header names the first line of its
|
||||
// hunk, in the rendering as in the diff, so it is looked for the way a line of the file
|
||||
// is; a file header names no line at all and a rendering may spread it over as many
|
||||
// rows as it likes, so a selection touching one of them takes the whole header.
|
||||
//
|
||||
// plainDiff fetches the diff of the given repo-relative files, and is asked only for
|
||||
// the files the selection touches, so that copying three lines of a commit's diff
|
||||
// doesn't fetch the whole of it. fromDiff is "" when no selected row could be found in
|
||||
// the diff, e.g. because the selection covers nothing but a renderer's decoration.
|
||||
func (self *DiffLineHelper) PlainDiffOfSelection(
|
||||
view *gocui.View, first int, last int, plainDiff func(paths []string) string,
|
||||
) (aboveDiff string, fromDiff string) {
|
||||
worktreePath := self.c.Git().RepoPaths.WorktreePath()
|
||||
aboveDiff = self.textAboveDiff(view, first, last)
|
||||
|
||||
// The files in the order they are shown, and per file what the selection covers of
|
||||
// its diff.
|
||||
paths := []string{}
|
||||
selection := map[string]*selectedDiffLines{}
|
||||
for _, info := range self.DiffLinesInViewRange(view, first, last) {
|
||||
// A row that is neither a line of the file nor a header — the "\ No newline at
|
||||
// end of file" marker — names nothing to look for. It comes along anyway when it
|
||||
// falls within a run.
|
||||
if info.Type == types.DiffLineOther {
|
||||
continue
|
||||
}
|
||||
selected, ok := selection[info.Path]
|
||||
if !ok {
|
||||
paths = append(paths, info.Path)
|
||||
selected = &selectedDiffLines{lines: map[patchLine]bool{}}
|
||||
selection[info.Path] = selected
|
||||
}
|
||||
if info.Type == types.DiffLineFileHeader {
|
||||
selected.header = true
|
||||
} else {
|
||||
selected.lines[patchLineOf(info)] = true
|
||||
}
|
||||
}
|
||||
|
||||
relPaths := repoRelativePaths(worktreePath, paths)
|
||||
if len(relPaths) == 0 {
|
||||
return aboveDiff, ""
|
||||
}
|
||||
|
||||
diffLines := strings.Split(strings.TrimSuffix(plainDiff(relPaths), "\n"), "\n")
|
||||
runs := map[string][2]int{}
|
||||
for i, parsed := range parseAllDiffLinesFromBuffer(diffLines) {
|
||||
if !parsed.ok {
|
||||
continue
|
||||
}
|
||||
info := diffLineInfoIn(worktreePath, parsed.parsed)
|
||||
selected := selection[info.Path]
|
||||
if selected == nil || !selected.covers(info) {
|
||||
continue
|
||||
}
|
||||
if run, ok := runs[info.Path]; ok {
|
||||
runs[info.Path] = [2]int{run[0], i}
|
||||
} else {
|
||||
runs[info.Path] = [2]int{i, i}
|
||||
}
|
||||
}
|
||||
|
||||
text := strings.Builder{}
|
||||
for _, path := range paths {
|
||||
run, ok := runs[path]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, line := range diffLines[run[0] : run[1]+1] {
|
||||
text.WriteString(line)
|
||||
text.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return aboveDiff, text.String()
|
||||
}
|
||||
|
||||
// textAboveDiff returns the text of the selected rows that sit above the diff, as they
|
||||
// read on screen. What a diff view shows before its first file — a commit's message and
|
||||
// git's summary of it — is part of no file's diff, so there is nothing to look those
|
||||
// rows up in and the rendering is all we have of them.
|
||||
//
|
||||
// It is "" for a view that shows no file at all: with no diff on screen there is
|
||||
// nothing for the text to be above, and a rendering we couldn't read as a diff is
|
||||
// exactly what we don't want on the clipboard.
|
||||
func (self *DiffLineHelper) textAboveDiff(view *gocui.View, first int, last int) string {
|
||||
firstRow, ok := view.BufferLineForViewLine(first)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
lastRow, ok := view.BufferLineForViewLine(last)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
startOfDiff := slices.IndexFunc(self.filePaths(view), func(path string) bool { return path != "" })
|
||||
if startOfDiff == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
lastRow = min(lastRow, startOfDiff-1)
|
||||
if lastRow < firstRow {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(view.BufferLines()[firstRow:lastRow+1], "\n") + "\n"
|
||||
}
|
||||
|
||||
// selectedDiffLines is what a selection covers of one file's diff.
|
||||
type selectedDiffLines struct {
|
||||
// The lines of the file to look for, hunk headers among them, by the identity that
|
||||
// names them in any rendering of the diff.
|
||||
lines map[patchLine]bool
|
||||
// Whether the file's header is covered, in whole or in part.
|
||||
header bool
|
||||
}
|
||||
|
||||
// covers reports whether the given line of a file's diff is one the selection holds.
|
||||
//
|
||||
// A file header is answered for by kind rather than looked for: the two ways a row's
|
||||
// identity is recovered disagree about what to call it — a parse of the diff says the
|
||||
// file's first line, a renderer's record says the file has no line — and a rendering
|
||||
// may show the header as one row or as ten. So a selection that touches it holds every
|
||||
// line of it.
|
||||
func (self *selectedDiffLines) covers(info types.DiffLineInfo) bool {
|
||||
if info.Type == types.DiffLineFileHeader {
|
||||
return self.header
|
||||
}
|
||||
return self.lines[patchLineOf(info)]
|
||||
}
|
||||
|
||||
// repoRelativePaths turns the absolute paths a diff line's identity carries into the
|
||||
// repo-relative ones git speaks, dropping any that lies outside the worktree — a diff
|
||||
// renderer states the path however it likes, and one we can't place is one we can't
|
||||
// ask git about.
|
||||
func repoRelativePaths(worktreePath string, paths []string) []string {
|
||||
relPaths := make([]string, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
relPath, err := filepath.Rel(worktreePath, path)
|
||||
if err != nil || strings.HasPrefix(relPath, "..") {
|
||||
continue
|
||||
}
|
||||
relPaths = append(relPaths, filepath.ToSlash(relPath))
|
||||
}
|
||||
return relPaths
|
||||
}
|
||||
@@ -11,6 +11,30 @@ import (
|
||||
// cursor and a click speak. They are all built on the identities recovered in
|
||||
// diff_line_helper.go, which is where the answering stops and the recovering starts.
|
||||
|
||||
// DiffLinesInViewRange returns the identity of every diff line shown by the rows in
|
||||
// the inclusive view-line range [first, last] of view's rendered diff, in display
|
||||
// order. Rows whose identity can't be recovered are left out, as are the wrapped
|
||||
// segments of a row already counted.
|
||||
//
|
||||
// A row can show more than one diff line — a side-by-side rendering puts a deletion
|
||||
// beside the addition replacing it — and all of them are reported: what the user
|
||||
// pointed at is the row, so everything on it is selected.
|
||||
func (self *DiffLineHelper) DiffLinesInViewRange(view *gocui.View, first int, last int) []types.DiffLineInfo {
|
||||
identities := self.resolveDiffLineIdentities(view.DiffLineContents())
|
||||
|
||||
infos := []types.DiffLineInfo{}
|
||||
previousBufferLine := -1
|
||||
for viewLine := first; viewLine <= last; viewLine++ {
|
||||
bufferLine, ok := view.BufferLineForViewLine(viewLine)
|
||||
if !ok || bufferLine == previousBufferLine || bufferLine >= len(identities) {
|
||||
continue
|
||||
}
|
||||
previousBufferLine = bufferLine
|
||||
infos = append(infos, identities[bufferLine]...)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// changeLines resolves view's rendered diff to one flag per buffer line: whether
|
||||
// that row is a change line (an addition or a deletion), as opposed to context, a
|
||||
// header, or a row whose identity couldn't be recovered. Those are the rows a
|
||||
|
||||
@@ -60,7 +60,7 @@ type WindowArrangementArgs struct {
|
||||
ContentHeightForWindow func(window string) int
|
||||
// Whether the main panel is split (as is the case e.g. when a file has both
|
||||
// staged and unstaged changes)
|
||||
SplitMainPanel bool
|
||||
MainPanes types.MainPanes
|
||||
// The current screen mode (normal, half, full)
|
||||
ScreenMode types.ScreenMode
|
||||
// The content shown on the bottom left of the screen when showing a loader
|
||||
@@ -103,7 +103,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
|
||||
ContentHeightForWindow: func(window string) int {
|
||||
return self.windowHelper.GetContextForWindow(window).TotalContentHeight()
|
||||
},
|
||||
SplitMainPanel: repoState.GetSplitMainPanel(),
|
||||
MainPanes: repoState.GetMainPanes(),
|
||||
ScreenMode: repoState.GetScreenMode(),
|
||||
AppStatus: appStatus,
|
||||
InformationStr: informationStr,
|
||||
@@ -215,36 +215,27 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
|
||||
}
|
||||
|
||||
func mainSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
||||
// if we're not in split mode we can just show the one main panel. Likewise if
|
||||
// the main panel is focused and we're in full-screen mode
|
||||
if !args.SplitMainPanel || (args.ScreenMode == types.SCREEN_FULL && args.CurrentWindow == "main") {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
Weight: 1,
|
||||
},
|
||||
mainPane := &boxlayout.Box{Window: "main", Weight: 1}
|
||||
secondaryPane := &boxlayout.Box{Window: "secondary", Weight: 1}
|
||||
|
||||
switch args.MainPanes {
|
||||
case types.MainPaneOnly:
|
||||
return []*boxlayout.Box{mainPane}
|
||||
case types.SecondaryPaneOnly:
|
||||
return []*boxlayout.Box{secondaryPane}
|
||||
case types.BothMainPanes:
|
||||
// In full-screen mode the focused one takes the whole section anyway.
|
||||
if args.ScreenMode == types.SCREEN_FULL {
|
||||
if args.CurrentWindow == "main" {
|
||||
return []*boxlayout.Box{mainPane}
|
||||
}
|
||||
if args.CurrentWindow == "secondary" {
|
||||
return []*boxlayout.Box{secondaryPane}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if args.CurrentWindow == "secondary" && args.ScreenMode == types.SCREEN_FULL {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "secondary",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "secondary",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
return []*boxlayout.Box{mainPane, secondaryPane}
|
||||
}
|
||||
|
||||
func getMidSectionWeights(args WindowArrangementArgs) (int, int) {
|
||||
@@ -382,7 +373,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
||||
}
|
||||
|
||||
func splitMainPanelSideBySide(args WindowArrangementArgs) bool {
|
||||
if !args.SplitMainPanel {
|
||||
if args.MainPanes != types.BothMainPanes {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestGetWindowDimensions(t *testing.T) {
|
||||
// Each panel shows its first tab by default; for the special-cased
|
||||
// panels (status, stash) the view name matches the window name.
|
||||
ActiveViewForWindow: func(window string) string { return window },
|
||||
SplitMainPanel: false,
|
||||
MainPanes: types.MainPaneOnly,
|
||||
ScreenMode: types.SCREEN_NORMAL,
|
||||
AppStatus: "",
|
||||
InformationStr: "information",
|
||||
|
||||
@@ -79,6 +79,13 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
GetDisabledReason: self.diffSelectionDisabledReason,
|
||||
Tooltip: self.c.Tr.EditFileTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.CopyToClipboard),
|
||||
Handler: self.copySelection,
|
||||
Description: self.c.Tr.CopySelectedTextToClipboard,
|
||||
DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.CopySelectedTextToClipboard),
|
||||
GetDisabledReason: self.diffSelectionDisabledReason,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.PrevHunk),
|
||||
Handler: self.prevChangeBlock,
|
||||
@@ -248,6 +255,63 @@ func (self *MainViewController) diffMainViewType() types.DiffMainViewType {
|
||||
return types.DiffMainViewTypeNone
|
||||
}
|
||||
|
||||
// diffSource returns the panel beneath the focused main view, as the thing that can
|
||||
// hand out the diff it rendered there. nil when this pane isn't on the stack, or the
|
||||
// panel beneath shows no diff.
|
||||
func (self *MainViewController) diffSource() types.FocusedMainViewDiffSource {
|
||||
if !self.c.Context().IsInStack(self.context) {
|
||||
return nil
|
||||
}
|
||||
sidePanel := self.c.Context().NextInStack(self.context)
|
||||
if sidePanel == nil {
|
||||
return nil
|
||||
}
|
||||
return sidePanel.GetFocusedMainViewDiffSource()
|
||||
}
|
||||
|
||||
// copySelection copies the selected diff lines to the clipboard — not as the diff
|
||||
// renderer drew them, but as they read in the diff itself, which is both what you meant
|
||||
// to copy and the only form a renderer can't have mangled. A selection that is all
|
||||
// additions or all deletions loses its +/- column, so that it can be pasted straight
|
||||
// into code.
|
||||
//
|
||||
// The rows above the diff (a commit's message, git's summary of it) belong to no file,
|
||||
// so they are copied as they stand on screen.
|
||||
func (self *MainViewController) copySelection() error {
|
||||
text := self.textOfSelection()
|
||||
if text == "" {
|
||||
self.c.ErrorToast(self.c.Tr.SelectionNotFoundInDiffToast)
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard)
|
||||
if err := self.c.OS().CopyToClipboard(text); err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.Toast(self.c.Tr.SelectedDiffLinesCopiedToast)
|
||||
return nil
|
||||
}
|
||||
|
||||
// textOfSelection is what copying the selection puts on the clipboard. It is "" when
|
||||
// none of the selected rows could be placed in the diff, which is what a rendering's
|
||||
// own decoration comes to.
|
||||
func (self *MainViewController) textOfSelection() string {
|
||||
source := self.diffSource()
|
||||
if source == nil {
|
||||
return ""
|
||||
}
|
||||
view := self.context.GetView()
|
||||
first, last := view.SelectedLineRange()
|
||||
aboveDiff, fromDiff := self.c.Helpers().DiffLine.PlainDiffOfSelection(view, first, last,
|
||||
func(paths []string) string { return source.PlainDiff(self.context, paths) })
|
||||
if aboveDiff == "" {
|
||||
// Only text that is all diff has a +/- column to lose: a line of a commit message
|
||||
// may begin with a '-' without being a deletion of anything.
|
||||
fromDiff = dropDiffPrefix(fromDiff)
|
||||
}
|
||||
return aboveDiff + fromDiff
|
||||
}
|
||||
|
||||
// diffSelectState returns this pane's diff selection mode state.
|
||||
func (self *MainViewController) diffSelectState() *types.DiffSelectState {
|
||||
return self.context.DiffSelectState()
|
||||
|
||||
@@ -37,6 +37,20 @@ func (self *ReflogCommitsController) context() *context.ReflogCommitsContext {
|
||||
return self.c.Contexts().ReflogCommits
|
||||
}
|
||||
|
||||
func (self *ReflogCommitsController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self
|
||||
}
|
||||
|
||||
// PlainDiff hands out the reflog entry's diff for the given files — the same diff its
|
||||
// main view shows, only without the entry's message and stat above it.
|
||||
func (self *ReflogCommitsController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
|
||||
commit := self.context().GetSelected()
|
||||
if commit == nil {
|
||||
return ""
|
||||
}
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(commit.ParentRefName(), commit.RefName(), paths)
|
||||
}
|
||||
|
||||
func (self *ReflogCommitsController) GetOnRenderToMain() func() {
|
||||
return func() {
|
||||
self.c.Helpers().Diff.WithDiffModeCheck(func() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
@@ -64,6 +65,22 @@ func (self *SwitchToDiffFilesController) GetOnDoubleClick() func() error {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SwitchToDiffFilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self
|
||||
}
|
||||
|
||||
// PlainDiff hands out the diff of the panel's selected commit (or range of them) for
|
||||
// the given files — the same diff its main view shows, only without the commit's
|
||||
// message and stat above it.
|
||||
func (self *SwitchToDiffFilesController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
|
||||
ref := self.context.GetSelectedRef()
|
||||
if ref == nil {
|
||||
return ""
|
||||
}
|
||||
from, to := context.FromAndToForDiff(ref, self.context.GetSelectedRefRangeForDiffFiles())
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(from, to, paths)
|
||||
}
|
||||
|
||||
func (self *SwitchToDiffFilesController) enter() error {
|
||||
ref := self.context.GetSelectedRef()
|
||||
refsRange := self.context.GetSelectedRefRangeForDiffFiles()
|
||||
|
||||
@@ -70,7 +70,13 @@ func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewM
|
||||
}
|
||||
|
||||
func (self *SwitchToFocusedMainViewController) handleFocusMainView() error {
|
||||
return self.focusMainView(self.c.Contexts().Normal, -1)
|
||||
// Usually the main pane, but the content can be in the secondary one alone: a file
|
||||
// with nothing but staged changes shows them there.
|
||||
mainViewContext := self.c.Contexts().Normal
|
||||
if self.c.State().GetRepoState().GetMainPanes() == types.SecondaryPaneOnly {
|
||||
mainViewContext = self.c.Contexts().NormalSecondary
|
||||
}
|
||||
return self.focusMainView(mainViewContext, -1)
|
||||
}
|
||||
|
||||
func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *context.MainContext, clickedLineIdx int) error {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// WorkingTreeDiffActions implements what the files panel offers on the diff it renders
|
||||
// into the focused main view: the diff itself, for the commands that need to read lines
|
||||
// out of it rather than off the screen.
|
||||
type WorkingTreeDiffActions struct {
|
||||
c *ControllerCommon
|
||||
}
|
||||
|
||||
var _ types.FocusedMainViewDiffSource = &WorkingTreeDiffActions{}
|
||||
|
||||
func NewWorkingTreeDiffActions(c *ControllerCommon) *WorkingTreeDiffActions {
|
||||
return &WorkingTreeDiffActions{c: c}
|
||||
}
|
||||
|
||||
func (self *WorkingTreeDiffActions) context() *context.WorkingTreeContext {
|
||||
return self.c.Contexts().Files
|
||||
}
|
||||
|
||||
// PlainDiff hands out the working tree's diff for the given files, taken from the
|
||||
// side of the index that the asking pane shows.
|
||||
func (self *WorkingTreeDiffActions) PlainDiff(pane types.DiffPaneContext, paths []string) string {
|
||||
node := self.context().GetSelected()
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
// An error means there is no diff to be had, which for our purposes is the same as
|
||||
// an empty one.
|
||||
diff, _ := self.c.Git().WorkingTree.
|
||||
WorktreeFileDiffCmdObj(node, true, self.showsStagedSide(pane), paths).
|
||||
RunWithOutput()
|
||||
return diff
|
||||
}
|
||||
|
||||
// showsStagedSide reports whether the given main pane is the one showing the staged
|
||||
// side of a file's diff, which is always the lower one.
|
||||
func (self *WorkingTreeDiffActions) showsStagedSide(pane types.DiffPaneContext) bool {
|
||||
return pane.GetKey() == self.c.Contexts().NormalSecondary.GetKey()
|
||||
}
|
||||
+13
-12
@@ -22,12 +22,7 @@ func (gui *Gui) scrollDownView(view *gocui.View) {
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
var view *gocui.View
|
||||
if gui.c.Context().Current().GetWindowName() == "secondary" {
|
||||
view = gui.secondaryView()
|
||||
} else {
|
||||
view = gui.mainView()
|
||||
}
|
||||
view := gui.mainSectionView()
|
||||
|
||||
if view.Name() == "mergeConflicts" {
|
||||
// although we have this same logic in the controller, this method can be invoked
|
||||
@@ -43,12 +38,7 @@ func (gui *Gui) scrollUpMain() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain() error {
|
||||
var view *gocui.View
|
||||
if gui.c.Context().Current().GetWindowName() == "secondary" {
|
||||
view = gui.secondaryView()
|
||||
} else {
|
||||
view = gui.mainView()
|
||||
}
|
||||
view := gui.mainSectionView()
|
||||
|
||||
if view.Name() == "mergeConflicts" {
|
||||
gui.State.Contexts.MergeConflicts.SetUserScrolling(true)
|
||||
@@ -59,6 +49,17 @@ func (gui *Gui) scrollDownMain() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mainSectionView returns the view that the keys for scrolling the main section act
|
||||
// on: the pane the focus is in when it is in one of them, and otherwise the pane the
|
||||
// section is showing — which is the lower one whenever it has the section to itself.
|
||||
func (gui *Gui) mainSectionView() *gocui.View {
|
||||
if gui.c.Context().Current().GetWindowName() == "secondary" ||
|
||||
gui.State.MainPanes == types.SecondaryPaneOnly {
|
||||
return gui.secondaryView()
|
||||
}
|
||||
return gui.mainView()
|
||||
}
|
||||
|
||||
func (gui *Gui) mainView() *gocui.View {
|
||||
viewName := gui.helpers.Window.GetViewNameForWindow("main")
|
||||
view, _ := gui.g.View(viewName)
|
||||
|
||||
+9
-5
@@ -101,6 +101,10 @@ type Gui struct {
|
||||
// this tells us whether our views have been initially set up
|
||||
ViewsSetup bool
|
||||
|
||||
// the label for the key that focuses the main view, worn by whichever of the two
|
||||
// main panes that key focuses (see showFocusMainViewJumpLabelOn)
|
||||
focusMainViewJumpLabel string
|
||||
|
||||
Views types.Views
|
||||
|
||||
// Log of the commands/actions logged in the Command Log panel.
|
||||
@@ -231,7 +235,7 @@ type GuiRepoState struct {
|
||||
Model *types.Model
|
||||
Modes *types.Modes
|
||||
|
||||
SplitMainPanel bool
|
||||
MainPanes types.MainPanes
|
||||
|
||||
SearchState *types.SearchState
|
||||
// Lets us not load everything at once. Written and read from refresh
|
||||
@@ -321,12 +325,12 @@ func (self *GuiRepoState) GetSearchState() *types.SearchState {
|
||||
return self.SearchState
|
||||
}
|
||||
|
||||
func (self *GuiRepoState) SetSplitMainPanel(value bool) {
|
||||
self.SplitMainPanel = value
|
||||
func (self *GuiRepoState) SetMainPanes(value types.MainPanes) {
|
||||
self.MainPanes = value
|
||||
}
|
||||
|
||||
func (self *GuiRepoState) GetSplitMainPanel() bool {
|
||||
return self.SplitMainPanel
|
||||
func (self *GuiRepoState) GetMainPanes() types.MainPanes {
|
||||
return self.MainPanes
|
||||
}
|
||||
|
||||
func (gui *Gui) onSwitchToNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error {
|
||||
|
||||
+77
-4
@@ -110,8 +110,13 @@ func (gui *Gui) allMainContextPairs() []types.MainContextPair {
|
||||
func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) {
|
||||
gui.moveMainContextPairToTop(opts.Pair)
|
||||
|
||||
panes := mainPanesFor(opts)
|
||||
gui.handOverMainSection(opts.Pair, panes)
|
||||
|
||||
if opts.Main != nil {
|
||||
gui.RefreshMainView(opts.Main, opts.Pair.Main)
|
||||
} else {
|
||||
gui.clearMainView(opts.Pair.Main)
|
||||
}
|
||||
|
||||
if opts.Secondary != nil {
|
||||
@@ -134,7 +139,50 @@ func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
gui.splitMainPanel(opts.Secondary != nil)
|
||||
gui.setMainPanes(panes)
|
||||
}
|
||||
|
||||
// handOverMainSection carries the content of the main section from the pane that has
|
||||
// been showing it on its own to the pane about to, when a render moves the section's
|
||||
// content from one to the other — a file's changes going from unstaged to staged, say.
|
||||
//
|
||||
// The section is one region of the screen to the user, so a change of which pane holds
|
||||
// it has to look like that region re-rendering rather than blanking and filling in
|
||||
// again: the incoming pane shows what the outgoing one was showing, where it was
|
||||
// showing it, until its own render has read enough to be swapped in. It renders from
|
||||
// the top when it does, the content it took over not being its own (see
|
||||
// clearMainView).
|
||||
func (gui *Gui) handOverMainSection(pair types.MainContextPair, panes types.MainPanes) {
|
||||
// The lower pane is always the same view, being the only one a render can leave
|
||||
// holding the section on its own; the upper one is whichever view of the main
|
||||
// window this render is for, which moveMainContextPairToTop has just given a copy
|
||||
// of what that window was showing.
|
||||
upper, lower := pair.Main.GetView(), gui.Views.Secondary
|
||||
|
||||
var from, to *gocui.View
|
||||
switch {
|
||||
case gui.State.MainPanes == types.MainPaneOnly && panes == types.SecondaryPaneOnly:
|
||||
from, to = upper, lower
|
||||
case gui.State.MainPanes == types.SecondaryPaneOnly && panes == types.MainPaneOnly:
|
||||
from, to = lower, upper
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
gui.g.CopyContent(from, to)
|
||||
}
|
||||
|
||||
// mainPanesFor says which panes the given render occupies: the one it has content for,
|
||||
// or both when it has content for both.
|
||||
func mainPanesFor(opts types.RefreshMainOpts) types.MainPanes {
|
||||
switch {
|
||||
case opts.Secondary == nil:
|
||||
return types.MainPaneOnly
|
||||
case opts.Main == nil:
|
||||
return types.SecondaryPaneOnly
|
||||
default:
|
||||
return types.BothMainPanes
|
||||
}
|
||||
}
|
||||
|
||||
// clampDiffSelectionToContent brings the focused main view's selection back onto the
|
||||
@@ -162,10 +210,19 @@ func (gui *Gui) clampDiffSelectionToContent(view *gocui.View) {
|
||||
}
|
||||
|
||||
// clearMainView empties a pane that is being given nothing to show, selection and all.
|
||||
//
|
||||
// An emptied pane is showing nothing, so it also goes back to the top and stops
|
||||
// claiming the render it was showing: whatever it is given next is content the user
|
||||
// hasn't seen there, and is shown from the top like any other.
|
||||
func (gui *Gui) clearMainView(mainContext types.Context) {
|
||||
mainContext.GetView().Clear()
|
||||
view := mainContext.GetView()
|
||||
view.Clear()
|
||||
view.SetOrigin(0, 0)
|
||||
mainContext.SetHasSelectableContent(false)
|
||||
gui.State.ContextMgr.UpdateSelectionHighlights()
|
||||
if manager := gui.getViewBufferManagerForView(view); manager != nil {
|
||||
manager.ForgetRenderedContent()
|
||||
}
|
||||
}
|
||||
|
||||
// updateDiffSelectionVisibility works out whether a main pane holds anything for a
|
||||
@@ -289,8 +346,24 @@ func (gui *Gui) mainContextForView(view *gocui.View) *context.MainContext {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) splitMainPanel(splitMainPanel bool) {
|
||||
gui.State.SplitMainPanel = splitMainPanel
|
||||
func (gui *Gui) setMainPanes(panes types.MainPanes) {
|
||||
gui.State.MainPanes = panes
|
||||
|
||||
// The label for the key that focuses the main view belongs on the pane that key
|
||||
// focuses, which is the secondary one while it is the only one shown.
|
||||
if panes == types.SecondaryPaneOnly {
|
||||
gui.showFocusMainViewJumpLabelOn(gui.Views.Secondary)
|
||||
} else {
|
||||
gui.showFocusMainViewJumpLabelOn(gui.Views.Main)
|
||||
}
|
||||
}
|
||||
|
||||
// showFocusMainViewJumpLabelOn puts the main view's jump label on the given pane and
|
||||
// takes it off the other one, so that only the pane the key focuses wears it.
|
||||
func (gui *Gui) showFocusMainViewJumpLabelOn(view *gocui.View) {
|
||||
gui.Views.Main.TitlePrefix = ""
|
||||
gui.Views.Secondary.TitlePrefix = ""
|
||||
view.TitlePrefix = gui.focusMainViewJumpLabel
|
||||
}
|
||||
|
||||
// reApplySearch runs a search the view holds again over the content a render has just
|
||||
|
||||
@@ -461,8 +461,8 @@ type IRepoStateAccessor interface {
|
||||
SetScreenMode(ScreenMode)
|
||||
InSearchPrompt() bool
|
||||
GetSearchState() *SearchState
|
||||
SetSplitMainPanel(bool)
|
||||
GetSplitMainPanel() bool
|
||||
SetMainPanes(MainPanes)
|
||||
GetMainPanes() MainPanes
|
||||
GetMergeOrRebaseStartedInLazygit() bool
|
||||
SetMergeOrRebaseStartedInLazygit(bool)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,10 @@ type IBaseContext interface {
|
||||
// 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)
|
||||
// Likewise for the focused main view, which acts on the diff of whichever panel
|
||||
// is beneath it and so has to reach that panel's controller. nil for a panel
|
||||
// that shows no diff.
|
||||
AddFocusedMainViewDiffSource(FocusedMainViewDiffSource)
|
||||
|
||||
AddOnRenderToMainFn(func())
|
||||
AddOnFocusFn(func(OnFocusOpts))
|
||||
@@ -220,6 +224,21 @@ type DiffPaneContext interface {
|
||||
DiffSelectState() *DiffSelectState
|
||||
}
|
||||
|
||||
// FocusedMainViewDiffSource is how a side panel hands out the diff behind what it
|
||||
// renders into the focused main view: the diff of the given files as git writes it,
|
||||
// with no colour and no diff renderer in the way. What the main view shows is that
|
||||
// same diff after a renderer has had it, which may have restructured, reordered or
|
||||
// dropped parts of it — so anything that needs the diff itself, rather than a picture
|
||||
// of it, asks the panel that produced it.
|
||||
//
|
||||
// paths are repo-relative, and are asked for rather than assumed so that a few lines
|
||||
// of a commit's diff can be had without fetching the whole thing. pane says which of
|
||||
// the two main panes is asking, since a panel can show a different diff in each — the
|
||||
// files panel shows the unstaged changes in one and the staged ones in the other.
|
||||
type FocusedMainViewDiffSource interface {
|
||||
PlainDiff(pane DiffPaneContext, paths []string) string
|
||||
}
|
||||
|
||||
type IListContext interface {
|
||||
Context
|
||||
|
||||
@@ -315,6 +334,11 @@ type HasKeybindings interface {
|
||||
// HandleFocus has already been called (so the main view is up to date). Should return nil if it
|
||||
// decides not to do anything with the click.
|
||||
GetOnClick() func(opts gocui.ViewMouseBindingOpts) error
|
||||
|
||||
// Implement this in a side-panel controller to hand out the diff behind what your
|
||||
// panel renders into the focused main view, for the commands that act on a
|
||||
// selection in it. nil for a controller whose panel shows no diff.
|
||||
GetFocusedMainViewDiffSource() FocusedMainViewDiffSource
|
||||
}
|
||||
|
||||
type IController interface {
|
||||
|
||||
@@ -40,3 +40,11 @@ type DiffLineInfo struct {
|
||||
func (self DiffLineInfo) IsChange() bool {
|
||||
return self.Type == DiffLineAdded || self.Type == DiffLineDeleted
|
||||
}
|
||||
|
||||
// IsContent reports whether the row is a line of the file itself — a change or a
|
||||
// context line — as opposed to a header or a marker. Those are the rows that have a
|
||||
// position in the file, and so can be looked for in another rendering of the same
|
||||
// diff, or in the diff itself.
|
||||
func (self DiffLineInfo) IsContent() bool {
|
||||
return self.IsChange() || self.Type == DiffLineContext
|
||||
}
|
||||
|
||||
@@ -13,6 +13,19 @@ func NewMainContextPair(main Context, secondary Context) MainContextPair {
|
||||
return MainContextPair{Main: main, Secondary: secondary}
|
||||
}
|
||||
|
||||
// MainPanes says which of the two panes of the main section are shown. Most content
|
||||
// takes the main pane alone; content with two sides to it — the working tree's
|
||||
// unstaged and staged changes, a commit's diff and the patch built from it — takes
|
||||
// both; and content whose only side is the second one takes the secondary pane alone,
|
||||
// so that it has the whole section rather than sitting under an empty pane.
|
||||
type MainPanes int
|
||||
|
||||
const (
|
||||
MainPaneOnly MainPanes = iota
|
||||
BothMainPanes
|
||||
SecondaryPaneOnly
|
||||
)
|
||||
|
||||
type MainViewPairs struct {
|
||||
Normal MainContextPair
|
||||
MergeConflicts MainContextPair
|
||||
|
||||
+3
-3
@@ -283,11 +283,11 @@ func (gui *Gui) configureViewProperties() {
|
||||
}
|
||||
}
|
||||
|
||||
gui.focusMainViewJumpLabel = ""
|
||||
if gui.c.UserConfig().Gui.ShowPanelJumps {
|
||||
gui.Views.Main.TitlePrefix = keyToTitlePrefix(gui.c.UserConfig().Keybinding.Universal.FocusMainView)
|
||||
} else {
|
||||
gui.Views.Main.TitlePrefix = ""
|
||||
gui.focusMainViewJumpLabel = keyToTitlePrefix(gui.c.UserConfig().Keybinding.Universal.FocusMainView)
|
||||
}
|
||||
gui.showFocusMainViewJumpLabelOn(gui.Views.Main)
|
||||
|
||||
// Index the tab strips by view so we can both set them on views that are
|
||||
// part of a multi-tab panel and clear them on views that no longer are
|
||||
|
||||
@@ -736,6 +736,8 @@ type TranslationSet struct {
|
||||
CopyPathToClipboard string
|
||||
CommitPrefixPatternError string
|
||||
CopySelectedTextToClipboard string
|
||||
SelectedDiffLinesCopiedToast string
|
||||
SelectionNotFoundInDiffToast string
|
||||
NoFilesStagedTitle string
|
||||
NoFilesStagedPrompt string
|
||||
BranchNotFoundTitle string
|
||||
@@ -1925,6 +1927,8 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CopyTagToClipboard: "Copy tag to clipboard",
|
||||
CopyPathToClipboard: "Copy path to clipboard",
|
||||
CopySelectedTextToClipboard: "Copy selected text to clipboard",
|
||||
SelectedDiffLinesCopiedToast: "Selected diff lines copied to clipboard",
|
||||
SelectionNotFoundInDiffToast: "Nothing in the selection could be found in the diff",
|
||||
CommitPrefixPatternError: "Error in commitPrefix pattern",
|
||||
NoFilesStagedTitle: "No files staged",
|
||||
NoFilesStagedPrompt: "You have not staged any files. Commit all files?",
|
||||
|
||||
@@ -88,6 +88,17 @@ func (self *ViewDriver) IsImmediatelyBelow(upper *ViewDriver) *ViewDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
// TitlePrefix asserts on the label a view wears in front of its title, which is the
|
||||
// key that jumps to it.
|
||||
func (self *ViewDriver) TitlePrefix(expected *TextMatcher) *ViewDriver {
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
actual := self.getView().TitlePrefix
|
||||
return expected.context(fmt.Sprintf("%s title prefix", self.context)).test(actual)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *ViewDriver) Clear() *ViewDriver {
|
||||
// clearing multiple times in case there's multiple lines
|
||||
// (the clear button only clears a single line at a time)
|
||||
|
||||
@@ -54,6 +54,6 @@ var ResolveWithoutTrailingLf = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("M file").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().Content(Contains("-a1\n+a2\n").DoesNotContain("-no eol"))
|
||||
t.Views().Secondary().Content(Contains("-a1\n+a2\n").DoesNotContain("-no eol"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -73,6 +73,7 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Focus().
|
||||
SelectedLine(Contains("file1"))
|
||||
|
||||
t.Views().Main().Content(Contains("+second line"))
|
||||
// The patch was applied to the index, so the file's changes are all staged.
|
||||
t.Views().Secondary().Content(Contains("+second line"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ var DirectoryDiffWithRenamedFiles = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Equals(" R dir/nested/file3 → file3"),
|
||||
)
|
||||
|
||||
t.Views().Main().ContainsLines(
|
||||
t.Views().Secondary().ContainsLines(
|
||||
Equals("diff --git a/file1 b/dir/file1"),
|
||||
Equals("similarity index 100%"),
|
||||
Equals("rename from file1"),
|
||||
@@ -51,7 +51,7 @@ var DirectoryDiffWithRenamedFiles = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
SelectNextItem().
|
||||
SelectedLine(Equals(" ▼ dir"))
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Equals("diff --git a/file1 b/dir/file1"),
|
||||
Equals("similarity index 100%"),
|
||||
@@ -75,7 +75,7 @@ var DirectoryDiffWithRenamedFiles = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Equals(" R file1 → file1"),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Equals("diff --git a/file1 b/dir/file1"),
|
||||
Equals("similarity index 100%"),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var PaneShownAgainStartsAtTheTop = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A pane that was emptied while it wasn't shown starts at the top when it comes back, rather than where it was left",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 120,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
lines := make([]string, 40)
|
||||
for i := range lines {
|
||||
lines[i] = fmt.Sprintf("line%02d", i+1)
|
||||
}
|
||||
shell.CreateFileAndAdd("file1", strings.Join(lines, "\n")+"\n")
|
||||
shell.CreateFileAndAdd("file2", "one\n")
|
||||
shell.Commit("one")
|
||||
|
||||
// More staged changes in file1 than fit in the pane they are shown in, so that
|
||||
// there is a position in it to lose, plus an unstaged change to give the file a
|
||||
// second pane.
|
||||
for i := range lines {
|
||||
lines[i] = strings.ToUpper(lines[i])
|
||||
}
|
||||
shell.UpdateFileAndAdd("file1", strings.Join(lines, "\n")+"\n")
|
||||
shell.UpdateFile("file1", strings.Join(lines, "\n")+"unstaged\n")
|
||||
|
||||
shell.UpdateFile("file2", "two\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
NavigateToLine(Contains("file1"))
|
||||
|
||||
t.Views().Secondary().
|
||||
IsVisible().
|
||||
Title(Equals("Staged changes")).
|
||||
ScrollWheelDown().
|
||||
ScrollWheelDown().
|
||||
OriginYAtLeast(1)
|
||||
|
||||
// A file with nothing staged leaves that pane with nothing to show, so it goes
|
||||
// away and is emptied.
|
||||
t.Views().Files().NavigateToLine(Contains("file2"))
|
||||
t.Views().Secondary().IsInvisible()
|
||||
|
||||
t.Views().Files().NavigateToLine(Contains("file1"))
|
||||
t.Views().Secondary().
|
||||
IsVisible().
|
||||
Content(Contains("+LINE40")).
|
||||
OriginY(0)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// changedLines is a file's worth of numbered lines, prefixed so that each file's diff
|
||||
// can be told from the other's on screen.
|
||||
func changedLines(prefix string) string {
|
||||
lines := make([]string, 40)
|
||||
for i := range lines {
|
||||
lines[i] = fmt.Sprintf("%s%02d", prefix, i+1)
|
||||
}
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
var PaneTakingOverStartsAtTheTop = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A pane taking the main section over shows its diff from the top, rather than at the offset it was left at",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 120,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", changedLines("one"))
|
||||
shell.CreateFileAndAdd("file2", changedLines("two"))
|
||||
shell.Commit("one")
|
||||
|
||||
// One file's changes are unstaged and the other's are staged, so each is shown
|
||||
// in a pane of its own — and selecting one after the other hands the section
|
||||
// from one pane to the other. Both diffs are longer than the section, so either
|
||||
// pane can be scrolled.
|
||||
shell.UpdateFile("file1", changedLines("ONE"))
|
||||
shell.UpdateFileAndAdd("file2", changedLines("TWO"))
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
NavigateToLine(Contains("file1"))
|
||||
|
||||
t.Views().Secondary().IsInvisible()
|
||||
t.Views().Main().
|
||||
IsVisible().
|
||||
Title(Equals("Unstaged changes")).
|
||||
ScrollWheelDown().
|
||||
ScrollWheelDown().
|
||||
OriginYAtLeast(1)
|
||||
|
||||
t.Views().Files().NavigateToLine(Contains("file2"))
|
||||
|
||||
t.Views().Main().IsInvisible()
|
||||
t.Views().Secondary().
|
||||
IsVisible().
|
||||
Title(Equals("Staged changes")).
|
||||
Content(Contains("+TWO40")).
|
||||
OriginY(0).
|
||||
ScrollWheelDown().
|
||||
ScrollWheelDown().
|
||||
OriginYAtLeast(1)
|
||||
|
||||
// Back to the pane that was left scrolled: what it is given is a diff the user
|
||||
// hasn't seen there, so it starts at the top like any other.
|
||||
t.Views().Files().NavigateToLine(Contains("file1"))
|
||||
|
||||
t.Views().Secondary().IsInvisible()
|
||||
t.Views().Main().
|
||||
IsVisible().
|
||||
Title(Equals("Unstaged changes")).
|
||||
Content(Contains("+ONE40")).
|
||||
OriginY(0)
|
||||
|
||||
t.Views().Files().NavigateToLine(Contains("file2"))
|
||||
|
||||
t.Views().Main().IsInvisible()
|
||||
t.Views().Secondary().
|
||||
IsVisible().
|
||||
Title(Equals("Staged changes")).
|
||||
Content(Contains("+TWO40")).
|
||||
OriginY(0)
|
||||
},
|
||||
})
|
||||
@@ -34,7 +34,7 @@ var RenameSimilarityThresholdChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Universal.FocusMainView).
|
||||
Tap(func() {
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Press(keys.Universal.IncreaseRenameSimilarityThreshold)
|
||||
t.ExpectToast(Equals("Changed rename similarity threshold to 50%"))
|
||||
}).
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StagedChangesInLowerPane = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A file's staged changes are shown in the lower pane whether or not it also has unstaged ones",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("both", "one\n")
|
||||
shell.CreateFileAndAdd("indexOnly", "one\n")
|
||||
shell.CreateFileAndAdd("worktreeOnly", "one\n")
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFileAndAdd("both", "one\nstaged\n")
|
||||
shell.UpdateFile("both", "one\nstaged\nunstaged\n")
|
||||
// More staged lines than fit in the pane, so that it can be scrolled.
|
||||
staged := make([]string, 40)
|
||||
for i := range staged {
|
||||
staged[i] = fmt.Sprintf("staged%02d", i+1)
|
||||
}
|
||||
shell.UpdateFileAndAdd("indexOnly", "one\n"+strings.Join(staged, "\n")+"\n")
|
||||
shell.UpdateFile("worktreeOnly", "one\nunstaged\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
NavigateToLine(Contains("both"))
|
||||
|
||||
// With changes on both sides, each side has its own pane.
|
||||
t.Views().Main().
|
||||
Title(Equals("Unstaged changes")).
|
||||
Content(Contains("+unstaged"))
|
||||
t.Views().Secondary().
|
||||
Title(Equals("Staged changes")).
|
||||
Content(Contains("+staged"))
|
||||
|
||||
// With nothing unstaged, the staged side keeps its pane, which then has the
|
||||
// whole space to itself.
|
||||
t.Views().Files().NavigateToLine(Contains("indexOnly"))
|
||||
|
||||
t.Views().Main().IsInvisible()
|
||||
t.Views().Secondary().
|
||||
IsVisible().
|
||||
Title(Equals("Staged changes")).
|
||||
Content(Contains("+staged01")).
|
||||
// The key that focuses the diff wears its label, wherever the diff is.
|
||||
TitlePrefix(Equals("[0]")).
|
||||
OriginY(0)
|
||||
|
||||
// And the keys for scrolling the diff scroll the pane it is in.
|
||||
t.GlobalPress(keys.Universal.ScrollDownMain)
|
||||
t.Views().Secondary().OriginYAtLeast(1)
|
||||
t.GlobalPress(keys.Universal.ScrollUpMain)
|
||||
t.Views().Secondary().OriginY(0)
|
||||
|
||||
// And focusing the diff focuses the pane it is in.
|
||||
t.Views().Files().Press(keys.Universal.FocusMainView)
|
||||
t.Views().Secondary().IsFocused()
|
||||
t.Views().Secondary().PressEscape()
|
||||
|
||||
// With nothing staged, only the upper pane is shown.
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
NavigateToLine(Contains("worktreeOnly"))
|
||||
|
||||
t.Views().Secondary().IsInvisible()
|
||||
t.Views().Main().
|
||||
IsVisible().
|
||||
Title(Equals("Unstaged changes")).
|
||||
Content(Contains("+unstaged")).
|
||||
TitlePrefix(Equals("[0]"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var CopyRowsThatAreNoDiffLine = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Copying rows that stand for no line of the diff says so rather than copying nothing",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
cfg.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
|
||||
// A renderer that states which line of the file each row of its diff shows, and
|
||||
// ends with a row of its own that shows none. It ignores its input and prints
|
||||
// this one.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Command: `printf '\033]1717;1\007'; ` +
|
||||
`printf '\033]1717;1;f;;;file1\007file1\n'; ` +
|
||||
`printf '\033]1717;1;c;1;;file1\007 one\n'; ` +
|
||||
`printf '\033]1717;1;d;2;2;file1\007-two\n'; ` +
|
||||
`printf '\033]1717;1;a;2;;file1\007+TWO\n'; ` +
|
||||
`printf '\033]1717;1;c;3;;file1\007 three\n'; ` +
|
||||
`printf -- '--- that was the diff ---\n'; ` +
|
||||
`cat >/dev/null`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFile("file1", "one\nTWO\nthree\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-two"),
|
||||
).
|
||||
SelectNextItem().
|
||||
SelectNextItem().
|
||||
SelectNextItem().
|
||||
SelectedLines(
|
||||
Contains("that was the diff"),
|
||||
).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
t.ExpectToast(Equals("Nothing in the selection could be found in the diff"))
|
||||
t.FileSystem().PathNotPresent("clipboard")
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// The clipboard is emulated by a file, so that this works on CI too. A copy says so
|
||||
// with a toast, which the harness wants acknowledged before the next keypress.
|
||||
func expectClipboard(t *TestDriver, matcher *TextMatcher) {
|
||||
defer t.Shell().DeleteFile("clipboard")
|
||||
|
||||
t.ExpectToast(Equals("Selected diff lines copied to clipboard"))
|
||||
t.FileSystem().FileContent("clipboard", matcher)
|
||||
}
|
||||
|
||||
var CopySelectedDiffLines = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Copy the selected diff lines from the focused main view, as the diff reads rather than as the renderer drew it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = true
|
||||
// Emulate the clipboard by writing to a file.
|
||||
cfg.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
|
||||
// A renderer that decorates every line of a diff's body, so that what is on
|
||||
// screen is not what the diff says. It reads the +/- column, so it wants its
|
||||
// input uncoloured.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{
|
||||
Command: `awk '/^@@/ { body = 1 } body && /^[-+ ]/ { print $0 " <<<"; next } { print }'`,
|
||||
ColorArg: "never",
|
||||
},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
|
||||
shell.Commit("one")
|
||||
shell.UpdateFileAndAdd("file1", "one\nTWO\nthree\n")
|
||||
shell.Commit("two")
|
||||
|
||||
shell.UpdateFile("file1", "one\nTWO\nADD1\nADD2\nthree\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("+ADD1 <<<"),
|
||||
Contains("+ADD2 <<<"),
|
||||
).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
// The renderer's decoration is nowhere in what was copied, and a selection that
|
||||
// is all additions loses its '+' column, ready to be pasted into code.
|
||||
expectClipboard(t, Equals("ADD1\nADD2\n"))
|
||||
|
||||
// A selection reaching up into the hunk header takes the header with it, and what
|
||||
// comes out is a patch fragment rather than lines of code, so the columns stay.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains("+ADD1 <<<"),
|
||||
).
|
||||
Press(keys.Universal.ToggleRangeSelect).
|
||||
Press(keys.Universal.PrevItem).
|
||||
Press(keys.Universal.PrevItem).
|
||||
Press(keys.Universal.PrevItem).
|
||||
SelectedLines(
|
||||
Contains("@@ -1,3 +1,5 @@"),
|
||||
Contains(" one <<<"),
|
||||
Contains(" TWO <<<"),
|
||||
Contains("+ADD1 <<<"),
|
||||
).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
expectClipboard(t, Equals("@@ -1,3 +1,5 @@\n one\n TWO\n+ADD1\n"))
|
||||
|
||||
// A file header is taken whole, however much of it is selected: the rendering may
|
||||
// show it as any number of rows, none of which is a line of the file.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Press(keys.Universal.PrevItem).
|
||||
SelectedLines(
|
||||
Contains("+++ b/file1"),
|
||||
Contains("@@ -1,3 +1,5 @@"),
|
||||
Contains(" one <<<"),
|
||||
Contains(" TWO <<<"),
|
||||
Contains("+ADD1 <<<"),
|
||||
).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
expectClipboard(t, MatchesRegexp(
|
||||
`\Adiff --git a/file1 b/file1\nindex [0-9a-f]+\.\.[0-9a-f]+ 100644\n`+
|
||||
`--- a/file1\n\+\+\+ b/file1\n@@ -1,3 \+1,5 @@\n one\n TWO\n\+ADD1\n\z`))
|
||||
|
||||
// A commit's diff is copied the same way, through the panel that produced it.
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
NavigateToLine(Contains("two")).
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-two <<<"),
|
||||
Contains("+TWO <<<"),
|
||||
).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
// Both kinds of line are in the selection, so the columns stay: what comes out
|
||||
// is the diff itself.
|
||||
expectClipboard(t, Equals("-two\n+TWO\n"))
|
||||
|
||||
// What a diff view shows above the diff is part of no file's diff, a commit's
|
||||
// message among it, so a selection of those rows is copied as it stands on screen.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
NavigateToLine(Contains(" two")).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
expectClipboard(t, Equals(" two\n"))
|
||||
|
||||
// A selection reaching from there into the diff takes the screen's rows down to
|
||||
// the first file and the diff's own from there on.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Press(keys.Universal.ToggleRangeSelect).
|
||||
NavigateToLine(Contains("-two <<<")).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
expectClipboard(t, MatchesRegexp(
|
||||
`\A {4}two\n---\n file1 \|[^\n]*\n 1 file changed[^\n]*\n\n`+
|
||||
`diff --git a/file1 b/file1\nindex [0-9a-f]+\.\.[0-9a-f]+ 100644\n`+
|
||||
`--- a/file1\n\+\+\+ b/file1\n@@ -1,3 \+1,3 @@\n one\n-two\n\z`))
|
||||
},
|
||||
})
|
||||
@@ -26,7 +26,7 @@ var SelectLineWhenWholeFileIsOneHunk = NewIntegrationTest(NewIntegrationTestArgs
|
||||
|
||||
// Every line of the file is an addition, so widening to the change block would
|
||||
// select the file entire; hunk mode gives way to a single line.
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("+one"),
|
||||
|
||||
@@ -58,7 +58,7 @@ var Apply = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("file1").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("second line"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ var ApplyInReverse = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("D").Contains("file1").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("-file1 content"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -88,7 +88,7 @@ var ApplyInReverseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Equals(" M file2"),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains(" file1 content"),
|
||||
Contains("-more file1 content"),
|
||||
|
||||
@@ -63,7 +63,7 @@ var ApplyWithModifiedFileNoConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Equals("M file1").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("-1\n+11\n 2\n 3\n+4"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -62,12 +62,12 @@ var MoveRangeToIndex = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Equals(" A file2"),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("second line"))
|
||||
|
||||
t.Views().Files().Focus().NavigateToLine(Contains("file2"))
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("file two content"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ var MoveToIndex = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.Views().Files().
|
||||
Focus()
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("file1 content"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ var MoveToIndexPartOfAdjacentAddedLines = NewIntegrationTest(NewIntegrationTestA
|
||||
Contains("M").Contains("file1"),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
Content(Contains("+1st line\n 2nd line"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ var MoveToIndexPartial = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.Views().Files().
|
||||
Focus()
|
||||
|
||||
t.Views().Main().
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains(`-first line`),
|
||||
Contains(`+first line2`),
|
||||
|
||||
@@ -50,7 +50,7 @@ var Add = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
SelectNextItem().
|
||||
Tap(func() {
|
||||
t.Views().Main().Content(
|
||||
t.Views().Secondary().Content(
|
||||
Contains("[submodule \"my_submodule\"]").
|
||||
Contains("path = my_submodule_path").
|
||||
Contains("url = ../other_repo"),
|
||||
@@ -58,7 +58,7 @@ var Add = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
}).
|
||||
SelectNextItem().
|
||||
Tap(func() {
|
||||
t.Views().Main().Content(
|
||||
t.Views().Secondary().Content(
|
||||
Contains("Submodule my_submodule_path").
|
||||
Contains("(new submodule)"),
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
SelectNextItem()
|
||||
|
||||
t.Views().Main().Content(
|
||||
t.Views().Secondary().Content(
|
||||
Contains("-[submodule \"my_submodule_name\"]").
|
||||
Contains("- path = my_submodule_path").
|
||||
Contains("- url = ../my_submodule_name"),
|
||||
|
||||
@@ -46,7 +46,7 @@ var RemoveNested = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
NavigateToLine(Contains(".gitmodules"))
|
||||
|
||||
t.Views().Main().Content(
|
||||
t.Views().Secondary().Content(
|
||||
Contains("-[submodule \"innerSubName\"]").
|
||||
Contains("- path = modules/innerSubPath").
|
||||
Contains("- url = ../innerSubmodule"),
|
||||
|
||||
@@ -252,6 +252,8 @@ var tests = []*components.IntegrationTest{
|
||||
file.ExcludeWithoutInfoDir,
|
||||
file.Gitignore,
|
||||
file.GitignoreSpecialCharacters,
|
||||
file.PaneShownAgainStartsAtTheTop,
|
||||
file.PaneTakingOverStartsAtTheTop,
|
||||
file.RememberCommitMessageAfterFail,
|
||||
file.RenameSimilarityThresholdChange,
|
||||
file.RenamedFiles,
|
||||
@@ -260,6 +262,7 @@ var tests = []*components.IntegrationTest{
|
||||
file.StageChildrenRangeSelect,
|
||||
file.StageDeletedRangeSelect,
|
||||
file.StageRangeSelect,
|
||||
file.StagedChangesInLowerPane,
|
||||
filter_and_search.FilterByFileStatus,
|
||||
filter_and_search.FilterCommitFiles,
|
||||
filter_and_search.FilterCommitFilesToggleDirectory,
|
||||
@@ -364,6 +367,8 @@ var tests = []*components.IntegrationTest{
|
||||
interactive_rebase.SwapWithConflict,
|
||||
interactive_rebase.ViewFilesOfTodoEntries,
|
||||
main_view.ClickSelectsDiffLine,
|
||||
main_view.CopyRowsThatAreNoDiffLine,
|
||||
main_view.CopySelectedDiffLines,
|
||||
main_view.DragRangeWithAutoscroll,
|
||||
main_view.DragSelectsDiffLineRange,
|
||||
main_view.EditSelectedDiffLine,
|
||||
|
||||
@@ -251,6 +251,17 @@ func (self *ViewBufferManager) GetTaskKey() string {
|
||||
return self.taskKey
|
||||
}
|
||||
|
||||
// ForgetRenderedContent records that the view no longer shows the render whose key it
|
||||
// is holding, because it has been emptied. The key says what the view is showing, and
|
||||
// the next task is compared against it to tell whether that task renders something
|
||||
// new. A view with nothing in it is showing nothing, so whatever comes next is new.
|
||||
func (self *ViewBufferManager) ForgetRenderedContent() {
|
||||
self.taskIDMutex.Lock()
|
||||
defer self.taskIDMutex.Unlock()
|
||||
|
||||
self.taskKey = ""
|
||||
}
|
||||
|
||||
func NewViewBufferManager(
|
||||
log *logrus.Entry,
|
||||
writer io.Writer,
|
||||
|
||||
@@ -610,6 +610,50 @@ func TestKeepScrollPositionForNextTask(t *testing.T) {
|
||||
assert.EqualValues(t, 0, beforeStartCount.Load())
|
||||
}
|
||||
|
||||
// A view that has been emptied is showing nothing, so the render it was showing is no
|
||||
// longer the one to compare the next task against: running the same command again is
|
||||
// putting content into the view that isn't there any more, and starts from the top.
|
||||
func TestForgetRenderedContent(t *testing.T) {
|
||||
resetOrigin, getResetOriginCallCount := getCounter()
|
||||
|
||||
manager := NewViewBufferManager(
|
||||
utils.NewDummyLog(),
|
||||
io.Discard,
|
||||
func() {}, // beforeStart
|
||||
func() {}, // refreshView
|
||||
func() {}, // onEndOfInput
|
||||
resetOrigin,
|
||||
func() {}, // beginRender
|
||||
func() {}, // swapInRender
|
||||
func() gocui.Task { return gocui.NewFakeTask() },
|
||||
// no UI thread in the test; run the view mutations inline
|
||||
func(f func()) error { f(); return nil },
|
||||
)
|
||||
|
||||
runTaskToCompletion := func(key string) {
|
||||
start := func() (Cmd, io.Reader) {
|
||||
// not actually starting this because it's not necessary
|
||||
return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: 3}
|
||||
}
|
||||
done := make(chan struct{})
|
||||
_ = manager.NewTask(
|
||||
manager.NewCmdTask(start, "", LinesToRead{100, 50, nil}, func() { close(done) }), key)
|
||||
<-done
|
||||
}
|
||||
|
||||
runTaskToCompletion("cmd1")
|
||||
assert.Equal(t, 1, getResetOriginCallCount())
|
||||
|
||||
// Rendering the same command's output again leaves the view where it is, that being
|
||||
// what it already shows.
|
||||
runTaskToCompletion("cmd1")
|
||||
assert.Equal(t, 1, getResetOriginCallCount())
|
||||
|
||||
manager.ForgetRenderedContent()
|
||||
runTaskToCompletion("cmd1")
|
||||
assert.Equal(t, 2, getResetOriginCallCount(), "an emptied view is shown its content afresh")
|
||||
}
|
||||
|
||||
func TestNewCmdTaskRefresh(t *testing.T) {
|
||||
type scenario struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user