Skip to content

feat: tree bubble - #893

Open
dlvhdr wants to merge 3 commits into
charmbracelet:mainfrom
dlvhdr:dlvhdr/tree-bubble
Open

feat: tree bubble#893
dlvhdr wants to merge 3 commits into
charmbracelet:mainfrom
dlvhdr:dlvhdr/tree-bubble

Conversation

@dlvhdr

@dlvhdr dlvhdr commented Feb 27, 2026

Copy link
Copy Markdown
  • I have read CONTRIBUTING.md.
  • I have created a discussion that was approved by a maintainer (for new features).

@dlvhdr
dlvhdr requested a review from meowgorithm as a code owner February 27, 2026 11:36
@dlvhdr
dlvhdr changed the base branch from main to master February 27, 2026 11:37
@dlvhdr
dlvhdr force-pushed the dlvhdr/tree-bubble branch from 34a0a7b to ba6f094 Compare February 27, 2026 11:40
@dlvhdr dlvhdr mentioned this pull request Feb 27, 2026
@dlvhdr

dlvhdr commented Mar 28, 2026

Copy link
Copy Markdown
Author

@andreynering I see 2.1 was released. Just checking, is this planned for 2.2?

@dlvhdr

dlvhdr commented May 4, 2026

Copy link
Copy Markdown
Author

Anything else needed here @andreynering ?

Dolev Hadar and others added 2 commits June 19, 2026 16:39
refactor

test: add basic tests
Merge branch 'v2-exp' into dlvhdr/tree-bubble

Merge branch 'v2-exp' into dlvhdr/tree-bubble

fix: select keybind

fix: nil edge cases

fix: prevent extra help menu spacing and expose viewport scroll
@dlvhdr
dlvhdr force-pushed the dlvhdr/tree-bubble branch from ba6f094 to 8519a5d Compare June 19, 2026 13:45
@dlvhdr
dlvhdr changed the base branch from master to main June 19, 2026 13:50

@andrinoff andrinoff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution!

On top of the comments below, there's no bubble tree added in README!

Comment thread tree/styles.go Outdated
Comment thread tree/tree.go Outdated
Comment thread tree/node.go
Comment thread tree/tree.go
Comment thread tree/tree.go
Comment thread tree/styles.go Outdated
Comment thread tree/styles.go Outdated
Comment thread tree/styles.go Outdated
Comment thread tree/styles.go
Comment on lines +58 to +61
s.nodeFunc = func(_ Nodes, _ int) lipgloss.Style {
return s.NodeStyle
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code due to SetStyles overriding either way? (unsure)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be overwritten only when the user calls SetStyles with styles.NodeStyleFunc, which the default don't set.

Comment thread tree/node.go

// Size returns the number of nodes in the tree.
// Note that if a child isn't open, its size is 1.
func (t *Node) Size() int {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Size() is len(AllNodes()), allocating per call. This combined with child.Size() per child in tree.go is O(n^2) per setAttributes

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does setAttibutes get called in this flow?
This is basically recursive calls to tree.Children() to figure out how many children each node has. Since lipgloss implemented this where if a child is collapsed, the number of children is 1, I did the same.

I figured the user would already know how many total items he put in the tree but for scrolling purposes, and understanding the vertical size of the tree, I would expose it like lipgloss.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I mixed up two things. no issue with the Size() API. my concern is only how often it's recomputed.

setAttributes runs in New, SetNodes and toggleNode, so on every expand/collapse, and Size()/AllNodes() is hit three more times per cursor move.

each node asks every child for its size and answering means walking that childs whole subtree, so nodes get recounted once per ancestor above them. cost grows with depth, not node count (which i mistakenly said). wide trees will be fine, but deep ones won't

Comment thread go.mod Outdated

@andrinoff andrinoff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a little more!

p.s. lint fails

Comment thread list/keys.go
Comment on lines +70 to +73
Select: key.NewBinding(
key.WithKeys("v"),
key.WithHelp("v", "select"),
),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is there a change in list?

Comment thread tree/tree.go
Comment on lines +562 to +569
func (m Model) cursorView() string {
if m.cursorCharacter == "" {
return ""
}
cursor := strings.Split(strings.Repeat(" ", m.root.Size()), "")
cursor[m.yOffset] = m.cursorCharacter
return m.styles.CursorStyle.Render(lipgloss.JoinVertical(lipgloss.Left, cursor...))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

draws one cursor row per node, but the tree renders one row per line. those are the same number only if every node is exactly one line tall.

Comment thread tree/tree.go
// SetNodes sets the tree to the given root node.
func (m *Model) SetNodes(t *Node) {
m.root = t
m.root.value = t.GivenValue()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not nil guarded. the same thing in AllNodes (:582)

Comment thread tree/node.go

// Size returns the number of nodes in the tree.
// Note that if a child isn't open, its size is 1.
func (t *Node) Size() int {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I mixed up two things. no issue with the Size() API. my concern is only how often it's recomputed.

setAttributes runs in New, SetNodes and toggleNode, so on every expand/collapse, and Size()/AllNodes() is hit three more times per cursor move.

each node asks every child for its size and answering means walking that childs whole subtree, so nodes get recounted once per ancestor above them. cost grows with depth, not node count (which i mistakenly said). wide trees will be fine, but deep ones won't

Comment thread tree/tree.go
m.setAttributes()
m.updateStyles()
m.updateViewport(0)
m.setRootStyles(m.styles)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) styles are installed after the viewport content is built, so we will have 1 unstyled frame. reordering will fix this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants