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

235 lines
6.7 KiB
Go

//go:build treesitter && cgo
package builtin
import (
"fmt"
"os"
"path/filepath"
"strings"
"unsafe"
sitter "github.com/tree-sitter/go-tree-sitter"
tree_sitter_javascript "github.com/tree-sitter/tree-sitter-javascript/bindings/go"
tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go"
tree_sitter_rust "github.com/tree-sitter/tree-sitter-rust/bindings/go"
tree_sitter_typescript "github.com/tree-sitter/tree-sitter-typescript/bindings/go"
)
type codeIndexTreeSitterSpec struct {
language func() unsafe.Pointer
query string
}
func (c codeIndex) parseTreeSitter(path string) ([]codeSymbol, bool, error) {
spec, ok := codeIndexTreeSitterSpecForExt(filepath.Ext(path))
if !ok {
return nil, false, nil
}
source, err := os.ReadFile(path)
if err != nil {
return nil, true, err
}
language := sitter.NewLanguage(spec.language())
parser := sitter.NewParser()
defer parser.Close()
if err := parser.SetLanguage(language); err != nil {
return nil, true, err
}
tree := parser.Parse(source, nil)
if tree == nil {
return nil, true, fmt.Errorf("tree-sitter parse returned nil")
}
defer tree.Close()
query, qerr := sitter.NewQuery(language, spec.query)
if qerr != nil {
return nil, true, qerr
}
defer query.Close()
cursor := sitter.NewQueryCursor()
defer cursor.Close()
cursor.SetTimeoutMicros(50_000)
captureNames := query.CaptureNames()
matches := cursor.Matches(query, tree.RootNode(), source)
var out []codeSymbol
for match := matches.Next(); match != nil; match = matches.Next() {
var nameNode *sitter.Node
var symbolNode *sitter.Node
var captureKind string
for _, capture := range match.Captures {
captureName := captureNames[capture.Index]
kind, role, ok := splitCodeIndexTreeSitterCapture(captureName)
if !ok {
continue
}
switch role {
case "name":
captureKind = kind
node := capture.Node
nameNode = &node
case "symbol":
if captureKind == "" {
captureKind = kind
}
node := capture.Node
symbolNode = &node
}
}
if nameNode == nil || captureKind == "" {
continue
}
if symbolNode == nil {
symbolNode = nameNode
}
name := strings.TrimSpace(nameNode.Utf8Text(source))
if name == "" {
continue
}
kind := normalizeCodeIndexKind(captureKind)
parent := treeSitterParentName(kind, symbolNode, source)
out = append(out, codeSymbol{
Name: name,
Kind: kind,
File: c.displayPath(path),
Line: int(symbolNode.StartPosition().Row) + 1,
Parent: parent,
Signature: treeSitterLine(source, int(symbolNode.StartByte())),
})
}
if cursor.DidExceedMatchLimit() {
return out, true, fmt.Errorf("tree-sitter query exceeded match limit")
}
return out, true, nil
}
func codeIndexTreeSitterEnabled() bool {
return true
}
func codeIndexTreeSitterSpecForExt(ext string) (codeIndexTreeSitterSpec, bool) {
switch ext {
case ".js", ".jsx":
return codeIndexTreeSitterSpec{language: tree_sitter_javascript.Language, query: treeSitterJavaScriptQuery}, true
case ".ts":
return codeIndexTreeSitterSpec{language: tree_sitter_typescript.LanguageTypescript, query: treeSitterTypeScriptQuery}, true
case ".tsx":
return codeIndexTreeSitterSpec{language: tree_sitter_typescript.LanguageTSX, query: treeSitterTypeScriptQuery}, true
case ".py":
return codeIndexTreeSitterSpec{language: tree_sitter_python.Language, query: treeSitterPythonQuery}, true
case ".rs":
return codeIndexTreeSitterSpec{language: tree_sitter_rust.Language, query: treeSitterRustQuery}, true
default:
return codeIndexTreeSitterSpec{}, false
}
}
func splitCodeIndexTreeSitterCapture(name string) (kind, role string, ok bool) {
before, after, found := strings.Cut(name, ".")
if !found || before == "" || after == "" {
return "", "", false
}
if after != "name" && after != "symbol" {
return "", "", false
}
return before, after, true
}
func treeSitterParentName(kind string, node *sitter.Node, source []byte) string {
if kind != "method" {
return ""
}
for parent := node.Parent(); parent != nil; parent = parent.Parent() {
switch parent.Kind() {
case "abstract_class_declaration", "class_declaration", "class":
if name := parent.ChildByFieldName("name"); name != nil {
return strings.TrimSpace(name.Utf8Text(source))
}
}
}
return ""
}
func treeSitterLine(source []byte, start int) string {
if start < 0 {
start = 0
}
if start > len(source) {
start = len(source)
}
lineStart := start
for lineStart > 0 && source[lineStart-1] != '\n' && source[lineStart-1] != '\r' {
lineStart--
}
lineEnd := start
for lineEnd < len(source) && source[lineEnd] != '\n' && source[lineEnd] != '\r' {
lineEnd++
}
return strings.TrimSpace(string(source[lineStart:lineEnd]))
}
const treeSitterJavaScriptQuery = `
(function_declaration
name: (identifier) @func.name) @func.symbol
(generator_function_declaration
name: (identifier) @func.name) @func.symbol
(class_declaration
name: (identifier) @class.name) @class.symbol
(method_definition
name: [(property_identifier) (private_property_identifier)] @method.name) @method.symbol
(lexical_declaration
(variable_declarator
name: (identifier) @func.name
value: [(arrow_function) (function_expression)])) @func.symbol
(variable_declaration
(variable_declarator
name: (identifier) @func.name
value: [(arrow_function) (function_expression)])) @func.symbol
`
const treeSitterTypeScriptQuery = `
(function_declaration
name: (identifier) @func.name) @func.symbol
(generator_function_declaration
name: (identifier) @func.name) @func.symbol
(class_declaration
name: (type_identifier) @class.name) @class.symbol
(abstract_class_declaration
name: (type_identifier) @class.name) @class.symbol
(method_definition
name: [(property_identifier) (private_property_identifier)] @method.name) @method.symbol
(interface_declaration
name: (type_identifier) @interface.name) @interface.symbol
(type_alias_declaration
name: (type_identifier) @type.name) @type.symbol
(enum_declaration
name: (identifier) @enum.name) @enum.symbol
(lexical_declaration
(variable_declarator
name: (identifier) @func.name
value: [(arrow_function) (function_expression)])) @func.symbol
(variable_declaration
(variable_declarator
name: (identifier) @func.name
value: [(arrow_function) (function_expression)])) @func.symbol
`
const treeSitterPythonQuery = `
(function_definition
name: (identifier) @func.name) @func.symbol
(class_definition
name: (identifier) @class.name) @class.symbol
`
const treeSitterRustQuery = `
(function_item
name: (identifier) @fn.name) @fn.symbol
(struct_item
name: (type_identifier) @struct.name) @struct.symbol
(enum_item
name: (type_identifier) @enum.name) @enum.symbol
(trait_item
name: (type_identifier) @trait.name) @trait.symbol
`