Filter file views rather than search (#5273)

### PR Description

Way back when I implemented filter functionality I could not muster the
motivation to apply it to the file views (files and commit files), but
it really should be used on them.

The text filter matches against the full file path (not just the
filename), which is more useful for navigating large directory trees.

When a text filter is active, all operations respect it consistently:
- **Diffs**: selecting a filtered directory shows only the diff of
visible files, not all files in the directory
- **Staging/unstaging**: pressing space on a filtered directory only
stages/unstages visible files
- **Toggle all (a)**: only toggles staging for visible files
- **Custom patch**: toggling a filtered directory only adds visible
files to the patch

Full disclosure, opus 4.6 wrote this entire PR but reviewing its changes
they seem reasonable, and I have done some local testing and everything
works as expected.
This commit is contained in:
Stefan Haller 2026-03-07 20:06:36 +01:00 committed by GitHub
commit 35911da3a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 669 additions and 101 deletions

View file

@ -71,7 +71,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
| `` / `` | Filter the current view by text | |
## Commit summary
@ -160,7 +160,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
| `` / `` | Filter the current view by text | |
## Input prompt

View file

@ -119,7 +119,7 @@ _凡例`c-b` はctrl+b、`a-b` はalt+b、`B` はshift+bを意味
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
| `` 0 `` | メインビューにフォーカス | |
| `` / `` | 現在のビューをテキストで検索 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## コミット概要
@ -242,7 +242,7 @@ _凡例`c-b` はctrl+b、`a-b` はalt+b、`B` はshift+bを意味
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
| `` 0 `` | メインビューにフォーカス | |
| `` / `` | 現在のビューをテキストで検索 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## メインパネル(ステージング)

View file

@ -349,7 +349,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 검색 시작 | |
| `` / `` | Filter the current view by text | |
## 커밋메시지
@ -405,7 +405,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 검색 시작 | |
| `` / `` | Filter the current view by text | |
## 확인 패널

View file

@ -84,7 +84,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Start met zoeken | |
| `` / `` | Filter the current view by text | |
## Bevestigingspaneel
@ -149,7 +149,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Start met zoeken | |
| `` / `` | Filter the current view by text | |
## Commits

View file

@ -260,7 +260,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Pliki commita
@ -280,7 +280,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Podsumowanie commita

View file

@ -84,7 +84,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
| `` / `` | Filter the current view by text | |
## Branches locais
@ -153,7 +153,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
| `` / `` | Filter the current view by text | |
## Commits

View file

@ -308,7 +308,7 @@ _Связки клавиш_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Найти | |
| `` / `` | Filter the current view by text | |
## Статус
@ -399,7 +399,7 @@ _Связки клавиш_
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Найти | |
| `` / `` | Filter the current view by text | |
## Хранилище

View file

@ -183,7 +183,7 @@ _图例`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | 聚焦主视图 | |
| `` / `` | 开始搜索 | |
| `` / `` | 通过文本过滤当前视图 | |
## 文件
@ -216,7 +216,7 @@ _图例`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | 聚焦主视图 | |
| `` / `` | 开始搜索 | |
| `` / `` | 通过文本过滤当前视图 | |
## 本地分支

View file

@ -253,11 +253,14 @@ func (self *WorkingTreeCommands) Exclude(filename string) error {
// WorktreeFileDiff returns the diff of a file
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
// for now we assume an error means the file was deleted
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached).RunWithOutput()
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, nil).RunWithOutput()
return s
}
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) *oscommands.CmdObj {
// WorktreeFileDiffCmdObj returns a command object for diffing a file or directory
// in the working tree. When pathOverrides is non-empty, those paths are used instead of
// the node's path (used to diff only filtered/visible files within a directory).
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, pathOverrides []string) *oscommands.CmdObj {
colorArg := self.pagerConfig.GetColorArg()
if plain {
colorArg = "never"
@ -270,6 +273,11 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
useExtDiff := extDiffCmd != "" && !plain
useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig() && !plain
paths := pathOverrides
if len(paths) == 0 {
paths = []string{node.GetPath()}
}
cmdArgs := NewGitCmd("diff").
ConfigIf(useExtDiff, "diff.external="+extDiffCmd).
ArgIfElse(useExtDiff || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
@ -282,7 +290,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
ArgIf(noIndex, "--no-index").
Arg("--").
ArgIf(noIndex, "/dev/null").
Arg(node.GetPath()).
Arg(paths...).
ArgIf(prevPath != "", prevPath).
Dir(self.repoPaths.worktreePath).
ToArgv()
@ -293,10 +301,10 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
return self.ShowFileDiffCmdObj(from, to, reverse, []string{fileName}, plain).RunWithOutput()
}
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) *oscommands.CmdObj {
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileNames []string, plain bool) *oscommands.CmdObj {
contextSize := self.UserConfig().Git.DiffContextSize
colorArg := self.pagerConfig.GetColorArg()
@ -321,7 +329,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
ArgIf(reverse, "-R").
ArgIf(!plain && self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space").
Arg("--").
Arg(fileName).
Arg(fileNames...).
Dir(self.repoPaths.worktreePath).
ToArgv()

View file

@ -3,7 +3,6 @@ package context
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
@ -17,13 +16,12 @@ type CommitFilesContext struct {
*filetree.CommitFileTreeViewModel
*ListContextTrait
*DynamicTitleBuilder
*SearchTrait
}
var (
_ types.IListContext = (*CommitFilesContext)(nil)
_ types.DiffableContext = (*CommitFilesContext)(nil)
_ types.ISearchableContext = (*CommitFilesContext)(nil)
_ types.IFilterableContext = (*CommitFilesContext)(nil)
)
func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
@ -48,7 +46,6 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
ctx := &CommitFilesContext{
CommitFileTreeViewModel: viewModel,
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.CommitFilesDynamicTitle),
SearchTrait: NewSearchTrait(c),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
@ -68,9 +65,6 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
},
}
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
return ctx
}
@ -93,10 +87,6 @@ func (self *CommitFilesContext) GetFromAndToForDiff() (string, string) {
return ref.ParentRefName(), ref.RefName()
}
func (self *CommitFilesContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return nil
}
func (self *CommitFilesContext) ReInit(ref models.Ref, refRange *types.RefRange) {
self.SetRef(ref)
self.SetRefRange(refRange)

View file

@ -1,7 +1,6 @@
package context
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
@ -13,12 +12,11 @@ import (
type WorkingTreeContext struct {
*filetree.FileTreeViewModel
*ListContextTrait
*SearchTrait
}
var (
_ types.IListContext = (*WorkingTreeContext)(nil)
_ types.ISearchableContext = (*WorkingTreeContext)(nil)
_ types.IFilterableContext = (*WorkingTreeContext)(nil)
)
func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
@ -38,7 +36,6 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
}
ctx := &WorkingTreeContext{
SearchTrait: NewSearchTrait(c),
FileTreeViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@ -56,12 +53,5 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
},
}
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
return ctx
}
func (self *WorkingTreeContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return nil
}

View file

@ -152,7 +152,8 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
from, to := self.context().GetFromAndToForDiff()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
paths := self.pathsForDiff(node)
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, false)
task := types.NewRunPtyTask(cmdObj.GetCmd())
self.c.RenderToMainViews(types.RefreshMainOpts{
@ -171,7 +172,7 @@ func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage
from, to := self.context().GetFromAndToForDiff()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, path, true)
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, []string{path}, true)
diff, err := cmdObj.RunWithOutput()
if err != nil {
return err
@ -571,6 +572,21 @@ func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName
}
}
// pathsForDiff returns the file paths to use for a diff command. When a text
// filter is active and the node is a directory, only the visible (filtered)
// file paths are returned so the diff reflects what the user sees.
func (self *CommitFilesController) pathsForDiff(node *filetree.CommitFileNode) []string {
if !node.IsFile() && self.context().IsFiltering() {
var paths []string
_ = node.ForEachFile(func(file *models.CommitFile) error {
paths = append(paths, file.Path)
return nil
})
return paths
}
return []string{node.GetPath()}
}
// NOTE: these functions are identical to those in files_controller.go (except for types) and
// could also be cleaned up with some generics
func normalisedSelectedCommitFileNodes(selectedNodes []*filetree.CommitFileNode) []*filetree.CommitFileNode {

View file

@ -294,7 +294,8 @@ func (self *FilesController) GetOnRenderToMain() func() {
split := self.c.UserConfig().Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
mainShowsStaged := !split && node.GetHasStagedChanges()
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged)
pathOverrides := self.pathOverridesForDiff(node)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, pathOverrides)
title := self.c.Tr.UnstagedChanges
if mainShowsStaged {
title = self.c.Tr.StagedChanges
@ -309,7 +310,7 @@ func (self *FilesController) GetOnRenderToMain() func() {
}
if split {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, pathOverrides)
title := self.c.Tr.StagedChanges
if mainShowsStaged {
@ -434,7 +435,19 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e
}
}
// When filtering, expand directory nodes to individual visible file paths
// so that only filtered files are staged/unstaged.
toPaths := func(nodes []*filetree.FileNode) []string {
if self.context().IsFiltering() {
var paths []string
for _, node := range nodes {
_ = node.ForEachFile(func(file *models.File) error {
paths = append(paths, file.Path)
return nil
})
}
return paths
}
return lo.Map(nodes, func(node *filetree.FileNode, _ int) string {
return node.GetPath()
})
@ -449,7 +462,7 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e
if len(unstagedSelectedNodes) > 0 {
var extraArgs []string
if self.context().GetFilter() == filetree.DisplayTracked {
if self.context().GetStatusFilter() == filetree.DisplayTracked {
extraArgs = []string{"-u"}
}
@ -469,22 +482,29 @@ func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) e
return err
}
// need to partition the paths into tracked and untracked (where we assume directories are tracked). Then we'll run the commands separately.
trackedNodes, untrackedNodes := utils.Partition(selectedNodes, func(node *filetree.FileNode) bool {
// We treat all directories as tracked. I'm not actually sure why we do this but
// it's been the existing behaviour for a while and nobody has complained
return !node.IsFile() || node.GetIsTracked()
})
if len(untrackedNodes) > 0 {
if err := self.c.Git().WorkingTree.UnstageUntrackedFiles(toPaths(untrackedNodes)); err != nil {
if self.context().IsFiltering() {
// When filtering, only unstage visible files
if err := self.unstageFilteredFiles(selectedNodes); err != nil {
return err
}
}
} else {
// need to partition the paths into tracked and untracked (where we assume directories are tracked). Then we'll run the commands separately.
trackedNodes, untrackedNodes := utils.Partition(selectedNodes, func(node *filetree.FileNode) bool {
// We treat all directories as tracked. I'm not actually sure why we do this but
// it's been the existing behaviour for a while and nobody has complained
return !node.IsFile() || node.GetIsTracked()
})
if len(trackedNodes) > 0 {
if err := self.c.Git().WorkingTree.UnstageTrackedFiles(toPaths(trackedNodes)); err != nil {
return err
if len(untrackedNodes) > 0 {
if err := self.c.Git().WorkingTree.UnstageUntrackedFiles(toPaths(untrackedNodes)); err != nil {
return err
}
}
if len(trackedNodes) > 0 {
if err := self.c.Git().WorkingTree.UnstageTrackedFiles(toPaths(trackedNodes)); err != nil {
return err
}
}
}
}
@ -503,6 +523,48 @@ func (self *FilesController) press(nodes []*filetree.FileNode) error {
return nil
}
// pathOverridesForDiff returns file paths to override the node's path in diff
// commands when a text filter is active and the node is a directory. This
// ensures the diff only shows filtered/visible files.
func (self *FilesController) pathOverridesForDiff(node *filetree.FileNode) []string {
if !node.IsFile() && self.context().IsFiltering() {
var paths []string
_ = node.ForEachFile(func(file *models.File) error {
paths = append(paths, file.Path)
return nil
})
return paths
}
return nil
}
// unstageFilteredFiles unstages only the visible (filtered) files from the
// given nodes, correctly partitioning by tracked/untracked.
func (self *FilesController) unstageFilteredFiles(nodes []*filetree.FileNode) error {
var trackedPaths, untrackedPaths []string
for _, node := range nodes {
_ = node.ForEachFile(func(file *models.File) error {
if file.Tracked || file.HasStagedChanges {
trackedPaths = append(trackedPaths, file.Path)
} else {
untrackedPaths = append(untrackedPaths, file.Path)
}
return nil
})
}
if len(untrackedPaths) > 0 {
if err := self.c.Git().WorkingTree.UnstageUntrackedFiles(untrackedPaths); err != nil {
return err
}
}
if len(trackedPaths) > 0 {
if err := self.c.Git().WorkingTree.UnstageTrackedFiles(trackedPaths); err != nil {
return err
}
}
return nil
}
func (self *FilesController) Context() types.Context {
return self.context()
}
@ -648,9 +710,21 @@ func (self *FilesController) toggleStagedAllWithLock() error {
return err
}
onlyTrackedFiles := self.context().GetFilter() == filetree.DisplayTracked
if err := self.c.Git().WorkingTree.StageAll(onlyTrackedFiles); err != nil {
return err
if self.context().IsFiltering() {
// When filtering, only stage visible files
var paths []string
_ = root.ForEachFile(func(file *models.File) error {
paths = append(paths, file.Path)
return nil
})
if err := self.c.Git().WorkingTree.StageFiles(paths, nil); err != nil {
return err
}
} else {
onlyTrackedFiles := self.context().GetStatusFilter() == filetree.DisplayTracked
if err := self.c.Git().WorkingTree.StageAll(onlyTrackedFiles); err != nil {
return err
}
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageAllFiles)
@ -659,8 +733,15 @@ func (self *FilesController) toggleStagedAllWithLock() error {
return err
}
if err := self.c.Git().WorkingTree.UnstageAll(); err != nil {
return err
if self.context().IsFiltering() {
// When filtering, only unstage visible files
if err := self.unstageFilteredFiles([]*filetree.FileNode{root}); err != nil {
return err
}
} else {
if err := self.c.Git().WorkingTree.UnstageAll(); err != nil {
return err
}
}
}
@ -836,7 +917,7 @@ func (self *FilesController) isResolvingConflicts() bool {
}
func (self *FilesController) handleStatusFilterPressed() error {
currentFilter := self.context().GetFilter()
currentFilter := self.context().GetStatusFilter()
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.FilteringMenuTitle,
Items: []*types.MenuItem{
@ -904,7 +985,7 @@ func (self *FilesController) filteringLabel(filter filetree.FileTreeDisplayFilte
}
func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
previousFilter := self.context().GetFilter()
previousFilter := self.context().GetStatusFilter()
self.context().FileTreeViewModel.SetStatusFilter(filter)
self.c.Contexts().Files.GetView().Subtitle = self.filteringLabel(filter)

View file

@ -584,11 +584,11 @@ func (self *RefreshHelper) refreshStateFiles() error {
// only taking over the filter if it hasn't already been set by the user.
if conflictFileCount > 0 && prevConflictFileCount == 0 {
if fileTreeViewModel.GetFilter() == filetree.DisplayAll {
if fileTreeViewModel.GetStatusFilter() == filetree.DisplayAll {
fileTreeViewModel.SetStatusFilter(filetree.DisplayConflicted)
self.c.Contexts().Files.GetView().Subtitle = self.c.Tr.FilterLabelConflictingFiles
}
} else if conflictFileCount == 0 && fileTreeViewModel.GetFilter() == filetree.DisplayConflicted {
} else if conflictFileCount == 0 && fileTreeViewModel.GetStatusFilter() == filetree.DisplayConflicted {
fileTreeViewModel.SetStatusFilter(filetree.DisplayAll)
self.c.Contexts().Files.GetView().Subtitle = ""
}

View file

@ -33,6 +33,8 @@ func NewSearchHelper(
func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) error {
state := self.searchState()
state.PrevSearchIndex = -1
state.Context = context
self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr))
@ -238,7 +240,7 @@ func (self *SearchHelper) ReApplyFilter(context types.Context) {
filterableContext, ok := context.(types.IFilterableContext)
if ok {
state := self.searchState()
if context == state.Context {
if context == state.Context && self.c.Context().Current().GetKey() == self.c.Contexts().Search.GetKey() {
filterableContext.SetSelection(0)
filterableContext.GetView().SetOriginY(0)
}

View file

@ -125,7 +125,7 @@ func (self *SubmodulesController) GetOnRenderToMain() func() {
if file == nil {
task = types.NewRenderStringTask(prefix)
} else {
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, nil)
task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
}
}

View file

@ -80,12 +80,12 @@ func (self *SwitchToDiffFilesController) enter() error {
}
}
commitFilesContext.ClearFilter()
commitFilesContext.ReInit(ref, refsRange)
commitFilesContext.SetSelection(0)
commitFilesContext.SetCanRebase(canRebase)
commitFilesContext.SetParentContext(self.context)
commitFilesContext.SetWindowName(self.context.GetWindowName())
commitFilesContext.ClearSearchString()
commitFilesContext.GetView().TitlePrefix = self.context.GetView().TitlePrefix
self.c.Refresh(types.RefreshOptions{

View file

@ -15,6 +15,8 @@ type ICommitFileTree interface {
GetAllItems() []*CommitFileNode
GetAllFiles() []*models.CommitFile
GetRoot() *CommitFileNode
SetTextFilter(filter string, useFuzzySearch bool)
GetTextFilter() string
}
type CommitFileTree struct {
@ -23,6 +25,8 @@ type CommitFileTree struct {
showTree bool
common *common.Common
collapsedPaths *CollapsedPaths
textFilter string
useFuzzySearch bool
}
func (self *CommitFileTree) CollapseAll() {
@ -93,15 +97,34 @@ func (self *CommitFileTree) GetAllFiles() []*models.CommitFile {
return self.getFiles()
}
func (self *CommitFileTree) getFilesForDisplay() []*models.CommitFile {
files := self.getFiles()
if self.textFilter != "" {
files = filterCommitFilesByText(files, self.textFilter, self.useFuzzySearch)
}
return files
}
func (self *CommitFileTree) SetTree() {
filesForDisplay := self.getFilesForDisplay()
showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree
if self.showTree {
self.tree = BuildTreeFromCommitFiles(self.getFiles(), showRootItem)
self.tree = BuildTreeFromCommitFiles(filesForDisplay, showRootItem)
} else {
self.tree = BuildFlatTreeFromCommitFiles(self.getFiles(), showRootItem)
self.tree = BuildFlatTreeFromCommitFiles(filesForDisplay, showRootItem)
}
}
func (self *CommitFileTree) SetTextFilter(filter string, useFuzzySearch bool) {
self.textFilter = filter
self.useFuzzySearch = useFuzzySearch
self.SetTree()
}
func (self *CommitFileTree) GetTextFilter() string {
return self.textFilter
}
func (self *CommitFileTree) IsCollapsed(path string) bool {
return self.collapsedPaths.IsCollapsed(path)
}

View file

@ -8,6 +8,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@ -39,6 +41,8 @@ type CommitFileTreeViewModel struct {
// we set this to true when you're viewing the files within the checked-out branch's commits.
// If you're viewing the files of some random other branch we can't do any rebase stuff.
canRebase bool
searchHistory *utils.HistoryBuffer[string]
}
var _ ICommitFileTreeViewModel = &CommitFileTreeViewModel{}
@ -52,6 +56,7 @@ func NewCommitFileTreeViewModel(getFiles func() []*models.CommitFile, common *co
ref: nil,
refRange: nil,
canRebase: false,
searchHistory: utils.NewHistoryBuffer[string](1000),
}
}
@ -203,3 +208,51 @@ func (self *CommitFileTreeViewModel) SelectPath(filepath string, showRootItem bo
self.SetSelection(index)
}
}
// IFilterableContext methods
func (self *CommitFileTreeViewModel) SetFilter(filter string, useFuzzySearch bool) {
self.ICommitFileTree.SetTextFilter(filter, useFuzzySearch)
}
func (self *CommitFileTreeViewModel) GetFilter() string {
return self.ICommitFileTree.GetTextFilter()
}
func (self *CommitFileTreeViewModel) ClearFilter() {
selectedNode := self.GetSelected()
var selectedPath string
if selectedNode != nil {
selectedPath = selectedNode.GetInternalPath()
}
self.ICommitFileTree.SetTextFilter("", false)
if selectedPath != "" {
self.ExpandToPath(selectedPath)
if idx, found := self.GetIndexForPath(selectedPath); found {
self.SetSelection(idx)
return
}
}
self.ClampSelection()
}
func (self *CommitFileTreeViewModel) ReApplyFilter(useFuzzySearch bool) {
self.ICommitFileTree.SetTextFilter(self.ICommitFileTree.GetTextFilter(), useFuzzySearch)
}
func (self *CommitFileTreeViewModel) IsFiltering() bool {
return self.ICommitFileTree.GetTextFilter() != ""
}
// used for type switch
func (self *CommitFileTreeViewModel) IsFilterableContext() {}
func (self *CommitFileTreeViewModel) FilterPrefix(tr *i18n.TranslationSet) string {
return tr.FilterPrefix
}
func (self *CommitFileTreeViewModel) GetSearchHistory() *utils.HistoryBuffer[string] {
return self.searchHistory
}

View file

@ -0,0 +1,48 @@
package filetree
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sahilm/fuzzy"
"github.com/samber/lo"
)
type filePathSource struct {
files []*models.File
}
func (s *filePathSource) String(i int) string {
return s.files[i].Path
}
func (s *filePathSource) Len() int {
return len(s.files)
}
func filterFilesByText(files []*models.File, filter string, useFuzzySearch bool) []*models.File {
source := &filePathSource{files: files}
matches := utils.FindFrom(filter, source, useFuzzySearch)
return lo.Map(matches, func(match fuzzy.Match, _ int) *models.File {
return files[match.Index]
})
}
type commitFilePathSource struct {
files []*models.CommitFile
}
func (s *commitFilePathSource) String(i int) string {
return s.files[i].Path
}
func (s *commitFilePathSource) Len() int {
return len(s.files)
}
func filterCommitFilesByText(files []*models.CommitFile, filter string, useFuzzySearch bool) []*models.CommitFile {
source := &commitFilePathSource{files: files}
matches := utils.FindFrom(filter, source, useFuzzySearch)
return lo.Map(matches, func(match fuzzy.Match, _ int) *models.CommitFile {
return files[match.Index]
})
}

View file

@ -46,8 +46,10 @@ type IFileTree interface {
GetFile(path string) *models.File
GetAllItems() []*FileNode
GetAllFiles() []*models.File
GetFilter() FileTreeDisplayFilter
GetStatusFilter() FileTreeDisplayFilter
GetRoot() *FileNode
SetTextFilter(filter string, useFuzzySearch bool)
GetTextFilter() string
}
type FileTree struct {
@ -57,6 +59,8 @@ type FileTree struct {
common *common.Common
filter FileTreeDisplayFilter
collapsedPaths *CollapsedPaths
textFilter string
useFuzzySearch bool
}
var _ IFileTree = &FileTree{}
@ -80,24 +84,31 @@ func (self *FileTree) ExpandToPath(path string) {
}
func (self *FileTree) getFilesForDisplay() []*models.File {
var files []*models.File
switch self.filter {
case DisplayAll:
return self.getFiles()
files = self.getFiles()
case DisplayStaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasStagedChanges })
files = self.FilterFiles(func(file *models.File) bool { return file.HasStagedChanges })
case DisplayUnstaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
files = self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
case DisplayTracked:
// untracked but staged files are technically not tracked by git
// but including such files in the filtered mode helps see what files are getting committed
return self.FilterFiles(func(file *models.File) bool { return file.Tracked || file.HasStagedChanges })
files = self.FilterFiles(func(file *models.File) bool { return file.Tracked || file.HasStagedChanges })
case DisplayUntracked:
return self.FilterFiles(func(file *models.File) bool { return !(file.Tracked || file.HasStagedChanges) })
files = self.FilterFiles(func(file *models.File) bool { return !(file.Tracked || file.HasStagedChanges) })
case DisplayConflicted:
return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
files = self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
default:
panic(fmt.Sprintf("Unexpected files display filter: %d", self.filter))
}
if self.textFilter != "" {
files = filterFilesByText(files, self.textFilter, self.useFuzzySearch)
}
return files
}
func (self *FileTree) ForceShowUntracked() bool {
@ -210,6 +221,16 @@ func (self *FileTree) CollapsedPaths() *CollapsedPaths {
return self.collapsedPaths
}
func (self *FileTree) GetFilter() FileTreeDisplayFilter {
func (self *FileTree) GetStatusFilter() FileTreeDisplayFilter {
return self.filter
}
func (self *FileTree) SetTextFilter(filter string, useFuzzySearch bool) {
self.textFilter = filter
self.useFuzzySearch = useFuzzySearch
self.SetTree()
}
func (self *FileTree) GetTextFilter() string {
return self.textFilter
}

View file

@ -8,6 +8,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@ -24,6 +25,7 @@ type FileTreeViewModel struct {
sync.RWMutex
types.IListCursor
IFileTree
searchHistory *utils.HistoryBuffer[string]
}
var _ IFileTreeViewModel = &FileTreeViewModel{}
@ -32,8 +34,9 @@ func NewFileTreeViewModel(getFiles func() []*models.File, common *common.Common,
fileTree := NewFileTree(getFiles, common, showTree)
listCursor := traits.NewListCursor(fileTree.Len)
return &FileTreeViewModel{
IFileTree: fileTree,
IListCursor: listCursor,
IFileTree: fileTree,
IListCursor: listCursor,
searchHistory: utils.NewHistoryBuffer[string](1000),
}
}
@ -220,3 +223,51 @@ func (self *FileTreeViewModel) ExpandAll() {
self.SetSelectedLineIdx(index)
}
}
// IFilterableContext methods
func (self *FileTreeViewModel) SetFilter(filter string, useFuzzySearch bool) {
self.IFileTree.SetTextFilter(filter, useFuzzySearch)
}
func (self *FileTreeViewModel) GetFilter() string {
return self.IFileTree.GetTextFilter()
}
func (self *FileTreeViewModel) ClearFilter() {
selectedNode := self.GetSelected()
var selectedPath string
if selectedNode != nil {
selectedPath = selectedNode.GetInternalPath()
}
self.IFileTree.SetTextFilter("", false)
if selectedPath != "" {
self.ExpandToPath(selectedPath)
if idx, found := self.GetIndexForPath(selectedPath); found {
self.SetSelection(idx)
return
}
}
self.ClampSelection()
}
func (self *FileTreeViewModel) ReApplyFilter(useFuzzySearch bool) {
self.IFileTree.SetTextFilter(self.IFileTree.GetTextFilter(), useFuzzySearch)
}
func (self *FileTreeViewModel) IsFiltering() bool {
return self.IFileTree.GetTextFilter() != ""
}
// used for type switch
func (self *FileTreeViewModel) IsFilterableContext() {}
func (self *FileTreeViewModel) FilterPrefix(tr *i18n.TranslationSet) string {
return tr.FilterPrefix
}
func (self *FileTreeViewModel) GetSearchHistory() *utils.HistoryBuffer[string] {
return self.searchHistory
}

View file

@ -8,7 +8,7 @@ import (
var FilterCommitFiles = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Basic commit file filtering by text",
ExtraCmdArgs: []string{},
Skip: true, // skipping until we have implemented file view filtering
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("folder1")

View file

@ -0,0 +1,61 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterCommitFilesToggleDirectory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Toggle a filtered directory for a custom patch only adds visible files",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir1")
shell.CreateFileAndAdd("dir1/apple-grape", "apple-grape content\n")
shell.CreateFileAndAdd("dir1/apple-orange", "apple-orange content\n")
shell.CreateFileAndAdd("dir1/grape-orange", "grape-orange content\n")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Equals(" A grape-orange"),
).
// Filter to show only "apple" files (staying in tree view)
FilterOrSearch("apple").
Lines(
// first item is always selected after filtering
Equals("▼ dir1").IsSelected(),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
).
// dir1 is already selected; toggle for patch
PressPrimaryAction().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" ● apple-grape"),
Equals(" ● apple-orange"),
)
t.Views().Information().Content(Contains("Building patch"))
// Verify only the filtered files are in the patch (not grape-orange)
t.Views().Secondary().Content(
Contains("apple-grape").
Contains("apple-orange").
DoesNotContain("grape-orange"),
)
},
})

View file

@ -8,7 +8,7 @@ import (
var FilterFiles = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Basic file filtering by text",
ExtraCmdArgs: []string{},
Skip: true, // Skipping until we have implemented file view filtering
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("folder1")

View file

@ -0,0 +1,50 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterFilesStageAll = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Toggle all staging with a filter only stages visible files",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir1")
shell.CreateFile("dir1/apple-grape", "apple-grape content\n")
shell.CreateFile("dir1/apple-orange", "apple-orange content\n")
shell.CreateFile("dir1/grape-orange", "grape-orange content\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
Equals(" ?? grape-orange"),
).
// Filter to show only "apple" files
FilterOrSearch("apple").
Lines(
// first item is always selected after filtering
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
).
// Stage all visible files
Press(keys.Files.ToggleStagedAll).
// Clear the filter and verify only apple files are staged
PressEscape()
t.Views().Files().
IsFocused().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Equals(" ?? grape-orange"),
)
},
})

View file

@ -0,0 +1,50 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterFilesStageDirectory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Staging a filtered directory only stages visible files",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir1")
shell.CreateFile("dir1/apple-grape", "apple-grape content\n")
shell.CreateFile("dir1/apple-orange", "apple-orange content\n")
shell.CreateFile("dir1/grape-orange", "grape-orange content\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Lines(
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
Equals(" ?? grape-orange"),
).
// Filter to show only "apple" files
FilterOrSearch("apple").
Lines(
// first item is always selected after filtering
Equals("▼ dir1").IsSelected(),
Equals(" ?? apple-grape"),
Equals(" ?? apple-orange"),
).
// dir1 is already selected; stage it
PressPrimaryAction().
// Clear the filter to see all files and verify only apple files are staged
PressEscape()
t.Views().Files().
IsFocused().
Lines(
Equals("▼ dir1"),
Equals(" A apple-grape"),
Equals(" A apple-orange"),
Equals(" ?? grape-orange"),
)
},
})

View file

@ -0,0 +1,63 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterPreservesSelectionOnModelChange = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when a filter is active and the model changes, the selection is preserved",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.NewBranch("branch-alpha")
shell.NewBranch("branch-beta")
shell.NewBranch("branch-gamma")
shell.NewBranch("checked-out-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("checked-out-branch").IsSelected(),
Contains("branch-alpha"),
Contains("branch-beta"),
Contains("branch-gamma"),
Contains("master"),
).
FilterOrSearch("branch-").
Lines(
Contains("branch-alpha").IsSelected(),
Contains("branch-beta"),
Contains("branch-gamma"),
).
// Move cursor to a non-zero position
SelectNextItem().
SelectNextItem().
Lines(
Contains("branch-alpha"),
Contains("branch-beta"),
Contains("branch-gamma").IsSelected(),
)
// Trigger a model update while staying on the Branches view.
// Using a shell command that creates a new branch sorting after
// branch-gamma, so the selection index still points to the same item.
t.GlobalPress(keys.Universal.ExecuteShellCommand)
t.ExpectPopup().Prompt().
Title(Equals("Shell command:")).
Type("git branch branch-zeta").
Confirm()
// Verify that the selection is still on branch-gamma (not reset to 0)
t.Views().Branches().
Lines(
Contains("branch-alpha"),
Contains("branch-beta"),
Contains("branch-gamma").IsSelected(),
Contains("branch-zeta"),
)
},
})

View file

@ -70,10 +70,7 @@ var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
).
FilterOrSearch("grape").
Lines(
Equals("▼ /"),
Equals(" A apple"),
Equals(" A grape").IsSelected(),
Equals(" A orange"),
Equals("A grape").IsSelected(),
).
PressEnter()
@ -91,15 +88,12 @@ var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ /"),
Equals(" A apple"),
Equals(" A grape").IsSelected(),
Equals(" A orange"),
Equals("A grape").IsSelected(),
).
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'grape'"))
}).
// cancel search
// cancel filter
PressEscape().
Tap(func() {
t.Views().Search().IsInvisible()

View file

@ -75,9 +75,7 @@ var NestedFilterTransient = NewIntegrationTest(NewIntegrationTestArgs{
).
FilterOrSearch("two").
Lines(
Equals("▼ /"),
Equals(" A file-one"),
Equals(" A file-two").IsSelected(),
Equals("A file-two").IsSelected(),
)
t.Views().Branches().
@ -96,7 +94,7 @@ var NestedFilterTransient = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().CommitFiles().
IsFocused().
// the search on the commit-files context has been cancelled
// the filter on the commit-files context has been cancelled
Lines(
Equals("▼ /").IsSelected(),
Equals(" A file-one"),

View file

@ -0,0 +1,64 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ToggleDirectory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Toggle a directory for a custom patch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir1")
shell.CreateFileAndAdd("dir1/file1", "file1 content\n")
shell.CreateFileAndAdd("dir1/file2", "file2 content\n")
shell.CreateFileAndAdd("dir1/file3", "file3 content\n")
shell.CreateFileAndAdd("other-file", "other content\n")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ /").IsSelected(),
Equals(" ▼ dir1"),
Equals(" A file1"),
Equals(" A file2"),
Equals(" A file3"),
Equals(" A other-file"),
).
NavigateToLine(Contains("dir1")).
PressPrimaryAction().
Lines(
Equals("▼ /"),
Equals(" ▼ dir1").IsSelected(),
Equals(" ● file1"),
Equals(" ● file2"),
Equals(" ● file3"),
Equals(" A other-file"),
)
t.Views().Information().Content(Contains("Building patch"))
// Toggle the directory again to remove all files from the patch
t.Views().CommitFiles().
PressPrimaryAction().
Lines(
Equals("▼ /"),
Equals(" ▼ dir1").IsSelected(),
Equals(" A file1"),
Equals(" A file2"),
Equals(" A file3"),
Equals(" A other-file"),
)
},
})

View file

@ -232,12 +232,16 @@ var tests = []*components.IntegrationTest{
file.StageRangeSelect,
filter_and_search.FilterByFileStatus,
filter_and_search.FilterCommitFiles,
filter_and_search.FilterCommitFilesToggleDirectory,
filter_and_search.FilterFiles,
filter_and_search.FilterFilesStageAll,
filter_and_search.FilterFilesStageDirectory,
filter_and_search.FilterFuzzy,
filter_and_search.FilterMenu,
filter_and_search.FilterMenuByKeybinding,
filter_and_search.FilterMenuCancelFilterWithEscape,
filter_and_search.FilterMenuWithNoKeybindings,
filter_and_search.FilterPreservesSelectionOnModelChange,
filter_and_search.FilterRemoteBranches,
filter_and_search.FilterRemotes,
filter_and_search.FilterSearchHistory,
@ -354,6 +358,7 @@ var tests = []*components.IntegrationTest{
patch_building.SelectAllFiles,
patch_building.SpecificSelection,
patch_building.StartNewPatch,
patch_building.ToggleDirectory,
patch_building.ToggleRange,
reflog.Checkout,
reflog.CherryPick,