1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-24 08:52:21 +02:00

move more view model logic into the files view model

This commit is contained in:
Jesse Duffield 2022-01-30 14:46:46 +11:00
parent 8ea7b7a62e
commit c084abb378
17 changed files with 392 additions and 323 deletions

View File

@ -11,7 +11,7 @@ import (
type ListContextTrait struct {
base types.IBaseContext
listTrait *ListTrait
list types.IList
viewTrait *ViewTrait
takeFocus func() error
@ -30,19 +30,19 @@ type ListContextTrait struct {
}
func (self *ListContextTrait) GetPanelState() types.IListPanelState {
return self.listTrait
return self.list
}
func (self *ListContextTrait) FocusLine() {
// we need a way of knowing whether we've rendered to the view yet.
self.viewTrait.FocusPoint(self.listTrait.GetSelectedLineIdx())
self.viewTrait.FocusPoint(self.list.GetSelectedLineIdx())
if self.RenderSelection {
min, max := self.viewTrait.ViewPortYBounds()
displayStrings := self.GetDisplayStrings(min, max)
content := utils.RenderDisplayStrings(displayStrings)
self.viewTrait.SetViewPortContent(content)
}
self.viewTrait.SetFooter(formatListFooter(self.listTrait.GetSelectedLineIdx(), self.listTrait.GetItemsLength()))
self.viewTrait.SetFooter(formatListFooter(self.list.GetSelectedLineIdx(), self.list.GetItemsLength()))
}
func formatListFooter(selectedLineIdx int, length int) string {
@ -52,8 +52,8 @@ func formatListFooter(selectedLineIdx int, length int) string {
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (self *ListContextTrait) HandleRender() error {
if self.GetDisplayStrings != nil {
self.listTrait.RefreshSelectedIdx()
content := utils.RenderDisplayStrings(self.GetDisplayStrings(0, self.listTrait.GetItemsLength()))
self.list.RefreshSelectedIdx()
content := utils.RenderDisplayStrings(self.GetDisplayStrings(0, self.list.GetItemsLength()))
self.viewTrait.SetContent(content)
self.c.Render()
}
@ -112,10 +112,12 @@ func (self *ListContextTrait) scroll(scrollFunc func()) error {
}
func (self *ListContextTrait) handleLineChange(change int) error {
before := self.listTrait.GetSelectedLineIdx()
self.listTrait.MoveSelectedLine(change)
after := self.listTrait.GetSelectedLineIdx()
before := self.list.GetSelectedLineIdx()
self.list.MoveSelectedLine(change)
after := self.list.GetSelectedLineIdx()
// doing this check so that if we're holding the up key at the start of the list
// we're not constantly re-rendering the main view.
if before != after {
return self.HandleFocus()
}
@ -132,15 +134,15 @@ func (self *ListContextTrait) HandleNextPage() error {
}
func (self *ListContextTrait) HandleGotoTop() error {
return self.handleLineChange(-self.listTrait.GetItemsLength())
return self.handleLineChange(-self.list.GetItemsLength())
}
func (self *ListContextTrait) HandleGotoBottom() error {
return self.handleLineChange(self.listTrait.GetItemsLength())
return self.handleLineChange(self.list.GetItemsLength())
}
func (self *ListContextTrait) HandleClick(onClick func() error) error {
prevSelectedLineIdx := self.listTrait.GetSelectedLineIdx()
prevSelectedLineIdx := self.list.GetSelectedLineIdx()
// because we're handling a click, we need to determine the new line idx based
// on the view itself.
newSelectedLineIdx := self.viewTrait.SelectedLineIdx()
@ -150,17 +152,16 @@ func (self *ListContextTrait) HandleClick(onClick func() error) error {
// we need to focus the view
if !alreadyFocused {
if err := self.takeFocus(); err != nil {
return err
}
}
if newSelectedLineIdx > self.listTrait.GetItemsLength()-1 {
if newSelectedLineIdx > self.list.GetItemsLength()-1 {
return nil
}
self.listTrait.SetSelectedLineIdx(newSelectedLineIdx)
self.list.SetSelectedLineIdx(newSelectedLineIdx)
if prevSelectedLineIdx == newSelectedLineIdx && alreadyFocused && onClick != nil {
return onClick()
@ -169,7 +170,7 @@ func (self *ListContextTrait) HandleClick(onClick func() error) error {
}
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
self.listTrait.SetSelectedLineIdx(selectedLineIdx)
self.list.SetSelectedLineIdx(selectedLineIdx)
return self.HandleFocus()
}

View File

@ -1,32 +0,0 @@
package context
import "github.com/jesseduffield/lazygit/pkg/gui/types"
type HasLength interface {
GetItemsLength() int
}
type ListTrait struct {
selectedIdx int
HasLength
}
var _ types.IListPanelState = (*ListTrait)(nil)
func (self *ListTrait) GetSelectedLineIdx() int {
return self.selectedIdx
}
func (self *ListTrait) SetSelectedLineIdx(value int) {
self.selectedIdx = clamp(value, 0, self.GetItemsLength()-1)
}
// moves the cursor up or down by the given amount
func (self *ListTrait) MoveSelectedLine(value int) {
self.SetSelectedLineIdx(self.selectedIdx + value)
}
// to be called when the model might have shrunk so that our selection is not not out of bounds
func (self *ListTrait) RefreshSelectedIdx() {
self.SetSelectedLineIdx(self.selectedIdx)
}

View File

@ -3,6 +3,7 @@ package context
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@ -39,7 +40,7 @@ func NewTagsContext(
viewTrait := NewViewTrait(getView)
listContextTrait := &ListContextTrait{
base: baseContext,
listTrait: list.ListTrait,
list: list,
viewTrait: viewTrait,
GetDisplayStrings: getDisplayStrings,
@ -62,7 +63,7 @@ func NewTagsContext(
}
type TagsViewModel struct {
*ListTrait
*traits.ListCursor
getModel func() []*models.Tag
}
@ -88,19 +89,7 @@ func NewTagsViewModel(getModel func() []*models.Tag) *TagsViewModel {
getModel: getModel,
}
self.ListTrait = &ListTrait{
selectedIdx: 0,
HasLength: self,
}
self.ListCursor = traits.NewListCursor(self)
return self
}
func clamp(x int, min int, max int) int {
if x < min {
return min
} else if x > max {
return max
}
return x
}

View File

@ -0,0 +1,43 @@
package traits
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type HasLength interface {
GetItemsLength() int
}
type ListCursor struct {
selectedIdx int
list HasLength
}
func NewListCursor(list HasLength) *ListCursor {
return &ListCursor{selectedIdx: 0, list: list}
}
var _ types.IListCursor = (*ListCursor)(nil)
func (self *ListCursor) GetSelectedLineIdx() int {
return self.selectedIdx
}
func (self *ListCursor) SetSelectedLineIdx(value int) {
self.selectedIdx = utils.Clamp(value, 0, self.list.GetItemsLength()-1)
}
// moves the cursor up or down by the given amount
func (self *ListCursor) MoveSelectedLine(delta int) {
self.SetSelectedLineIdx(self.selectedIdx + delta)
}
// to be called when the model might have shrunk so that our selection is not not out of bounds
func (self *ListCursor) RefreshSelectedIdx() {
self.SetSelectedLineIdx(self.selectedIdx)
}
func (self *ListCursor) GetItemsLength() int {
return self.list.GetItemsLength()
}

View File

@ -5,11 +5,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/sirupsen/logrus"
)
type WorkingTreeContext struct {
*WorkingTreeViewModal
*filetree.FileTreeViewModel
*BaseContext
*ListContextTrait
}
@ -37,11 +36,11 @@ func NewWorkingTreeContext(
self := &WorkingTreeContext{}
takeFocus := func() error { return c.PushContext(self) }
list := NewWorkingTreeViewModal(getModel, c.Log, c.UserConfig.Gui.ShowFileTree)
viewModel := filetree.NewFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree)
viewTrait := NewViewTrait(getView)
listContextTrait := &ListContextTrait{
base: baseContext,
listTrait: list.ListTrait,
list: viewModel,
viewTrait: viewTrait,
GetDisplayStrings: getDisplayStrings,
@ -58,44 +57,12 @@ func NewWorkingTreeContext(
self.BaseContext = baseContext
self.ListContextTrait = listContextTrait
self.WorkingTreeViewModal = list
self.FileTreeViewModel = viewModel
return self
}
type WorkingTreeViewModal struct {
*ListTrait
*filetree.FileTreeViewModel
getModel func() []*models.File
}
func (self *WorkingTreeViewModal) GetItemsLength() int {
return self.FileTreeViewModel.GetItemsLength()
}
func (self *WorkingTreeViewModal) GetSelectedFileNode() *filetree.FileNode {
if self.GetItemsLength() == 0 {
return nil
}
return self.FileTreeViewModel.GetItemAtIndex(self.selectedIdx)
}
func (self *WorkingTreeViewModal) GetSelectedItem() (types.ListItem, bool) {
item := self.GetSelectedFileNode()
func (self *WorkingTreeContext) GetSelectedItem() (types.ListItem, bool) {
item := self.FileTreeViewModel.GetSelectedFileNode()
return item, item != nil
}
func NewWorkingTreeViewModal(getModel func() []*models.File, log *logrus.Entry, showTree bool) *WorkingTreeViewModal {
self := &WorkingTreeViewModal{
getModel: getModel,
FileTreeViewModel: filetree.NewFileTreeViewModel(getModel, log, showTree),
}
self.ListTrait = &ListTrait{
selectedIdx: 0,
HasLength: self,
}
return self
}

View File

@ -653,20 +653,8 @@ func (self *FilesController) handleToggleDirCollapsed() error {
}
func (self *FilesController) toggleTreeView() error {
// get path of currently selected file
path := self.getSelectedPath()
self.getContext().FileTreeViewModel.ToggleShowTree()
// find that same node in the new format and move the cursor to it
if path != "" {
self.getContext().FileTreeViewModel.ExpandToPath(path)
index, found := self.getContext().FileTreeViewModel.GetIndexForPath(path)
if found {
self.getContext().GetPanelState().SetSelectedLineIdx(index)
}
}
return self.c.PostRefreshUpdate(self.getContext())
}

View File

@ -136,19 +136,19 @@ func TestCompress(t *testing.T) {
func TestGetFile(t *testing.T) {
scenarios := []struct {
name string
viewModel *FileTreeViewModel
viewModel *FileTree
path string
expected *models.File
}{
{
name: "valid case",
viewModel: NewFileTreeViewModel(func() []*models.File { return []*models.File{{Name: "blah/one"}, {Name: "blah/two"}} }, nil, false),
viewModel: NewFileTree(func() []*models.File { return []*models.File{{Name: "blah/one"}, {Name: "blah/two"}} }, nil, false),
path: "blah/two",
expected: &models.File{Name: "blah/two"},
},
{
name: "not found",
viewModel: NewFileTreeViewModel(func() []*models.File { return []*models.File{{Name: "blah/one"}, {Name: "blah/two"}} }, nil, false),
viewModel: NewFileTree(func() []*models.File { return []*models.File{{Name: "blah/one"}, {Name: "blah/two"}} }, nil, false),
path: "blah/three",
expected: nil,
},

View File

@ -0,0 +1,174 @@
package filetree
import (
"fmt"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/sirupsen/logrus"
)
type FileTreeDisplayFilter int
const (
DisplayAll FileTreeDisplayFilter = iota
DisplayStaged
DisplayUnstaged
// this shows files with merge conflicts
DisplayConflicted
)
type IFileTree interface {
InTreeMode() bool
ExpandToPath(path string)
FilterFiles(test func(*models.File) bool) []*models.File
SetFilter(filter FileTreeDisplayFilter)
ToggleShowTree()
GetItemAtIndex(index int) *FileNode
GetFile(path string) *models.File
GetIndexForPath(path string) (int, bool)
GetAllItems() []*FileNode
GetItemsLength() int
GetAllFiles() []*models.File
SetTree()
IsCollapsed(path string) bool
ToggleCollapsed(path string)
Tree() INode
CollapsedPaths() CollapsedPaths
GetFilter() FileTreeDisplayFilter
}
type FileTree struct {
getFiles func() []*models.File
tree *FileNode
showTree bool
log *logrus.Entry
filter FileTreeDisplayFilter
collapsedPaths CollapsedPaths
sync.RWMutex
}
func NewFileTree(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTree {
return &FileTree{
getFiles: getFiles,
log: log,
showTree: showTree,
filter: DisplayAll,
collapsedPaths: CollapsedPaths{},
RWMutex: sync.RWMutex{},
}
}
func (self *FileTree) InTreeMode() bool {
return self.showTree
}
func (self *FileTree) ExpandToPath(path string) {
self.collapsedPaths.ExpandToPath(path)
}
func (self *FileTree) getFilesForDisplay() []*models.File {
switch self.filter {
case DisplayAll:
return self.getFiles()
case DisplayStaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasStagedChanges })
case DisplayUnstaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
case DisplayConflicted:
return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
default:
panic(fmt.Sprintf("Unexpected files display filter: %d", self.filter))
}
}
func (self *FileTree) FilterFiles(test func(*models.File) bool) []*models.File {
result := make([]*models.File, 0)
for _, file := range self.getFiles() {
if test(file) {
result = append(result, file)
}
}
return result
}
func (self *FileTree) SetFilter(filter FileTreeDisplayFilter) {
self.filter = filter
self.SetTree()
}
func (self *FileTree) ToggleShowTree() {
self.showTree = !self.showTree
self.SetTree()
}
func (self *FileTree) GetItemAtIndex(index int) *FileNode {
// need to traverse the three depth first until we get to the index.
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
}
func (self *FileTree) GetFile(path string) *models.File {
for _, file := range self.getFiles() {
if file.Name == path {
return file
}
}
return nil
}
func (self *FileTree) GetIndexForPath(path string) (int, bool) {
index, found := self.tree.GetIndexForPath(path, self.collapsedPaths)
return index - 1, found
}
// note: this gets all items when the filter is taken into consideration. There may
// be hidden files that aren't included here. Files off the screen however will
// be included
func (self *FileTree) GetAllItems() []*FileNode {
if self.tree == nil {
return nil
}
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
}
func (self *FileTree) GetItemsLength() int {
return self.tree.Size(self.collapsedPaths) - 1 // ignoring root
}
func (self *FileTree) GetAllFiles() []*models.File {
return self.getFiles()
}
func (self *FileTree) SetTree() {
filesForDisplay := self.getFilesForDisplay()
if self.showTree {
self.tree = BuildTreeFromFiles(filesForDisplay)
} else {
self.tree = BuildFlatTreeFromFiles(filesForDisplay)
}
}
func (self *FileTree) IsCollapsed(path string) bool {
return self.collapsedPaths.IsCollapsed(path)
}
func (self *FileTree) ToggleCollapsed(path string) {
self.collapsedPaths.ToggleCollapsed(path)
}
func (self *FileTree) Tree() INode {
return self.tree
}
func (self *FileTree) CollapsedPaths() CollapsedPaths {
return self.collapsedPaths
}
func (self *FileTree) GetFilter() FileTreeDisplayFilter {
return self.filter
}

View File

@ -73,8 +73,8 @@ func TestFilterAction(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
mngr := &FileTreeViewModel{getFiles: s.files, filter: s.filter}
result := mngr.GetFilesForDisplay()
mngr := &FileTree{getFiles: func() []*models.File { return s.files }, filter: s.filter}
result := mngr.getFilesForDisplay()
assert.EqualValues(t, s.expected, result)
})
}

View File

@ -1,150 +1,139 @@
package filetree
import (
"fmt"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type FileTreeDisplayFilter int
const (
DisplayAll FileTreeDisplayFilter = iota
DisplayStaged
DisplayUnstaged
// this shows files with merge conflicts
DisplayConflicted
)
type FileTreeViewModel struct {
getFiles func() []*models.File
tree *FileNode
showTree bool
log *logrus.Entry
filter FileTreeDisplayFilter
collapsedPaths CollapsedPaths
sync.RWMutex
type IFileTreeViewModel interface {
IFileTree
types.IListCursor
}
// This combines our FileTree struct with a cursor that retains information about
// which item is selected. It also contains logic for repositioning that cursor
// after the files are refreshed
type FileTreeViewModel struct {
sync.RWMutex
IFileTree
types.IListCursor
}
var _ IFileTreeViewModel = &FileTreeViewModel{}
func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
viewModel := &FileTreeViewModel{
getFiles: getFiles,
log: log,
showTree: showTree,
filter: DisplayAll,
collapsedPaths: CollapsedPaths{},
RWMutex: sync.RWMutex{},
}
return viewModel
}
func (self *FileTreeViewModel) InTreeMode() bool {
return self.showTree
}
func (self *FileTreeViewModel) ExpandToPath(path string) {
self.collapsedPaths.ExpandToPath(path)
}
func (self *FileTreeViewModel) GetFilesForDisplay() []*models.File {
switch self.filter {
case DisplayAll:
return self.getFiles()
case DisplayStaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasStagedChanges })
case DisplayUnstaged:
return self.FilterFiles(func(file *models.File) bool { return file.HasUnstagedChanges })
case DisplayConflicted:
return self.FilterFiles(func(file *models.File) bool { return file.HasMergeConflicts })
default:
panic(fmt.Sprintf("Unexpected files display filter: %d", self.filter))
fileTree := NewFileTree(getFiles, log, showTree)
listCursor := traits.NewListCursor(fileTree)
return &FileTreeViewModel{
IFileTree: fileTree,
IListCursor: listCursor,
}
}
func (self *FileTreeViewModel) FilterFiles(test func(*models.File) bool) []*models.File {
result := make([]*models.File, 0)
for _, file := range self.getFiles() {
if test(file) {
result = append(result, file)
}
}
return result
}
func (self *FileTreeViewModel) SetFilter(filter FileTreeDisplayFilter) {
self.filter = filter
self.SetTree()
}
func (self *FileTreeViewModel) ToggleShowTree() {
self.showTree = !self.showTree
self.SetTree()
}
func (self *FileTreeViewModel) GetItemAtIndex(index int) *FileNode {
// need to traverse the three depth first until we get to the index.
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
}
func (self *FileTreeViewModel) GetFile(path string) *models.File {
for _, file := range self.getFiles() {
if file.Name == path {
return file
}
}
return nil
}
func (self *FileTreeViewModel) GetIndexForPath(path string) (int, bool) {
index, found := self.tree.GetIndexForPath(path, self.collapsedPaths)
return index - 1, found
}
func (self *FileTreeViewModel) GetAllItems() []*FileNode {
if self.tree == nil {
func (self *FileTreeViewModel) GetSelectedFileNode() *FileNode {
if self.GetItemsLength() == 0 {
return nil
}
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
}
func (self *FileTreeViewModel) GetItemsLength() int {
return self.tree.Size(self.collapsedPaths) - 1 // ignoring root
}
func (self *FileTreeViewModel) GetAllFiles() []*models.File {
return self.getFiles()
return self.GetItemAtIndex(self.GetSelectedLineIdx())
}
func (self *FileTreeViewModel) SetTree() {
filesForDisplay := self.GetFilesForDisplay()
if self.showTree {
self.tree = BuildTreeFromFiles(filesForDisplay)
} else {
self.tree = BuildFlatTreeFromFiles(filesForDisplay)
newFiles := self.GetAllFiles()
selectedNode := self.GetSelectedFileNode()
// for when you stage the old file of a rename and the new file is in a collapsed dir
for _, file := range newFiles {
if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path {
self.ExpandToPath(file.Name)
}
}
prevNodes := self.GetAllItems()
prevSelectedLineIdx := self.GetSelectedLineIdx()
self.IFileTree.SetTree()
if selectedNode != nil {
newNodes := self.GetAllItems()
newIdx := self.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], newNodes)
if newIdx != -1 && newIdx != prevSelectedLineIdx {
self.SetSelectedLineIdx(newIdx)
}
}
self.RefreshSelectedIdx()
}
// Let's try to find our file again and move the cursor to that.
// If we can't find our file, it was probably just removed by the user. In that
// case, we go looking for where the next file has been moved to. Given that the
// user could have removed a whole directory, we continue iterating through the old
// nodes until we find one that exists in the new set of nodes, then move the cursor
// to that.
// prevNodes starts from our previously selected node because we don't need to consider anything above that
func (self *FileTreeViewModel) findNewSelectedIdx(prevNodes []*FileNode, currNodes []*FileNode) int {
getPaths := func(node *FileNode) []string {
if node == nil {
return nil
}
if node.File != nil && node.File.IsRename() {
return node.File.Names()
} else {
return []string{node.Path}
}
}
for _, prevNode := range prevNodes {
selectedPaths := getPaths(prevNode)
for idx, node := range currNodes {
paths := getPaths(node)
// If you started off with a rename selected, and now it's broken in two, we want you to jump to the new file, not the old file.
// This is because the new should be in the same position as the rename was meaning less cursor jumping
foundOldFileInRename := prevNode.File != nil && prevNode.File.IsRename() && node.Path == prevNode.File.PreviousName
foundNode := utils.StringArraysOverlap(paths, selectedPaths) && !foundOldFileInRename
if foundNode {
return idx
}
}
}
return -1
}
func (self *FileTreeViewModel) SetFilter(filter FileTreeDisplayFilter) {
self.IFileTree.SetFilter(filter)
self.IListCursor.SetSelectedLineIdx(0)
}
// If we're going from flat to tree we want to select the same file.
// If we're going from tree to flat and we have a file selected we want to select that.
// If instead we've selected a directory we need to select the first file in that directory.
func (self *FileTreeViewModel) ToggleShowTree() {
selectedNode := self.GetSelectedFileNode()
self.IFileTree.ToggleShowTree()
if selectedNode == nil {
return
}
path := selectedNode.Path
if self.InTreeMode() {
self.ExpandToPath(path)
} else if len(selectedNode.Children) > 0 {
path = selectedNode.GetLeaves()[0].Path
}
index, found := self.GetIndexForPath(path)
if found {
self.SetSelectedLineIdx(index)
}
}
func (self *FileTreeViewModel) IsCollapsed(path string) bool {
return self.collapsedPaths.IsCollapsed(path)
}
func (self *FileTreeViewModel) ToggleCollapsed(path string) {
self.collapsedPaths.ToggleCollapsed(path)
}
func (self *FileTreeViewModel) Tree() INode {
return self.tree
}
func (self *FileTreeViewModel) CollapsedPaths() CollapsedPaths {
return self.collapsedPaths
}
func (self *FileTreeViewModel) GetFilter() FileTreeDisplayFilter {
return self.filter
}

View File

@ -334,7 +334,6 @@ type suggestionsPanelState struct {
}
type panelStates struct {
Files *filePanelState
Branches *branchPanelState
Remotes *remotePanelState
RemoteBranches *remoteBranchesState
@ -448,7 +447,6 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
BisectInfo: git_commands.NewNullBisectInfo(),
Panels: &panelStates{
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
Submodules: &submodulePanelState{listPanelState{SelectedLineIdx: -1}},
Branches: &branchPanelState{listPanelState{SelectedLineIdx: 0}},
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},

View File

@ -39,14 +39,14 @@ func (s *State) setConflictIndex(index int) {
if len(s.conflicts) == 0 {
s.conflictIndex = 0
} else {
s.conflictIndex = clamp(index, 0, len(s.conflicts)-1)
s.conflictIndex = utils.Clamp(index, 0, len(s.conflicts)-1)
}
s.setSelectionIndex(s.selectionIndex)
}
func (s *State) setSelectionIndex(index int) {
if selections := s.availableSelections(); len(selections) != 0 {
s.selectionIndex = clamp(index, 0, len(selections)-1)
s.selectionIndex = utils.Clamp(index, 0, len(selections)-1)
}
}
@ -183,12 +183,3 @@ func (s *State) ContentAfterConflictResolve(selection Selection) (bool, string,
return true, content, nil
}
func clamp(x int, min int, max int) int {
if x < min {
return min
} else if x > max {
return max
}
return x
}

View File

@ -21,7 +21,7 @@ const NESTED = "│ "
const NOTHING = " "
func RenderFileTree(
fileMgr *filetree.FileTreeViewModel,
fileMgr filetree.IFileTree,
diffName string,
submoduleConfigs []*models.SubmoduleConfig,
) []string {

View File

@ -69,7 +69,7 @@ M file1
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
viewModel := filetree.NewFileTreeViewModel(func() []*models.File { return s.files }, utils.NewDummyLog(), true)
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, utils.NewDummyLog(), true)
for _, path := range s.collapsedPaths {
viewModel.ToggleCollapsed(path)
}

View File

@ -332,7 +332,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
gui.takeOverMergeConflictScrolling()
}
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
gui.Views.Files.FocusPoint(0, gui.State.Contexts.Files.GetSelectedLineIdx())
return gui.filesRenderToMain()
}
@ -365,15 +365,7 @@ func (gui *Gui) refreshMergeState() error {
func (gui *Gui) refreshStateFiles() error {
state := gui.State
// keep track of where the cursor is currently and the current file names
// when we refresh, go looking for a matching name
// move the cursor to there.
selectedNode := gui.getSelectedFileNode()
fileTreeViewModel := state.Contexts.Files.WorkingTreeViewModal
prevNodes := fileTreeViewModel.GetAllItems()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
fileTreeViewModel := state.Contexts.Files.FileTreeViewModel
// If git thinks any of our files have inline merge conflicts, but they actually don't,
// we stage them.
@ -419,13 +411,7 @@ func (gui *Gui) refreshStateFiles() error {
gui.OnUIThread(func() error { return gui.promptToContinueRebase() })
}
// for when you stage the old file of a rename and the new file is in a collapsed dir
fileTreeViewModel.RWMutex.Lock()
for _, file := range files {
if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path {
fileTreeViewModel.ExpandToPath(file.Name)
}
}
// only taking over the filter if it hasn't already been set by the user.
// Though this does make it impossible for the user to actually say they want to display all if
@ -448,55 +434,9 @@ func (gui *Gui) refreshStateFiles() error {
return err
}
if selectedNode != nil {
newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], fileTreeViewModel.GetAllItems())
if newIdx != -1 && newIdx != prevSelectedLineIdx {
state.Panels.Files.SelectedLineIdx = newIdx
}
}
gui.refreshSelectedLine(state.Panels.Files, fileTreeViewModel.GetItemsLength())
return nil
}
// Let's try to find our file again and move the cursor to that.
// If we can't find our file, it was probably just removed by the user. In that
// case, we go looking for where the next file has been moved to. Given that the
// user could have removed a whole directory, we continue iterating through the old
// nodes until we find one that exists in the new set of nodes, then move the cursor
// to that.
// prevNodes starts from our previously selected node because we don't need to consider anything above that
func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*filetree.FileNode) int {
getPaths := func(node *filetree.FileNode) []string {
if node == nil {
return nil
}
if node.File != nil && node.File.IsRename() {
return node.File.Names()
} else {
return []string{node.Path}
}
}
for _, prevNode := range prevNodes {
selectedPaths := getPaths(prevNode)
for idx, node := range currNodes {
paths := getPaths(node)
// If you started off with a rename selected, and now it's broken in two, we want you to jump to the new file, not the old file.
// This is because the new should be in the same position as the rename was meaning less cursor jumping
foundOldFileInRename := prevNode.File != nil && prevNode.File.IsRename() && node.Path == prevNode.File.PreviousName
foundNode := utils.StringArraysOverlap(paths, selectedPaths) && !foundOldFileInRename
if foundNode {
return idx
}
}
}
return -1
}
// the reflogs panel is the only panel where we cache data, in that we only
// load entries that have been created since we last ran the call. This means
// we need to be more careful with how we use this, and to ensure we're emptying

View File

@ -81,6 +81,18 @@ type IListContext interface {
Context
}
type IList interface {
IListCursor
GetItemsLength() int
}
type IListCursor interface {
GetSelectedLineIdx() int
SetSelectedLineIdx(value int)
MoveSelectedLine(delta int)
RefreshSelectedIdx()
}
type IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int

View File

@ -59,6 +59,15 @@ func Max(x, y int) int {
return y
}
func Clamp(x int, min int, max int) int {
if x < min {
return min
} else if x > max {
return max
}
return x
}
func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes)