Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ keybinding:
copyFileInfoToClipboard: "y"
collapseAll: '-'
expandAll: =
toggleSubtreeExpansion: <a-enter>
branches:
createPullRequest: o
viewPullRequestOptions: O
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` f `` | Fetch | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` f `` | フェッチ | リモートから変更をフェッチします。 |
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | メインビューにフォーカス | |
| `` / `` | 現在のビューをテキストで検索 | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` f `` | Fetch | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 검색 시작 | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_nl.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` f `` | Fetch | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Start met zoeken | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` f `` | Pobierz | Pobierz zmiany ze zdalnego serwera. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` f `` | Buscar | Buscar alterações do controle remoto. |
| `` - `` | 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 |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ _Связки клавиш_
| `` f `` | Получить изменения | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Найти | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` f `` | 抓取 | 从远程获取变更 |
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 开始搜索 | |

Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` f `` | 擷取 | 同步遠端異動 |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` <a-enter> `` | Recursively expand/collapse the selected directory | Recursively expand/collapse the selected directory in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 搜尋 | |

Expand Down
2 changes: 2 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ type KeybindingFilesConfig struct {
CopyFileInfoToClipboard string `yaml:"copyFileInfoToClipboard"`
CollapseAll string `yaml:"collapseAll"`
ExpandAll string `yaml:"expandAll"`
ToggleSubtreeExpansion string `yaml:"toggleSubtreeExpansion"`
}

type KeybindingBranchesConfig struct {
Expand Down Expand Up @@ -967,6 +968,7 @@ func GetDefaultConfig() *UserConfig {
CopyFileInfoToClipboard: "y",
CollapseAll: "-",
ExpandAll: "=",
ToggleSubtreeExpansion: "<a-enter>",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",
Expand Down
24 changes: 24 additions & 0 deletions pkg/gui/controllers/files_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
Tooltip: self.c.Tr.ExpandAllTooltip,
GetDisabledReason: self.require(self.isInTreeMode),
},
{
Key: opts.GetKey(opts.Config.Files.ToggleSubtreeExpansion),
Handler: self.toggleSubtreeExpansion,
Description: self.c.Tr.ToggleSubtreeExpansion,
Tooltip: self.c.Tr.ToggleSubtreeExpansionTooltip,
GetDisabledReason: self.require(self.isInTreeMode),
},
}
}

Expand Down Expand Up @@ -1192,6 +1199,23 @@ func (self *FilesController) handleToggleDirCollapsed() error {
return nil
}

func (self *FilesController) toggleSubtreeExpansion() error {
node := self.context().GetSelected()
if node == nil {
return nil
}

if node.File == nil {
// toggle this directory and all descendants
self.context().FileTreeViewModel.ToggleSubtreeExpansion(node.GetInternalPath())
self.c.PostRefreshUpdate(self.context())
return nil
}

// if it's a file, behave like normal enter
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1})
}

func (self *FilesController) toggleTreeView() error {
self.context().FileTreeViewModel.ToggleShowTree()

Expand Down
61 changes: 60 additions & 1 deletion pkg/gui/filetree/collapsed_paths.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package filetree

import "github.com/jesseduffield/generics/set"
import (
"strings"

"github.com/jesseduffield/generics/set"
)

type CollapsedPaths struct {
collapsedPaths *set.Set[string]
Expand Down Expand Up @@ -37,7 +41,62 @@ func (self *CollapsedPaths) ToggleCollapsed(path string) {
}
}

// Expand removes the given path from the collapsed set (i.e. mark expanded)
func (self *CollapsedPaths) Expand(path string) {
self.collapsedPaths.Remove(path)
}

func (self *CollapsedPaths) ExpandAll() {
// Could be cleaner if Set had a Clear() method...
self.collapsedPaths.RemoveSlice(self.collapsedPaths.ToSlice())
}

// ToggleSubtreeExpansionOn toggles the collapsed state for the given path and all
// descendant directories within the provided tree root. It's a generic helper
// shared by different tree implementations to avoid code duplication.
func ToggleSubtreeExpansionOn[T any](self *CollapsedPaths, root *Node[T], path string) {
if root == nil {
return
}

if self.IsCollapsed(path) {
expandSubtree(self, root, path)
} else {
collapseSubtree(self, root, path)
}
}

// expandSubtree expands the given path and all its descendant directories
func expandSubtree[T any](self *CollapsedPaths, root *Node[T], path string) {
collectAndApply(self, root, path, func(self *CollapsedPaths, p string) {
self.Expand(p)
})
}

// collapseSubtree collapses the given path and all its descendant directories
func collapseSubtree[T any](self *CollapsedPaths, root *Node[T], path string) {
collectAndApply(self, root, path, func(self *CollapsedPaths, p string) {
self.Collapse(p)
})
}

// collectAndApply traverses the tree and applies the given operation to all
// directories that are the target path or its descendants
func collectAndApply[T any](self *CollapsedPaths, root *Node[T], path string, apply func(*CollapsedPaths, string)) {
var collect func(n *Node[T])
collect = func(n *Node[T]) {
if n == nil {
return
}
if !n.IsFile() {
p := n.GetInternalPath()
if p == path || strings.HasPrefix(p, path+"/") {
apply(self, p)
}
}
for _, ch := range n.Children {
collect(ch)
}
}
collect(root)
}
38 changes: 38 additions & 0 deletions pkg/gui/filetree/collapsed_paths_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package filetree

import "testing"

func TestCollapsedPathsBasic(t *testing.T) {
cp := NewCollapsedPaths()

p := "./a"

// initially not collapsed
if cp.IsCollapsed(p) {
t.Fatalf("expected %s to be expanded", p)
}

// collapse
cp.Collapse(p)
if !cp.IsCollapsed(p) {
t.Fatalf("expected %s to be collapsed", p)
}

// toggle -> expand
cp.ToggleCollapsed(p)
if cp.IsCollapsed(p) {
t.Fatalf("expected %s to be expanded after toggle", p)
}

// toggle -> collapse
cp.ToggleCollapsed(p)
if !cp.IsCollapsed(p) {
t.Fatalf("expected %s to be collapsed after toggle", p)
}

// expand
cp.Expand(p)
if cp.IsCollapsed(p) {
t.Fatalf("expected %s to be expanded after Expand", p)
}
}
4 changes: 4 additions & 0 deletions pkg/gui/filetree/commit_file_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,7 @@ func (self *CommitFileTree) GetFile(path string) *models.CommitFile {
func (self *CommitFileTree) InTreeMode() bool {
return self.showTree
}

func (self *CommitFileTree) ToggleSubtreeExpansion(path string) {
ToggleSubtreeExpansionOn(self.collapsedPaths, self.tree, path)
}
17 changes: 17 additions & 0 deletions pkg/gui/filetree/commit_file_tree_view_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ func (self *CommitFileTreeViewModel) ExpandAll() {
}
}

// ToggleSubtreeExpansion toggles the collapsed/expanded state for the given
// path and tries to preserve selection.
func (self *CommitFileTreeViewModel) ToggleSubtreeExpansion(path string) {
selectedNode := self.GetSelected()

self.ICommitFileTree.ToggleSubtreeExpansion(path)

if selectedNode == nil {
return
}

index, found := self.GetIndexForPath(selectedNode.path)
if found {
self.SetSelectedLineIdx(index)
}
}

// Try to select the given path if present. If it doesn't exist, or one of the parent directories is
// collapsed, do nothing.
// Note that filepath is an actual file path, not an internal tree path as with e.g.
Expand Down
5 changes: 5 additions & 0 deletions pkg/gui/filetree/file_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type ITree[T any] interface {
SetTree()
IsCollapsed(path string) bool
ToggleCollapsed(path string)
ToggleSubtreeExpansion(path string)
CollapsedPaths() *CollapsedPaths
CollapseAll()
ExpandAll()
Expand Down Expand Up @@ -213,3 +214,7 @@ func (self *FileTree) CollapsedPaths() *CollapsedPaths {
func (self *FileTree) GetFilter() FileTreeDisplayFilter {
return self.filter
}

func (self *FileTree) ToggleSubtreeExpansion(path string) {
ToggleSubtreeExpansionOn(self.collapsedPaths, self.tree, path)
}
18 changes: 18 additions & 0 deletions pkg/gui/filetree/file_tree_view_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,21 @@ func (self *FileTreeViewModel) ExpandAll() {
self.SetSelectedLineIdx(index)
}
}

// ToggleSubtreeExpansion toggles the collapsed/expanded state of the given path
// and attempts to preserve selection on that path after the change.
func (self *FileTreeViewModel) ToggleSubtreeExpansion(path string) {
selectedNode := self.GetSelected()

self.IFileTree.ToggleSubtreeExpansion(path)

// After changing collapsed state, try to keep selection on the same item
if selectedNode == nil {
return
}

index, found := self.GetIndexForPath(selectedNode.path)
if found {
self.SetSelectedLineIdx(index)
}
}
40 changes: 40 additions & 0 deletions pkg/gui/filetree/subtree_toggle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package filetree

import (
"testing"

"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/stretchr/testify/assert"
)

func TestFileTreeViewModel_ToggleSubtreeExpansion_Recursive(t *testing.T) {
files := []*models.File{
{Path: "a/b/c/file1"},
{Path: "a/b/d/file2"},
{Path: "a/e/file3"},
}

cmn := common.NewDummyCommon()
vm := NewFileTreeViewModel(func() []*models.File { return files }, cmn, true)
vm.SetTree()

// ensure initially nothing is collapsed
if vm.IsCollapsed("./a") || vm.IsCollapsed("./a/b") || vm.IsCollapsed("./a/b/c") {
t.Fatalf("expected no collapsed paths initially")
}

// collapse recursively at ./a
vm.ToggleSubtreeExpansion("./a")

// now a and descendants should be collapsed
assert.True(t, vm.IsCollapsed("./a"))
assert.True(t, vm.IsCollapsed("./a/b"))
assert.True(t, vm.IsCollapsed("./a/b/c"))

// toggle again should expand them
vm.ToggleSubtreeExpansion("./a")
assert.False(t, vm.IsCollapsed("./a"))
assert.False(t, vm.IsCollapsed("./a/b"))
assert.False(t, vm.IsCollapsed("./a/b/c"))
}
Loading