Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ mcp-language-server

# Temporary files
*~

# MCP Server config
.mcp.json

# Markdown files with notes
notes
15 changes: 15 additions & 0 deletions .mcp.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"mcpServers": {
"language-server": {
"type": "stdio",
"command": "/Users/orsen/Develop/mcp-language-server/mcp-language-server",
"args": [
"--workspace",
"/Users/orsen/Develop/mcp-language-server",
"--lsp",
"gopls"
],
"env": {}
}
}
}
4 changes: 3 additions & 1 deletion internal/lsp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
CodeLens: &protocol.CodeLensClientCapabilities{
DynamicRegistration: true,
},
DocumentSymbol: protocol.DocumentSymbolClientCapabilities{},
DocumentSymbol: protocol.DocumentSymbolClientCapabilities{
HierarchicalDocumentSymbolSupport: true,
},
CodeAction: protocol.CodeActionClientCapabilities{
CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
CodeActionKind: protocol.ClientCodeActionKindOptions{
Expand Down
87 changes: 54 additions & 33 deletions internal/tools/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,70 +42,91 @@ func GetDiagnosticsForFile(ctx context.Context, client *lsp.Client, filePath str
return "No diagnostics found for " + filePath, nil
}

// Create a summary header
summary := fmt.Sprintf("Diagnostics for %s (%d issues)\n",
filePath,
len(diagnostics))

// Format the diagnostics
var formattedDiagnostics []string
for _, diag := range diagnostics {
formattedDiagnostics = append(formattedDiagnostics, summary)

for i, diag := range diagnostics {
severity := getSeverityString(diag.Severity)
location := fmt.Sprintf("Line %d, Column %d",
location := fmt.Sprintf("L%d:C%d",
diag.Range.Start.Line+1,
diag.Range.Start.Character+1)

// Get the file content for context if needed
var codeContext string
startLine := diag.Range.Start.Line + 1
var startLine uint32

// Always get at least the line with the diagnostic
content, err := os.ReadFile(filePath)
if err == nil {
lines := strings.Split(string(content), "\n")
if int(diag.Range.Start.Line) < len(lines) {
codeContext = strings.TrimSpace(lines[diag.Range.Start.Line])

// Truncate line if it's too long
const maxLineLength = 80
if len(codeContext) > maxLineLength {
startChar := int(diag.Range.Start.Character)
if startChar > maxLineLength/2 {
codeContext = "..." + codeContext[startChar-maxLineLength/2:]
}
if len(codeContext) > maxLineLength {
codeContext = codeContext[:maxLineLength] + "..."
}
}
}
}

// Get more context if requested
if includeContext {
content, loc, err := GetFullDefinition(ctx, client, protocol.Location{
extendedContext, loc, err := GetFullDefinition(ctx, client, protocol.Location{
URI: uri,
Range: diag.Range,
})
startLine = loc.Range.Start.Line + 1
if err != nil {
log.Printf("failed to get file content: %v", err)
} else {
codeContext = content
}
} else {
// Read just the line with the error
content, err := os.ReadFile(filePath)
if err == nil {
lines := strings.Split(string(content), "\n")
if int(diag.Range.Start.Line) < len(lines) {
codeContext = lines[diag.Range.Start.Line]
startLine = loc.Range.Start.Line + 1
if showLineNumbers {
extendedContext = addLineNumbers(extendedContext, int(startLine))
}
codeContext = extendedContext
}
}

formattedDiag := fmt.Sprintf(
"%s\n[%s] %s\n"+
"Location: %s\n"+
"Message: %s\n",
strings.Repeat("=", 60),
// Create a concise diagnostic entry
var formattedDiag strings.Builder
formattedDiag.WriteString(fmt.Sprintf("%d. [%s] %s - %s\n",
i+1,
severity,
filePath,
location,
diag.Message)
diag.Message))

// Add source and code if present, but keep it compact
var details []string
if diag.Source != "" {
formattedDiag += fmt.Sprintf("Source: %s\n", diag.Source)
details = append(details, fmt.Sprintf("Source: %s", diag.Source))
}

if diag.Code != nil {
formattedDiag += fmt.Sprintf("Code: %v\n", diag.Code)
details = append(details, fmt.Sprintf("Code: %v", diag.Code))
}

formattedDiag += strings.Repeat("=", 60)
if len(details) > 0 {
formattedDiag.WriteString(fmt.Sprintf(" %s\n", strings.Join(details, ", ")))
}

// Add code context
if codeContext != "" {
if showLineNumbers {
codeContext = addLineNumbers(codeContext, int(startLine))
}
formattedDiag += fmt.Sprintf("\n%s\n", codeContext)
formattedDiag.WriteString(fmt.Sprintf(" > %s\n", codeContext))
}

formattedDiagnostics = append(formattedDiagnostics, formattedDiag)
formattedDiagnostics = append(formattedDiagnostics, formattedDiag.String())
}

return strings.Join(formattedDiagnostics, "\n"), nil
return strings.Join(formattedDiagnostics, ""), nil
}

func getSeverityString(severity protocol.DiagnosticSeverity) string {
Expand Down
93 changes: 93 additions & 0 deletions internal/tools/document_symbols.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package tools

import (
"context"
"fmt"
"strings"

"github.com/isaacphi/mcp-language-server/internal/lsp"
"github.com/isaacphi/mcp-language-server/internal/protocol"
"github.com/isaacphi/mcp-language-server/internal/utilities"
)

// GetDocumentSymbols retrieves all symbols in a document and formats them in a hierarchical structure
func GetDocumentSymbols(ctx context.Context, client *lsp.Client, filePath string, showLineNumbers bool) (string, error) {
// Open the file if not already open
err := client.OpenFile(ctx, filePath)
if err != nil {
return "", fmt.Errorf("could not open file: %v", err)
}

// Convert to URI format for LSP protocol
uri := protocol.DocumentUri("file://" + filePath)

// Create the document symbol parameters
symParams := protocol.DocumentSymbolParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: uri,
},
}

// Execute the document symbol request
symResult, err := client.DocumentSymbol(ctx, symParams)
if err != nil {
return "", fmt.Errorf("failed to get document symbols: %v", err)
}

symbols, err := symResult.Results()
if err != nil {
return "", fmt.Errorf("failed to process document symbols: %v", err)
}

if len(symbols) == 0 {
return fmt.Sprintf("No symbols found in %s", filePath), nil
}

var result strings.Builder
result.WriteString(fmt.Sprintf("Symbols in %s\n\n", filePath))

// Format symbols hierarchically
formatSymbols(&result, symbols, 0, showLineNumbers)

return result.String(), nil
}

// formatSymbols recursively formats symbols with proper indentation
func formatSymbols(sb *strings.Builder, symbols []protocol.DocumentSymbolResult, level int, showLineNumbers bool) {
indent := strings.Repeat(" ", level)

for _, sym := range symbols {
// Get symbol information
name := sym.GetName()

// Format location information
location := ""
if showLineNumbers {
r := sym.GetRange()
if r.Start.Line == r.End.Line {
location = fmt.Sprintf("Line %d", r.Start.Line+1)
} else {
location = fmt.Sprintf("Lines %d-%d", r.Start.Line+1, r.End.Line+1)
}
}

// Use the shared utility to extract kind information
kindStr := utilities.ExtractSymbolKind(sym)

// Format the symbol entry
if location != "" {
sb.WriteString(fmt.Sprintf("%s%s %s (%s)\n", indent, kindStr, name, location))
} else {
sb.WriteString(fmt.Sprintf("%s%s %s\n", indent, kindStr, name))
}

// Format children if it's a DocumentSymbol
if ds, ok := sym.(*protocol.DocumentSymbol); ok && len(ds.Children) > 0 {
childSymbols := make([]protocol.DocumentSymbolResult, len(ds.Children))
for i := range ds.Children {
childSymbols[i] = &ds.Children[i]
}
formatSymbols(sb, childSymbols, level+1, showLineNumbers)
}
}
}
Loading