Files
2026-07-13 13:33:09 +08:00

529 lines
13 KiB
Go

package file_map
import (
"context"
"fmt"
"os"
"plandex-server/syntax"
"strings"
shared "plandex-shared"
tree_sitter "github.com/smacker/go-tree-sitter"
)
var verboseLogging = os.Getenv("VERBOSE_LOGGING") == "true"
// FileMap represents a file's important definitions
type FileMap struct {
Definitions []Definition
}
type Definition struct {
Type string // "function", "class", "key", "selector", "instruction" etc
Signature string // The full signature/header without implementation
Comments []string // Any comments that precede this definition
TagAttrs []string // For xml style markup tags, the class and id attributes
TagReps int // For tags, the number of times this tag is repeated
Line int // Line number where definition starts
Children []Definition // For parent types that can contain nested definitions
}
type Node struct {
Type string
Lang shared.Language
TsNode *tree_sitter.Node
Bytes []byte
}
func MapFile(ctx context.Context, filename string, content []byte) (*FileMap, error) {
if !shared.HasFileMapSupport(filename) {
// return nil, fmt.Errorf("unsupported file type: %s", filename)
return &FileMap{
Definitions: []Definition{
{
Type: "no_map",
Signature: "[NO MAP]",
},
},
}, nil
}
lang := syntax.GetLanguageForPath(filename)
if lang == "" {
// return nil, fmt.Errorf("unsupported file type: %s", ext)
return &FileMap{
Definitions: []Definition{},
}, nil
}
if !shared.IsTreeSitterLanguage(lang) {
switch lang {
case shared.LanguageMarkdown:
return &FileMap{
Definitions: mapMarkdownSimple(content),
}, nil
default:
// return nil, fmt.Errorf("unsupported file type: %s", ext)
return &FileMap{
Definitions: []Definition{},
}, nil
}
}
// Get appropriate parser
var parser *tree_sitter.Parser
var fallbackParser *tree_sitter.Parser
var fallbackLang shared.Language
parser, lang, fallbackParser, fallbackLang = syntax.GetParserForPath(filename)
if parser == nil && fallbackParser == nil {
// return nil, fmt.Errorf("unsupported file type: %s", ext)
return &FileMap{
Definitions: []Definition{},
}, nil
}
if parser != nil {
defer parser.Close()
}
if fallbackParser != nil {
defer fallbackParser.Close()
}
var tree *tree_sitter.Tree
var err error
if parser != nil {
// Parse file
tree, err = parser.ParseCtx(ctx, nil, content)
if err != nil {
// return nil, fmt.Errorf("failed to parse file: %v", err)
return &FileMap{
Definitions: []Definition{},
}, nil
}
defer tree.Close()
}
if tree == nil || tree.RootNode().Type() == "error" {
fallbackTree, err := fallbackParser.ParseCtx(ctx, nil, content)
if err != nil {
// return nil, fmt.Errorf("failed to parse file: %v", err)
return &FileMap{
Definitions: []Definition{},
}, nil
}
defer fallbackTree.Close()
if fallbackTree.RootNode().Type() != "error" {
return &FileMap{
Definitions: mapNode(fallbackTree.RootNode(), content, fallbackLang),
}, nil
}
}
return &FileMap{
Definitions: mapNode(tree.RootNode(), content, lang),
}, nil
}
func mapNode(node *tree_sitter.Node, content []byte, lang shared.Language) []Definition {
switch lang {
case shared.LanguageHtml:
return mapMarkup(content)
case shared.LanguageSvelte:
return mapSvelte(content)
default:
return mapTraditional(Node{
Lang: lang,
TsNode: node,
Bytes: content,
}, nil)
}
}
// For traditional programming languages
func mapTraditional(baseNode Node, parentNode *Node) []Definition {
var defs []Definition
cursor := tree_sitter.NewTreeCursor(baseNode.TsNode)
defer cursor.Close()
// potentially too much output even for verbose logging — uncomment if you need to see the full tree
// if verboseLogging {
// fmt.Println("mapTraditional", baseNode.TsNode)
// }
if cursor.GoToFirstChild() {
for {
tsNode := cursor.CurrentNode()
node := Node{
Type: tsNode.Type(),
Lang: baseNode.Lang,
TsNode: tsNode,
Bytes: baseNode.Bytes,
}
if isIncludeAndContinueNode(node) {
if verboseLogging {
fmt.Println("include and continue node", cursor.CurrentNode().Type())
}
if !cursor.GoToNextSibling() {
break
}
continue
}
if verboseLogging {
fmt.Println()
fmt.Println("node", node.Type)
// fmt.Println("content", string(node.Content(content)))
fmt.Println()
}
// Check if this is a definition node
if isDefinitionNode(node, parentNode) {
if verboseLogging {
fmt.Println("definition node", node.Type)
}
def := Definition{
Type: node.Type,
Line: int(tsNode.StartPoint().Row) + 1,
}
if isAssignmentNode(node) {
if verboseLogging {
fmt.Println("assignment node", node.Type)
}
// Try different field names for identifiers
// fmt.Printf("assignment node: %s\n", node.Type)
sig := ""
assignmentBoundary := findAssignmentBoundary(node)
if assignmentBoundary != nil {
start := tsNode.StartByte()
end := assignmentBoundary.TsNode.StartByte()
sig = string(node.Bytes[start:end])
sig = strings.TrimSuffix(strings.TrimSpace(sig), "=")
} else {
identifiers := findIdentifier(node)
if len(identifiers) > 0 {
if verboseLogging {
fmt.Println("found identifiers", len(identifiers))
}
start := tsNode.StartByte()
end := identifiers[len(identifiers)-1].TsNode.EndByte()
sig = string(node.Bytes[start:end])
} else {
if verboseLogging {
fmt.Println("no identifier found", node.Type)
}
sig = string(node.TsNode.Content(node.Bytes)) + " "
}
}
def.Signature = sig
} else if isPassThroughParentNode(node) {
if verboseLogging {
fmt.Println("pass through parent node", node.Type)
}
start := tsNode.StartByte()
firstChild := firstDefinitionChild(node)
if firstChild != nil {
if verboseLogging {
fmt.Println("firstChild", firstChild.Type)
}
end := firstChild.TsNode.StartByte()
sig := string(node.Bytes[start:end])
sig = strings.TrimSpace(sig)
if verboseLogging {
fmt.Println("got pass through parent signature", def.Signature)
fmt.Println("recursing into first child", firstChild.Type)
}
children := mapTraditional(node, nil)
if sig == "" {
// collapse if signature is empty
if len(children) > 0 {
sig = children[0].Signature
grandchildren := children[0].Children
sibs := children[1:]
children = append(grandchildren, sibs...)
}
}
def.Signature = sig
def.Children = children
} else {
if verboseLogging {
fmt.Println("no first child found", node.Type)
}
}
} else {
if verboseLogging {
fmt.Println("not assignment node", node.Type)
fmt.Println("looking for implementation boundary")
}
// Get signature (up to body)
if body := findImplementationBoundary(node); body != nil {
if verboseLogging {
fmt.Println("found implementation boundary", body.Type)
}
start := tsNode.StartByte()
var end uint32
if tsNode == body.TsNode {
if verboseLogging {
fmt.Println("node == body")
}
firstChild := firstDefinitionChild(*body)
if firstChild != nil {
if verboseLogging {
fmt.Println("firstChild != nil")
fmt.Println("firstChild", firstChild.Type)
}
end = firstChild.TsNode.StartByte()
} else {
if verboseLogging {
fmt.Println("firstChild == nil")
}
end = body.TsNode.EndByte()
}
} else {
end = body.TsNode.StartByte()
}
if verboseLogging {
fmt.Println("start", start)
fmt.Println("end", end)
}
def.Signature = string(node.Bytes[start:end])
if verboseLogging {
fmt.Println("got signature", def.Signature)
}
// If this is a parent type node, recurse into the body
if isParentNode(node) {
if verboseLogging {
fmt.Println("isParentNode, recursing into body", node.Type)
}
def.Children = mapTraditional(*body, &node)
}
} else {
if verboseLogging {
fmt.Println("no implementation boundary found", node.Type)
}
def.Signature = string(node.TsNode.Content(node.Bytes))
}
}
// Get preceding comments
// no comments for now to minimize tokens
// def.Comments = getPrecedingComments(node)
defs = append(defs, def)
} else {
if verboseLogging {
fmt.Println("not definition node", node.Type)
}
}
if !cursor.GoToNextSibling() {
break
}
}
}
return defs
}
// // Get preceding comments
// func getPrecedingComments(node Node) []string {
// var comments []string
// const maxCommentLength = 1000
// prevNode := node.TsNode.PrevSibling()
// for prevNode != nil {
// if !strings.Contains(prevNode.Type(), "comment") {
// break
// }
// comment := string(prevNode.Content(node.Bytes))
// if len(comment) > maxCommentLength {
// comment = comment[:maxCommentLength] + "..."
// }
// comments = append([]string{comment}, comments...)
// prevNode = prevNode.PrevSibling()
// }
// return comments
// }
// func mapConfig(node *tree_sitter.Node, content []byte) []Definition {
// cursor := tree_sitter.NewTreeCursor(node)
// defer cursor.Close()
// var walkConfig func(*tree_sitter.Node) []Definition
// walkConfig = func(node *tree_sitter.Node) []Definition {
// var defs []Definition
// // Handle key-value pairs
// switch node.Type() {
// case "block_mapping_pair": // YAML
// if key := node.ChildByFieldName("key"); key != nil {
// def := Definition{
// Type: "key",
// Signature: string(key.Content(content)),
// Line: int(key.StartPoint().Row) + 1,
// }
// // Handle nested structures
// if val := node.ChildByFieldName("value"); val != nil {
// switch val.Type() {
// case "block_mapping": // nested YAML map
// def.Children = walkConfig(val)
// case "block_sequence": // YAML array
// // Could track sequences if needed
// }
// }
// defs = append(defs, def)
// }
// case "pair": // TOML/JSON
// // Similar pattern for TOML/JSON
// case "field": // CUE/HCL
// // Similar pattern for CUE/HCL
// }
// return defs
// }
// return walkConfig(node)
// }
func (m *FileMap) String() string {
var b strings.Builder
var writeDefinition func(def *Definition, depth int)
writeDefinition = func(def *Definition, depth int) {
if def.Type == "svelte-style" {
b.WriteString("\n")
}
// Indent
if depth > 0 {
b.WriteString(strings.Repeat(" ", depth))
b.WriteString("- ")
}
// Write signature (for tags, include attrs)
if def.Type == "tag" {
// Extract tag name from signature (it's the first word)
tagName := strings.Fields(def.Signature)[0]
// Build full representation with attrs
if len(def.TagAttrs) > 0 {
if def.TagReps > 1 {
b.WriteString(fmt.Sprintf("[%dx]", def.TagReps))
}
b.WriteString(fmt.Sprintf("%s%s", tagName, strings.Join(def.TagAttrs, "")))
} else {
b.WriteString(tagName)
}
} else {
b.WriteString(strings.TrimSpace(def.Signature))
}
b.WriteString("\n")
// Write children with increased depth
for _, child := range def.Children {
writeDefinition(&child, depth+1)
}
if def.Type == "svelte-script" {
b.WriteString("\n")
}
}
// Write all top-level definitions
for _, def := range m.Definitions {
writeDefinition(&def, 0)
}
return b.String()
}
func mapMarkdownSimple(content []byte) []Definition {
var defs []Definition
lines := strings.Split(string(content), "\n")
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
// Skip empty lines
if trimmedLine == "" {
continue
}
// Check for ATX headings (# style)
if strings.HasPrefix(trimmedLine, "#") {
heading := trimmedLine
level := 0
// Count heading level
for strings.HasPrefix(heading, "#") {
level++
heading = strings.TrimPrefix(heading, "#")
}
heading = strings.TrimSpace(heading)
// Only add if there's actual heading content
if heading != "" {
defs = append(defs, Definition{
Type: fmt.Sprintf("h%d", level),
Signature: heading,
Line: i + 1,
})
}
continue
}
// Check for Setext headings (=== or --- style)
if i > 0 && len(trimmedLine) > 0 {
// Check if line consists entirely of = or -
isAllEquals := strings.TrimSpace(strings.ReplaceAll(trimmedLine, "=", "")) == ""
isAllDashes := strings.TrimSpace(strings.ReplaceAll(trimmedLine, "-", "")) == ""
// Must have at least 2 characters and previous line must not be empty
prevLine := strings.TrimSpace(lines[i-1])
if len(trimmedLine) >= 2 && prevLine != "" {
if isAllEquals {
// Level 1 heading
defs = append(defs, Definition{
Type: "h1",
Signature: prevLine,
Line: i, // Use previous line's number
})
} else if isAllDashes {
// Level 2 heading
defs = append(defs, Definition{
Type: "h2",
Signature: prevLine,
Line: i, // Use previous line's number
})
}
}
}
}
return defs
}