diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 30ce0377d..3b68c0e2a 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -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{""}, }, Branches: KeybindingBranchesConfig{ CopyPullRequestURL: Keybinding{""}, diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index d129b3f90..3a80786fe 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -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() diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 656da1bf5..b268d0adf 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -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 { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 2e83fed9b..ecd3389cc 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -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.", diff --git a/pkg/integration/tests/commit/collapse_parent_files.go b/pkg/integration/tests/commit/collapse_parent_files.go new file mode 100644 index 000000000..4419e7b0d --- /dev/null +++ b/pkg/integration/tests/commit/collapse_parent_files.go @@ -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(), + ) + }, +}) diff --git a/pkg/integration/tests/file/collapse_parent.go b/pkg/integration/tests/file/collapse_parent.go new file mode 100644 index 000000000..185ed5052 --- /dev/null +++ b/pkg/integration/tests/file/collapse_parent.go @@ -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(), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 07a12e2be..8291a6a10 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -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,