Replaces placeholder with full csharp@0.1.0 plugin. Adds helper/ package (locate, process, client, protocol) for JSON-RPC over stdio to ctx-roslyn-helper. project.go finds .sln (fallback: single .csproj), loads it, retrieves projectSummary, formats dense markdown with project details, reference graph, and multi-targeting section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
250 lines
6.2 KiB
Go
250 lines
6.2 KiB
Go
package csharp
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/ricarneiro/ctx/internal/output"
|
|
"github.com/ricarneiro/ctx/internal/plugins/csharp/helper"
|
|
)
|
|
|
|
// WriteSummary formats a ProjectSummary as dense markdown.
|
|
func WriteSummary(w io.Writer, s *helper.ProjectSummary) error {
|
|
output.H1(w, "Solution: "+s.SolutionName)
|
|
writeOverview(w, s)
|
|
output.H2(w, "Projects")
|
|
writeProjects(w, s.Projects)
|
|
writeReferenceGraph(w, s.Projects)
|
|
writeMultiTargeting(w, s.Projects)
|
|
return nil
|
|
}
|
|
|
|
func writeOverview(w io.Writer, s *helper.ProjectSummary) {
|
|
exeCount, libCount := 0, 0
|
|
totalDocs := 0
|
|
for _, p := range s.Projects {
|
|
if p.Type == "exe" {
|
|
exeCount++
|
|
} else {
|
|
libCount++
|
|
}
|
|
totalDocs += p.DocumentCount
|
|
}
|
|
|
|
projectSummary := fmt.Sprintf("%d", len(s.Projects))
|
|
parts := []string{}
|
|
if exeCount > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d exe", exeCount))
|
|
}
|
|
if libCount > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d lib", libCount))
|
|
}
|
|
if len(parts) > 0 {
|
|
projectSummary += " (" + strings.Join(parts, ", ") + ")"
|
|
}
|
|
|
|
output.KeyValue(w, "path", "`"+s.SolutionPath+"`")
|
|
output.KeyValue(w, "projects", projectSummary)
|
|
output.KeyValue(w, "documents", fmt.Sprintf("%d", totalDocs))
|
|
fmt.Fprintln(w)
|
|
}
|
|
|
|
func writeProjects(w io.Writer, projects []helper.ProjectInfo) {
|
|
for _, p := range projects {
|
|
output.H3(w, fmt.Sprintf("%s (%s)", p.Name, p.Type))
|
|
|
|
fmt.Fprintf(w, "- **path:** `%s`\n", p.Path)
|
|
|
|
targets := strings.Join(p.TargetFrameworks, ", ")
|
|
if targets == "" {
|
|
targets = "_(unknown)_"
|
|
}
|
|
fmt.Fprintf(w, "- **target:** %s\n", targets)
|
|
fmt.Fprintf(w, "- **namespace:** `%s`\n", p.RootNamespace)
|
|
fmt.Fprintf(w, "- **documents:** %d\n", p.DocumentCount)
|
|
|
|
// Project references
|
|
if len(p.ProjectReferences) == 0 {
|
|
fmt.Fprintf(w, "- **references:** _(none)_\n")
|
|
} else {
|
|
refs := make([]string, len(p.ProjectReferences))
|
|
for i, r := range p.ProjectReferences {
|
|
refs[i] = "`" + r + "`"
|
|
}
|
|
fmt.Fprintf(w, "- **references:**\n")
|
|
for _, r := range refs {
|
|
fmt.Fprintf(w, " - %s\n", r)
|
|
}
|
|
}
|
|
|
|
// Package references
|
|
if len(p.PackageReferences) == 0 {
|
|
fmt.Fprintf(w, "- **packages:** _(none)_\n")
|
|
} else {
|
|
pkgs := make([]string, len(p.PackageReferences))
|
|
for i, pkg := range p.PackageReferences {
|
|
pkgs[i] = pkg.Name + " " + pkg.Version
|
|
}
|
|
fmt.Fprintf(w, "- **packages:** %s\n", strings.Join(pkgs, ", "))
|
|
}
|
|
|
|
fmt.Fprintln(w)
|
|
}
|
|
}
|
|
|
|
func writeReferenceGraph(w io.Writer, projects []helper.ProjectInfo) {
|
|
// Build reverse dependency map: who depends on each project
|
|
dependents := map[string][]string{}
|
|
refMap := map[string][]string{}
|
|
nameSet := map[string]bool{}
|
|
|
|
for _, p := range projects {
|
|
nameSet[p.Name] = true
|
|
refMap[p.Name] = p.ProjectReferences
|
|
for _, ref := range p.ProjectReferences {
|
|
dependents[ref] = append(dependents[ref], p.Name)
|
|
}
|
|
}
|
|
|
|
// Check if there are any references at all
|
|
hasRefs := false
|
|
for _, p := range projects {
|
|
if len(p.ProjectReferences) > 0 {
|
|
hasRefs = true
|
|
break
|
|
}
|
|
}
|
|
|
|
output.H2(w, "Reference graph")
|
|
|
|
if !hasRefs {
|
|
fmt.Fprintf(w, "No inter-project references.\n\n")
|
|
return
|
|
}
|
|
|
|
// For complex graphs (>5 projects with references), use flat list
|
|
refsCount := 0
|
|
for _, p := range projects {
|
|
if len(p.ProjectReferences) > 0 {
|
|
refsCount++
|
|
}
|
|
}
|
|
|
|
if len(projects) > 5 && refsCount > 3 {
|
|
for _, p := range projects {
|
|
if len(p.ProjectReferences) > 0 {
|
|
refs := make([]string, len(p.ProjectReferences))
|
|
for i, r := range p.ProjectReferences {
|
|
refs[i] = "`" + r + "`"
|
|
}
|
|
fmt.Fprintf(w, "- `%s` → %s\n", p.Name, strings.Join(refs, ", "))
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
return
|
|
}
|
|
|
|
// DFS from roots (projects with no dependents)
|
|
roots := []string{}
|
|
for _, p := range projects {
|
|
if len(dependents[p.Name]) == 0 {
|
|
roots = append(roots, p.Name)
|
|
}
|
|
}
|
|
|
|
if len(roots) == 0 {
|
|
// Cycle or all have dependents — fall back to flat list
|
|
for _, p := range projects {
|
|
if len(p.ProjectReferences) > 0 {
|
|
refs := make([]string, len(p.ProjectReferences))
|
|
for i, r := range p.ProjectReferences {
|
|
refs[i] = "`" + r + "`"
|
|
}
|
|
fmt.Fprintf(w, "- `%s` → %s\n", p.Name, strings.Join(refs, ", "))
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
return
|
|
}
|
|
|
|
var sb strings.Builder
|
|
visited := map[string]bool{}
|
|
for _, root := range roots {
|
|
dfsRender(&sb, root, refMap, visited, 0)
|
|
}
|
|
output.CodeBlock(w, "", strings.TrimRight(sb.String(), "\n"))
|
|
}
|
|
|
|
func dfsRender(sb *strings.Builder, name string, refMap map[string][]string, visited map[string]bool, depth int) {
|
|
indent := strings.Repeat(" ", depth)
|
|
refs := refMap[name]
|
|
if len(refs) == 0 {
|
|
if depth == 0 {
|
|
fmt.Fprintf(sb, "%s%s (no deps)\n", indent, name)
|
|
} else {
|
|
fmt.Fprintf(sb, "%s%s\n", indent, name)
|
|
}
|
|
return
|
|
}
|
|
|
|
if visited[name] {
|
|
fmt.Fprintf(sb, "%s%s (see above)\n", indent, name)
|
|
return
|
|
}
|
|
visited[name] = true
|
|
|
|
for i, ref := range refs {
|
|
if i == 0 {
|
|
fmt.Fprintf(sb, "%s%s → %s\n", indent, name, ref)
|
|
} else {
|
|
fmt.Fprintf(sb, "%s%s └──→ %s\n", indent, strings.Repeat(" ", len(name)), ref)
|
|
}
|
|
dfsRender(sb, ref, refMap, visited, depth+1)
|
|
}
|
|
}
|
|
|
|
func writeMultiTargeting(w io.Writer, projects []helper.ProjectInfo) {
|
|
output.H2(w, "Multi-targeting")
|
|
|
|
// Collect all unique frameworks
|
|
frameworkSets := map[string][]string{} // project name → frameworks
|
|
allFrameworks := map[string]bool{}
|
|
|
|
for _, p := range projects {
|
|
if len(p.TargetFrameworks) > 1 {
|
|
frameworkSets[p.Name] = p.TargetFrameworks
|
|
}
|
|
for _, tf := range p.TargetFrameworks {
|
|
allFrameworks[tf] = true
|
|
}
|
|
}
|
|
|
|
if len(frameworkSets) == 0 {
|
|
// All same framework, or single framework each
|
|
if len(allFrameworks) == 1 {
|
|
for tf := range allFrameworks {
|
|
fmt.Fprintf(w, "None — all projects target `%s`.\n\n", tf)
|
|
}
|
|
} else if len(allFrameworks) == 0 {
|
|
fmt.Fprintf(w, "No target frameworks detected.\n\n")
|
|
} else {
|
|
// Multiple different single targets
|
|
for _, p := range projects {
|
|
if len(p.TargetFrameworks) > 0 {
|
|
fmt.Fprintf(w, "- `%s` targets: `%s`\n", p.Name, strings.Join(p.TargetFrameworks, "`, `"))
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, p := range projects {
|
|
if tfs, ok := frameworkSets[p.Name]; ok {
|
|
fmt.Fprintf(w, "- `%s` targets: `%s`\n", p.Name, strings.Join(tfs, "`, `"))
|
|
}
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|