mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-02 23:27:32 +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 {
|
type ListContextTrait struct {
|
||||||
base types.IBaseContext
|
base types.IBaseContext
|
||||||
listTrait *ListTrait
|
list types.IList
|
||||||
viewTrait *ViewTrait
|
viewTrait *ViewTrait
|
||||||
|
|
||||||
takeFocus func() error
|
takeFocus func() error
|
||||||
@ -30,19 +30,19 @@ type ListContextTrait struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) GetPanelState() types.IListPanelState {
|
func (self *ListContextTrait) GetPanelState() types.IListPanelState {
|
||||||
return self.listTrait
|
return self.list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) FocusLine() {
|
func (self *ListContextTrait) FocusLine() {
|
||||||
// we need a way of knowing whether we've rendered to the view yet.
|
// 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 {
|
if self.RenderSelection {
|
||||||
min, max := self.viewTrait.ViewPortYBounds()
|
min, max := self.viewTrait.ViewPortYBounds()
|
||||||
displayStrings := self.GetDisplayStrings(min, max)
|
displayStrings := self.GetDisplayStrings(min, max)
|
||||||
content := utils.RenderDisplayStrings(displayStrings)
|
content := utils.RenderDisplayStrings(displayStrings)
|
||||||
self.viewTrait.SetViewPortContent(content)
|
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 {
|
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
|
// 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 {
|
func (self *ListContextTrait) HandleRender() error {
|
||||||
if self.GetDisplayStrings != nil {
|
if self.GetDisplayStrings != nil {
|
||||||
self.listTrait.RefreshSelectedIdx()
|
self.list.RefreshSelectedIdx()
|
||||||
content := utils.RenderDisplayStrings(self.GetDisplayStrings(0, self.listTrait.GetItemsLength()))
|
content := utils.RenderDisplayStrings(self.GetDisplayStrings(0, self.list.GetItemsLength()))
|
||||||
self.viewTrait.SetContent(content)
|
self.viewTrait.SetContent(content)
|
||||||
self.c.Render()
|
self.c.Render()
|
||||||
}
|
}
|
||||||
@ -112,10 +112,12 @@ func (self *ListContextTrait) scroll(scrollFunc func()) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) handleLineChange(change int) error {
|
func (self *ListContextTrait) handleLineChange(change int) error {
|
||||||
before := self.listTrait.GetSelectedLineIdx()
|
before := self.list.GetSelectedLineIdx()
|
||||||
self.listTrait.MoveSelectedLine(change)
|
self.list.MoveSelectedLine(change)
|
||||||
after := self.listTrait.GetSelectedLineIdx()
|
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 {
|
if before != after {
|
||||||
return self.HandleFocus()
|
return self.HandleFocus()
|
||||||
}
|
}
|
||||||
@ -132,15 +134,15 @@ func (self *ListContextTrait) HandleNextPage() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) HandleGotoTop() error {
|
func (self *ListContextTrait) HandleGotoTop() error {
|
||||||
return self.handleLineChange(-self.listTrait.GetItemsLength())
|
return self.handleLineChange(-self.list.GetItemsLength())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) HandleGotoBottom() error {
|
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 {
|
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
|
// because we're handling a click, we need to determine the new line idx based
|
||||||
// on the view itself.
|
// on the view itself.
|
||||||
newSelectedLineIdx := self.viewTrait.SelectedLineIdx()
|
newSelectedLineIdx := self.viewTrait.SelectedLineIdx()
|
||||||
@ -150,17 +152,16 @@ func (self *ListContextTrait) HandleClick(onClick func() error) error {
|
|||||||
|
|
||||||
// we need to focus the view
|
// we need to focus the view
|
||||||
if !alreadyFocused {
|
if !alreadyFocused {
|
||||||
|
|
||||||
if err := self.takeFocus(); err != nil {
|
if err := self.takeFocus(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newSelectedLineIdx > self.listTrait.GetItemsLength()-1 {
|
if newSelectedLineIdx > self.list.GetItemsLength()-1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.listTrait.SetSelectedLineIdx(newSelectedLineIdx)
|
self.list.SetSelectedLineIdx(newSelectedLineIdx)
|
||||||
|
|
||||||
if prevSelectedLineIdx == newSelectedLineIdx && alreadyFocused && onClick != nil {
|
if prevSelectedLineIdx == newSelectedLineIdx && alreadyFocused && onClick != nil {
|
||||||
return onClick()
|
return onClick()
|
||||||
@ -169,7 +170,7 @@ func (self *ListContextTrait) HandleClick(onClick func() error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
|
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
|
||||||
self.listTrait.SetSelectedLineIdx(selectedLineIdx)
|
self.list.SetSelectedLineIdx(selectedLineIdx)
|
||||||
return self.HandleFocus()
|
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 (
|
import (
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"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/gui/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ func NewTagsContext(
|
|||||||
viewTrait := NewViewTrait(getView)
|
viewTrait := NewViewTrait(getView)
|
||||||
listContextTrait := &ListContextTrait{
|
listContextTrait := &ListContextTrait{
|
||||||
base: baseContext,
|
base: baseContext,
|
||||||
listTrait: list.ListTrait,
|
list: list,
|
||||||
viewTrait: viewTrait,
|
viewTrait: viewTrait,
|
||||||
|
|
||||||
GetDisplayStrings: getDisplayStrings,
|
GetDisplayStrings: getDisplayStrings,
|
||||||
@ -62,7 +63,7 @@ func NewTagsContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TagsViewModel struct {
|
type TagsViewModel struct {
|
||||||
*ListTrait
|
*traits.ListCursor
|
||||||
getModel func() []*models.Tag
|
getModel func() []*models.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,19 +89,7 @@ func NewTagsViewModel(getModel func() []*models.Tag) *TagsViewModel {
|
|||||||
getModel: getModel,
|
getModel: getModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ListTrait = &ListTrait{
|
self.ListCursor = traits.NewListCursor(self)
|
||||||
selectedIdx: 0,
|
|
||||||
HasLength: self,
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type WorkingTreeContext struct {
|
type WorkingTreeContext struct {
|
||||||
*WorkingTreeViewModal
|
*filetree.FileTreeViewModel
|
||||||
*BaseContext
|
*BaseContext
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
@ -37,11 +36,11 @@ func NewWorkingTreeContext(
|
|||||||
self := &WorkingTreeContext{}
|
self := &WorkingTreeContext{}
|
||||||
takeFocus := func() error { return c.PushContext(self) }
|
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)
|
viewTrait := NewViewTrait(getView)
|
||||||
listContextTrait := &ListContextTrait{
|
listContextTrait := &ListContextTrait{
|
||||||
base: baseContext,
|
base: baseContext,
|
||||||
listTrait: list.ListTrait,
|
list: viewModel,
|
||||||
viewTrait: viewTrait,
|
viewTrait: viewTrait,
|
||||||
|
|
||||||
GetDisplayStrings: getDisplayStrings,
|
GetDisplayStrings: getDisplayStrings,
|
||||||
@ -58,44 +57,12 @@ func NewWorkingTreeContext(
|
|||||||
|
|
||||||
self.BaseContext = baseContext
|
self.BaseContext = baseContext
|
||||||
self.ListContextTrait = listContextTrait
|
self.ListContextTrait = listContextTrait
|
||||||
self.WorkingTreeViewModal = list
|
self.FileTreeViewModel = viewModel
|
||||||
|
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkingTreeViewModal struct {
|
func (self *WorkingTreeContext) GetSelectedItem() (types.ListItem, bool) {
|
||||||
*ListTrait
|
item := self.FileTreeViewModel.GetSelectedFileNode()
|
||||||
*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()
|
|
||||||
return item, item != nil
|
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 {
|
func (self *FilesController) toggleTreeView() error {
|
||||||
// get path of currently selected file
|
|
||||||
path := self.getSelectedPath()
|
|
||||||
|
|
||||||
self.getContext().FileTreeViewModel.ToggleShowTree()
|
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())
|
return self.c.PostRefreshUpdate(self.getContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,19 +136,19 @@ func TestCompress(t *testing.T) {
|
|||||||
func TestGetFile(t *testing.T) {
|
func TestGetFile(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
viewModel *FileTreeViewModel
|
viewModel *FileTree
|
||||||
path string
|
path string
|
||||||
expected *models.File
|
expected *models.File
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid case",
|
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",
|
path: "blah/two",
|
||||||
expected: &models.File{Name: "blah/two"},
|
expected: &models.File{Name: "blah/two"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "not found",
|
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",
|
path: "blah/three",
|
||||||
expected: nil,
|
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 {
|
for _, s := range scenarios {
|
||||||
s := s
|
s := s
|
||||||
t.Run(s.name, func(t *testing.T) {
|
t.Run(s.name, func(t *testing.T) {
|
||||||
mngr := &FileTreeViewModel{getFiles: s.files, filter: s.filter}
|
mngr := &FileTree{getFiles: func() []*models.File { return s.files }, filter: s.filter}
|
||||||
result := mngr.GetFilesForDisplay()
|
result := mngr.getFilesForDisplay()
|
||||||
assert.EqualValues(t, s.expected, result)
|
assert.EqualValues(t, s.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,150 +1,139 @@
|
|||||||
package filetree
|
package filetree
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"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"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileTreeDisplayFilter int
|
type IFileTreeViewModel interface {
|
||||||
|
IFileTree
|
||||||
const (
|
types.IListCursor
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
|
||||||
viewModel := &FileTreeViewModel{
|
fileTree := NewFileTree(getFiles, log, showTree)
|
||||||
getFiles: getFiles,
|
listCursor := traits.NewListCursor(fileTree)
|
||||||
log: log,
|
return &FileTreeViewModel{
|
||||||
showTree: showTree,
|
IFileTree: fileTree,
|
||||||
filter: DisplayAll,
|
IListCursor: listCursor,
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *FileTreeViewModel) FilterFiles(test func(*models.File) bool) []*models.File {
|
func (self *FileTreeViewModel) GetSelectedFileNode() *FileNode {
|
||||||
result := make([]*models.File, 0)
|
if self.GetItemsLength() == 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 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.tree.Flatten(self.collapsedPaths)[1:] // ignoring root
|
return self.GetItemAtIndex(self.GetSelectedLineIdx())
|
||||||
}
|
|
||||||
|
|
||||||
func (self *FileTreeViewModel) GetItemsLength() int {
|
|
||||||
return self.tree.Size(self.collapsedPaths) - 1 // ignoring root
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *FileTreeViewModel) GetAllFiles() []*models.File {
|
|
||||||
return self.getFiles()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *FileTreeViewModel) SetTree() {
|
func (self *FileTreeViewModel) SetTree() {
|
||||||
filesForDisplay := self.GetFilesForDisplay()
|
newFiles := self.GetAllFiles()
|
||||||
if self.showTree {
|
selectedNode := self.GetSelectedFileNode()
|
||||||
self.tree = BuildTreeFromFiles(filesForDisplay)
|
|
||||||
} else {
|
// for when you stage the old file of a rename and the new file is in a collapsed dir
|
||||||
self.tree = BuildFlatTreeFromFiles(filesForDisplay)
|
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 {
|
type panelStates struct {
|
||||||
Files *filePanelState
|
|
||||||
Branches *branchPanelState
|
Branches *branchPanelState
|
||||||
Remotes *remotePanelState
|
Remotes *remotePanelState
|
||||||
RemoteBranches *remoteBranchesState
|
RemoteBranches *remoteBranchesState
|
||||||
@ -448,7 +447,6 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
|||||||
BisectInfo: git_commands.NewNullBisectInfo(),
|
BisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
Panels: &panelStates{
|
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
|
// 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}},
|
Submodules: &submodulePanelState{listPanelState{SelectedLineIdx: -1}},
|
||||||
Branches: &branchPanelState{listPanelState{SelectedLineIdx: 0}},
|
Branches: &branchPanelState{listPanelState{SelectedLineIdx: 0}},
|
||||||
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
||||||
|
@ -39,14 +39,14 @@ func (s *State) setConflictIndex(index int) {
|
|||||||
if len(s.conflicts) == 0 {
|
if len(s.conflicts) == 0 {
|
||||||
s.conflictIndex = 0
|
s.conflictIndex = 0
|
||||||
} else {
|
} else {
|
||||||
s.conflictIndex = clamp(index, 0, len(s.conflicts)-1)
|
s.conflictIndex = utils.Clamp(index, 0, len(s.conflicts)-1)
|
||||||
}
|
}
|
||||||
s.setSelectionIndex(s.selectionIndex)
|
s.setSelectionIndex(s.selectionIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) setSelectionIndex(index int) {
|
func (s *State) setSelectionIndex(index int) {
|
||||||
if selections := s.availableSelections(); len(selections) != 0 {
|
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
|
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 = " "
|
const NOTHING = " "
|
||||||
|
|
||||||
func RenderFileTree(
|
func RenderFileTree(
|
||||||
fileMgr *filetree.FileTreeViewModel,
|
fileMgr filetree.IFileTree,
|
||||||
diffName string,
|
diffName string,
|
||||||
submoduleConfigs []*models.SubmoduleConfig,
|
submoduleConfigs []*models.SubmoduleConfig,
|
||||||
) []string {
|
) []string {
|
||||||
|
@ -69,7 +69,7 @@ M file1
|
|||||||
for _, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
s := s
|
s := s
|
||||||
t.Run(s.name, func(t *testing.T) {
|
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 {
|
for _, path := range s.collapsedPaths {
|
||||||
viewModel.ToggleCollapsed(path)
|
viewModel.ToggleCollapsed(path)
|
||||||
}
|
}
|
||||||
|
@ -332,7 +332,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
|||||||
gui.takeOverMergeConflictScrolling()
|
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()
|
return gui.filesRenderToMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,15 +365,7 @@ func (gui *Gui) refreshMergeState() error {
|
|||||||
func (gui *Gui) refreshStateFiles() error {
|
func (gui *Gui) refreshStateFiles() error {
|
||||||
state := gui.State
|
state := gui.State
|
||||||
|
|
||||||
// keep track of where the cursor is currently and the current file names
|
fileTreeViewModel := state.Contexts.Files.FileTreeViewModel
|
||||||
// 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
|
|
||||||
|
|
||||||
// If git thinks any of our files have inline merge conflicts, but they actually don't,
|
// If git thinks any of our files have inline merge conflicts, but they actually don't,
|
||||||
// we stage them.
|
// we stage them.
|
||||||
@ -419,13 +411,7 @@ func (gui *Gui) refreshStateFiles() error {
|
|||||||
gui.OnUIThread(func() error { return gui.promptToContinueRebase() })
|
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()
|
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.
|
// 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
|
// 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
|
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
|
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
|
// 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
|
// 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
|
// 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
|
Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IList interface {
|
||||||
|
IListCursor
|
||||||
|
GetItemsLength() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type IListCursor interface {
|
||||||
|
GetSelectedLineIdx() int
|
||||||
|
SetSelectedLineIdx(value int)
|
||||||
|
MoveSelectedLine(delta int)
|
||||||
|
RefreshSelectedIdx()
|
||||||
|
}
|
||||||
|
|
||||||
type IListPanelState interface {
|
type IListPanelState interface {
|
||||||
SetSelectedLineIdx(int)
|
SetSelectedLineIdx(int)
|
||||||
GetSelectedLineIdx() int
|
GetSelectedLineIdx() int
|
||||||
|
@ -59,6 +59,15 @@ func Max(x, y int) int {
|
|||||||
return y
|
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 {
|
func AsJson(i interface{}) string {
|
||||||
bytes, _ := json.MarshalIndent(i, "", " ")
|
bytes, _ := json.MarshalIndent(i, "", " ")
|
||||||
return string(bytes)
|
return string(bytes)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user