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:
parent
8ea7b7a62e
commit
c084abb378
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
43
pkg/gui/context/traits/list_cursor.go
Normal file
43
pkg/gui/context/traits/list_cursor.go
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
|
174
pkg/gui/filetree/file_tree.go
Normal file
174
pkg/gui/filetree/file_tree.go
Normal 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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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}},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ const NESTED = "│ "
|
||||
const NOTHING = " "
|
||||
|
||||
func RenderFileTree(
|
||||
fileMgr *filetree.FileTreeViewModel,
|
||||
fileMgr filetree.IFileTree,
|
||||
diffName string,
|
||||
submoduleConfigs []*models.SubmoduleConfig,
|
||||
) []string {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user