simplify moving by flattening tree + add click to load file
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
.gemini
|
.gemini
|
||||||
prompts
|
prompts
|
||||||
|
mend
|
||||||
+29
-30
@@ -12,10 +12,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error {
|
func walkFileSystemAndBuildTree(rootPath string, node *FsNode, flatTree *[]*FsNode) error {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return errors.New("node cannot be nil")
|
return errors.New("node cannot be nil")
|
||||||
}
|
}
|
||||||
@@ -28,46 +27,46 @@ func walkFileSystemAndBuildTree(rootPath string, node *FsNode) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
files := make([]os.DirEntry, 0)
|
||||||
|
folders := make([]os.DirEntry, 0)
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
// dot folders and files skipped
|
// dot folders and files skipped
|
||||||
if len(entry.Name()) > 0 && entry.Name()[0] == '.' {
|
if len(entry.Name()) > 0 && entry.Name()[0] == '.' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
childPath := filepath.Join(rootPath, entry.Name())
|
|
||||||
|
|
||||||
var nodeType FsNodeType
|
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
nodeType = FolderNode
|
folders = append(folders, entry)
|
||||||
} else {
|
} else {
|
||||||
nodeType = FileNode
|
files = append(files, entry)
|
||||||
}
|
|
||||||
|
|
||||||
childNode := &FsNode{
|
|
||||||
nodeType: nodeType,
|
|
||||||
path: childPath,
|
|
||||||
children: make([]*FsNode, 0),
|
|
||||||
parent: node,
|
|
||||||
expanded: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
node.children = append(node.children, childNode)
|
|
||||||
|
|
||||||
// Recursively walk subdirectories
|
|
||||||
if entry.IsDir() {
|
|
||||||
if err := walkFileSystemAndBuildTree(childPath, childNode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort children: files first, then folders
|
*flatTree = append(*flatTree, node) // add self
|
||||||
sort.Slice(node.children, func(i, j int) bool {
|
for _, file := range files {
|
||||||
if node.children[i].nodeType == node.children[j].nodeType {
|
newNode := &FsNode{
|
||||||
return node.children[i].FileName() < node.children[j].FileName()
|
nodeType: FileNode,
|
||||||
|
path: filepath.Join(rootPath, file.Name()),
|
||||||
|
children: make([]*FsNode, 0),
|
||||||
|
parent: node,
|
||||||
|
expanded: false,
|
||||||
}
|
}
|
||||||
return node.children[i].nodeType == FileNode
|
node.children = append(node.children, newNode)
|
||||||
})
|
*flatTree = append(*flatTree, newNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, folder := range folders {
|
||||||
|
newNode := &FsNode{
|
||||||
|
nodeType: FolderNode,
|
||||||
|
path: filepath.Join(rootPath, folder.Name()),
|
||||||
|
children: make([]*FsNode, 0),
|
||||||
|
parent: node,
|
||||||
|
expanded: true, // all expanded by default
|
||||||
|
}
|
||||||
|
node.children = append(node.children, newNode)
|
||||||
|
walkFileSystemAndBuildTree(newNode.path, newNode, flatTree)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -37,7 +37,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := walkFileSystemAndBuildTree(tmpDir, root)
|
flatTree := make([]*FsNode, 0)
|
||||||
|
err := walkFileSystemAndBuildTree(tmpDir, root, &flatTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("walkFileSystemAndBuildTree() error = %v", err)
|
t.Fatalf("walkFileSystemAndBuildTree() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,7 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if folderCount != 2 {
|
if folderCount != 2 {
|
||||||
t.Errorf("folder count = %d, want 2", folderCount)
|
t.Errorf("folder count = %d, want 2", folderCount)
|
||||||
}
|
} // Update viewport with selected file content
|
||||||
|
|
||||||
// Verify nested structure
|
// Verify nested structure
|
||||||
if folder1 == nil {
|
if folder1 == nil {
|
||||||
@@ -92,7 +93,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("error when node is nil", func(t *testing.T) {
|
t.Run("error when node is nil", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
err := walkFileSystemAndBuildTree(tmpDir, nil)
|
flatTree := make([]*FsNode, 0)
|
||||||
|
err := walkFileSystemAndBuildTree(tmpDir, nil, &flatTree)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("walkFileSystemAndBuildTree() with nil node should return error")
|
t.Fatal("walkFileSystemAndBuildTree() with nil node should return error")
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := walkFileSystemAndBuildTree(tmpDir, root)
|
flatTree := make([]*FsNode, 0)
|
||||||
|
err := walkFileSystemAndBuildTree(tmpDir, root, &flatTree)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("walkFileSystemAndBuildTree() with existing children should return error")
|
t.Fatal("walkFileSystemAndBuildTree() with existing children should return error")
|
||||||
}
|
}
|
||||||
@@ -127,7 +130,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := walkFileSystemAndBuildTree("/nonexistent/path", root)
|
flatTree := make([]*FsNode, 0)
|
||||||
|
err := walkFileSystemAndBuildTree("/nonexistent/path", root, &flatTree)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("walkFileSystemAndBuildTree() with nonexistent path should return error")
|
t.Fatal("walkFileSystemAndBuildTree() with nonexistent path should return error")
|
||||||
}
|
}
|
||||||
@@ -142,7 +146,8 @@ func TestWalkFileSystemAndBuildTree(t *testing.T) {
|
|||||||
expanded: true,
|
expanded: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := walkFileSystemAndBuildTree(tmpDir, root)
|
flatTree := make([]*FsNode, 0)
|
||||||
|
err := walkFileSystemAndBuildTree(tmpDir, root, &flatTree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("walkFileSystemAndBuildTree() error = %v", err)
|
t.Fatalf("walkFileSystemAndBuildTree() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-66
@@ -45,7 +45,15 @@ func (n *FsNode) FileName() string {
|
|||||||
// ==================== FsNode definition ====================
|
// ==================== FsNode definition ====================
|
||||||
type FsTree struct {
|
type FsTree struct {
|
||||||
root *FsNode
|
root *FsNode
|
||||||
selected *FsNode // currently selected node
|
selected int
|
||||||
|
lines []*FsNode // flattened view of nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *FsTree) selectedNode() *FsNode {
|
||||||
|
if t.selected <= 0 || t.selected >= len(t.lines) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.lines[t.selected]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFsTree(rootPath string) *FsTree {
|
func NewFsTree(rootPath string) *FsTree {
|
||||||
@@ -55,16 +63,19 @@ func NewFsTree(rootPath string) *FsTree {
|
|||||||
children: make([]*FsNode, 0),
|
children: make([]*FsNode, 0),
|
||||||
expanded: true,
|
expanded: true,
|
||||||
}
|
}
|
||||||
walkFileSystemAndBuildTree(rootPath, root)
|
flatTree := make([]*FsNode, 0)
|
||||||
|
walkFileSystemAndBuildTree(rootPath, root, &flatTree)
|
||||||
|
|
||||||
if len(root.children) > 0 {
|
if len(root.children) > 0 {
|
||||||
return &FsTree{
|
return &FsTree{
|
||||||
root: root,
|
root: root,
|
||||||
selected: root.children[0],
|
selected: 1, // first child
|
||||||
|
lines: flatTree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &FsTree{
|
return &FsTree{
|
||||||
root: root,
|
root: root,
|
||||||
|
lines: flatTree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,96 +127,55 @@ func (t *FsTree) ToggleExpand(node *FsNode) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point I think there HAS to be a simpler way to do this
|
|
||||||
// but since I'm going to refactor it some day let's leave this
|
|
||||||
// as it is
|
|
||||||
func (t *FsTree) move(delta int) error {
|
func (t *FsTree) move(delta int) error {
|
||||||
if t.selected == nil {
|
if t.selectedNode() == 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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// base case: if it's a folder and expanded and going down
|
newIndex := t.selected + delta
|
||||||
if delta == 1 && t.selected.nodeType == FolderNode && t.selected.expanded && len(t.selected.children) > 0 {
|
if newIndex < 1 || newIndex >= len(t.lines) {
|
||||||
t.selected = t.selected.children[0]
|
return nil // noop
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// base case: if it's the first element of a folder and going up
|
|
||||||
if delta == -1 && t.selected.parent != t.root {
|
|
||||||
parent := t.selected.parent
|
|
||||||
if len(parent.children) > 0 && parent.children[0] == t.selected {
|
|
||||||
t.selected = parent
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// same level sibling move
|
|
||||||
// the idea is really simple for now:
|
|
||||||
// - go up the parent and find the sibling in the direction
|
|
||||||
// - if no sibling go up again
|
|
||||||
// - if no parent stop
|
|
||||||
siblingFor := t.selected
|
|
||||||
for siblingFor.parent != nil {
|
|
||||||
parent := siblingFor.parent
|
|
||||||
// O(n) for now, though easy to obtimize with index tracking
|
|
||||||
nextIdx := -1
|
|
||||||
for idx, child := range parent.children {
|
|
||||||
if child == siblingFor {
|
|
||||||
nextIdx = idx + delta
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextIdx >= 0 && nextIdx < len(parent.children) {
|
|
||||||
// found case
|
|
||||||
t.selected = parent.children[nextIdx]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// not found case, go up again
|
|
||||||
siblingFor = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// case: folder and I'm going up, I want to go as deep and last
|
|
||||||
if delta == -1 && t.selected.nodeType == FolderNode {
|
|
||||||
current := t.selected
|
|
||||||
for {
|
|
||||||
if len(current.children) == 0 || !current.expanded {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
current = current.children[len(current.children)-1]
|
|
||||||
}
|
|
||||||
t.selected = current
|
|
||||||
}
|
}
|
||||||
|
t.selected = 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.selected = line
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *FsTree) ToggleSelectedExpand() error {
|
func (t *FsTree) ToggleSelectedExpand() error {
|
||||||
if t.selected == nil {
|
if t.selectedNode() == nil {
|
||||||
return errors.New("no node is currently selected")
|
return errors.New("no node is currently selected")
|
||||||
}
|
}
|
||||||
if t.selected.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.selected)
|
t.ToggleExpand(t.selectedNode())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *FsTree) GetSelectedContent() (string, error) {
|
func (t *FsTree) GetSelectedContent() (string, error) {
|
||||||
if t.selected == nil {
|
if t.selectedNode() == nil {
|
||||||
return "", errors.New("no node is currently selected")
|
return "", errors.New("no node is currently selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.selected.nodeType != FileNode {
|
if t.selectedNode().nodeType != FileNode {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBytes, err := os.ReadFile(t.selected.path)
|
contentBytes, err := os.ReadFile(t.selectedNode().path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -239,9 +209,9 @@ func (t *FsTree) renderNode(node *FsNode, depth int, builder *strings.Builder) {
|
|||||||
icon = lipgloss.NewStyle().Faint(true).Render(prevIndent + icon + indent)
|
icon = lipgloss.NewStyle().Faint(true).Render(prevIndent + icon + indent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply highlight if selected
|
// highlight if selected
|
||||||
fileName := node.FileName()
|
fileName := node.FileName()
|
||||||
if node == t.selected {
|
if node == t.selectedNode() {
|
||||||
fileName = lipgloss.NewStyle().Foreground(styles.Highlight).Bold(true).Render(fileName)
|
fileName = lipgloss.NewStyle().Foreground(styles.Highlight).Bold(true).Render(fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.spinner, cmd = m.spinner.Update(msg)
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
case tea.MouseMsg:
|
||||||
|
if msg.Type == tea.MouseLeft {
|
||||||
|
// within file tree
|
||||||
|
if msg.X < m.width {
|
||||||
|
if err := m.tree.SelectNodeAtLine(msg.Y); err == nil {
|
||||||
|
content, err := m.tree.GetSelectedContent()
|
||||||
|
if err == nil {
|
||||||
|
m.viewport.SetContent(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "q", "ctrl+c":
|
case "q", "ctrl+c":
|
||||||
@@ -161,6 +173,7 @@ func main() {
|
|||||||
p := tea.NewProgram(
|
p := tea.NewProgram(
|
||||||
createModel(),
|
createModel(),
|
||||||
tea.WithAltScreen(), // full screen tui
|
tea.WithAltScreen(), // full screen tui
|
||||||
|
tea.WithMouseCellMotion(),
|
||||||
)
|
)
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
fmt.Printf("Error: %v", err)
|
fmt.Printf("Error: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user