From 0173ef5f4d2c3dffbf319892af4a28ef02d073f7 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 10 Jan 2025 07:38:59 -0800 Subject: [PATCH 1/6] feat(tree): set Children on Leafs (converts their Node type) --- tree/children.go | 18 ++++++++++++++++++ tree/tree.go | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/tree/children.go b/tree/children.go index 6727092a..e5291925 100644 --- a/tree/children.go +++ b/tree/children.go @@ -1,5 +1,7 @@ package tree +import "slices" + // Children is the interface that wraps the basic methods of a tree model. type Children interface { // At returns the content item of the given index. @@ -18,6 +20,22 @@ func (n NodeChildren) Append(child Node) NodeChildren { return n } +// Insert inserts a child to the list at the given index. +func (n NodeChildren) Insert(index int, child Node) NodeChildren { + if index < 0 || len(n) < index+1 { + return n + } + return slices.Insert(n, index, child) +} + +// Replace swaps the child at the given index with the given child. +func (n NodeChildren) Replace(index int, child Node) NodeChildren { + if index < 0 || len(n) < index+1 { + return n + } + return slices.Replace(n, index, index+1, child) +} + // Remove removes a child from the list at the given index. func (n NodeChildren) Remove(index int) NodeChildren { if index < 0 || len(n) < index+1 { diff --git a/tree/tree.go b/tree/tree.go index 94f5dd08..edb0602c 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -38,6 +38,7 @@ type Node interface { Value() string Children() Children Hidden() bool + Child(...any) *Tree } // Leaf is a node without children. @@ -56,6 +57,16 @@ func (s Leaf) Value() string { return s.value } +// Child adds a child to this Tree, converting a Leaf to a Tree in the process. +func (s *Leaf) Child(children ...any) *Tree { + t := &Tree{ + value: s.value, + hidden: s.hidden, + children: NodeChildren(nil), + } + return t.Child(children) +} + // Hidden returns whether a Leaf node is hidden. func (s Leaf) Hidden() bool { return s.hidden @@ -147,7 +158,7 @@ func (t *Tree) Child(children ...any) *Tree { t.children = t.children.(NodeChildren).Append(item) case fmt.Stringer: s := Leaf{value: item.String()} - t.children = t.children.(NodeChildren).Append(s) + t.children = t.children.(NodeChildren).Append(&s) case string: s := Leaf{value: item} t.children = t.children.(NodeChildren).Append(&s) @@ -180,9 +191,6 @@ func ensureParent(nodes Children, item *Tree) (*Tree, int) { parent.Child(item.children.At(i)) } return parent, j - case Leaf: - item.value = parent.Value() - return item, j case *Leaf: item.value = parent.Value() return item, j From 96619fb7febc1f0c500a80b07a20ee00003afedc Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 10 Jan 2025 07:50:16 -0800 Subject: [PATCH 2/6] test(tree): add NodeChildren Replace testable example --- tree/example_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tree/example_test.go diff --git a/tree/example_test.go b/tree/example_test.go new file mode 100644 index 00000000..de0c007c --- /dev/null +++ b/tree/example_test.go @@ -0,0 +1,65 @@ +package tree_test + +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/tree" + "github.com/charmbracelet/x/ansi" +) + +// Leaf Examples + +func ExampleNodeChildren_Replace() { + enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) + itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) + + t := tree. + Root("⁜ Makeup"). + Child( + "Glossier", + "Fenty Beauty", + tree.New().Child( + "Gloss Bomb Universal Lip Luminizer", + "Hot Cheeks Velour Blushlighter", + ), + "Nyx", + "Mac", + "Milk", + ). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(enumeratorStyle). + RootStyle(rootStyle). + ItemStyle(itemStyle) + // Add a Tree as a Child of "Glossier" + // This rewrites all of the tree data to do one replace. Not the most + // efficient. Maybe we can improve on this. + // + // That is how we're handling any Child manipulation in the Child() func as + // well. Because the children are an interface it's a bit trickier. We need + // to do an assignment, can't just manipulate the children directly. + t.SetChildren(t.Children().(tree.NodeChildren). + Replace(0, t.Children().At(0).Child( + tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"), + ))) + + // Add a Leaf as a Child of "Glossier" + t.Children().At(0).Child("Makeup") + fmt.Println(ansi.Strip(t.String())) + + // Output: + // ⁜ Makeup + // ├── Glossier + // │ ├── Apparel + // │ │ ├── Pink Hoodie + // │ │ ╰── Baseball Cap + // │ ╰── Makeup + // ├── Fenty Beauty + // │ ├── Gloss Bomb Universal Lip Luminizer + // │ ╰── Hot Cheeks Velour Blushlighter + // ├── Nyx + // ├── Mac + // ╰── Milk + // +} From 5be296693d24fe9409d0465c48cde69b88f1bbc1 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 10 Jan 2025 09:16:55 -0800 Subject: [PATCH 3/6] feat(tree): add SetChildren, Insert, and Replace to Node interface --- tree/example_test.go | 67 ++++++++++++++++++++++++++++++-------- tree/tree.go | 77 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 15 deletions(-) diff --git a/tree/example_test.go b/tree/example_test.go index de0c007c..b664f481 100644 --- a/tree/example_test.go +++ b/tree/example_test.go @@ -8,9 +8,7 @@ import ( "github.com/charmbracelet/x/ansi" ) -// Leaf Examples - -func ExampleNodeChildren_Replace() { +func ExampleTree_Replace() { enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) @@ -33,21 +31,13 @@ func ExampleNodeChildren_Replace() { RootStyle(rootStyle). ItemStyle(itemStyle) // Add a Tree as a Child of "Glossier" - // This rewrites all of the tree data to do one replace. Not the most - // efficient. Maybe we can improve on this. - // - // That is how we're handling any Child manipulation in the Child() func as - // well. Because the children are an interface it's a bit trickier. We need - // to do an assignment, can't just manipulate the children directly. - t.SetChildren(t.Children().(tree.NodeChildren). - Replace(0, t.Children().At(0).Child( - tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"), - ))) + t.Replace(0, t.Children().At(0).Child( + tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"), + )) // Add a Leaf as a Child of "Glossier" t.Children().At(0).Child("Makeup") fmt.Println(ansi.Strip(t.String())) - // Output: // ⁜ Makeup // ├── Glossier @@ -63,3 +53,52 @@ func ExampleNodeChildren_Replace() { // ╰── Milk // } + +func ExampleTree_Insert() { + // Styles are here in case we want to test that styles are properly inherited... + enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) + itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) + + t := tree. + Root("⁜ Makeup"). + Child( + "Glossier", + "Fenty Beauty", + tree.New().Child( + "Gloss Bomb Universal Lip Luminizer", + "Hot Cheeks Velour Blushlighter", + ), + "Nyx", + "Mac", + "Milk", + ). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(enumeratorStyle). + RootStyle(rootStyle). + ItemStyle(itemStyle) + // Adds a new Tree Node after Fenty Beauty + t.Insert(2, tree.Root("Lancôme").Child("Juicy Tubes Lip Gloss", "Lash Idôle", "Teint Idôle Highlighter")) + + // Adds a new Tree Node in Fenty Beauty + t.Replace(1, t.Children().At(1).Insert(0, "Blurring Skin Tint")) + + // Adds a new Tree Node to a Leaf (Mac) + t.Replace(4, t.Children().At(4).Insert(0, "Glow Play Cushion Blush")) + fmt.Println(ansi.Strip(t.String())) + // Output: + //⁜ Makeup + //├── Glossier + //├── Fenty Beauty + //│ ├── Blurring Skin Tint + //│ ├── Gloss Bomb Universal Lip Luminizer + //│ ╰── Hot Cheeks Velour Blushlighter + //├── Lancôme + //│ ├── Juicy Tubes Lip Gloss + //│ ├── Lash Idôle + //│ ╰── Teint Idôle Highlighter + //├── Nyx + //├── Mac + //│ ╰── Glow Play Cushion Blush + //╰── Milk +} diff --git a/tree/tree.go b/tree/tree.go index edb0602c..d3257f9c 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -39,6 +39,9 @@ type Node interface { Children() Children Hidden() bool Child(...any) *Tree + SetChildren(...any) *Tree + Insert(int, any) *Tree + Replace(int, any) *Tree } // Leaf is a node without children. @@ -67,6 +70,21 @@ func (s *Leaf) Child(children ...any) *Tree { return t.Child(children) } +// SetChildren turns the Leaf into a Tree with the given children. +func (s *Leaf) SetChildren(children ...any) *Tree { + return s.Child(children) +} + +// Replace turns the Leaf into a Tree with the given child. +func (s *Leaf) Replace(_ int, child any) *Tree { + return s.Child(child) +} + +// Insert turns the Leaf into a Tree with the given child. +func (s *Leaf) Insert(_ int, child any) *Tree { + return s.Child(child) +} + // Hidden returns whether a Leaf node is hidden. func (s Leaf) Hidden() bool { return s.hidden @@ -129,7 +147,64 @@ func (t *Tree) String() string { return t.ensureRenderer().render(t, true, "") } -// Child adds a child to this tree. +// SetChildren overwrites a Tree's Children. +func (t *Tree) SetChildren(children ...any) *Tree { + t.children = NodeChildren(nil) + return t.Child(children) +} + +// Replace swaps the child at the given index with the given child. +func (t *Tree) Replace(index int, child any) *Tree { + nodes := t.anyToNode(child) + t.children = t.children.(NodeChildren).Replace(index, nodes[0]) + return t +} + +// Insert child at the given index. +func (t *Tree) Insert(index int, child any) *Tree { + nodes := t.anyToNode(child) + t.children = t.children.(NodeChildren).Insert(index, nodes[0]) + return t +} + +// TODO probably don't need this to be an []any +func (t *Tree) anyToNode(children ...any) []Node { + var nodes []Node + for _, child := range children { + switch item := child.(type) { + case *Tree: + child, _ := child.(*Tree) + nodes = append(nodes, child) + case Children: + for i := 0; i < item.Length(); i++ { + nodes = append(nodes, item.At(i)) + } + case Node: + nodes = append(nodes, item) + case fmt.Stringer: + s := Leaf{value: item.String()} + nodes = append(nodes, &s) + case string: + s := Leaf{value: item} + nodes = append(nodes, &s) + case []any: + return t.anyToNode(item...) + case []string: + ss := make([]any, 0, len(item)) + for _, s := range item { + ss = append(ss, s) + } + return t.anyToNode(ss...) + case nil: + continue + default: + return t.anyToNode(fmt.Sprintf("%v", item)) + } + } + return nodes +} + +// Child adds a child to this Tree. // // If a Child Tree is passed without a root, it will be parented to it's sibling // child (auto-nesting). From b0718a40b70cc5d44599a6747c9413808130ec94 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 10 Jan 2025 10:09:36 -0800 Subject: [PATCH 4/6] docs(tree): tidy examples --- tree/example_test.go | 91 ++++++++++++++++++++++++++++++++++---------- tree/tree.go | 8 +++- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/tree/example_test.go b/tree/example_test.go index b664f481..95e013d7 100644 --- a/tree/example_test.go +++ b/tree/example_test.go @@ -3,16 +3,41 @@ package tree_test import ( "fmt" - "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/tree" "github.com/charmbracelet/x/ansi" ) -func ExampleTree_Replace() { - enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) - rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) - itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) +func ExampleLeaf_Insert() { + t := tree. + Root("⁜ Makeup"). + Child( + "Glossier", + "Fenty Beauty", + tree.New().Child( + "Gloss Bomb Universal Lip Luminizer", + "Hot Cheeks Velour Blushlighter", + ), + "Nyx", + "Mac", + "Milk", + ). + Enumerator(tree.RoundedEnumerator) + // Adds a new Tree Node to a Leaf (Mac). + t.Replace(3, t.Children().At(3).Insert(0, "Glow Play Cushion Blush")) + fmt.Println(ansi.Strip(t.String())) + // Output: + //⁜ Makeup + //├── Glossier + //├── Fenty Beauty + //│ ├── Gloss Bomb Universal Lip Luminizer + //│ ╰── Hot Cheeks Velour Blushlighter + //├── Nyx + //├── Mac + //│ ╰── Glow Play Cushion Blush + //╰── Milk +} +func ExampleLeaf_Replace() { t := tree. Root("⁜ Makeup"). Child( @@ -26,16 +51,48 @@ func ExampleTree_Replace() { "Mac", "Milk", ). - Enumerator(tree.RoundedEnumerator). - EnumeratorStyle(enumeratorStyle). - RootStyle(rootStyle). - ItemStyle(itemStyle) - // Add a Tree as a Child of "Glossier" + Enumerator(tree.RoundedEnumerator) + // Add Glow Play Cushion Blush to Mac Leaf. + t.Replace(3, t.Children().At(3).Replace(0, "Glow Play Cushion Blush")) + fmt.Println(ansi.Strip(t.String())) + // Output: + //⁜ Makeup + //├── Glossier + //├── Fenty Beauty + //│ ├── Gloss Bomb Universal Lip Luminizer + //│ ╰── Hot Cheeks Velour Blushlighter + //├── Nyx + //├── Mac + //│ ╰── Glow Play Cushion Blush + //╰── Milk +} + +// Tree Examples + +func ExampleTree_Replace() { + t := tree. + Root("⁜ Makeup"). + Child( + "Glossier", + "Fenty Beauty", + tree.New().Child( + "Gloss Bomb Universal Lip Luminizer", + "Hot Cheeks Velour Blushlighter", + ), + "Nyx", + "Mac", + "Milk", + ). + Enumerator(tree.RoundedEnumerator) + // Add a Tree as a Child of "Glossier". At this stage "Glossier" is a Leaf, + // so we re-assign the value of "Glossier" in the "Makeup" Tree to its new + // Tree value returned from Child(). t.Replace(0, t.Children().At(0).Child( tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"), )) - // Add a Leaf as a Child of "Glossier" + // Add a Leaf as a Child of "Glossier". At this stage "Glossier" is a Tree, + // so we don't need to use [Tree.Replace] on the parent tree. t.Children().At(0).Child("Makeup") fmt.Println(ansi.Strip(t.String())) // Output: @@ -55,11 +112,6 @@ func ExampleTree_Replace() { } func ExampleTree_Insert() { - // Styles are here in case we want to test that styles are properly inherited... - enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) - rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) - itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) - t := tree. Root("⁜ Makeup"). Child( @@ -73,11 +125,8 @@ func ExampleTree_Insert() { "Mac", "Milk", ). - Enumerator(tree.RoundedEnumerator). - EnumeratorStyle(enumeratorStyle). - RootStyle(rootStyle). - ItemStyle(itemStyle) - // Adds a new Tree Node after Fenty Beauty + Enumerator(tree.RoundedEnumerator) + // Adds a new Tree Node after Fenty Beauty. t.Insert(2, tree.Root("Lancôme").Child("Juicy Tubes Lip Gloss", "Lash Idôle", "Teint Idôle Highlighter")) // Adds a new Tree Node in Fenty Beauty diff --git a/tree/tree.go b/tree/tree.go index d3257f9c..376c00f0 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -75,12 +75,16 @@ func (s *Leaf) SetChildren(children ...any) *Tree { return s.Child(children) } -// Replace turns the Leaf into a Tree with the given child. +// Replace turns the Leaf into a Tree with the given child. Because of the type +// change, you'll need to reassign the result of this function to the value with +// [Tree.Replace]. func (s *Leaf) Replace(_ int, child any) *Tree { return s.Child(child) } -// Insert turns the Leaf into a Tree with the given child. +// Insert turns the Leaf into a Tree with the given child. Because of the type +// change, you'll need to reassign the result of this function to the value with +// [Tree.Replace]. func (s *Leaf) Insert(_ int, child any) *Tree { return s.Child(child) } From 54aafadcccbbfa5d302e89dfb0416981ffdc7775 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Fri, 10 Jan 2025 10:13:00 -0800 Subject: [PATCH 5/6] test(tree): add TestInheritedStyles for items added to existing Tree --- tree/testdata/TestInheritedStyles.golden | 12 +++++++++ tree/tree_test.go | 32 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tree/testdata/TestInheritedStyles.golden diff --git a/tree/testdata/TestInheritedStyles.golden b/tree/testdata/TestInheritedStyles.golden new file mode 100644 index 00000000..780caa73 --- /dev/null +++ b/tree/testdata/TestInheritedStyles.golden @@ -0,0 +1,12 @@ +⁜ Makeup +├── Glossier +│  ├── Apparel +│  │  ├── Pink Hoodie +│  │  ╰── Baseball Cap +│  ╰── Makeup +├── Fenty Beauty +│  ├── Gloss Bomb Universal Lip Luminizer +│  ╰── Hot Cheeks Velour Blushlighter +├── Nyx +├── Mac +╰── Milk \ No newline at end of file diff --git a/tree/tree_test.go b/tree/tree_test.go index 3ee1fd34..3bcf8c74 100644 --- a/tree/tree_test.go +++ b/tree/tree_test.go @@ -426,6 +426,38 @@ func TestRootStyle(t *testing.T) { golden.RequireEqual(t, []byte(tree.String())) } +func TestInheritedStyles(t *testing.T) { + enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35")) + itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")) + + tr := tree. + Root("⁜ Makeup"). + Child( + "Glossier", + "Fenty Beauty", + tree.New().Child( + "Gloss Bomb Universal Lip Luminizer", + "Hot Cheeks Velour Blushlighter", + ), + "Nyx", + "Mac", + "Milk", + ). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(enumeratorStyle). + RootStyle(rootStyle). + ItemStyle(itemStyle) + // Add a Tree as a Child of "Glossier" + tr.Replace(0, tr.Children().At(0).Child( + tree.Root("Apparel").Child("Pink Hoodie", "Baseball Cap"), + )) + + // Add a Leaf as a Child of "Glossier" + tr.Children().At(0).Child("Makeup") + golden.RequireEqual(t, []byte(tr.String())) +} + func TestAt(t *testing.T) { data := tree.NewStringData("Foo", "Bar") From 2ee0ac25f93e38c664c12a4a4f49f9af65637a26 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 6 Feb 2025 13:45:55 -0800 Subject: [PATCH 6/6] chore: (tree): remove TODOs --- tree/tree.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tree/tree.go b/tree/tree.go index 376c00f0..41d63b1c 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -171,7 +171,6 @@ func (t *Tree) Insert(index int, child any) *Tree { return t } -// TODO probably don't need this to be an []any func (t *Tree) anyToNode(children ...any) []Node { var nodes []Node for _, child := range children {