unfiltered walker tree

This commit is contained in:
2026-09-15 11:16:27 +01:00
parent 6c365e9cde
commit 4396a6f619
4 changed files with 252 additions and 175 deletions
+53 -107
View File
@@ -52,11 +52,15 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
}
if !singleFile {
root := tree.Build(result.Entries)
treeRoot, err := tree.BuildFullTree(result.AbsRoot, result.Cfg)
if err != nil {
return fmt.Errorf("cannot build tree: %w", err)
}
rootName := filepath.Base(result.AbsRoot)
sb.WriteString("## Directory Tree\n\n")
sb.WriteString(fenceChars + "\n")
sb.WriteString(renderTreeForLLM(root, rootName))
sb.WriteString(renderTreeWithLegend(treeRoot, rootName))
sb.WriteString(fenceChars + "\n\n")
}
@@ -117,13 +121,9 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
}
}
var generated []string
indexBasename := "index_" + repoName + ".md"
rootPath := filepath.Join(outputDir, indexBasename)
generated = append(generated, rootPath)
if err := writeRootSlice(rootPath, absRoot, rootName, repoName, rootFiles, topLevelDirs, result.OmitTest, result.Commit, result.Instructions); err != nil {
return nil, fmt.Errorf("failed to write root slice: %w", err)
treeRoot, err := tree.BuildFullTree(absRoot, result.Cfg)
if err != nil {
return nil, fmt.Errorf("cannot build full tree: %w", err)
}
var dirNames []string
@@ -132,6 +132,21 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
}
sort.Strings(dirNames)
for _, dirName := range dirNames {
if node, ok := treeRoot.Children[dirName]; ok {
node.Ref = filepath.Join(dirName, dirName+"_"+repoName+".md")
}
}
var generated []string
indexBasename := "index_" + repoName + ".md"
rootPath := filepath.Join(outputDir, indexBasename)
generated = append(generated, rootPath)
if err := writeRootSlice(rootPath, absRoot, rootName, repoName, rootFiles, topLevelDirs, result.OmitTest, result.Commit, result.Instructions, treeRoot); err != nil {
return nil, fmt.Errorf("failed to write root slice: %w", err)
}
for _, dirName := range dirNames {
sliceName := strings.TrimSuffix(dirName, "/") + "_" + repoName + ".md"
sliceDir := filepath.Join(outputDir, dirName)
@@ -144,17 +159,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
absDir := filepath.Join(absRoot, dirName)
dirEntries := topLevelDirs[dirName]
treeEntries := make([]walker.Entry, len(dirEntries))
for i, e := range dirEntries {
sepIdx := strings.Index(e.RelPath, string(filepath.Separator))
if sepIdx >= 0 {
treeEntries[i] = walker.Entry{RelPath: e.RelPath[sepIdx+1:], Size: e.Size}
} else {
treeEntries[i] = e
}
}
if err := writeDirSlice(slicePath, absDir, absRoot, dirName, treeEntries, dirEntries, indexBasename, result.OmitTest, result.Commit); err != nil {
if err := writeDirSlice(slicePath, absDir, absRoot, dirName, dirEntries, indexBasename, result.OmitTest, result.Commit, treeRoot); err != nil {
return nil, fmt.Errorf("failed to write slice %s: %w", sliceName, err)
}
}
@@ -162,7 +167,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
return generated, nil
}
func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, omitTest bool, commit *git.CommitInfo, instContent string) error {
func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, omitTest bool, commit *git.CommitInfo, instContent string, treeRoot *tree.Node) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("cannot create root file: %w", err)
@@ -179,10 +184,9 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
sb.WriteString("\n")
}
treeLines := buildTreeWithRefs(rootName, repoName, topLevelDirs, rootFiles)
sb.WriteString("## Directory Tree\n\n")
sb.WriteString(fenceChars + "\n")
sb.WriteString(treeLines)
sb.WriteString(renderTreeWithLegend(treeRoot, rootName))
sb.WriteString(fenceChars + "\n\n")
sb.WriteString("## Vertical Slices Index\n\n")
@@ -238,7 +242,7 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
return err
}
func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntries []walker.Entry, rootFileName string, omitTest bool, commit *git.CommitInfo) error {
func writeDirSlice(path, absDir, absRoot, dirName string, fileEntries []walker.Entry, rootFileName string, omitTest bool, commit *git.CommitInfo, treeRoot *tree.Node) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("cannot create slice file: %w", err)
@@ -254,8 +258,7 @@ func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntri
sb.WriteString("## Directory Tree\n\n")
sb.WriteString(fenceChars + "\n")
sliceTree := buildTreeFromEntries(dirName, treeEntries)
sb.WriteString(sliceTree)
sb.WriteString(renderTreeWithLegend(treeRoot, filepath.Base(absRoot)))
sb.WriteString(fenceChars + "\n\n")
if len(fileEntries) > 0 {
@@ -287,93 +290,16 @@ func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntri
return err
}
func buildTreeWithRefs(rootName, repoName string, topLevelDirs map[string][]walker.Entry, rootFiles []walker.Entry) string {
var sb strings.Builder
sb.WriteString(rootName)
sb.WriteString("/\n")
var dirNames []string
for k := range topLevelDirs {
dirNames = append(dirNames, k)
}
sort.Strings(dirNames)
sort.Slice(rootFiles, func(i, j int) bool {
return rootFiles[i].RelPath < rootFiles[j].RelPath
})
type treeItem struct {
name string
isDir bool
sliceRef string
}
items := make([]treeItem, 0, len(dirNames)+len(rootFiles))
for _, d := range dirNames {
sliceRef := filepath.Join(d, strings.TrimSuffix(d, "/")+"_"+repoName+".md")
items = append(items, treeItem{
name: d,
isDir: true,
sliceRef: sliceRef,
})
}
for _, rf := range rootFiles {
items = append(items, treeItem{
name: rf.RelPath,
isDir: false,
})
}
sort.Slice(items, func(i, j int) bool {
return items[i].name < items[j].name
})
for _, item := range items {
sb.WriteString("+- ")
sb.WriteString(item.name)
if item.isDir {
sb.WriteString("/")
sb.WriteString(" -> see ")
sb.WriteString(item.sliceRef)
}
sb.WriteString("\n")
}
return sb.String()
}
func buildTreeFromEntries(rootName string, entries []walker.Entry) string {
root := &tree.Node{IsDir: true, Children: make(map[string]*tree.Node)}
for _, e := range entries {
parts := strings.Split(e.RelPath, string(filepath.Separator))
cur := root
for i, part := range parts {
isLeaf := i == len(parts)-1
if _, ok := cur.Children[part]; !ok {
node := &tree.Node{Name: part, IsDir: !isLeaf}
if !isLeaf {
node.Children = make(map[string]*tree.Node)
}
cur.Children[part] = node
}
cur = cur.Children[part]
}
}
func renderTreeWithLegend(root *tree.Node, rootName string) string {
var sb strings.Builder
sb.WriteString(rootName)
sb.WriteString("/\n")
renderNodeLLM(&sb, root, "")
return sb.String()
if hasAnyAnnotation(root) {
sb.WriteString("Legend: ~ ignored | ~ tests omitted | ~ omitted | ~ exceeded --max-depth\n")
}
func renderTreeForLLM(root *tree.Node, rootName string) string {
var sb strings.Builder
sb.WriteString(rootName)
sb.WriteString("/\n")
renderNodeLLM(&sb, root, "")
return sb.String()
}
@@ -395,6 +321,14 @@ func renderNodeLLM(sb *strings.Builder, node *tree.Node, prefix string) {
if child.IsDir {
sb.WriteString("/")
}
if child.Annotation != "" {
sb.WriteString(" ~ ")
sb.WriteString(child.Annotation)
}
if child.Ref != "" {
sb.WriteString(" -> see ")
sb.WriteString(child.Ref)
}
sb.WriteString("\n")
if child.IsDir && child.Children != nil {
@@ -409,6 +343,18 @@ func renderNodeLLM(sb *strings.Builder, node *tree.Node, prefix string) {
}
}
func hasAnyAnnotation(node *tree.Node) bool {
if node.Annotation != "" {
return true
}
for _, child := range node.Children {
if hasAnyAnnotation(child) {
return true
}
}
return false
}
func readFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
+20 -60
View File
@@ -1,76 +1,36 @@
package tree
import (
"sort"
"strings"
"git.flo.fo/FLO/dirmd/internal/walker"
)
type Node struct {
Name string
IsDir bool
Children map[string]*Node
Annotation string
Ref string
}
func Build(entries []walker.Entry) *Node {
root := &Node{IsDir: true, Children: make(map[string]*Node)}
func splitPath(relPath string) []string {
var parts []string
start := 0
for i := 0; i <= len(relPath); i++ {
if i == len(relPath) || relPath[i] == '/' || relPath[i] == '\\' {
if i > start {
parts = append(parts, relPath[start:i])
}
start = i + 1
}
}
return parts
}
for _, e := range entries {
parts := strings.Split(e.RelPath, "/")
func addPath(root *Node, rel string, isDir bool) *Node {
parts := splitPath(rel)
cur := root
for i, part := range parts {
isLeaf := i == len(parts)-1
if _, ok := cur.Children[part]; !ok {
cur.Children[part] = &Node{Name: part, IsDir: !isLeaf, Children: make(map[string]*Node)}
cur.Children[part] = &Node{Name: part, IsDir: i < len(parts)-1, Children: make(map[string]*Node)}
}
cur = cur.Children[part]
}
}
return root
}
func Render(root *Node, rootName string) string {
var sb strings.Builder
sb.WriteString(rootName)
sb.WriteString("/\n")
renderNode(&sb, root, "")
return sb.String()
}
func renderNode(sb *strings.Builder, node *Node, prefix string) {
keys := make([]string, 0, len(node.Children))
for k := range node.Children {
keys = append(keys, k)
}
sort.Strings(keys)
for i, key := range keys {
child := node.Children[key]
isLast := i == len(keys)-1
if isLast {
sb.WriteString(prefix + "└── ")
} else {
sb.WriteString(prefix + "├── ")
}
sb.WriteString(child.Name)
if child.IsDir {
sb.WriteString("/")
}
sb.WriteString("\n")
if child.IsDir {
newPrefix := prefix
if isLast {
newPrefix += " "
} else {
newPrefix += "│ "
}
renderNode(sb, child, newPrefix)
}
}
cur.IsDir = isDir
return cur
}
+170
View File
@@ -0,0 +1,170 @@
package tree
import (
"fmt"
"io/fs"
"path/filepath"
"strings"
"git.flo.fo/FLO/dirmd/internal/config"
"git.flo.fo/FLO/dirmd/internal/filter"
)
func BuildFullTree(absRoot string, cfg *config.Config) (*Node, error) {
root := &Node{IsDir: true, Children: make(map[string]*Node)}
err := filepath.WalkDir(absRoot, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
rel, rerr := filepath.Rel(absRoot, path)
if rerr != nil || rel == "." {
return nil
}
if filter.IsSymlink(path) {
return nil
}
depth := strings.Count(rel, string(filepath.Separator)) + 1
if depth > cfg.MaxDepth {
node := addPath(root, rel, d.IsDir())
node.Annotation = "exceeded --max-depth"
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
name := filepath.Base(path)
if d.IsDir() {
if isIgnoredName(name, cfg.Ignores) {
node := addPath(root, rel, true)
node.Annotation = "ignored (--ignore)"
return filepath.SkipDir
}
if cfg.OmitTest && isTestDir(rel) {
node := addPath(root, rel, true)
node.Annotation = "tests omitted (--omit-test)"
return filepath.SkipDir
}
addPath(root, rel, true)
return nil
}
if annotation, ok := fileAnnotation(rel, name, cfg); ok {
node := addPath(root, rel, false)
node.Annotation = annotation
return nil
}
addPath(root, rel, false)
return nil
})
if err != nil {
return nil, fmt.Errorf("cannot walk %s: %w", absRoot, err)
}
return root, nil
}
func isIgnoredName(name string, ignores []string) bool {
if strings.HasPrefix(name, ".") {
return true
}
for _, pattern := range ignores {
if matched, _ := filepath.Match(pattern, name); matched {
return true
}
}
return false
}
func isTestDir(rel string) bool {
slashPath := filepath.ToSlash(rel)
return strings.Contains(slashPath, "/tests/") || strings.HasPrefix(slashPath, "tests/")
}
func fileAnnotation(rel, name string, cfg *config.Config) (string, bool) {
if strings.HasPrefix(name, ".") {
return "", false
}
for _, pattern := range cfg.Ignores {
if matched, _ := filepath.Match(pattern, name); matched {
return "ignored (--ignore)", true
}
}
if cfg.OmitTest && isTestFileName(rel, name) {
return "tests omitted (--omit-test)", true
}
if cfg.OmitMd && isNonReadmeMarkdown(name) {
return "omitted (--omit-md)", true
}
if cfg.FrontendOnly && !isFrontendOrReadme(rel, name) {
return "omitted (--frontend-only)", true
}
if cfg.SkipFrontend && isFrontendExt(rel) {
return "omitted (--skip-frontend)", true
}
return "", false
}
func isTestFileName(rel, name string) bool {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".go":
return strings.HasSuffix(strings.ToLower(name), "_test.go")
case ".java":
base := strings.TrimSuffix(name, filepath.Ext(name))
if strings.HasSuffix(base, "Test") || strings.HasSuffix(base, "Tests") {
return true
}
slashPath := filepath.ToSlash(rel)
return strings.Contains(slashPath, "/src/test/")
case ".py":
lower := strings.ToLower(name)
if strings.HasPrefix(lower, "test_") || strings.HasSuffix(lower, "_test.py") {
return true
}
if strings.EqualFold(name, "conftest.py") {
return true
}
return isTestDir(rel)
}
return false
}
func isNonReadmeMarkdown(name string) bool {
ext := strings.ToLower(filepath.Ext(name))
if ext != ".md" && ext != ".markdown" {
return false
}
return !strings.EqualFold(name, "README.md")
}
func isFrontendOrReadme(rel, name string) bool {
if isFrontendExt(rel) {
return true
}
return strings.EqualFold(name, "README.md")
}
func isFrontendExt(rel string) bool {
ext := strings.ToLower(filepath.Ext(rel))
for _, fe := range frontendExts {
if strings.EqualFold(ext, fe) {
return true
}
}
return false
}
var frontendExts = []string{
".html", ".htm", ".gohtml", ".tmpl",
".css", ".scss", ".sass", ".less",
".js", ".jsx", ".mjs", ".cjs",
".ts", ".tsx",
".vue", ".svelte",
}
+3 -2
View File
@@ -23,6 +23,7 @@ type Result struct {
AbsRoot string
SingleFile bool
OmitTest bool
Cfg *config.Config
Commit *git.CommitInfo
Instructions string
}
@@ -67,7 +68,7 @@ func processSingleFile(cfg *config.Config) (*Result, error) {
Entries: []Entry{{RelPath: filepath.Base(cfg.AbsRoot), Size: info.Size()}},
AbsRoot: filepath.Dir(cfg.AbsRoot),
SingleFile: true,
OmitTest: cfg.OmitTest,
Cfg: cfg,
Commit: commitInfo,
Instructions: instContent,
}, nil
@@ -83,7 +84,7 @@ func walkDirectory(cfg *config.Config) (*Result, error) {
OmitMd: cfg.OmitMd,
}
result := &Result{AbsRoot: cfg.AbsRoot, OmitTest: cfg.OmitTest}
result := &Result{AbsRoot: cfg.AbsRoot, Cfg: cfg, OmitTest: cfg.OmitTest}
result.Commit = git.GetCommitInfo(cfg.AbsRoot)