From 8699f49f9f86b980df01e7e05e8f16fffd6a6236 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sat, 16 Nov 2024 20:19:52 +0200 Subject: [PATCH 1/2] WIP: file tree v2 --- go.mod | 8 +- go.sum | 10 +-- pkg/constants/constants.go | 2 +- pkg/ui/mainModel.go | 7 ++ pkg/ui/panes/filetreev2/filetree.go | 111 ++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 pkg/ui/panes/filetreev2/filetree.go diff --git a/go.mod b/go.mod index 2d0db04..913c0ab 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,13 @@ go 1.22.6 require ( github.com/bluekeyes/go-gitdiff v0.8.0 - github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.1.0 + github.com/charmbracelet/bubbles v0.0.0-unpublished + github.com/charmbracelet/bubbletea v1.1.1 github.com/charmbracelet/lipgloss v0.13.0 github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/x/ansi v0.3.2 github.com/muesli/reflow v0.3.0 + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a ) require ( @@ -24,10 +25,11 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect ) + +replace github.com/charmbracelet/bubbles v0.0.0-unpublished => /Users/dlvhdr/code/playground/bubbles diff --git a/go.sum b/go.sum index ddfcc6b..a7cf689 100644 --- a/go.sum +++ b/go.sum @@ -6,18 +6,16 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/bluekeyes/go-gitdiff v0.8.0 h1:Nn1wfw3/XeKoc3lWk+2bEXGUHIx36kj80FM1gVcBk+o= github.com/bluekeyes/go-gitdiff v0.8.0/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= -github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= -github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= -github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index eb94470..fedf570 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -2,5 +2,5 @@ package constants const ( SearchingFileTreeWidth = 50 - OpenFileTreeWidth = 26 + OpenFileTreeWidth = 50 ) diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index a371c32..c6288a4 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "os" "strings" "github.com/bluekeyes/go-gitdiff/gitdiff" @@ -18,6 +19,7 @@ import ( "github.com/dlvhdr/diffnav/pkg/ui/common" "github.com/dlvhdr/diffnav/pkg/ui/panes/diffviewer" "github.com/dlvhdr/diffnav/pkg/ui/panes/filetree" + "github.com/dlvhdr/diffnav/pkg/ui/panes/filetreev2" "github.com/dlvhdr/diffnav/pkg/utils" ) @@ -32,6 +34,7 @@ type mainModel struct { files []*gitdiff.File cursor int fileTree filetree.Model + fileTreeV2 filetreev2.Model diffViewer diffviewer.Model width int height int @@ -47,6 +50,7 @@ type mainModel struct { func New(input string) mainModel { m := mainModel{input: input, isShowingFileTree: true} m.fileTree = filetree.New() + m.fileTreeV2 = filetreev2.New() m.diffViewer = diffviewer.New() m.help = help.New() @@ -132,6 +136,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } m.fileTree = m.fileTree.SetFiles(m.files) + m.fileTreeV2 = m.fileTreeV2.SetFiles(m.files) cmd = m.setCursor(0) cmds = append(cmds, cmd) @@ -230,6 +235,8 @@ func (m mainModel) View() string { width := m.sidebarWidth() if m.searching { content = m.resultsVp.View() + } else if os.Getenv("DIFFNAV_FILETREE") == "v2" { + content = m.fileTreeV2.View() } else { content = m.fileTree.View() } diff --git a/pkg/ui/panes/filetreev2/filetree.go b/pkg/ui/panes/filetreev2/filetree.go new file mode 100644 index 0000000..d9c5b2c --- /dev/null +++ b/pkg/ui/panes/filetreev2/filetree.go @@ -0,0 +1,111 @@ +package filetreev2 + +import ( + "os" + "path/filepath" + "strings" + + "github.com/bluekeyes/go-gitdiff/gitdiff" + "github.com/charmbracelet/bubbles/tree" + tea "github.com/charmbracelet/bubbletea" + ltree "github.com/charmbracelet/lipgloss/tree" + + "github.com/dlvhdr/diffnav/pkg/constants" + "github.com/dlvhdr/diffnav/pkg/filenode" +) + +type Model struct { + t tree.Model + files []*gitdiff.File +} + +func New() Model { + return Model{ + t: tree.New(nil, constants.OpenFileTreeWidth, 50), + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + return m, nil +} + +func (m Model) View() string { + return m.t.View() +} + +func (m Model) SetFiles(files []*gitdiff.File) Model { + m.files = files + t := buildFullFileTree(files) + m.t.SetNodes(t) + return m +} + +func (m Model) SetCursor(cursor int) Model { + return m +} + +func buildFullFileTree(files []*gitdiff.File) *tree.Node { + t := tree.Root(".") + for _, file := range files { + subTree := t + + name := filenode.GetFileName(file) + dir := filepath.Dir(name) + parts := strings.Split(dir, string(os.PathSeparator)) + path := "" + + // walk the tree to find existing path + for _, part := range parts { + found := false + children := subTree.ChildNodes() + for j := 0; j < len(children); j++ { + child := children[j] + if child.Value() == part { + subTree = child + path = path + part + string(os.PathSeparator) + found = true + break + } + } + if !found { + break + } + } + + // path does not exist from this point, need to creat it + leftover := strings.TrimPrefix(name, path) + parts = strings.Split(leftover, string(os.PathSeparator)) + for i, part := range parts { + var c *tree.Node + if i == len(parts)-1 { + subTree.Child(filenode.FileNode{File: file}) + } else { + c = tree.Root(part) + subTree.Child(c) + subTree = c + } + } + } + + return t +} + +var enumerator = func(children ltree.Children, index int) string { + return "│" +} + +var indenter = func(children ltree.Children, index int) string { + if children.Length()-1 == index { + return " " + } + return "│" +} + +// SetSize implements the Component interface. +func (m *Model) SetSize(width, height int) tea.Cmd { + return nil +} From 7cf56c524869bd328f50ba71d25ef35db09549b8 Mon Sep 17 00:00:00 2001 From: Dolev Hadar Date: Sun, 1 Dec 2024 13:59:22 +0200 Subject: [PATCH 2/2] WIP --- examples/long_file_name.txt | 22 +++++++++ pkg/filenode/file_node.go | 14 +++--- pkg/ui/mainModel.go | 4 +- pkg/ui/panes/filetree/filetree.go | 19 ++++++++ pkg/ui/panes/filetreev2/filetree.go | 69 ++++++++++++++++++++++++++--- 5 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 examples/long_file_name.txt diff --git a/examples/long_file_name.txt b/examples/long_file_name.txt new file mode 100644 index 0000000..362e03c --- /dev/null +++ b/examples/long_file_name.txt @@ -0,0 +1,22 @@ +diff --git a/some-long-file-name-that-cannot-get-longer.go b/some-long-file-name-that-cannot-get-longer.go +index 7eb0f0a..8cf1723 100644 +--- a/some-long-file-name-that-cannot-get-longer.go ++++ b/some-long-file-name-that-cannot-get-longer.go +@@ -31,7 +31,7 @@ func initialFileTreeModel() ftModel { + } + + func (m ftModel) Init() tea.Cmd { +- return fetchFileTree ++ return nil + } + + func (m ftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +@@ -75,6 +75,8 @@ func (m ftModel) View() string { + // t.Child(file) + // } + ++ // test ++ + if m.cursor == i { + t = t.Child(lipgloss.NewStyle().Foreground(lipgloss.Color("99")).Render(file)) + } else { diff --git a/pkg/filenode/file_node.go b/pkg/filenode/file_node.go index 9409e20..bdb919a 100644 --- a/pkg/filenode/file_node.go +++ b/pkg/filenode/file_node.go @@ -25,13 +25,13 @@ func (f FileNode) Path() string { func (f FileNode) Value() string { icon := " " status := " " - if f.File.IsNew { - status += lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Render("") - } else if f.File.IsDelete { - status += lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render("") - } else { - status += lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Render("") - } + // if f.File.IsNew { + // status += lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Render("") + // } else if f.File.IsDelete { + // status += lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render("") + // } else { + // status += lipgloss.NewStyle().Foreground(lipgloss.Color("3")).Render("") + // } depthWidth := f.Depth * 2 iconsWidth := lipgloss.Width(icon) + lipgloss.Width(status) diff --git a/pkg/ui/mainModel.go b/pkg/ui/mainModel.go index c6288a4..05b9f61 100644 --- a/pkg/ui/mainModel.go +++ b/pkg/ui/mainModel.go @@ -127,8 +127,8 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height dfCmd := m.diffViewer.SetSize(m.width-m.sidebarWidth(), m.height-footerHeight-headerHeight) cmds = append(cmds, dfCmd) - ftCmd := m.fileTree.SetSize(m.sidebarWidth(), m.height-footerHeight-headerHeight-searchHeight) - cmds = append(cmds, ftCmd) + m.fileTree.SetSize(m.sidebarWidth(), m.height-footerHeight-headerHeight-searchHeight) + m.fileTreeV2.SetSize(m.sidebarWidth(), m.height-footerHeight-headerHeight-searchHeight) case fileTreeMsg: m.files = msg.files diff --git a/pkg/ui/panes/filetree/filetree.go b/pkg/ui/panes/filetree/filetree.go index 45a64c5..757e2e7 100644 --- a/pkg/ui/panes/filetree/filetree.go +++ b/pkg/ui/panes/filetree/filetree.go @@ -205,6 +205,24 @@ func buildFullFileTree(files []*gitdiff.File) *tree.Tree { return t } +// Given a tree with nodes that have only one child, collapse the tree by +// merging these nodes with their parents, as long as the parent has only one child as well. +// For example, the tree: +// . +// ├── a +// │ └── b +// │ └── c +// +// will be collapsed to: +// . +// ├── a/b +// │ └── c +// +// This tree wouldn't be collapsed: +// ├── a +// │ ├── b +// │ │ └── c +// │ └── d func collapseTree(t *tree.Tree) *tree.Tree { children := t.Children() newT := tree.Root(t.Value()) @@ -243,6 +261,7 @@ func collapseTree(t *tree.Tree) *tree.Tree { const dirIcon = " " +// updates the file nodes's depth and yOffsets? func truncateTree(t *tree.Tree, depth int, numNodes int, numChildren int) (*tree.Tree, int) { newT := tree.Root(utils.TruncateString(dirIcon+t.Value(), constants.OpenFileTreeWidth-depth*2)) numNodes++ diff --git a/pkg/ui/panes/filetreev2/filetree.go b/pkg/ui/panes/filetreev2/filetree.go index d9c5b2c..a153f02 100644 --- a/pkg/ui/panes/filetreev2/filetree.go +++ b/pkg/ui/panes/filetreev2/filetree.go @@ -20,9 +20,13 @@ type Model struct { } func New() Model { - return Model{ - t: tree.New(nil, constants.OpenFileTreeWidth, 50), + m := Model{ + t: tree.New(nil, constants.OpenFileTreeWidth, 0), } + + m.t.Enumerator(enumerator).Indenter(indenter) + + return m } func (m Model) Init() tea.Cmd { @@ -40,7 +44,9 @@ func (m Model) View() string { func (m Model) SetFiles(files []*gitdiff.File) Model { m.files = files t := buildFullFileTree(files) - m.t.SetNodes(t) + collapsed := collapseTree(t) + // t, _ = truncateTree(t, 0, 0, 0) + m.t.SetNodes(collapsed) return m } @@ -76,7 +82,7 @@ func buildFullFileTree(files []*gitdiff.File) *tree.Node { } } - // path does not exist from this point, need to creat it + // path does not exist from this point, need to create it leftover := strings.TrimPrefix(name, path) parts = strings.Split(leftover, string(os.PathSeparator)) for i, part := range parts { @@ -94,18 +100,67 @@ func buildFullFileTree(files []*gitdiff.File) *tree.Node { return t } +// Given a tree with nodes that have only one child, collapse the tree by +// merging these nodes with their parents, as long as the parent has only one child as well. +// For example, the tree: +// . +// ├── a +// │ └── b +// │ └── c +// +// will be collapsed to: +// . +// ├── a/b +// │ └── c +// +// This tree wouldn't be collapsed: +// ├── a +// │ ├── b +// │ │ └── c +// │ └── d +func collapseTree(t *tree.Node) *tree.Node { + children := t.ChildNodes() + newT := tree.Root(t.GivenValue()) + if len(children) == 0 { + return newT + } + + // recursively collapse children + for _, child := range children { + if child.Size() > 1 { + collapsedChild := collapseTree(child) + newT.Child(collapsedChild) + } else { + newT.Child(child.GivenValue()) + } + } + + // all children are collapsed, now check if the parent can be collapsed + newChildren := newT.ChildNodes() + if len(newChildren) == 1 { + child := newChildren[0] + if t.GivenValue() == "." { + return child + } + + val := t.Value() + string(os.PathSeparator) + child.Value() + collapsed := tree.Root(val).Child(child.ChildNodes()) + return collapsed + } + + return newT +} + var enumerator = func(children ltree.Children, index int) string { return "│" } var indenter = func(children ltree.Children, index int) string { - if children.Length()-1 == index { - return " " - } return "│" } // SetSize implements the Component interface. func (m *Model) SetSize(width, height int) tea.Cmd { + m.t.SetSize(width, height) return nil }