improve fstree selection and line logic
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
.vscode
|
||||||
.gemini
|
.gemini
|
||||||
prompts
|
prompts
|
||||||
mend
|
mend
|
||||||
+67
-47
@@ -57,6 +57,10 @@ type FsNode struct {
|
|||||||
children []*FsNode
|
children []*FsNode
|
||||||
parent *FsNode // for fast traversal up the tree
|
parent *FsNode // for fast traversal up the tree
|
||||||
expanded bool // makes sense only for folder nodes
|
expanded bool // makes sense only for folder nodes
|
||||||
|
line int
|
||||||
|
// in the flattree
|
||||||
|
prev *FsNode
|
||||||
|
next *FsNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *FsNode) FileName() string {
|
func (n *FsNode) FileName() string {
|
||||||
@@ -67,8 +71,8 @@ func (n *FsNode) FileName() string {
|
|||||||
type FsTree struct {
|
type FsTree struct {
|
||||||
root *FsNode
|
root *FsNode
|
||||||
lines map[int]*FsNode // flattened view of nodes for easy line access, map so that I can handle blank padding
|
lines map[int]*FsNode // flattened view of nodes for easy line access, map so that I can handle blank padding
|
||||||
selectedLine int
|
selectedNode *FsNode
|
||||||
hoverLine int
|
hoveredNode *FsNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Bubble Tea Interface Implementation ====================
|
// ==================== Bubble Tea Interface Implementation ====================
|
||||||
@@ -85,27 +89,26 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case MsgToggleExpand:
|
case MsgToggleExpand:
|
||||||
_ = t.ToggleSelectedExpand()
|
_ = t.ToggleSelectedExpand()
|
||||||
case MsgLeftClickLine:
|
case MsgLeftClickLine:
|
||||||
node := t.lines[m.Line]
|
nodeAtLine := t.lines[m.Line]
|
||||||
switch node.nodeType {
|
if nodeAtLine != nil {
|
||||||
case FolderNode:
|
t.selectedNode = nodeAtLine
|
||||||
_ = t.ToggleExpand(node)
|
if nodeAtLine.nodeType == FolderNode {
|
||||||
case FileNode:
|
_ = t.ToggleExpand(nodeAtLine)
|
||||||
_ = t.SelectNodeAtLine(m.Line)
|
}
|
||||||
}
|
}
|
||||||
case MsgCreateNode:
|
case MsgCreateNode:
|
||||||
_ = t.CreateNode(m.Parent, m.Name, m.NodeType)
|
_ = t.CreateNode(m.Parent, m.Name, m.NodeType)
|
||||||
case MsgDeleteNode:
|
case MsgDeleteNode:
|
||||||
_ = t.DeleteNode(m.Node)
|
_ = t.DeleteNode(m.Node)
|
||||||
case MsgHover:
|
case MsgHover:
|
||||||
t.hoverLine = m.Line
|
t.hoveredNode = t.lines[m.Line]
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) View() string {
|
func (t *FsTree) View() string {
|
||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
curLine := -1
|
t.renderNode(t.root, 0, builder)
|
||||||
t.renderNode(t.root, 0, builder, &curLine)
|
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,20 +129,17 @@ func NewFsTree(rootPath string) *FsTree {
|
|||||||
if len(root.children) > 0 {
|
if len(root.children) > 0 {
|
||||||
tree = &FsTree{
|
tree = &FsTree{
|
||||||
root: root,
|
root: root,
|
||||||
selectedLine: 0,
|
selectedNode: root.children[0],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tree = &FsTree{
|
||||||
|
root: root,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
tree = &FsTree{
|
|
||||||
root: root,
|
|
||||||
}
|
}
|
||||||
tree.buildLines()
|
tree.buildLines()
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) selectedNode() *FsNode {
|
|
||||||
return t.lines[t.selectedLine]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *FsTree) CreateNode(parent *FsNode, name string, nodeType FsNodeType) error {
|
func (t *FsTree) CreateNode(parent *FsNode, name string, nodeType FsNodeType) error {
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
return errors.New("parent node cannot be nil")
|
return errors.New("parent node cannot be nil")
|
||||||
@@ -192,66 +192,67 @@ func (t *FsTree) ToggleExpand(node *FsNode) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) move(delta int) error {
|
func (t *FsTree) move(delta int) error {
|
||||||
if t.selectedNode() == nil {
|
selected := t.selectedNode
|
||||||
|
if selected == nil {
|
||||||
return errors.New("no node is currently selected")
|
return errors.New("no node is currently selected")
|
||||||
}
|
}
|
||||||
if delta != -1 && delta != 1 {
|
if delta != -1 && delta != 1 {
|
||||||
return errors.New("delta must be either -1 (up) or 1 (down)")
|
return errors.New("delta must be either -1 (up) or 1 (down)")
|
||||||
}
|
}
|
||||||
|
|
||||||
newIndex := t.selectedLine + delta
|
var next *FsNode
|
||||||
if newIndex == -1 || t.lines[newIndex] == nil {
|
if delta == 1 {
|
||||||
return nil // noop
|
next = selected.next
|
||||||
|
} else {
|
||||||
|
next = selected.prev
|
||||||
|
}
|
||||||
|
if next != nil {
|
||||||
|
t.selectedNode = next
|
||||||
}
|
}
|
||||||
|
|
||||||
t.selectedLine = newIndex
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) MoveUp() error { return t.move(-1) }
|
func (t *FsTree) MoveUp() error { return t.move(-1) }
|
||||||
func (t *FsTree) MoveDown() error { return t.move(1) }
|
func (t *FsTree) MoveDown() error { return t.move(1) }
|
||||||
|
|
||||||
func (t *FsTree) SelectNodeAtLine(line int) error {
|
|
||||||
if line <= 0 || line >= len(t.lines) {
|
|
||||||
return errors.New("line number out of bounds")
|
|
||||||
}
|
|
||||||
t.selectedLine = line
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *FsTree) ToggleSelectedExpand() error {
|
func (t *FsTree) ToggleSelectedExpand() error {
|
||||||
if t.selectedNode() == nil {
|
if t.selectedNode == nil {
|
||||||
return errors.New("no node is currently selected")
|
return errors.New("no node is currently selected")
|
||||||
}
|
}
|
||||||
if t.selectedNode().nodeType != FolderNode {
|
if t.selectedNode.nodeType != FolderNode {
|
||||||
return errors.New("only folder nodes can be expanded or collapsed")
|
return errors.New("only folder nodes can be expanded or collapsed")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.ToggleExpand(t.selectedNode())
|
t.ToggleExpand(t.selectedNode)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) GetSelectedContent() (string, error) {
|
func (t *FsTree) GetSelectedContent() (string, error) {
|
||||||
if t.selectedNode() == nil {
|
if t.selectedNode == nil {
|
||||||
return "", errors.New("no node is currently selected")
|
return "", errors.New("no node is currently selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.selectedNode().nodeType != FileNode {
|
if t.selectedNode.nodeType != FileNode {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBytes, err := os.ReadFile(t.selectedNode().path)
|
contentBytes, err := os.ReadFile(t.selectedNode.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return string(contentBytes), nil
|
return string(contentBytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder, currentLine *int) {
|
func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder) {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
folderInRoot := false
|
||||||
|
if node.nodeType == FolderNode && depth == 1 && node.prev != nil {
|
||||||
|
folderInRoot = true
|
||||||
|
}
|
||||||
|
|
||||||
indent := strings.Repeat(" ", depth)
|
indent := strings.Repeat(" ", depth)
|
||||||
prevIndent := strings.Repeat(" ", max(depth-1, 0))
|
prevIndent := strings.Repeat(" ", max(depth-1, 0))
|
||||||
|
|
||||||
@@ -271,8 +272,8 @@ func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder, c
|
|||||||
// highlight if selected or hovered
|
// highlight if selected or hovered
|
||||||
// note: the logic of lines cache needs to match render
|
// note: the logic of lines cache needs to match render
|
||||||
fileName := node.FileName()
|
fileName := node.FileName()
|
||||||
isSelected := *currentLine == t.selectedLine
|
isSelected := node == t.selectedNode
|
||||||
isHovered := *currentLine == t.hoverLine
|
isHovered := node == t.hoveredNode
|
||||||
|
|
||||||
if isSelected {
|
if isSelected {
|
||||||
fileName = lipgloss.NewStyle().Foreground(styles.Highlight).Bold(true).Render(fileName)
|
fileName = lipgloss.NewStyle().Foreground(styles.Highlight).Bold(true).Render(fileName)
|
||||||
@@ -281,14 +282,17 @@ func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
line := icon + " " + fileName + "\n"
|
line := icon + " " + fileName + "\n"
|
||||||
|
|
||||||
if depth > 0 {
|
if depth > 0 {
|
||||||
|
if folderInRoot {
|
||||||
|
line = "\n" + line
|
||||||
|
}
|
||||||
builder.WriteString(line)
|
builder.WriteString(line)
|
||||||
}
|
}
|
||||||
*currentLine++
|
|
||||||
|
|
||||||
if node.expanded {
|
if node.expanded {
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
t.renderNode(child, depth+1, builder, currentLine)
|
t.renderNode(child, depth+1, builder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,22 +301,38 @@ func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder, c
|
|||||||
func (t *FsTree) buildLines() {
|
func (t *FsTree) buildLines() {
|
||||||
t.lines = make(map[int]*FsNode)
|
t.lines = make(map[int]*FsNode)
|
||||||
line := -1
|
line := -1
|
||||||
t.buildLinesRec(t.root, &line) // root has -1 depth and -1 index, as it's not meant to be rendered
|
flatTree := make([]*FsNode, 0)
|
||||||
|
t.buildLinesRec(t.root, 0, &line, &flatTree) // root has -1 depth and -1 index, as it's not meant to be rendered
|
||||||
// everything is a child of root
|
// everything is a child of root
|
||||||
|
flatTree = flatTree[1:] // skip root
|
||||||
|
|
||||||
|
for i, node := range flatTree {
|
||||||
|
if i > 0 {
|
||||||
|
node.prev = flatTree[i-1]
|
||||||
|
}
|
||||||
|
if i < len(flatTree)-1 {
|
||||||
|
node.next = flatTree[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: isn't meant to be used directly
|
// Deprecated: isn't meant to be used directly
|
||||||
func (t *FsTree) buildLinesRec(node *FsNode, currentLine *int) {
|
func (t *FsTree) buildLinesRec(node *FsNode, depth int, currentLine *int, flatTree *[]*FsNode) {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if node.nodeType == FolderNode && depth == 1 && *currentLine != 0 {
|
||||||
|
(*currentLine)++
|
||||||
|
}
|
||||||
t.lines[*currentLine] = node
|
t.lines[*currentLine] = node
|
||||||
|
node.line = *currentLine
|
||||||
|
*flatTree = append(*flatTree, node)
|
||||||
(*currentLine)++
|
(*currentLine)++
|
||||||
|
|
||||||
if node.expanded {
|
if node.expanded {
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
t.buildLinesRec(child, currentLine)
|
t.buildLinesRec(child, depth+1, currentLine, flatTree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user