Skip to content

Commit ec038b1

Browse files
JordanCoinclaude
andcommitted
Add --search flag for cross-file section search
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c97f9c5 commit ec038b1

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func main() {
6565
// Parse flags
6666
var sectionFilter string
6767
var expandSection string
68+
var searchQuery string
6869
var showRefs bool
6970
var jsonMode bool
7071
for i := 2; i < len(os.Args); i++ {
@@ -79,6 +80,11 @@ func main() {
7980
expandSection = os.Args[i+1]
8081
i++
8182
}
83+
case "--search":
84+
if i+1 < len(os.Args) {
85+
searchQuery = os.Args[i+1]
86+
i++
87+
}
8288
case "--refs", "-r":
8389
showRefs = true
8490
case "--json", "-j":
@@ -103,6 +109,8 @@ func main() {
103109
if jsonMode {
104110
absPath, _ := filepath.Abs(target)
105111
outputJSON(docs, absPath)
112+
} else if searchQuery != "" {
113+
render.SearchResults(docs, searchQuery)
106114
} else if showRefs {
107115
render.RefsTree(docs, target)
108116
} else {
@@ -149,6 +157,8 @@ func main() {
149157
if jsonMode {
150158
absPath, _ := filepath.Abs(target)
151159
outputJSON([]*parser.Document{doc}, absPath)
160+
} else if searchQuery != "" {
161+
render.SearchResults([]*parser.Document{doc}, searchQuery)
152162
} else if expandSection != "" {
153163
render.ExpandSection(doc, expandSection)
154164
} else if sectionFilter != "" {
@@ -282,8 +292,10 @@ Examples:
282292
docmap README.md --section "API" # Filter to section
283293
docmap README.md --expand "API" # Show section content
284294
docmap . --refs # Show cross-references between docs
295+
docmap docs/ --search "auth" # Search across all files
285296
286297
Flags:
298+
--search <query> Search sections across all files
287299
-s, --section <name> Filter to a specific section
288300
-e, --expand <name> Show full content of a section
289301
-r, --refs Show cross-references between markdown files

render/tree.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,92 @@ func getTopSections(sections []*parser.Section, maxDepth int) []*parser.Section
272272
return result
273273
}
274274

275+
// SearchResult holds a matched section with its file context
276+
type SearchResult struct {
277+
Filename string
278+
Path string // e.g. "endpoints > Ban Member"
279+
Section *parser.Section
280+
}
281+
282+
// SearchResults searches all docs for sections matching the query and renders results
283+
func SearchResults(docs []*parser.Document, query string) {
284+
query = strings.ToLower(query)
285+
var results []SearchResult
286+
287+
for _, doc := range docs {
288+
searchSections(doc.Filename, doc.Sections, "", query, &results)
289+
}
290+
291+
if len(results) == 0 {
292+
fmt.Printf("No sections matching '%s'\n", query)
293+
return
294+
}
295+
296+
// Header
297+
fmt.Printf("%s%d matches for '%s'%s\n\n", bold, len(results), query, reset)
298+
299+
for i, r := range results {
300+
isLast := i == len(results)-1
301+
connector := "├── "
302+
if isLast {
303+
connector = "└── "
304+
}
305+
306+
tokenStr := dim + fmt.Sprintf("(%s)", formatTokens(r.Section.Tokens)) + reset
307+
filePart := dim + r.Filename + " > " + reset
308+
if r.Path != "" {
309+
filePart = dim + r.Filename + " > " + r.Path + " > " + reset
310+
}
311+
fmt.Printf("%s%s%s%s%s%s %s\n", dim, connector, reset, filePart, bold+cyan+r.Section.Title+reset, "", tokenStr)
312+
313+
// Show children summary if present
314+
if len(r.Section.Children) > 0 {
315+
childPrefix := "│ "
316+
if isLast {
317+
childPrefix = " "
318+
}
319+
for j, child := range r.Section.Children {
320+
if j >= 5 {
321+
fmt.Printf("%s%s... %d more%s\n", childPrefix, dim, len(r.Section.Children)-5, reset)
322+
break
323+
}
324+
childIsLast := j == len(r.Section.Children)-1 || j == 4
325+
childConn := "├─ "
326+
if childIsLast {
327+
childConn = "└─ "
328+
}
329+
fmt.Printf("%s%s%s%s %s\n", childPrefix, dim, childConn, child.Title+reset, dim+fmt.Sprintf("(%s)", formatTokens(child.Tokens))+reset)
330+
}
331+
}
332+
}
333+
334+
fmt.Println()
335+
}
336+
337+
func searchSections(filename string, sections []*parser.Section, parentPath string, query string, results *[]SearchResult) {
338+
for _, s := range sections {
339+
titleMatch := strings.Contains(strings.ToLower(s.Title), query)
340+
contentMatch := strings.Contains(strings.ToLower(s.Content), query)
341+
342+
if titleMatch || contentMatch {
343+
*results = append(*results, SearchResult{
344+
Filename: filename,
345+
Path: parentPath,
346+
Section: s,
347+
})
348+
}
349+
350+
// Search children
351+
childPath := parentPath
352+
if childPath != "" {
353+
childPath += " > " + s.Title
354+
} else {
355+
childPath = s.Title
356+
}
357+
searchSections(filename, s.Children, childPath, query, results)
358+
}
359+
}
360+
275361
// RefsTree renders document references (links to other .md files)
276362
func RefsTree(docs []*parser.Document, dirName string) {
277363
// Build reference graph

0 commit comments

Comments
 (0)