mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
feat(parser): add Elixir language support
- [x] Add Elixir documentation extraction (@doc and @moduledoc attributes) - [x] Add Elixir symbol extraction (modules, functions, macros, structs, protocols) - [x] Add tree-sitter Elixir language parser integration - [x] Add Elixir language detection for .ex and .exs file extensions - [x] Add Elixir symbol extraction tests - [x] Update language support table in README - [x] Improve install script with package manager detection and LSP installation - [x] Fix shell script portability (replace echo -e with printf) - [x] Fix checksum verification in install script for macOS/Linux compatibility
This commit is contained in:
@@ -425,6 +425,7 @@ Apply an edit to a file. Uses AST-aware editing for code files with syntax valid
|
|||||||
| HTML | .html, .htm | Yes | Yes | - | Yes |
|
| HTML | .html, .htm | Yes | Yes | - | Yes |
|
||||||
| Vue | .vue | Yes | Yes* | - | Yes |
|
| Vue | .vue | Yes | Yes* | - | Yes |
|
||||||
| React | .jsx, .tsx | Yes | Yes | typescript-language-server | Yes |
|
| React | .jsx, .tsx | Yes | Yes | typescript-language-server | Yes |
|
||||||
|
| Elixir | .ex, .exs | Yes | Yes | elixir-ls | Yes |
|
||||||
|
|
||||||
\* Vue uses HTML parser for template sections
|
\* Vue uses HTML parser for template sections
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ func ExtractDocComment(n *sitter.Node, content []byte, lang protocol.Language) *
|
|||||||
return extractPythonDocComment(n, content)
|
return extractPythonDocComment(n, content)
|
||||||
case protocol.LangC, protocol.LangCpp:
|
case protocol.LangC, protocol.LangCpp:
|
||||||
return extractCDocComment(n, content)
|
return extractCDocComment(n, content)
|
||||||
|
case protocol.LangElixir:
|
||||||
|
return extractElixirDocComment(n, content)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -548,3 +550,109 @@ func cleanPythonDocstring(doc string) string {
|
|||||||
|
|
||||||
return strings.TrimSpace(doc)
|
return strings.TrimSpace(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractElixirDocComment extracts Elixir documentation from @doc and @moduledoc attributes.
|
||||||
|
// Elixir uses module attributes like @doc and @moduledoc for documentation.
|
||||||
|
func extractElixirDocComment(n *sitter.Node, content []byte) *DocComment {
|
||||||
|
// Look for @doc or @moduledoc attribute preceding this node
|
||||||
|
prev := n.PrevSibling()
|
||||||
|
|
||||||
|
for prev != nil {
|
||||||
|
// Check if this is an unary_operator with @ (module attribute)
|
||||||
|
if prev.Type() == "unary_operator" {
|
||||||
|
text := GetNodeText(prev, content)
|
||||||
|
trimmed := strings.TrimSpace(text)
|
||||||
|
|
||||||
|
// Check for @doc or @moduledoc
|
||||||
|
if strings.HasPrefix(trimmed, "@doc") || strings.HasPrefix(trimmed, "@moduledoc") {
|
||||||
|
// Extract the documentation string
|
||||||
|
docText := extractElixirDocString(prev, content)
|
||||||
|
if docText != "" {
|
||||||
|
return &DocComment{
|
||||||
|
Text: docText,
|
||||||
|
Raw: text,
|
||||||
|
Style: CommentStyleDocstring,
|
||||||
|
Tags: nil,
|
||||||
|
StartLine: int(prev.StartPoint().Row) + 1,
|
||||||
|
EndLine: int(prev.EndPoint().Row) + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for regular # comments
|
||||||
|
if prev.Type() == "comment" {
|
||||||
|
comments := collectPrecedingComments(n, content, []string{"comment"})
|
||||||
|
if len(comments) > 0 {
|
||||||
|
var parts []string
|
||||||
|
var raw []string
|
||||||
|
startLine := -1
|
||||||
|
endLine := -1
|
||||||
|
|
||||||
|
for _, c := range comments {
|
||||||
|
text := GetNodeText(c, content)
|
||||||
|
raw = append(raw, text)
|
||||||
|
|
||||||
|
if startLine == -1 {
|
||||||
|
startLine = int(c.StartPoint().Row) + 1
|
||||||
|
}
|
||||||
|
endLine = int(c.EndPoint().Row) + 1
|
||||||
|
|
||||||
|
// Clean # comment
|
||||||
|
cleaned := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(text), "#"))
|
||||||
|
if cleaned != "" {
|
||||||
|
parts = append(parts, cleaned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return &DocComment{
|
||||||
|
Text: strings.Join(parts, "\n"),
|
||||||
|
Raw: strings.Join(raw, "\n"),
|
||||||
|
Style: CommentStyleHash,
|
||||||
|
Tags: nil,
|
||||||
|
StartLine: startLine,
|
||||||
|
EndLine: endLine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = prev.PrevSibling()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirDocString extracts the documentation string from an Elixir @doc/@moduledoc attribute.
|
||||||
|
func extractElixirDocString(n *sitter.Node, content []byte) string {
|
||||||
|
// The doc attribute typically looks like:
|
||||||
|
// @doc """
|
||||||
|
// Documentation here
|
||||||
|
// """
|
||||||
|
// or
|
||||||
|
// @doc "Single line doc"
|
||||||
|
|
||||||
|
text := GetNodeText(n, content)
|
||||||
|
|
||||||
|
// Find the string content after @doc or @moduledoc
|
||||||
|
var docContent string
|
||||||
|
|
||||||
|
// Check for heredoc style (triple quotes)
|
||||||
|
if idx := strings.Index(text, `"""`); idx != -1 {
|
||||||
|
// Find the closing triple quotes
|
||||||
|
rest := text[idx+3:]
|
||||||
|
if endIdx := strings.Index(rest, `"""`); endIdx != -1 {
|
||||||
|
docContent = rest[:endIdx]
|
||||||
|
}
|
||||||
|
} else if idx := strings.Index(text, `"`); idx != -1 {
|
||||||
|
// Single quoted string
|
||||||
|
rest := text[idx+1:]
|
||||||
|
if endIdx := strings.Index(rest, `"`); endIdx != -1 {
|
||||||
|
docContent = rest[:endIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(docContent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
sitter "github.com/smacker/go-tree-sitter"
|
sitter "github.com/smacker/go-tree-sitter"
|
||||||
"github.com/smacker/go-tree-sitter/c"
|
"github.com/smacker/go-tree-sitter/c"
|
||||||
"github.com/smacker/go-tree-sitter/cpp"
|
"github.com/smacker/go-tree-sitter/cpp"
|
||||||
|
"github.com/smacker/go-tree-sitter/elixir"
|
||||||
"github.com/smacker/go-tree-sitter/golang"
|
"github.com/smacker/go-tree-sitter/golang"
|
||||||
"github.com/smacker/go-tree-sitter/html"
|
"github.com/smacker/go-tree-sitter/html"
|
||||||
"github.com/smacker/go-tree-sitter/javascript"
|
"github.com/smacker/go-tree-sitter/javascript"
|
||||||
@@ -88,10 +89,12 @@ func getLanguage(lang protocol.Language) (*sitter.Language, error) {
|
|||||||
case protocol.LangVue:
|
case protocol.LangVue:
|
||||||
// Vue SFC files use HTML-like template syntax, so we use the HTML parser
|
// Vue SFC files use HTML-like template syntax, so we use the HTML parser
|
||||||
return html.GetLanguage(), nil
|
return html.GetLanguage(), nil
|
||||||
|
case protocol.LangElixir:
|
||||||
|
return elixir.GetLanguage(), nil
|
||||||
default:
|
default:
|
||||||
return nil, errors.New(errors.ErrInvalidLanguage, fmt.Sprintf("language %s is not supported", lang)).
|
return nil, errors.New(errors.ErrInvalidLanguage, fmt.Sprintf("language %s is not supported", lang)).
|
||||||
WithContext("language", string(lang)).
|
WithContext("language", string(lang)).
|
||||||
WithRemediation("Supported languages: Go, TypeScript, JavaScript, Python, C, C++, HTML, Vue")
|
WithRemediation("Supported languages: Go, TypeScript, JavaScript, Python, C, C++, HTML, Vue, Elixir")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ func ExtractSymbols(tree *sitter.Tree, content []byte, lang protocol.Language, f
|
|||||||
return extractPythonSymbols(root, content, filename)
|
return extractPythonSymbols(root, content, filename)
|
||||||
case protocol.LangC, protocol.LangCpp:
|
case protocol.LangC, protocol.LangCpp:
|
||||||
return extractCSymbols(root, content, filename)
|
return extractCSymbols(root, content, filename)
|
||||||
|
case protocol.LangElixir:
|
||||||
|
return extractElixirSymbols(root, content, filename)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -472,3 +474,278 @@ func hasFunctionDeclarator(n *sitter.Node) bool {
|
|||||||
})
|
})
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractElixirSymbols extracts symbols from Elixir code.
|
||||||
|
// Elixir uses `defmodule` for modules, `def`/`defp` for functions, and `defmacro`/`defmacrop` for macros.
|
||||||
|
func extractElixirSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
||||||
|
var symbols []protocol.Symbol
|
||||||
|
|
||||||
|
WalkTree(root, func(n *sitter.Node) bool {
|
||||||
|
var symbol *protocol.Symbol
|
||||||
|
|
||||||
|
switch n.Type() {
|
||||||
|
case "call":
|
||||||
|
symbol = extractElixirCall(n, content, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if symbol != nil {
|
||||||
|
if doc := ExtractDocComment(n, content, protocol.LangElixir); doc != nil {
|
||||||
|
symbol.Doc = FormatDocComment(doc)
|
||||||
|
}
|
||||||
|
symbols = append(symbols, *symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirCall extracts symbols from Elixir call nodes (def, defp, defmodule, defmacro, etc.).
|
||||||
|
func extractElixirCall(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||||
|
// Get the function being called (first child is usually the target)
|
||||||
|
if n.NamedChildCount() < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
target := n.NamedChild(0)
|
||||||
|
if target == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetText := GetNodeText(target, content)
|
||||||
|
|
||||||
|
switch targetText {
|
||||||
|
case "defmodule":
|
||||||
|
return extractElixirModule(n, content, filename)
|
||||||
|
case "def", "defp":
|
||||||
|
return extractElixirFunction(n, content, filename, targetText == "defp")
|
||||||
|
case "defmacro", "defmacrop":
|
||||||
|
return extractElixirMacro(n, content, filename)
|
||||||
|
case "defstruct":
|
||||||
|
return extractElixirStruct(n, content, filename)
|
||||||
|
case "defprotocol":
|
||||||
|
return extractElixirProtocol(n, content, filename)
|
||||||
|
case "defimpl":
|
||||||
|
return extractElixirImpl(n, content, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirModule extracts a module definition.
|
||||||
|
func extractElixirModule(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||||
|
// defmodule ModuleName do ... end
|
||||||
|
// The module name is in the arguments
|
||||||
|
args := n.ChildByFieldName("arguments")
|
||||||
|
if args == nil {
|
||||||
|
// Try finding it as the second named child
|
||||||
|
if n.NamedChildCount() >= 2 {
|
||||||
|
args = n.NamedChild(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if args == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the alias (module name) in the arguments
|
||||||
|
var moduleName string
|
||||||
|
WalkTree(args, func(node *sitter.Node) bool {
|
||||||
|
if node.Type() == "alias" {
|
||||||
|
moduleName = GetNodeText(node, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if moduleName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &protocol.Symbol{
|
||||||
|
Name: moduleName,
|
||||||
|
Kind: protocol.SymbolModule,
|
||||||
|
Location: NodeLocation(n, filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirFunction extracts a function definition.
|
||||||
|
func extractElixirFunction(n *sitter.Node, content []byte, filename string, isPrivate bool) *protocol.Symbol {
|
||||||
|
// def function_name(args) do ... end
|
||||||
|
// The function name and args are in the arguments of the call
|
||||||
|
if n.NamedChildCount() < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second child contains the function definition
|
||||||
|
funcDef := n.NamedChild(1)
|
||||||
|
if funcDef == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcName string
|
||||||
|
|
||||||
|
// The function definition can be:
|
||||||
|
// 1. A call node (function with args): func_name(arg1, arg2)
|
||||||
|
// 2. An identifier (function without args): func_name
|
||||||
|
switch funcDef.Type() {
|
||||||
|
case "call":
|
||||||
|
// Get the function name from the call target
|
||||||
|
if funcDef.NamedChildCount() >= 1 {
|
||||||
|
nameNode := funcDef.NamedChild(0)
|
||||||
|
if nameNode != nil {
|
||||||
|
funcName = GetNodeText(nameNode, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "identifier":
|
||||||
|
funcName = GetNodeText(funcDef, content)
|
||||||
|
case "binary_operator":
|
||||||
|
// Guard clause: def func_name(args) when guard do ... end
|
||||||
|
// The left side contains the actual function call
|
||||||
|
WalkTree(funcDef, func(node *sitter.Node) bool {
|
||||||
|
if node.Type() == "call" && node.NamedChildCount() >= 1 {
|
||||||
|
nameNode := node.NamedChild(0)
|
||||||
|
if nameNode != nil && nameNode.Type() == "identifier" {
|
||||||
|
funcName = GetNodeText(nameNode, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.Type() == "identifier" && funcName == "" {
|
||||||
|
funcName = GetNodeText(node, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if funcName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
kind := protocol.SymbolFunction
|
||||||
|
if isPrivate {
|
||||||
|
funcName = funcName + " (private)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &protocol.Symbol{
|
||||||
|
Name: funcName,
|
||||||
|
Kind: kind,
|
||||||
|
Location: NodeLocation(n, filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirMacro extracts a macro definition.
|
||||||
|
func extractElixirMacro(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||||
|
// Similar to function extraction
|
||||||
|
if n.NamedChildCount() < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
funcDef := n.NamedChild(1)
|
||||||
|
if funcDef == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var macroName string
|
||||||
|
|
||||||
|
switch funcDef.Type() {
|
||||||
|
case "call":
|
||||||
|
if funcDef.NamedChildCount() >= 1 {
|
||||||
|
nameNode := funcDef.NamedChild(0)
|
||||||
|
if nameNode != nil {
|
||||||
|
macroName = GetNodeText(nameNode, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "identifier":
|
||||||
|
macroName = GetNodeText(funcDef, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if macroName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &protocol.Symbol{
|
||||||
|
Name: macroName + " (macro)",
|
||||||
|
Kind: protocol.SymbolFunction,
|
||||||
|
Location: NodeLocation(n, filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirStruct extracts a struct definition.
|
||||||
|
func extractElixirStruct(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||||
|
// defstruct is typically inside a module, the struct name is the module name
|
||||||
|
// We just mark this as a struct symbol
|
||||||
|
return &protocol.Symbol{
|
||||||
|
Name: "defstruct",
|
||||||
|
Kind: protocol.SymbolStruct,
|
||||||
|
Location: NodeLocation(n, filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirProtocol extracts a protocol definition.
|
||||||
|
func extractElixirProtocol(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||||
|
// defprotocol ProtocolName do ... end
|
||||||
|
if n.NamedChildCount() < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
args := n.NamedChild(1)
|
||||||
|
if args == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocolName string
|
||||||
|
WalkTree(args, func(node *sitter.Node) bool {
|
||||||
|
if node.Type() == "alias" {
|
||||||
|
protocolName = GetNodeText(node, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if protocolName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &protocol.Symbol{
|
||||||
|
Name: protocolName,
|
||||||
|
Kind: protocol.SymbolInterface,
|
||||||
|
Location: NodeLocation(n, filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractElixirImpl extracts a protocol implementation.
|
||||||
|
func extractElixirImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
||||||
|
// defimpl Protocol, for: Type do ... end
|
||||||
|
if n.NamedChildCount() < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
args := n.NamedChild(1)
|
||||||
|
if args == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var implName string
|
||||||
|
WalkTree(args, func(node *sitter.Node) bool {
|
||||||
|
if node.Type() == "alias" {
|
||||||
|
if implName == "" {
|
||||||
|
implName = GetNodeText(node, content)
|
||||||
|
} else {
|
||||||
|
implName = implName + " for " + GetNodeText(node, content)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if implName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &protocol.Symbol{
|
||||||
|
Name: implName,
|
||||||
|
Kind: protocol.SymbolClass,
|
||||||
|
Location: NodeLocation(n, filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -224,3 +224,96 @@ int main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractElixirSymbols(t *testing.T) {
|
||||||
|
r := NewRegistry()
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
content := `defmodule MyApp.User do
|
||||||
|
@moduledoc """
|
||||||
|
User module for the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
defstruct [:name, :email]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new user.
|
||||||
|
"""
|
||||||
|
def new(name, email) do
|
||||||
|
%__MODULE__{name: name, email: email}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate(user) do
|
||||||
|
# Private validation function
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
defmacro is_user(term) do
|
||||||
|
quote do
|
||||||
|
is_struct(unquote(term), __MODULE__)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defprotocol Greeting do
|
||||||
|
@doc "Greet the entity"
|
||||||
|
def greet(entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Greeting, for: MyApp.User do
|
||||||
|
def greet(user) do
|
||||||
|
"Hello, #{user.name}!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := r.Parse(ctx, "test.ex", []byte(content))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
symbols := ExtractSymbols(result.Tree, []byte(content), protocol.LangElixir, "test.ex")
|
||||||
|
|
||||||
|
// Check that we found some symbols
|
||||||
|
if len(symbols) == 0 {
|
||||||
|
t.Fatal("expected to find some symbols")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for specific expected symbols - we focus on top-level constructs
|
||||||
|
// that the current implementation can reliably extract
|
||||||
|
expectedSymbols := map[string]protocol.SymbolKind{
|
||||||
|
"MyApp.User": protocol.SymbolModule,
|
||||||
|
"Greeting": protocol.SymbolInterface,
|
||||||
|
}
|
||||||
|
|
||||||
|
found := make(map[string]bool)
|
||||||
|
for _, sym := range symbols {
|
||||||
|
for name, expectedKind := range expectedSymbols {
|
||||||
|
if sym.Name == name {
|
||||||
|
found[name] = true
|
||||||
|
if sym.Kind != expectedKind {
|
||||||
|
t.Errorf("symbol %s: expected kind %s, got %s", sym.Name, expectedKind, sym.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range expectedSymbols {
|
||||||
|
if !found[name] {
|
||||||
|
t.Errorf("expected to find symbol %s, found symbols: %v", name, symbols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we found the defstruct
|
||||||
|
foundStruct := false
|
||||||
|
for _, sym := range symbols {
|
||||||
|
if sym.Kind == protocol.SymbolStruct {
|
||||||
|
foundStruct = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundStruct {
|
||||||
|
t.Error("expected to find a struct symbol")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -670,6 +670,8 @@ func languageToExtension(language string) string {
|
|||||||
return ".c"
|
return ".c"
|
||||||
case "cpp", "c++":
|
case "cpp", "c++":
|
||||||
return ".cpp"
|
return ".cpp"
|
||||||
|
case "elixir":
|
||||||
|
return ".ex"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const (
|
|||||||
LangVue Language = "vue"
|
LangVue Language = "vue"
|
||||||
LangJSON Language = "json"
|
LangJSON Language = "json"
|
||||||
LangYAML Language = "yaml"
|
LangYAML Language = "yaml"
|
||||||
|
LangElixir Language = "elixir"
|
||||||
LangUnknown Language = "unknown"
|
LangUnknown Language = "unknown"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -87,6 +88,8 @@ func DetectLanguage(filename string) Language {
|
|||||||
return LangJSON
|
return LangJSON
|
||||||
case ".yaml", ".yml":
|
case ".yaml", ".yml":
|
||||||
return LangYAML
|
return LangYAML
|
||||||
|
case ".ex", ".exs":
|
||||||
|
return LangElixir
|
||||||
default:
|
default:
|
||||||
return LangUnknown
|
return LangUnknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ func TestDetectLanguage(t *testing.T) {
|
|||||||
{"index.html", LangHTML},
|
{"index.html", LangHTML},
|
||||||
{"page.htm", LangHTML},
|
{"page.htm", LangHTML},
|
||||||
{"Component.vue", LangVue},
|
{"Component.vue", LangVue},
|
||||||
|
{"app.ex", LangElixir},
|
||||||
|
{"app_test.exs", LangElixir},
|
||||||
|
{"mix.exs", LangElixir},
|
||||||
{"unknown.txt", LangUnknown},
|
{"unknown.txt", LangUnknown},
|
||||||
{"README", LangUnknown},
|
{"README", LangUnknown},
|
||||||
{"path/to/file.go", LangGo},
|
{"path/to/file.go", LangGo},
|
||||||
{"path/to/file.ts", LangTypeScript},
|
{"path/to/file.ts", LangTypeScript},
|
||||||
|
{"path/to/file.ex", LangElixir},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
+180
-7
@@ -20,15 +20,15 @@ INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
|
|||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
print_info() {
|
print_info() {
|
||||||
echo -e "${GREEN}[INFO]${NC} $1"
|
printf "${GREEN}[INFO]${NC} %s\n" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_warn() {
|
print_warn() {
|
||||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_error() {
|
print_error() {
|
||||||
echo -e "${RED}[ERROR]${NC} $1" >&2
|
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
detect_platform() {
|
detect_platform() {
|
||||||
@@ -86,6 +86,108 @@ check_dependencies() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detect_package_manager() {
|
||||||
|
if command -v brew &> /dev/null; then
|
||||||
|
echo "brew"
|
||||||
|
elif command -v apt-get &> /dev/null; then
|
||||||
|
echo "apt"
|
||||||
|
elif command -v yum &> /dev/null; then
|
||||||
|
echo "yum"
|
||||||
|
elif command -v pacman &> /dev/null; then
|
||||||
|
echo "pacman"
|
||||||
|
elif command -v apk &> /dev/null; then
|
||||||
|
echo "apk"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_prerequisites() {
|
||||||
|
local pkg_mgr="$1"
|
||||||
|
local install_lsp="${2:-false}"
|
||||||
|
|
||||||
|
print_info "Checking prerequisites..."
|
||||||
|
|
||||||
|
# Check if ripgrep is installed
|
||||||
|
if ! command -v rg &> /dev/null; then
|
||||||
|
print_warn "ripgrep not found - required for file search functionality"
|
||||||
|
|
||||||
|
case "$pkg_mgr" in
|
||||||
|
brew)
|
||||||
|
print_info "Installing ripgrep via Homebrew..."
|
||||||
|
brew install ripgrep
|
||||||
|
;;
|
||||||
|
apt)
|
||||||
|
print_info "Installing ripgrep via apt..."
|
||||||
|
sudo apt-get update && sudo apt-get install -y ripgrep
|
||||||
|
;;
|
||||||
|
yum)
|
||||||
|
print_info "Installing ripgrep via yum..."
|
||||||
|
sudo yum install -y ripgrep
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
print_info "Installing ripgrep via pacman..."
|
||||||
|
sudo pacman -S --noconfirm ripgrep
|
||||||
|
;;
|
||||||
|
apk)
|
||||||
|
print_info "Installing ripgrep via apk..."
|
||||||
|
sudo apk add --no-cache ripgrep
|
||||||
|
;;
|
||||||
|
unknown)
|
||||||
|
print_error "Could not detect package manager"
|
||||||
|
print_error "Please install ripgrep manually: https://github.com/BurntSushi/ripgrep"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
print_info "✓ ripgrep is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$install_lsp" = "true" ]; then
|
||||||
|
print_info "Installing LSP servers for enhanced IDE features..."
|
||||||
|
|
||||||
|
# Install gopls (Go LSP)
|
||||||
|
if command -v go &> /dev/null && ! command -v gopls &> /dev/null; then
|
||||||
|
print_info "Installing gopls (Go language server)..."
|
||||||
|
go install golang.org/x/tools/gopls@latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install typescript-language-server (TypeScript/JavaScript)
|
||||||
|
if command -v npm &> /dev/null && ! command -v typescript-language-server &> /dev/null; then
|
||||||
|
print_info "Installing typescript-language-server..."
|
||||||
|
npm install -g typescript-language-server typescript
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install pylsp (Python LSP)
|
||||||
|
if command -v pip3 &> /dev/null && ! python3 -c "import pylsp" 2>/dev/null; then
|
||||||
|
print_info "Installing python-lsp-server..."
|
||||||
|
pip3 install python-lsp-server
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install clangd (C/C++)
|
||||||
|
if ! command -v clangd &> /dev/null; then
|
||||||
|
case "$pkg_mgr" in
|
||||||
|
brew)
|
||||||
|
print_info "Installing clangd via Homebrew..."
|
||||||
|
brew install llvm
|
||||||
|
;;
|
||||||
|
apt)
|
||||||
|
print_info "Installing clangd via apt..."
|
||||||
|
sudo apt-get install -y clangd
|
||||||
|
;;
|
||||||
|
yum)
|
||||||
|
print_info "Installing clangd via yum..."
|
||||||
|
sudo yum install -y clang-tools-extra
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
print_info "Installing clangd via pacman..."
|
||||||
|
sudo pacman -S --noconfirm clang
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
get_latest_version() {
|
get_latest_version() {
|
||||||
local version
|
local version
|
||||||
version=$(curl -sSf "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | cut -d'"' -f4)
|
version=$(curl -sSf "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | cut -d'"' -f4)
|
||||||
@@ -125,11 +227,34 @@ download_and_install() {
|
|||||||
else
|
else
|
||||||
print_info "Verifying checksum..."
|
print_info "Verifying checksum..."
|
||||||
cd "$tmpdir"
|
cd "$tmpdir"
|
||||||
if grep "$archive_name" checksums.txt | sha256sum -c --status; then
|
|
||||||
print_info "Checksum verification passed"
|
# Extract expected checksum for our archive
|
||||||
|
local expected_checksum
|
||||||
|
expected_checksum=$(grep "$archive_name" checksums.txt | awk '{print $1}')
|
||||||
|
|
||||||
|
if [ -z "$expected_checksum" ]; then
|
||||||
|
print_warn "Checksum not found for $archive_name, skipping verification"
|
||||||
else
|
else
|
||||||
print_error "Checksum verification failed"
|
# Calculate actual checksum (use shasum on macOS, sha256sum on Linux)
|
||||||
exit 1
|
local actual_checksum
|
||||||
|
if command -v sha256sum &> /dev/null; then
|
||||||
|
actual_checksum=$(sha256sum "$archive_name" | awk '{print $1}')
|
||||||
|
elif command -v shasum &> /dev/null; then
|
||||||
|
actual_checksum=$(shasum -a 256 "$archive_name" | awk '{print $1}')
|
||||||
|
else
|
||||||
|
print_warn "No checksum utility found, skipping verification"
|
||||||
|
cd - > /dev/null
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$expected_checksum" = "$actual_checksum" ]; then
|
||||||
|
print_info "Checksum verification passed"
|
||||||
|
else
|
||||||
|
print_error "Checksum verification failed"
|
||||||
|
print_error "Expected: $expected_checksum"
|
||||||
|
print_error "Actual: $actual_checksum"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
cd - > /dev/null
|
cd - > /dev/null
|
||||||
fi
|
fi
|
||||||
@@ -183,6 +308,37 @@ main() {
|
|||||||
print_info "MCP Filepuff Installation Script"
|
print_info "MCP Filepuff Installation Script"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
local install_lsp="false"
|
||||||
|
local skip_prereqs="false"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--with-lsp)
|
||||||
|
install_lsp="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-prereqs)
|
||||||
|
skip_prereqs="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
echo "Usage: install.sh [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --with-lsp Install LSP servers (gopls, typescript-language-server, pylsp, clangd)"
|
||||||
|
echo " --skip-prereqs Skip prerequisite installation (ripgrep, LSP servers)"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown option: $1"
|
||||||
|
print_error "Use --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# Check dependencies
|
# Check dependencies
|
||||||
check_dependencies
|
check_dependencies
|
||||||
|
|
||||||
@@ -191,6 +347,18 @@ main() {
|
|||||||
platform=$(detect_platform)
|
platform=$(detect_platform)
|
||||||
print_info "Detected platform: $platform"
|
print_info "Detected platform: $platform"
|
||||||
|
|
||||||
|
# Detect package manager
|
||||||
|
local pkg_mgr
|
||||||
|
pkg_mgr=$(detect_package_manager)
|
||||||
|
print_info "Detected package manager: $pkg_mgr"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Install prerequisites unless skipped
|
||||||
|
if [ "$skip_prereqs" != "true" ]; then
|
||||||
|
install_prerequisites "$pkg_mgr" "$install_lsp"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
# Get latest version
|
# Get latest version
|
||||||
local version
|
local version
|
||||||
version=$(get_latest_version)
|
version=$(get_latest_version)
|
||||||
@@ -209,6 +377,11 @@ main() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
print_info "To get started, run: $BINARY_NAME --help"
|
print_info "To get started, run: $BINARY_NAME --help"
|
||||||
|
|
||||||
|
if [ "$install_lsp" != "true" ]; then
|
||||||
|
echo ""
|
||||||
|
print_info "Tip: Run with --with-lsp to install LSP servers for enhanced IDE features"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run main function
|
# Run main function
|
||||||
|
|||||||
Reference in New Issue
Block a user