From a5f3515ad87f978c24d9454d45a454d824eb0897 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Tue, 16 Jan 2024 17:28:14 +1100 Subject: [PATCH] Set groundwork for better disabled reasons with range select Something dumb that we're currently doing is expecting list items to define an ID method which returns a string. We use that when copying items to clipboard with ctrl+o and when getting a ref name for diffing. This commit gets us a little deeper into that hole by explicitly requiring list items to implement that method so that we can easily use the new helper functions in list_controller_trait.go. In future we need to just remove the whole ID thing entirely but I'm too lazy to do that right now. --- pkg/gui/context/branches_context.go | 9 --- pkg/gui/context/commit_files_context.go | 9 --- pkg/gui/context/filtered_list_view_model.go | 4 +- pkg/gui/context/list_renderer_test.go | 41 ++++++---- pkg/gui/context/list_view_model.go | 37 ++++++++- pkg/gui/context/local_commits_context.go | 9 --- pkg/gui/context/menu_context.go | 10 --- pkg/gui/context/reflog_commits_context.go | 9 --- pkg/gui/context/remote_branches_context.go | 20 ++--- pkg/gui/context/remotes_context.go | 9 --- pkg/gui/context/stash_context.go | 9 --- pkg/gui/context/sub_commits_context.go | 9 --- pkg/gui/context/submodules_context.go | 9 --- pkg/gui/context/suggestions_context.go | 9 --- pkg/gui/context/tags_context.go | 9 --- pkg/gui/context/working_tree_context.go | 9 --- pkg/gui/context/worktrees_context.go | 9 --- .../controllers/basic_commits_controller.go | 2 + pkg/gui/controllers/bisect_controller.go | 1 + pkg/gui/controllers/branches_controller.go | 1 + .../controllers/commits_files_controller.go | 1 + pkg/gui/controllers/files_controller.go | 1 + .../controllers/files_remove_controller.go | 1 + pkg/gui/controllers/git_flow_controller.go | 1 + pkg/gui/controllers/list_controller_trait.go | 79 ++++++++++++++++--- pkg/gui/controllers/menu_controller.go | 1 + .../controllers/reflog_commits_controller.go | 1 + .../controllers/remote_branches_controller.go | 1 + pkg/gui/controllers/remotes_controller.go | 1 + pkg/gui/controllers/stash_controller.go | 1 + pkg/gui/controllers/sub_commits_controller.go | 1 + pkg/gui/controllers/submodules_controller.go | 1 + pkg/gui/controllers/suggestions_controller.go | 1 + .../switch_to_diff_files_controller.go | 3 + .../switch_to_sub_commits_controller.go | 3 + pkg/gui/controllers/tags_controller.go | 1 + .../worktree_options_controller.go | 1 + pkg/gui/controllers/worktrees_controller.go | 1 + .../filetree/commit_file_tree_view_model.go | 24 ++++++ pkg/gui/filetree/file_tree_view_model.go | 31 ++++++++ pkg/gui/types/common.go | 6 ++ pkg/gui/types/context.go | 1 + pkg/gui/types/suggestion.go | 5 ++ 43 files changed, 237 insertions(+), 154 deletions(-) diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index 5905168ea..d2647ef84 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -59,15 +59,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { return self } -func (self *BranchesContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *BranchesContext) GetSelectedRef() types.Ref { branch := self.GetSelected() if branch == nil { diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go index fbfff7144..7af968fb7 100644 --- a/pkg/gui/context/commit_files_context.go +++ b/pkg/gui/context/commit_files_context.go @@ -72,15 +72,6 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext { return ctx } -func (self *CommitFilesContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *CommitFilesContext) GetDiffTerminals() []string { return []string{self.GetRef().RefName()} } diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go index c8abbe4a1..2c2841964 100644 --- a/pkg/gui/context/filtered_list_view_model.go +++ b/pkg/gui/context/filtered_list_view_model.go @@ -1,12 +1,12 @@ package context -type FilteredListViewModel[T any] struct { +type FilteredListViewModel[T HasID] struct { *FilteredList[T] *ListViewModel[T] *SearchHistory } -func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] { +func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] { filteredList := NewFilteredList(getList, getFilterFields) self := &FilteredListViewModel[T]{ diff --git a/pkg/gui/context/list_renderer_test.go b/pkg/gui/context/list_renderer_test.go index 98e3f60aa..99c476427 100644 --- a/pkg/gui/context/list_renderer_test.go +++ b/pkg/gui/context/list_renderer_test.go @@ -9,10 +9,17 @@ import ( "github.com/stretchr/testify/assert" ) +// wrapping string in my own type to give it an ID method which is required for list items +type mystring string + +func (self mystring) ID() string { + return string(self) +} + func TestListRenderer_renderLines(t *testing.T) { scenarios := []struct { name string - modelStrings []string + modelStrings []mystring nonModelIndices []int startIdx int endIdx int @@ -20,7 +27,7 @@ func TestListRenderer_renderLines(t *testing.T) { }{ { name: "Render whole list", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, startIdx: 0, endIdx: 3, expectedOutput: ` @@ -30,7 +37,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Partial list, beginning", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, startIdx: 0, endIdx: 2, expectedOutput: ` @@ -39,7 +46,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Partial list, end", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, startIdx: 1, endIdx: 3, expectedOutput: ` @@ -48,7 +55,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Pass an endIdx greater than the model length", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, startIdx: 2, endIdx: 5, expectedOutput: ` @@ -56,7 +63,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Whole list with section headers", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, nonModelIndices: []int{1, 3}, startIdx: 0, endIdx: 5, @@ -69,7 +76,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Multiple consecutive headers", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, nonModelIndices: []int{0, 0, 2, 2, 2}, startIdx: 0, endIdx: 8, @@ -85,7 +92,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Partial list with headers, beginning", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, nonModelIndices: []int{1, 3}, startIdx: 0, endIdx: 3, @@ -96,7 +103,7 @@ func TestListRenderer_renderLines(t *testing.T) { }, { name: "Partial list with headers, end (beyond end index)", - modelStrings: []string{"a", "b", "c"}, + modelStrings: []mystring{"a", "b", "c"}, nonModelIndices: []int{1, 3}, startIdx: 2, endIdx: 7, @@ -108,7 +115,7 @@ func TestListRenderer_renderLines(t *testing.T) { } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - viewModel := NewListViewModel[string](func() []string { return s.modelStrings }) + viewModel := NewListViewModel[mystring](func() []mystring { return s.modelStrings }) var getNonModelItems func() []*NonModelItem if s.nonModelIndices != nil { getNonModelItems = func() []*NonModelItem { @@ -124,7 +131,7 @@ func TestListRenderer_renderLines(t *testing.T) { list: viewModel, getDisplayStrings: func(startIdx int, endIdx int) [][]string { return lo.Map(s.modelStrings[startIdx:endIdx], - func(s string, _ int) []string { return []string{s} }) + func(s mystring, _ int) []string { return []string{string(s)} }) }, getNonModelItems: getNonModelItems, } @@ -138,6 +145,12 @@ func TestListRenderer_renderLines(t *testing.T) { } } +type myint int + +func (self myint) ID() string { + return fmt.Sprint(int(self)) +} + func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) { scenarios := []struct { name string @@ -222,8 +235,8 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) { assert.Equal(t, len(s.modelIndices), len(s.expectedViewIndices)) assert.Equal(t, len(s.viewIndices), len(s.expectedModelIndices)) - modelInts := lo.Range(s.numModelItems) - viewModel := NewListViewModel[int](func() []int { return modelInts }) + modelInts := lo.Map(lo.Range(s.numModelItems), func(i int, _ int) myint { return myint(i) }) + viewModel := NewListViewModel[myint](func() []myint { return modelInts }) var getNonModelItems func() []*NonModelItem if s.nonModelIndices != nil { getNonModelItems = func() []*NonModelItem { @@ -236,7 +249,7 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) { list: viewModel, getDisplayStrings: func(startIdx int, endIdx int) [][]string { return lo.Map(modelInts[startIdx:endIdx], - func(i int, _ int) []string { return []string{fmt.Sprint(i)} }) + func(i myint, _ int) []string { return []string{fmt.Sprint(i)} }) }, getNonModelItems: getNonModelItems, } diff --git a/pkg/gui/context/list_view_model.go b/pkg/gui/context/list_view_model.go index 22416bff1..bf8c80e23 100644 --- a/pkg/gui/context/list_view_model.go +++ b/pkg/gui/context/list_view_model.go @@ -3,14 +3,19 @@ package context import ( "github.com/jesseduffield/lazygit/pkg/gui/context/traits" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) -type ListViewModel[T any] struct { +type HasID interface { + ID() string +} + +type ListViewModel[T HasID] struct { *traits.ListCursor getModel func() []T } -func NewListViewModel[T any](getModel func() []T) *ListViewModel[T] { +func NewListViewModel[T HasID](getModel func() []T) *ListViewModel[T] { self := &ListViewModel[T]{ getModel: getModel, } @@ -32,6 +37,34 @@ func (self *ListViewModel[T]) GetSelected() T { return self.getModel()[self.GetSelectedLineIdx()] } +func (self *ListViewModel[T]) GetSelectedItemId() string { + if self.Len() == 0 { + return "" + } + + return self.GetSelected().ID() +} + +func (self *ListViewModel[T]) GetSelectedItems() ([]T, int, int) { + if self.Len() == 0 { + return nil, -1, -1 + } + + startIdx, endIdx := self.GetSelectionRange() + + return self.getModel()[startIdx : endIdx+1], startIdx, endIdx +} + +func (self *ListViewModel[T]) GetSelectedItemIds() ([]string, int, int) { + selectedItems, startIdx, endIdx := self.GetSelectedItems() + + ids := lo.Map(selectedItems, func(item T, _ int) string { + return item.ID() + }) + + return ids, startIdx, endIdx +} + func (self *ListViewModel[T]) GetItems() []T { return self.getModel() } diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 61a40b30b..5ff361e09 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -92,15 +92,6 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { return ctx } -func (self *LocalCommitsContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - type LocalCommitsViewModel struct { *ListViewModel[*models.Commit] diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 131aa8665..bb1060de6 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -45,16 +45,6 @@ func NewMenuContext( } } -// TODO: remove this thing. -func (self *MenuContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.Label -} - type MenuViewModel struct { c *ContextCommon menuItems []*types.MenuItem diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index 8dc52cde7..65137d633 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -59,15 +59,6 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { } } -func (self *ReflogCommitsContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *ReflogCommitsContext) CanRebase() bool { return false } diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index 82d37b613..884d3debb 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -4,6 +4,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type RemoteBranchesContext struct { @@ -53,15 +54,6 @@ func NewRemoteBranchesContext( } } -func (self *RemoteBranchesContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *RemoteBranchesContext) GetSelectedRef() types.Ref { remoteBranch := self.GetSelected() if remoteBranch == nil { @@ -70,6 +62,16 @@ func (self *RemoteBranchesContext) GetSelectedRef() types.Ref { return remoteBranch } +func (self *RemoteBranchesContext) GetSelectedRefs() ([]types.Ref, int, int) { + items, startIdx, endIdx := self.GetSelectedItems() + + refs := lo.Map(items, func(item *models.RemoteBranch, _ int) types.Ref { + return item + }) + + return refs, startIdx, endIdx +} + func (self *RemoteBranchesContext) GetDiffTerminals() []string { itemId := self.GetSelectedItemId() diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go index 035fb2321..ec59d5fd7 100644 --- a/pkg/gui/context/remotes_context.go +++ b/pkg/gui/context/remotes_context.go @@ -47,15 +47,6 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext { } } -func (self *RemotesContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *RemotesContext) GetDiffTerminals() []string { itemId := self.GetSelectedItemId() diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index 2b86d945f..c8d487688 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -49,15 +49,6 @@ func NewStashContext( } } -func (self *StashContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *StashContext) CanRebase() bool { return false } diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 1f795b44d..7a797e61d 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -175,15 +175,6 @@ func (self *SubCommitsViewModel) GetShowBranchHeads() bool { return self.showBranchHeads } -func (self *SubCommitsContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *SubCommitsContext) CanRebase() bool { return false } diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go index 2cffd82d6..82deb25af 100644 --- a/pkg/gui/context/submodules_context.go +++ b/pkg/gui/context/submodules_context.go @@ -43,12 +43,3 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext { }, } } - -func (self *SubmodulesContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 30781fce1..59908fe5e 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -63,15 +63,6 @@ func NewSuggestionsContext( } } -func (self *SuggestionsContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.Value -} - func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) { self.State.Suggestions = suggestions self.SetSelection(0) diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index 3da5a9576..d827564dd 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -52,15 +52,6 @@ func NewTagsContext( } } -func (self *TagsContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} - func (self *TagsContext) GetSelectedRef() types.Ref { tag := self.GetSelected() if tag == nil { diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go index f3bc91929..6fa462cb1 100644 --- a/pkg/gui/context/working_tree_context.go +++ b/pkg/gui/context/working_tree_context.go @@ -58,12 +58,3 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext { return ctx } - -func (self *WorkingTreeContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} diff --git a/pkg/gui/context/worktrees_context.go b/pkg/gui/context/worktrees_context.go index c616dd49e..3e45f2d45 100644 --- a/pkg/gui/context/worktrees_context.go +++ b/pkg/gui/context/worktrees_context.go @@ -46,12 +46,3 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext { }, } } - -func (self *WorktreesContext) GetSelectedItemId() string { - item := self.GetSelected() - if item == nil { - return "" - } - - return item.ID() -} diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index 386877b4d..6c378ecf0 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -16,6 +16,7 @@ type ContainsCommits interface { types.Context types.IListContext GetSelected() *models.Commit + GetSelectedItems() ([]*models.Commit, int, int) GetCommits() []*models.Commit GetSelectedLineIdx() int } @@ -36,6 +37,7 @@ func NewBasicCommitsController(c *ControllerCommon, context ContainsCommits) *Ba c, context, context.GetSelected, + context.GetSelectedItems, ), } } diff --git a/pkg/gui/controllers/bisect_controller.go b/pkg/gui/controllers/bisect_controller.go index deb4f1b7a..2f9a7ec36 100644 --- a/pkg/gui/controllers/bisect_controller.go +++ b/pkg/gui/controllers/bisect_controller.go @@ -30,6 +30,7 @@ func NewBisectController( c, c.Contexts().LocalCommits, c.Contexts().LocalCommits.GetSelected, + c.Contexts().LocalCommits.GetSelectedItems, ), } } diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 8cac9537d..dbd15ef93 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -33,6 +33,7 @@ func NewBranchesController( c, c.Contexts().Branches, c.Contexts().Branches.GetSelected, + c.Contexts().Branches.GetSelectedItems, ), } } diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index a5333e448..b3c628cf5 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -28,6 +28,7 @@ func NewCommitFilesController( c, c.Contexts().CommitFiles, c.Contexts().CommitFiles.GetSelected, + c.Contexts().CommitFiles.GetSelectedItems, ), } } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 3d418bf8e..ed2c5c28b 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -28,6 +28,7 @@ func NewFilesController( c, c.Contexts().Files, c.Contexts().Files.GetSelected, + c.Contexts().Files.GetSelectedItems, ), } } diff --git a/pkg/gui/controllers/files_remove_controller.go b/pkg/gui/controllers/files_remove_controller.go index 9b21557de..57314c61b 100644 --- a/pkg/gui/controllers/files_remove_controller.go +++ b/pkg/gui/controllers/files_remove_controller.go @@ -28,6 +28,7 @@ func NewFilesRemoveController( c, c.Contexts().Files, c.Contexts().Files.GetSelected, + c.Contexts().Files.GetSelectedItems, ), } } diff --git a/pkg/gui/controllers/git_flow_controller.go b/pkg/gui/controllers/git_flow_controller.go index c8da4bd0c..45ce0a5d0 100644 --- a/pkg/gui/controllers/git_flow_controller.go +++ b/pkg/gui/controllers/git_flow_controller.go @@ -25,6 +25,7 @@ func NewGitFlowController( c, c.Contexts().Branches, c.Contexts().Branches.GetSelected, + c.Contexts().Branches.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/list_controller_trait.go b/pkg/gui/controllers/list_controller_trait.go index fa60223b9..0edaa0114 100644 --- a/pkg/gui/controllers/list_controller_trait.go +++ b/pkg/gui/controllers/list_controller_trait.go @@ -6,20 +6,23 @@ import "github.com/jesseduffield/lazygit/pkg/gui/types" // ensuring a single item is selected, etc. type ListControllerTrait[T comparable] struct { - c *ControllerCommon - context types.IListContext - getSelected func() T + c *ControllerCommon + context types.IListContext + getSelectedItem func() T + getSelectedItems func() ([]T, int, int) } func NewListControllerTrait[T comparable]( c *ControllerCommon, context types.IListContext, getSelected func() T, + getSelectedItems func() ([]T, int, int), ) *ListControllerTrait[T] { return &ListControllerTrait[T]{ - c: c, - context: context, - getSelected: getSelected, + c: c, + context: context, + getSelectedItem: getSelected, + getSelectedItems: getSelectedItems, } } @@ -47,7 +50,7 @@ func (self *ListControllerTrait[T]) singleItemSelected(callbacks ...func(T) *typ } var zeroValue T - item := self.getSelected() + item := self.getSelectedItem() if item == zeroValue { return &types.DisabledReason{Text: self.c.Tr.NoItemSelected} } @@ -62,11 +65,46 @@ func (self *ListControllerTrait[T]) singleItemSelected(callbacks ...func(T) *typ } } +// Ensures that at least one item is selected. +func (self *ListControllerTrait[T]) itemRangeSelected(callbacks ...func([]T, int, int) *types.DisabledReason) func() *types.DisabledReason { + return func() *types.DisabledReason { + items, startIdx, endIdx := self.getSelectedItems() + if len(items) == 0 { + return &types.DisabledReason{Text: self.c.Tr.NoItemSelected} + } + + for _, callback := range callbacks { + if reason := callback(items, startIdx, endIdx); reason != nil { + return reason + } + } + + return nil + } +} + +func (self *ListControllerTrait[T]) itemsSelected(callbacks ...func([]T) *types.DisabledReason) func() *types.DisabledReason { //nolint:unused + return func() *types.DisabledReason { + items, _, _ := self.getSelectedItems() + if len(items) == 0 { + return &types.DisabledReason{Text: self.c.Tr.NoItemSelected} + } + + for _, callback := range callbacks { + if reason := callback(items); reason != nil { + return reason + } + } + + return nil + } +} + // Passes the selected item to the callback. Used for handler functions. func (self *ListControllerTrait[T]) withItem(callback func(T) error) func() error { return func() error { var zeroValue T - commit := self.getSelected() + commit := self.getSelectedItem() if commit == zeroValue { return self.c.ErrorMsg(self.c.Tr.NoItemSelected) } @@ -75,12 +113,35 @@ func (self *ListControllerTrait[T]) withItem(callback func(T) error) func() erro } } +func (self *ListControllerTrait[T]) withItems(callback func([]T) error) func() error { + return func() error { + items, _, _ := self.getSelectedItems() + if len(items) == 0 { + return self.c.ErrorMsg(self.c.Tr.NoItemSelected) + } + + return callback(items) + } +} + +// like withItems but also passes the start and end index of the selection +func (self *ListControllerTrait[T]) withItemsRange(callback func([]T, int, int) error) func() error { + return func() error { + items, startIdx, endIdx := self.getSelectedItems() + if len(items) == 0 { + return self.c.ErrorMsg(self.c.Tr.NoItemSelected) + } + + return callback(items, startIdx, endIdx) + } +} + // Like withItem, but doesn't show an error message if no item is selected. // Use this for click actions (it's a no-op to click empty space) func (self *ListControllerTrait[T]) withItemGraceful(callback func(T) error) func() error { return func() error { var zeroValue T - commit := self.getSelected() + commit := self.getSelectedItem() if commit == zeroValue { return nil } diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 133840cef..a64189138 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -22,6 +22,7 @@ func NewMenuController( c, c.Contexts().Menu, c.Contexts().Menu.GetSelected, + c.Contexts().Menu.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/reflog_commits_controller.go b/pkg/gui/controllers/reflog_commits_controller.go index 6e0228784..d9ca3fd02 100644 --- a/pkg/gui/controllers/reflog_commits_controller.go +++ b/pkg/gui/controllers/reflog_commits_controller.go @@ -23,6 +23,7 @@ func NewReflogCommitsController( c, c.Contexts().ReflogCommits, c.Contexts().ReflogCommits.GetSelected, + c.Contexts().ReflogCommits.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index 25797003b..9d9959448 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -26,6 +26,7 @@ func NewRemoteBranchesController( c, c.Contexts().RemoteBranches, c.Contexts().RemoteBranches.GetSelected, + c.Contexts().RemoteBranches.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index ebd232935..c0ee75022 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -32,6 +32,7 @@ func NewRemotesController( c, c.Contexts().Remotes, c.Contexts().Remotes.GetSelected, + c.Contexts().Remotes.GetSelectedItems, ), c: c, setRemoteBranches: setRemoteBranches, diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index ddef24283..6a413addd 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -24,6 +24,7 @@ func NewStashController( c, c.Contexts().Stash, c.Contexts().Stash.GetSelected, + c.Contexts().Stash.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/sub_commits_controller.go b/pkg/gui/controllers/sub_commits_controller.go index a4ebfb5cd..0acd1f1c4 100644 --- a/pkg/gui/controllers/sub_commits_controller.go +++ b/pkg/gui/controllers/sub_commits_controller.go @@ -24,6 +24,7 @@ func NewSubCommitsController( c, c.Contexts().SubCommits, c.Contexts().SubCommits.GetSelected, + c.Contexts().SubCommits.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index dc43ff35e..dc3952ea0 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -29,6 +29,7 @@ func NewSubmodulesController( c, c.Contexts().Submodules, c.Contexts().Submodules.GetSelected, + c.Contexts().Submodules.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index dbb2b9812..d6e6151ff 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -22,6 +22,7 @@ func NewSuggestionsController( c, c.Contexts().Suggestions, c.Contexts().Suggestions.GetSelected, + c.Contexts().Suggestions.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go index 069726147..5207aeaf5 100644 --- a/pkg/gui/controllers/switch_to_diff_files_controller.go +++ b/pkg/gui/controllers/switch_to_diff_files_controller.go @@ -36,6 +36,9 @@ func NewSwitchToDiffFilesController( c, context, context.GetSelectedRef, + func() ([]types.Ref, int, int) { + panic("Not implemented") + }, ), c: c, context: context, diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index d7bb0a97d..70fe573d7 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -32,6 +32,9 @@ func NewSwitchToSubCommitsController( c, context, context.GetSelectedRef, + func() ([]types.Ref, int, int) { + panic("Not implemented") + }, ), c: c, context: context, diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index 7baebc54c..31fa5ccc0 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -25,6 +25,7 @@ func NewTagsController( c, c.Contexts().Tags, c.Contexts().Tags.GetSelected, + c.Contexts().Tags.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/controllers/worktree_options_controller.go b/pkg/gui/controllers/worktree_options_controller.go index 01cc9b362..e158be3b1 100644 --- a/pkg/gui/controllers/worktree_options_controller.go +++ b/pkg/gui/controllers/worktree_options_controller.go @@ -26,6 +26,7 @@ func NewWorktreeOptionsController(c *ControllerCommon, context CanViewWorktreeOp c, context, context.GetSelectedItemId, + context.GetSelectedItemIds, ), c: c, context: context, diff --git a/pkg/gui/controllers/worktrees_controller.go b/pkg/gui/controllers/worktrees_controller.go index b634d0607..5bbde1770 100644 --- a/pkg/gui/controllers/worktrees_controller.go +++ b/pkg/gui/controllers/worktrees_controller.go @@ -28,6 +28,7 @@ func NewWorktreesController( c, c.Contexts().Worktrees, c.Contexts().Worktrees.GetSelected, + c.Contexts().Worktrees.GetSelectedItems, ), c: c, } diff --git a/pkg/gui/filetree/commit_file_tree_view_model.go b/pkg/gui/filetree/commit_file_tree_view_model.go index d7bc447a1..f5cba0edd 100644 --- a/pkg/gui/filetree/commit_file_tree_view_model.go +++ b/pkg/gui/filetree/commit_file_tree_view_model.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context/traits" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" "github.com/sirupsen/logrus" ) @@ -69,6 +70,29 @@ func (self *CommitFileTreeViewModel) GetSelected() *CommitFileNode { return self.Get(self.GetSelectedLineIdx()) } +func (self *CommitFileTreeViewModel) GetSelectedItemId() string { + item := self.GetSelected() + if item == nil { + return "" + } + + return item.ID() +} + +func (self *CommitFileTreeViewModel) GetSelectedItems() ([]*CommitFileNode, int, int) { + panic("Not implemented") +} + +func (self *CommitFileTreeViewModel) GetSelectedItemIds() ([]string, int, int) { + selectedItems, startIdx, endIdx := self.GetSelectedItems() + + ids := lo.Map(selectedItems, func(item *CommitFileNode, _ int) string { + return item.ID() + }) + + return ids, startIdx, endIdx +} + func (self *CommitFileTreeViewModel) GetSelectedFile() *models.CommitFile { node := self.GetSelected() if node == nil { diff --git a/pkg/gui/filetree/file_tree_view_model.go b/pkg/gui/filetree/file_tree_view_model.go index 2364087d3..05cc9cb89 100644 --- a/pkg/gui/filetree/file_tree_view_model.go +++ b/pkg/gui/filetree/file_tree_view_model.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/context/traits" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" "github.com/sirupsen/logrus" ) @@ -43,6 +44,36 @@ func (self *FileTreeViewModel) GetSelected() *FileNode { return self.Get(self.GetSelectedLineIdx()) } +func (self *FileTreeViewModel) GetSelectedItemId() string { + item := self.GetSelected() + if item == nil { + return "" + } + + return item.ID() +} + +func (self *FileTreeViewModel) GetSelectedItems() ([]*FileNode, int, int) { + startIdx, endIdx := self.GetSelectionRange() + + nodes := []*FileNode{} + for i := startIdx; i <= endIdx; i++ { + nodes = append(nodes, self.Get(i)) + } + + return nodes, startIdx, endIdx +} + +func (self *FileTreeViewModel) GetSelectedItemIds() ([]string, int, int) { + selectedItems, startIdx, endIdx := self.GetSelectedItems() + + ids := lo.Map(selectedItems, func(item *FileNode, _ int) string { + return item.ID() + }) + + return ids, startIdx, endIdx +} + func (self *FileTreeViewModel) GetSelectedFile() *models.File { node := self.GetSelected() if node == nil { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 9053e43f9..86bf63548 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -242,6 +242,12 @@ type MenuItem struct { Section *MenuSection } +// Defining this for the sake of conforming to the HasID interface, which is used +// in list contexts. +func (self *MenuItem) ID() string { + return self.Label +} + type Model struct { CommitFiles []*models.CommitFile Files []*models.File diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 860a49588..92b07a729 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -136,6 +136,7 @@ type IListContext interface { Context GetSelectedItemId() string + GetSelectedItemIds() ([]string, int, int) IsItemVisible(item HasUrn) bool GetList() IList diff --git a/pkg/gui/types/suggestion.go b/pkg/gui/types/suggestion.go index ed8b6ef44..1d4516932 100644 --- a/pkg/gui/types/suggestion.go +++ b/pkg/gui/types/suggestion.go @@ -6,3 +6,8 @@ type Suggestion struct { // label is what is actually displayed so it can e.g. contain color Label string } + +// Conforming to the HasID interface, which is needed for list contexts +func (self *Suggestion) ID() string { + return self.Value +}