Add a keybinding to collapse the parent directory of the selected file (#1)

When a file is deeply nested (e.g. dir/subdir/file), there was no quick
way to collapse an ancestor directory without walking back up the tree
one row at a time. This adds <backspace> in the Files and Commit Files
panels to jump the selection to the parent directory and collapse it,
skipping over directories that got merged away by path compression.
Pressing it again from the root collapses the root itself.

Relates to upstream repo's #4984 and #4095.
This commit is contained in:
Trương Duy Khánh 2026-07-28 09:21:48 +07:00 committed by GitHub
parent 292035709f
commit 19c702f9ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 201 additions and 0 deletions

View file

@ -583,6 +583,7 @@ type KeybindingFilesConfig struct {
CopyFileInfoToClipboard Keybinding `yaml:"copyFileInfoToClipboard"`
CollapseAll Keybinding `yaml:"collapseAll"`
ExpandAll Keybinding `yaml:"expandAll"`
CollapseParent Keybinding `yaml:"collapseParent"`
}
type KeybindingBranchesConfig struct {
@ -1099,6 +1100,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
CopyFileInfoToClipboard: Keybinding{"y"},
CollapseAll: Keybinding{"-"},
ExpandAll: Keybinding{"="},
CollapseParent: Keybinding{"<backspace>"},
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: Keybinding{"<ctrl+y>"},

View file

@ -132,6 +132,13 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []
Tooltip: self.c.Tr.ExpandAllTooltip,
GetDisabledReason: self.require(self.isInTreeMode),
},
{
Keys: opts.GetKeys(opts.Config.Files.CollapseParent),
Handler: self.collapseParent,
Description: self.c.Tr.CollapseParent,
Tooltip: self.c.Tr.CollapseParentTooltip,
GetDisabledReason: self.require(self.isInTreeMode),
},
}
return bindings
@ -603,6 +610,27 @@ func (self *CommitFilesController) expandAll() error {
return nil
}
func (self *CommitFilesController) collapseParent() error {
node := self.getSelectedItem()
if node == nil {
return nil
}
viewModel := self.context().CommitFileTreeViewModel
parentPath, idx, found := collapseParentPath(node.GetInternalPath(), viewModel.GetIndexForPath)
if !found {
return nil
}
viewModel.CollapsedPaths().Collapse(parentPath)
viewModel.SetSelectedLineIdx(idx)
self.c.PostRefreshUpdate(self.context())
return nil
}
func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return func(mainViewName string, clickedLineIdx int) error {
node := self.getSelectedItem()

View file

@ -206,6 +206,13 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
Tooltip: self.c.Tr.ExpandAllTooltip,
GetDisabledReason: self.require(self.isInTreeMode),
},
{
Keys: opts.GetKeys(opts.Config.Files.CollapseParent),
Handler: self.collapseParent,
Description: self.c.Tr.CollapseParent,
Tooltip: self.c.Tr.CollapseParentTooltip,
GetDisabledReason: self.require(self.isInTreeMode),
},
}
}
@ -720,6 +727,53 @@ func (self *FilesController) expandAll() error {
return nil
}
func (self *FilesController) collapseParent() error {
node := self.context().GetSelected()
if node == nil {
return nil
}
viewModel := self.context().FileTreeViewModel
parentPath, idx, found := collapseParentPath(node.GetInternalPath(), viewModel.GetIndexForPath)
if !found {
return nil
}
viewModel.CollapsedPaths().Collapse(parentPath)
viewModel.SetSelectedLineIdx(idx)
self.c.PostRefreshUpdate(self.context())
return nil
}
// collapseParentPath walks up the internal-path segments of the tree from the
// given node until it finds an existing ancestor node, since a compressed
// directory (e.g. "dir/subdir" merged into one visual node) means the
// immediate one-segment-up path isn't always addressable. The root node's
// parent is itself, so backspacing from the top collapses the root.
func collapseParentPath(internalPath string, getIndexForPath func(string) (int, bool)) (string, int, bool) {
segments := strings.Split(internalPath, "/")
if len(segments) == 1 {
if idx, ok := getIndexForPath(internalPath); ok {
return internalPath, idx, true
}
return "", 0, false
}
for len(segments) > 1 {
segments = segments[:len(segments)-1]
candidate := strings.Join(segments, "/")
if idx, ok := getIndexForPath(candidate); ok {
return candidate, idx, true
}
}
return "", 0, false
}
func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
node := self.context().GetSelected()
if node == nil {

View file

@ -294,6 +294,8 @@ type TranslationSet struct {
CollapseAllTooltip string
ExpandAll string
ExpandAllTooltip string
CollapseParent string
CollapseParentTooltip string
DisabledInFlatView string
FileEnter string
FileEnterTooltip string
@ -1438,6 +1440,8 @@ func EnglishTranslationSet() *TranslationSet {
CollapseAllTooltip: "Collapse all directories in the files tree",
ExpandAll: "Expand all files",
ExpandAllTooltip: "Expand all directories in the file tree",
CollapseParent: "Collapse parent directory",
CollapseParentTooltip: "Move selection up to the parent directory and collapse it",
DisabledInFlatView: "Not available in flat view",
FileEnter: `Stage lines / Collapse directory`,
FileEnterTooltip: "If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it.",

View file

@ -0,0 +1,54 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CollapseParentFiles = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Pressing backspace in the commit files panel jumps to the parent directory and collapses it",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("dir1/subd1/subfile0", "file0\n")
shell.CreateFileAndAdd("dir2/d2_file1", "d2f1 content\n")
shell.Commit("add files in two dirs")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("add files in two dirs").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ /").IsSelected(),
Equals(" ▼ dir1/subd1"),
Equals(" A subfile0"),
Equals(" ▼ dir2"),
Equals(" A d2_file1"),
).
NavigateToLine(Contains("subfile0"))
// backspace jumps to and collapses the immediate (compressed) parent "dir1/subd1"
t.Views().CommitFiles().
Press(keys.Files.CollapseParent).
Lines(
Equals("▼ /"),
Equals(" ▶ dir1/subd1").IsSelected(),
Equals(" ▼ dir2"),
Equals(" A d2_file1"),
)
// backspace again jumps up to and collapses the root
t.Views().CommitFiles().
Press(keys.Files.CollapseParent).
Lines(
Equals("▶ /").IsSelected(),
)
},
})

View file

@ -0,0 +1,57 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CollapseParent = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Pressing backspace on a selected file jumps to its parent directory and collapses it",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir")
shell.CreateDir("dir/subdir")
shell.CreateFile("dir/subdir/file-one", "original content\n")
shell.CreateDir("dir2")
shell.CreateFile("dir2/file-two", "original content\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Equals("▼ /").IsSelected(),
Equals(" ▼ dir/subdir"),
Equals(" ?? file-one"),
Equals(" ▼ dir2"),
Equals(" ?? file-two"),
).
// select "file-one" nested two levels deep
NavigateToLine(Contains("file-one"))
// backspace jumps to and collapses the immediate (compressed) parent "dir/subdir"
t.Views().Files().
Press(keys.Files.CollapseParent).
Lines(
Equals("▼ /"),
Equals(" ▶ dir/subdir").IsSelected(),
Equals(" ▼ dir2"),
Equals(" ?? file-two"),
)
// backspace again jumps up to and collapses the root
t.Views().Files().
Press(keys.Files.CollapseParent).
Lines(
Equals("▶ /").IsSelected(),
)
// backspace on an already-collapsed root is a no-op
t.Views().Files().
Press(keys.Files.CollapseParent).
Lines(
Equals("▶ /").IsSelected(),
)
},
})

View file

@ -110,6 +110,7 @@ var tests = []*components.IntegrationTest{
commit.CheckoutFileFromCommit,
commit.CheckoutFileFromRangeSelectionOfCommits,
commit.CheckoutFileWithLocalModifications,
commit.CollapseParentFiles,
commit.Commit,
commit.CommitMultiline,
commit.CommitSkipHooks,
@ -227,6 +228,7 @@ var tests = []*components.IntegrationTest{
diff.RenameSimilarityThresholdChange,
file.ClickArrowToCollapse,
file.CollapseExpand,
file.CollapseParent,
file.CopyMenu,
file.DirWithUntrackedFile,
file.DiscardAllDirChanges,