mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-05 22:23:50 +00:00
5ad975ee7a
* v2.0: token-optimization overhaul Additive (backward-compatible flags): - file_read: skeleton mode, strip (imports/license/block_comments), compact_line_numbers, 8-char etag with prefix-match compat - ast_query: format=verbose|compact|location, pagination cursor - file_search: cluster mode, pagination cursor - lsp_query (references): compact output Breaking (v2): - Preambles removed; opt-in verbose=true restores - edit_apply: response=count|diff|none, default count - ping tool removed - symbol_at/find_definition/find_references merged into lsp_query - Tool descriptions trimmed -83%, help moved to filepuff://help/<tool> - Batch file_read dedups by etag Protocol: - ResourceLink returned for file_read >64 KiB (force_inline override) - OnAfterInitialize hook reads capabilities.experimental.filepuff for session defaults (default_format, default_max_results, default_cluster, compact_refs, line_numbers, resource_link_threshold) * fix: drop --max-total-count from ripgrep args The flag does not exist in stable ripgrep (confirmed up to 15.1.0 -- "unrecognized flag --max-total-count, similar flags that are available: --max-count"). Every file_search call failed on hosts with stock rg. --max-count is per-file, not a drop-in replacement, so rely on the in-process truncation in parseOutput that was already the documented safety net.
346 lines
9.3 KiB
Go
346 lines
9.3 KiB
Go
// Package parser provides skeleton rendering for source files.
|
|
package parser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
|
|
sitter "github.com/smacker/go-tree-sitter"
|
|
)
|
|
|
|
// SkeletonFile returns a skeleton representation of the file: top-level declarations
|
|
// with signatures and doc-comments intact, but function/method bodies replaced with
|
|
// a language-appropriate placeholder.
|
|
//
|
|
// Supported: go, typescript, javascript, python, rust.
|
|
// Other languages fall back to symbols_only (AST summary text).
|
|
// Returns (skeletonText, isFullSkeleton, error).
|
|
func SkeletonFile(ctx context.Context, reg *Registry, filename string, content []byte) (string, bool, error) {
|
|
result, err := reg.Parse(ctx, filename, content)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
lang := protocol.DetectLanguage(filename)
|
|
|
|
switch lang {
|
|
case protocol.LangGo:
|
|
return skeletonGo(result.Tree, content), true, nil
|
|
case protocol.LangTypeScript, protocol.LangJavaScript:
|
|
return skeletonTS(result.Tree, content), true, nil
|
|
case protocol.LangPython:
|
|
return skeletonPython(result.Tree, content), true, nil
|
|
case protocol.LangRust:
|
|
return skeletonRust(result.Tree, content), true, nil
|
|
default:
|
|
// TODO: skeleton for c, cpp, elixir, html, vue — fall back to symbols_only
|
|
syms := ExtractSymbols(result.Tree, content, lang, filename)
|
|
return renderSymbolsOnly(syms, filename, lang, content), false, nil
|
|
}
|
|
}
|
|
|
|
// renderSymbolsOnly renders a simple symbol list (fallback for unsupported languages).
|
|
func renderSymbolsOnly(syms []protocol.Symbol, _ string, lang protocol.Language, content []byte) string {
|
|
lines := strings.Split(string(content), "\n")
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("// skeleton unavailable for %s — symbol list only\n", lang))
|
|
for _, s := range syms {
|
|
sb.WriteString(fmt.Sprintf("// %s %s (line %d)\n", s.Kind, s.Name, s.Location.Line))
|
|
if s.Doc != "" {
|
|
sb.WriteString(fmt.Sprintf("// doc: %s\n", s.Doc))
|
|
}
|
|
if s.Location.Line >= 1 && s.Location.Line <= len(lines) {
|
|
sb.WriteString(lines[s.Location.Line-1] + " { ... }\n")
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// ---- Go skeleton ----
|
|
|
|
// skeletonGoBodyNodes lists Go node types that have a body field to replace.
|
|
var skeletonGoBodyNodes = map[string]string{
|
|
"function_declaration": "body",
|
|
"method_declaration": "body",
|
|
}
|
|
|
|
func skeletonGo(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonGoNode(root, content, &sb)
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonGoNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
if bodyField, ok := skeletonGoBodyNodes[nodeType]; ok {
|
|
body := node.ChildByFieldName(bodyField)
|
|
if body != nil {
|
|
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(sig)
|
|
sb.WriteString("{ ... }\n\n")
|
|
return
|
|
}
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
switch nodeType {
|
|
case "source_file":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonGoNode(child, content, sb)
|
|
}
|
|
return
|
|
case "comment":
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
default:
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// ---- TypeScript / JavaScript skeleton ----
|
|
|
|
func skeletonTS(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonTSNode(root, content, &sb)
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonTSNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
switch nodeType {
|
|
case "program":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonTSNode(child, content, sb)
|
|
}
|
|
return
|
|
case "function_declaration":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(sig)
|
|
sb.WriteString("{ ... }\n\n")
|
|
return
|
|
}
|
|
case "class_declaration":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
header := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(header)
|
|
sb.WriteString("{\n")
|
|
for i := 0; i < int(body.ChildCount()); i++ {
|
|
child := body.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
if child.Type() == "method_definition" {
|
|
methBody := child.ChildByFieldName("body")
|
|
if methBody != nil {
|
|
methSig := strings.TrimRight(string(content[child.StartByte():methBody.StartByte()]), " \t")
|
|
sb.WriteString(" ")
|
|
sb.WriteString(methSig)
|
|
sb.WriteString("{ ... }\n")
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString(" ")
|
|
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("}\n\n")
|
|
return
|
|
}
|
|
case "comment":
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// ---- Python skeleton ----
|
|
|
|
func skeletonPython(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonPythonNode(root, content, &sb, "")
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonPythonNode(node *sitter.Node, content []byte, sb *strings.Builder, indent string) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
switch nodeType {
|
|
case "module":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonPythonNode(child, content, sb, indent)
|
|
}
|
|
return
|
|
case "function_definition", "decorated_definition":
|
|
nodeText := string(content[node.StartByte():node.EndByte()])
|
|
lines := strings.SplitN(nodeText, "\n", 2)
|
|
sb.WriteString(indent)
|
|
sb.WriteString(lines[0])
|
|
sb.WriteString("\n")
|
|
sb.WriteString(indent)
|
|
sb.WriteString(" ...\n\n")
|
|
return
|
|
case "class_definition":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
header := string(content[node.StartByte():body.StartByte()])
|
|
firstLine := strings.SplitN(header, "\n", 2)[0]
|
|
sb.WriteString(indent)
|
|
sb.WriteString(firstLine)
|
|
sb.WriteString("\n")
|
|
for i := 0; i < int(body.ChildCount()); i++ {
|
|
child := body.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
if child.Type() == "function_definition" || child.Type() == "decorated_definition" {
|
|
childText := string(content[child.StartByte():child.EndByte()])
|
|
childLines := strings.SplitN(childText, "\n", 2)
|
|
sb.WriteString(indent + " ")
|
|
sb.WriteString(childLines[0])
|
|
sb.WriteString("\n")
|
|
sb.WriteString(indent + " ...")
|
|
sb.WriteString("\n")
|
|
continue
|
|
}
|
|
if child.Type() == "expression_statement" {
|
|
sb.WriteString(indent + " ")
|
|
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
|
sb.WriteString("\n")
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
case "comment":
|
|
sb.WriteString(indent)
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
sb.WriteString(indent)
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// ---- Rust skeleton ----
|
|
|
|
func skeletonRust(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonRustNode(root, content, &sb)
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonRustNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
switch nodeType {
|
|
case "source_file":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonRustNode(child, content, sb)
|
|
}
|
|
return
|
|
case "function_item":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(sig)
|
|
sb.WriteString("{ ... }\n\n")
|
|
return
|
|
}
|
|
case "impl_item":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
header := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(header)
|
|
sb.WriteString("{\n")
|
|
for i := 0; i < int(body.ChildCount()); i++ {
|
|
child := body.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
if child.Type() == "function_item" {
|
|
methBody := child.ChildByFieldName("body")
|
|
if methBody != nil {
|
|
methSig := strings.TrimRight(string(content[child.StartByte():methBody.StartByte()]), " \t")
|
|
sb.WriteString(" ")
|
|
sb.WriteString(methSig)
|
|
sb.WriteString("{ ... }\n")
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString(" ")
|
|
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("}\n\n")
|
|
return
|
|
}
|
|
case "line_comment", "block_comment":
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|