commit b75fbbedb93f26b20deeed7e1c8d65d230798605 Author: AJ Date: Fri Oct 24 20:28:13 2025 -0400 Added Files diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c9a095a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)", + "Bash(curl:*)", + "Bash(python:*)", + "Bash(powershell -Command \"$body = @{queryType=''getdocument''; filePath=''C:\\Users\\AJ\\Desktop\\PepLib\\PepLib\\Program.cs''} | ConvertTo-Json; Invoke-RestMethod -Uri ''http://localhost:59123/query'' -Method Post -Body $body -ContentType ''application/json'' | ConvertTo-Json -Depth 10\")", + "Bash(powershell -Command:*)", + "Bash(ls:*)", + "Bash(mkdir:*)", + "Skill(roslyn-api)", + "Bash(move:*)", + "Bash(tree:*)", + "Bash(dir:*)", + "Bash(git config:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.claude/skills/roslyn-api/SKILL.md b/.claude/skills/roslyn-api/SKILL.md new file mode 100644 index 0000000..62c282e --- /dev/null +++ b/.claude/skills/roslyn-api/SKILL.md @@ -0,0 +1,134 @@ +--- +name: roslyn-api +description: Use this for C# code analysis, querying .NET projects, finding symbols, getting diagnostics, or any Roslyn/semantic analysis tasks using the bridge server +--- + +# Roslyn API Testing Guide + +Use this guide when testing or accessing the Claude Roslyn Bridge HTTP endpoints. + +## Server Info +- **Base URL**: `http://localhost:59123/query` +- **Method**: POST +- **Content-Type**: application/json + +## Correct Command Syntax + +### Using curl (Recommended on Windows) + +```bash +# Test if server is running +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getprojects\"}" + +# Get document info +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getdocument\",\"filePath\":\"C:\\\\path\\\\to\\\\file.cs\"}" + +# Get symbol at position +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getsymbol\",\"filePath\":\"C:\\\\Users\\\\AJ\\\\Desktop\\\\PepLib\\\\PepLib\\\\Program.cs\",\"line\":10,\"column\":5}" +``` + +**IMPORTANT curl syntax rules:** +- Use `-X POST` (NOT `-Method POST`) +- Use `-H` for headers (NOT `-Headers`) +- Use `-d` for data (NOT `-Body`) +- Escape backslashes in file paths: `\\\\` becomes `\\` in JSON + +### Using PowerShell (Alternative) + +Run these commands **directly in PowerShell**, NOT via `powershell -Command`: + +```powershell +# Test server +$body = @{queryType='getprojects'} | ConvertTo-Json +Invoke-RestMethod -Uri 'http://localhost:59123/query' -Method Post -Body $body -ContentType 'application/json' + +# Get document +$body = @{ + queryType='getdocument' + filePath='C:\path\to\file.cs' +} | ConvertTo-Json +Invoke-RestMethod -Uri 'http://localhost:59123/query' -Method Post -Body $body -ContentType 'application/json' +``` + +**IMPORTANT PowerShell rules:** +- Run directly in PowerShell console, NOT via `bash -c` or `powershell -Command` +- Use single quotes around URI and content type +- File paths don't need escaping in PowerShell hash tables + +## Quick Reference: All Endpoints + +### Query Endpoints + +| Endpoint | Required Fields | Optional Fields | +|----------|----------------|-----------------| +| `getprojects` | - | - | +| `getdocument` | `filePath` | - | +| `getsymbol` | `filePath`, `line`, `column` | - | +| `getdiagnostics` | - | `filePath` | +| `findreferences` | `filePath`, `line`, `column` | - | +| `findsymbol` | `symbolName` | `parameters.kind` | +| `gettypemembers` | `symbolName` | `parameters.includeInherited` | +| `gettypehierarchy` | `symbolName` | `parameters.direction` | +| `findimplementations` | `symbolName` OR `filePath`+`line`+`column` | - | +| `getnamespacetypes` | `symbolName` | - | +| `getcallhierarchy` | `filePath`, `line`, `column` | `parameters.direction` | +| `getsolutionoverview` | - | - | +| `getsymbolcontext` | `filePath`, `line`, `column` | - | +| `searchcode` | `symbolName` (regex) | `parameters.scope` | + +### Editing Endpoints + +| Endpoint | Required Fields | Optional Fields | +|----------|----------------|-----------------| +| `formatdocument` | `filePath` | - | +| `organizeusings` | `filePath` | - | +| `renamesymbol` | `filePath`, `line`, `column`, `parameters.newName` | - | +| `addmissingusing` | `filePath`, `line`, `column` | - | +| `applycodefix` | `filePath`, `line`, `column` | - | + +## Common Test Examples + +```bash +# Get all projects in solution +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getprojects\"}" + +# Get solution overview +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getsolutionoverview\"}" + +# Get diagnostics for entire solution +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getdiagnostics\"}" + +# Find all classes containing "Helper" +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"searchcode\",\"symbolName\":\".*Helper\",\"parameters\":{\"scope\":\"classes\"}}" + +# Format a document +curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"formatdocument\",\"filePath\":\"C:\\\\path\\\\to\\\\file.cs\"}" +``` + +## Response Format + +Success: +```json +{ + "success": true, + "message": "Optional message", + "data": { /* Response data */ } +} +``` + +Error: +```json +{ + "success": false, + "error": "Error message", + "data": null +} +``` + +## Notes + +- Line numbers are **1-based** +- Column numbers are **0-based** +- File paths in JSON need escaped backslashes: `C:\\path\\to\\file.cs` +- All workspace modifications use VS threading model (`JoinableTaskFactory.SwitchToMainThreadAsync()`) +- The Visual Studio extension must be running for endpoints to work diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7c9243 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ + +#Ignore thumbnails created by Windows +Thumbs.db +#Ignore files built by Visual Studio +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ +[Rr]elease*/ +_ReSharper*/ +[Tt]est[Rr]esult* +.vs/ +.idea/ +#Nuget packages folder +packages/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5aef38e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ + # Repository Guidelines + + ## Project Structure & Module Organization + - `RoslynBridge/` – VSIX source (C#). Key folders: `Server/` (HTTP host), `Services/` (Roslyn queries, refactorings), `Models/`, `Constants/`. + - Build artifacts: `RoslynBridge/bin/` and `RoslynBridge/obj/`. + - Claude skills: `.claude/skills/roslyn-api/SKILL.md` (HTTP query reference for local discovery/testing). + + ## Build, Test, and Development Commands + - Build (CLI): `msbuild RoslynBridge.sln /p:Configuration=Debug` + - Build (VS): Open `RoslynBridge.sln`, set `RoslynBridge` as startup, press F5 to launch the Experimental Instance. + - Health check (server running in VS): + - PowerShell: `$b=@{queryType='getprojects'}|ConvertTo-Json; Invoke-RestMethod -Uri 'http://localhost:59123/query' -Method Post -Body $b -ContentType 'application/json'` + - curl (Windows): `curl -X POST http://localhost:59123/query -H "Content-Type: application/json" -d "{\"queryType\":\"getprojects\"}"` + + ## Coding Style & Naming Conventions + - Language: C# (net48 VSIX). Use 4‑space indentation; braces on new lines. + - Naming: `PascalCase` for types/methods; `camelCase` for locals; private fields as `_camelCase`. + - Prefer `async`/`await`, avoid blocking the UI thread; use `JoinableTaskFactory` when switching. + - Keep nullable annotations consistent with project settings. + - Run Format Document and Organize Usings before commits. + + ## Testing Guidelines + - No unit test project yet. Validate via HTTP endpoints (see SKILL.md): `getprojects`, `getdiagnostics`, `getsolutionoverview`, `getsymbol`, etc. + - Expected response shape: `{ success, message, data, error }` (JSON). + - Lines are 1‑based; columns 0‑based. File paths in JSON require escaped backslashes. + + ## Commit & Pull Request Guidelines + - Commits: concise, imperative subject (e.g., "Add diagnostics endpoint"), with short body explaining rationale and scope. + - PRs: include description, linked issues, sample requests/responses, and screenshots when UI/VS behavior is affected. + - Checklist: builds clean, `getdiagnostics` shows no new errors, code formatted, usings organized. + + ## Security & Configuration Tips + - Server binds to `http://localhost:59123/` and accepts only POST; CORS is permissive for local tooling. Do not expose externally. + - Endpoints: `/query` (main), `/health` route exists but still requires POST. + - Adjust port/paths in `RoslynBridge/Constants/ServerConstants.cs` if needed. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c1b9549 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 AJ Isaacs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/RoslynBridge.sln b/RoslynBridge.sln new file mode 100644 index 0000000..1daf1bb --- /dev/null +++ b/RoslynBridge.sln @@ -0,0 +1,21 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoslynBridge", "RoslynBridge\RoslynBridge.csproj", "{B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/RoslynBridge/ClaudeRoslynBridgePackage.cs b/RoslynBridge/ClaudeRoslynBridgePackage.cs new file mode 100644 index 0000000..c67b35f --- /dev/null +++ b/RoslynBridge/ClaudeRoslynBridgePackage.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.VisualStudio.Shell; +using Task = System.Threading.Tasks.Task; + +namespace RoslynBridge +{ + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [Guid(ClaudeRoslynBridgePackage.PackageGuidString)] + [ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)] + [ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] + public sealed class ClaudeRoslynBridgePackage : AsyncPackage + { + public const string PackageGuidString = "b2c3d4e5-f6a7-4b5c-9d8e-0f1a2b3c4d5e"; + private HttpBridgeServer? _bridgeServer; + + protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + { + await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + try + { + // Initialize the HTTP bridge server + _bridgeServer = new HttpBridgeServer(this); + await _bridgeServer.StartAsync(); + + await base.InitializeAsync(cancellationToken, progress); + } + catch (Exception ex) + { + // Log error - you might want to add proper logging here + System.Diagnostics.Debug.WriteLine($"Failed to initialize Roslyn Bridge: {ex}"); + throw; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _bridgeServer?.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/RoslynBridge/Constants/ServerConstants.cs b/RoslynBridge/Constants/ServerConstants.cs new file mode 100644 index 0000000..b87a422 --- /dev/null +++ b/RoslynBridge/Constants/ServerConstants.cs @@ -0,0 +1,43 @@ +namespace RoslynBridge.Constants +{ + public static class ServerConstants + { + public const int DefaultPort = 59123; + public const string LocalhostUrl = "http://localhost"; + public const string QueryEndpoint = "/query"; + public const string HealthEndpoint = "/health"; + public const string ContentTypeJson = "application/json"; + + public static string GetServerUrl(int port = DefaultPort) => $"{LocalhostUrl}:{port}/"; + } + + public static class QueryTypes + { + // Query endpoints + public const string GetSymbol = "getsymbol"; + public const string GetDocument = "getdocument"; + public const string GetProjects = "getprojects"; + public const string GetDiagnostics = "getdiagnostics"; + public const string FindReferences = "findreferences"; + public const string GetSemanticModel = "getsemanticmodel"; + public const string GetSyntaxTree = "getsyntaxtree"; + + // Discovery endpoints + public const string FindSymbol = "findsymbol"; + public const string GetTypeMembers = "gettypemembers"; + public const string GetTypeHierarchy = "gettypehierarchy"; + public const string FindImplementations = "findimplementations"; + public const string GetNamespaceTypes = "getnamespacetypes"; + public const string GetCallHierarchy = "getcallhierarchy"; + public const string GetSolutionOverview = "getsolutionoverview"; + public const string GetSymbolContext = "getsymbolcontext"; + public const string SearchCode = "searchcode"; + + // Editing endpoints + public const string ApplyCodeFix = "applycodefix"; + public const string FormatDocument = "formatdocument"; + public const string RenameSymbol = "renamesymbol"; + public const string OrganizeUsings = "organizeusings"; + public const string AddMissingUsing = "addmissingusing"; + } +} diff --git a/RoslynBridge/Models/CallHierarchyModels.cs b/RoslynBridge/Models/CallHierarchyModels.cs new file mode 100644 index 0000000..7279736 --- /dev/null +++ b/RoslynBridge/Models/CallHierarchyModels.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace RoslynBridge.Models +{ + public class CallHierarchyInfo + { + public string? SymbolName { get; set; } + public List? Calls { get; set; } + } + + public class CallInfo + { + public string? CallerName { get; set; } + public string? CallerType { get; set; } + public LocationInfo? Location { get; set; } + } +} diff --git a/RoslynBridge/Models/DiagnosticModels.cs b/RoslynBridge/Models/DiagnosticModels.cs new file mode 100644 index 0000000..f630007 --- /dev/null +++ b/RoslynBridge/Models/DiagnosticModels.cs @@ -0,0 +1,17 @@ +namespace RoslynBridge.Models +{ + public class DiagnosticInfo + { + public string? Id { get; set; } + public string? Severity { get; set; } + public string? Message { get; set; } + public LocationInfo? Location { get; set; } + } + + public class CodeFixInfo + { + public string? Title { get; set; } + public string? DiagnosticId { get; set; } + public string? Description { get; set; } + } +} diff --git a/RoslynBridge/Models/DocumentModels.cs b/RoslynBridge/Models/DocumentModels.cs new file mode 100644 index 0000000..f725906 --- /dev/null +++ b/RoslynBridge/Models/DocumentModels.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace RoslynBridge.Models +{ + public class DocumentInfo + { + public string? FilePath { get; set; } + public string? Name { get; set; } + public string? ProjectName { get; set; } + public List? Usings { get; set; } + public List? Classes { get; set; } + public List? Interfaces { get; set; } + public List? Enums { get; set; } + } + + public class DocumentChangeInfo + { + public string? FilePath { get; set; } + public List? Changes { get; set; } + public string? NewText { get; set; } + } + + public class TextChangeInfo + { + public int StartLine { get; set; } + public int StartColumn { get; set; } + public int EndLine { get; set; } + public int EndColumn { get; set; } + public string? OldText { get; set; } + public string? NewText { get; set; } + } +} diff --git a/RoslynBridge/Models/LocationModels.cs b/RoslynBridge/Models/LocationModels.cs new file mode 100644 index 0000000..bf045b9 --- /dev/null +++ b/RoslynBridge/Models/LocationModels.cs @@ -0,0 +1,11 @@ +namespace RoslynBridge.Models +{ + public class LocationInfo + { + public string? FilePath { get; set; } + public int StartLine { get; set; } + public int StartColumn { get; set; } + public int EndLine { get; set; } + public int EndColumn { get; set; } + } +} diff --git a/RoslynBridge/Models/ProjectModels.cs b/RoslynBridge/Models/ProjectModels.cs new file mode 100644 index 0000000..609ac60 --- /dev/null +++ b/RoslynBridge/Models/ProjectModels.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace RoslynBridge.Models +{ + public class ProjectInfo + { + public string? Name { get; set; } + public string? FilePath { get; set; } + public List? Documents { get; set; } + public List? References { get; set; } + } + + public class ProjectSummary + { + public string? Name { get; set; } + public int FileCount { get; set; } + public List? TopNamespaces { get; set; } + } + + public class SolutionOverview + { + public int ProjectCount { get; set; } + public int DocumentCount { get; set; } + public List? TopLevelNamespaces { get; set; } + public List? Projects { get; set; } + } +} diff --git a/RoslynBridge/Models/RefactoringModels.cs b/RoslynBridge/Models/RefactoringModels.cs new file mode 100644 index 0000000..ffe2b4e --- /dev/null +++ b/RoslynBridge/Models/RefactoringModels.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace RoslynBridge.Models +{ + public class RenameResult + { + public List? ChangedDocuments { get; set; } + public int TotalChanges { get; set; } + } +} diff --git a/RoslynBridge/Models/RequestModels.cs b/RoslynBridge/Models/RequestModels.cs new file mode 100644 index 0000000..0bae635 --- /dev/null +++ b/RoslynBridge/Models/RequestModels.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace RoslynBridge.Models +{ + public class QueryRequest + { + public string? QueryType { get; set; } + public string? FilePath { get; set; } + public string? SymbolName { get; set; } + public int? Line { get; set; } + public int? Column { get; set; } + public Dictionary? Parameters { get; set; } + } + + public class QueryResponse + { + public bool Success { get; set; } + public string? Message { get; set; } + public object? Data { get; set; } + public string? Error { get; set; } + } +} diff --git a/RoslynBridge/Models/SymbolModels.cs b/RoslynBridge/Models/SymbolModels.cs new file mode 100644 index 0000000..42f855a --- /dev/null +++ b/RoslynBridge/Models/SymbolModels.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace RoslynBridge.Models +{ + public class SymbolInfo + { + public string? Name { get; set; } + public string? Kind { get; set; } + public string? Type { get; set; } + public string? ContainingType { get; set; } + public string? ContainingNamespace { get; set; } + public List? Locations { get; set; } + public string? Documentation { get; set; } + public List? Modifiers { get; set; } + } + + public class MemberInfo + { + public string? Name { get; set; } + public string? Kind { get; set; } + public string? ReturnType { get; set; } + public string? Signature { get; set; } + public string? Documentation { get; set; } + public List? Modifiers { get; set; } + public string? Accessibility { get; set; } + public bool IsStatic { get; set; } + public bool IsAbstract { get; set; } + public bool IsVirtual { get; set; } + public bool IsOverride { get; set; } + } + + public class TypeHierarchyInfo + { + public string? TypeName { get; set; } + public string? FullName { get; set; } + public List? BaseTypes { get; set; } + public List? Interfaces { get; set; } + public List? DerivedTypes { get; set; } + } + + public class NamespaceTypeInfo + { + public string? Name { get; set; } + public string? Kind { get; set; } + public string? FullName { get; set; } + public string? Summary { get; set; } + } + + public class SymbolContextInfo + { + public string? ContainingClass { get; set; } + public string? ContainingMethod { get; set; } + public string? ContainingNamespace { get; set; } + public string? SymbolAtPosition { get; set; } + public List? LocalVariables { get; set; } + public List? Parameters { get; set; } + } +} diff --git a/RoslynBridge/README.txt b/RoslynBridge/README.txt new file mode 100644 index 0000000..a2f39ed --- /dev/null +++ b/RoslynBridge/README.txt @@ -0,0 +1 @@ +See documentation for setup and instructions. \ No newline at end of file diff --git a/RoslynBridge/RoslynBridge.csproj b/RoslynBridge/RoslynBridge.csproj new file mode 100644 index 0000000..6172f31 --- /dev/null +++ b/RoslynBridge/RoslynBridge.csproj @@ -0,0 +1,134 @@ + + + + 17.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + v4.8 + latest + enable + + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E} + Library + Properties + RoslynBridge + RoslynBridge + v4.8 + true + true + true + false + false + true + true + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + Always + true + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RoslynBridge/RoslynBridgePackage.cs b/RoslynBridge/RoslynBridgePackage.cs new file mode 100644 index 0000000..a0be444 --- /dev/null +++ b/RoslynBridge/RoslynBridgePackage.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Server; +using Task = System.Threading.Tasks.Task; + +namespace RoslynBridge +{ + /// + /// Visual Studio package that provides Roslyn API access via HTTP bridge + /// + [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] + [Guid(RoslynBridgePackage.PackageGuidString)] + [ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)] + [ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] + public sealed class RoslynBridgePackage : AsyncPackage + { + public const string PackageGuidString = "b2c3d4e5-f6a7-4b5c-9d8e-0f1a2b3c4d5e"; + private BridgeServer? _bridgeServer; + + protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + { + await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + try + { + // Initialize the HTTP bridge server + _bridgeServer = new BridgeServer(this); + await _bridgeServer.StartAsync(); + + await base.InitializeAsync(cancellationToken, progress); + } + catch (Exception ex) + { + // Log error - you might want to add proper logging here + System.Diagnostics.Debug.WriteLine($"Failed to initialize Roslyn Bridge: {ex}"); + throw; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _bridgeServer?.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/RoslynBridge/Server/BridgeServer.cs b/RoslynBridge/Server/BridgeServer.cs new file mode 100644 index 0000000..f7222ab --- /dev/null +++ b/RoslynBridge/Server/BridgeServer.cs @@ -0,0 +1,217 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Constants; +using RoslynBridge.Models; +using RoslynBridge.Services; +using Task = System.Threading.Tasks.Task; + +namespace RoslynBridge.Server +{ + public class BridgeServer : IDisposable + { + private HttpListener? _listener; + private readonly AsyncPackage _package; + private readonly IRoslynQueryService _queryService; + private bool _isRunning; + private readonly int _port; + + public BridgeServer(AsyncPackage package, int port = ServerConstants.DefaultPort) + { + _package = package; + _port = port; + _queryService = new RoslynQueryService(package); + } + + public async Task StartAsync() + { + if (_isRunning) + { + return; + } + + try + { + await _queryService.InitializeAsync(); + + _listener = new HttpListener(); + _listener.Prefixes.Add(ServerConstants.GetServerUrl(_port)); + _listener.Start(); + _isRunning = true; + + System.Diagnostics.Debug.WriteLine($"Roslyn Bridge HTTP server started on port {_port}"); + + // Start listening for requests in the background +#pragma warning disable CS4014 + Task.Run(() => ListenForRequestsAsync()); +#pragma warning restore CS4014 + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to start HTTP server: {ex}"); + throw; + } + } + + private async Task ListenForRequestsAsync() + { + while (_isRunning && _listener != null && _listener.IsListening) + { + try + { + var context = await _listener.GetContextAsync(); +#pragma warning disable CS4014 + Task.Run(() => HandleRequestAsync(context)); +#pragma warning restore CS4014 + } + catch (HttpListenerException) + { + // Listener was stopped + break; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error in request listener: {ex}"); + } + } + } + + private async Task HandleRequestAsync(HttpListenerContext context) + { + var response = context.Response; + ConfigureCorsHeaders(response); + + try + { + // Handle CORS preflight + if (context.Request.HttpMethod == "OPTIONS") + { + response.StatusCode = 200; + response.Close(); + return; + } + + if (context.Request.HttpMethod != "POST") + { + await RespondWithError(response, 405, "Only POST requests are supported"); + return; + } + + // Read and parse request + var request = await ParseRequestAsync(context.Request); + if (request == null) + { + await RespondWithError(response, 400, "Invalid request format"); + return; + } + + // Route request + var queryResponse = await RouteRequestAsync(context.Request.Url?.AbsolutePath ?? "/", request); + response.StatusCode = queryResponse.Success ? 200 : 400; + await WriteResponseAsync(response, queryResponse); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error handling request: {ex}"); + await RespondWithError(response, 500, ex.Message); + } + } + + private static void ConfigureCorsHeaders(HttpListenerResponse response) + { + response.Headers.Add("Access-Control-Allow-Origin", "*"); + response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); + response.Headers.Add("Access-Control-Allow-Headers", "Content-Type"); + } + + private static async Task ParseRequestAsync(HttpListenerRequest request) + { + try + { + using var reader = new StreamReader(request.InputStream, request.ContentEncoding); + var requestBody = await reader.ReadToEndAsync(); + + return JsonSerializer.Deserialize(requestBody, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + } + catch + { + return null; + } + } + + private async Task RouteRequestAsync(string path, QueryRequest request) + { + return path.ToLowerInvariant() switch + { + ServerConstants.QueryEndpoint => await _queryService.ExecuteQueryAsync(request), + ServerConstants.HealthEndpoint => new QueryResponse + { + Success = true, + Message = "Roslyn Bridge is running" + }, + _ => new QueryResponse + { + Success = false, + Error = $"Unknown endpoint: {path}" + } + }; + } + + private static async Task RespondWithError(HttpListenerResponse response, int statusCode, string errorMessage) + { + response.StatusCode = statusCode; + await WriteResponseAsync(response, new QueryResponse + { + Success = false, + Error = errorMessage + }); + } + + private static async Task WriteResponseAsync(HttpListenerResponse response, QueryResponse queryResponse) + { + try + { + response.ContentType = ServerConstants.ContentTypeJson; + var json = JsonSerializer.Serialize(queryResponse, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }); + + var buffer = Encoding.UTF8.GetBytes(json); + response.ContentLength64 = buffer.Length; + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.Close(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error writing response: {ex}"); + } + } + + public void Dispose() + { + _isRunning = false; + + if (_listener != null) + { + try + { + _listener.Stop(); + _listener.Close(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error stopping HTTP server: {ex}"); + } + } + } + } +} diff --git a/RoslynBridge/Services/BaseRoslynService.cs b/RoslynBridge/Services/BaseRoslynService.cs new file mode 100644 index 0000000..b1ac8a1 --- /dev/null +++ b/RoslynBridge/Services/BaseRoslynService.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Models; +using System.Threading.Tasks; + +namespace RoslynBridge.Services +{ + public abstract class BaseRoslynService + { + protected readonly AsyncPackage Package; + protected readonly IWorkspaceProvider WorkspaceProvider; + + protected VisualStudioWorkspace? Workspace => WorkspaceProvider.Workspace; + + protected BaseRoslynService(AsyncPackage package, IWorkspaceProvider workspaceProvider) + { + Package = package; + WorkspaceProvider = workspaceProvider; + } + + protected async Task CreateSymbolInfoAsync(ISymbol symbol) + { + var locations = symbol.Locations.Where(loc => loc.IsInSource) + .Select(loc => new LocationInfo + { + FilePath = loc.SourceTree?.FilePath, + StartLine = loc.GetLineSpan().StartLinePosition.Line + 1, + StartColumn = loc.GetLineSpan().StartLinePosition.Character, + EndLine = loc.GetLineSpan().EndLinePosition.Line + 1, + EndColumn = loc.GetLineSpan().EndLinePosition.Character + }).ToList(); + + return await Task.FromResult(new RoslynBridge.Models.SymbolInfo + { + Name = symbol.Name, + Kind = symbol.Kind.ToString(), + Type = (symbol as ITypeSymbol)?.ToDisplayString(), + ContainingType = symbol.ContainingType?.Name, + ContainingNamespace = symbol.ContainingNamespace?.ToDisplayString(), + Locations = locations, + Documentation = symbol.GetDocumentationCommentXml(), + Modifiers = symbol.DeclaringSyntaxReferences.Length > 0 + ? symbol.DeclaringSyntaxReferences[0].GetSyntax().ChildTokens() + .Where(t => SyntaxFacts.IsKeywordKind(t.Kind())) + .Select(t => t.Text) + .ToList() + : new List() + }); + } + + protected DiagnosticInfo CreateDiagnosticInfo(Diagnostic diagnostic) + { + var location = diagnostic.Location.GetLineSpan(); + return new DiagnosticInfo + { + Id = diagnostic.Id, + Severity = diagnostic.Severity.ToString(), + Message = diagnostic.GetMessage(), + Location = new LocationInfo + { + FilePath = location.Path, + StartLine = location.StartLinePosition.Line + 1, + StartColumn = location.StartLinePosition.Character, + EndLine = location.EndLinePosition.Line + 1, + EndColumn = location.EndLinePosition.Character + } + }; + } + + protected List GetModifiers(ISymbol symbol) + { + var modifiers = new List(); + + if (symbol.IsStatic) modifiers.Add("static"); + if (symbol.IsAbstract) modifiers.Add("abstract"); + if (symbol.IsVirtual) modifiers.Add("virtual"); + if (symbol.IsOverride) modifiers.Add("override"); + if (symbol.IsSealed) modifiers.Add("sealed"); + + modifiers.Add(symbol.DeclaredAccessibility.ToString().ToLower()); + + return modifiers; + } + + protected string? ExtractSummary(string? xmlDoc) + { + if (string.IsNullOrEmpty(xmlDoc)) return null; + + var summaryStart = xmlDoc.IndexOf("", StringComparison.Ordinal); + var summaryEnd = xmlDoc.IndexOf("", StringComparison.Ordinal); + + if (summaryStart >= 0 && summaryEnd > summaryStart) + { + return xmlDoc.Substring(summaryStart + 9, summaryEnd - summaryStart - 9).Trim(); + } + + return null; + } + + protected IEnumerable GetNamespaces(INamespaceSymbol root) + { + yield return root; + foreach (var child in root.GetNamespaceMembers()) + { + foreach (var ns in GetNamespaces(child)) + { + yield return ns; + } + } + } + + protected INamedTypeSymbol? FindTypeInAssembly(INamespaceSymbol namespaceSymbol, string typeName) + { + var types = namespaceSymbol.GetTypeMembers(typeName); + if (types.Any()) + { + return types.First(); + } + + foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers()) + { + var result = FindTypeInAssembly(childNamespace, typeName); + if (result != null) + { + return result; + } + } + + return null; + } + + protected Document? FindDocument(string filePath) + { + var solution = Workspace?.CurrentSolution; + if (solution == null) return null; + + var ids = solution.GetDocumentIdsWithFilePath(filePath); + if (ids != null && ids.Any()) + { + return solution.GetDocument(ids[0]); + } + + var normalizedTarget = SafeFullPath(filePath); + var byFullPath = solution.Projects + .SelectMany(p => p.Documents) + .FirstOrDefault(d => SafeFullPath(d.FilePath) == normalizedTarget); + if (byFullPath != null) return byFullPath; + + var fileName = Path.GetFileName(filePath); + return solution.Projects + .SelectMany(p => p.Documents) + .FirstOrDefault(d => d.FilePath != null && + Path.GetFileName(d.FilePath).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + } + + private static string SafeFullPath(string? path) + { + try { return Path.GetFullPath(path ?? string.Empty).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant(); } + catch { return (path ?? string.Empty).Replace('/', '\\').TrimEnd('\\').ToLowerInvariant(); } + } + + protected QueryResponse CreateErrorResponse(string errorMessage) + { + return new QueryResponse { Success = false, Error = errorMessage }; + } + + protected QueryResponse CreateSuccessResponse(object? data = null, string? message = null) + { + return new QueryResponse + { + Success = true, + Data = data, + Message = message + }; + } + } +} diff --git a/RoslynBridge/Services/DiagnosticsService.cs b/RoslynBridge/Services/DiagnosticsService.cs new file mode 100644 index 0000000..0cbb212 --- /dev/null +++ b/RoslynBridge/Services/DiagnosticsService.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Models; + +namespace RoslynBridge.Services +{ + public class DiagnosticsService : BaseRoslynService + { + public DiagnosticsService(AsyncPackage package, IWorkspaceProvider workspaceProvider) + : base(package, workspaceProvider) + { + } + + public async Task GetDiagnosticsAsync(QueryRequest request) + { + var diagnostics = new List(); + + if (!string.IsNullOrEmpty(request.FilePath)) + { + var document = Workspace?.CurrentSolution.Projects + .SelectMany(p => p.Documents) + .FirstOrDefault(d => d.FilePath?.Equals(request.FilePath, StringComparison.OrdinalIgnoreCase) == true); + + if (document != null) + { + var semanticModel = await document.GetSemanticModelAsync(); + if (semanticModel != null) + { + var diags = semanticModel.GetDiagnostics(); + diagnostics.AddRange(diags.Select(d => CreateDiagnosticInfo(d))); + } + } + } + else + { + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation != null) + { + var diags = compilation.GetDiagnostics(); + diagnostics.AddRange(diags.Select(d => CreateDiagnosticInfo(d))); + } + } + } + + return new QueryResponse { Success = true, Data = diagnostics }; + } + + public async Task FindReferencesAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath) || !request.Line.HasValue || !request.Column.HasValue) + { + return new QueryResponse { Success = false, Error = "FilePath, Line, and Column are required" }; + } + + var document = Workspace?.CurrentSolution.Projects + .SelectMany(p => p.Documents) + .FirstOrDefault(d => d.FilePath?.Equals(request.FilePath, StringComparison.OrdinalIgnoreCase) == true); + + if (document == null) + { + return new QueryResponse { Success = false, Error = "Document not found" }; + } + + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + + if (semanticModel == null || syntaxRoot == null) + { + return new QueryResponse { Success = false, Error = "Could not get semantic model" }; + } + + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot.FindToken(position).Parent; + var symbol = semanticModel.GetSymbolInfo(node).Symbol; + + if (symbol == null) + { + return new QueryResponse { Success = false, Error = "Symbol not found" }; + } + + var references = await SymbolFinder.FindReferencesAsync(symbol, Workspace.CurrentSolution); + var locations = references.SelectMany(r => r.Locations) + .Select(loc => new LocationInfo + { + FilePath = loc.Document.FilePath, + StartLine = loc.Location.GetLineSpan().StartLinePosition.Line + 1, + StartColumn = loc.Location.GetLineSpan().StartLinePosition.Character, + EndLine = loc.Location.GetLineSpan().EndLinePosition.Line + 1, + EndColumn = loc.Location.GetLineSpan().EndLinePosition.Character + }).ToList(); + + return new QueryResponse { Success = true, Data = locations }; + } +} +} diff --git a/RoslynBridge/Services/DocumentQueryService.cs b/RoslynBridge/Services/DocumentQueryService.cs new file mode 100644 index 0000000..ad8c20d --- /dev/null +++ b/RoslynBridge/Services/DocumentQueryService.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Models; + +namespace RoslynBridge.Services +{ + public class DocumentQueryService : BaseRoslynService + { + public DocumentQueryService(AsyncPackage package, IWorkspaceProvider workspaceProvider) + : base(package, workspaceProvider) + { + } + + public async Task GetDocumentInfoAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath)) + { + return CreateErrorResponse("FilePath is required"); + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return CreateErrorResponse("Document not found"); + } + + var syntaxRoot = await document.GetSyntaxRootAsync(); + if (syntaxRoot == null) + { + return CreateErrorResponse("Could not get syntax tree"); + } + + var docInfo = new Models.DocumentInfo + { + FilePath = document.FilePath, + Name = document.Name, + ProjectName = document.Project.Name, + Usings = syntaxRoot.DescendantNodes().OfType() + .Select(u => u.Name?.ToString() ?? string.Empty).ToList(), + Classes = syntaxRoot.DescendantNodes().OfType() + .Select(c => c.Identifier.Text).ToList(), + Interfaces = syntaxRoot.DescendantNodes().OfType() + .Select(i => i.Identifier.Text).ToList(), + Enums = syntaxRoot.DescendantNodes().OfType() + .Select(e => e.Identifier.Text).ToList() + }; + + return CreateSuccessResponse(docInfo); + } + + public async Task GetProjectsAsync(QueryRequest request) + { + await Task.CompletedTask; + + var projects = Workspace?.CurrentSolution.Projects.Select(p => new Models.ProjectInfo + { + Name = p.Name, + FilePath = p.FilePath, + Documents = p.Documents.Select(d => d.FilePath ?? string.Empty).ToList(), + References = p.MetadataReferences.Select(r => r.Display ?? string.Empty).ToList() + }).ToList(); + + return CreateSuccessResponse(projects); + } + + public async Task GetSyntaxTreeAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath)) + { + return CreateErrorResponse("FilePath is required"); + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return CreateErrorResponse("Document not found"); + } + + var syntaxTree = await document.GetSyntaxTreeAsync(); + if (syntaxTree == null) + { + return CreateErrorResponse("Could not get syntax tree"); + } + + var root = await syntaxTree.GetRootAsync(); + return CreateSuccessResponse(new + { + FilePath = syntaxTree.FilePath, + Text = root.ToFullString() + }); + } + + public async Task GetSemanticModelAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath)) + { + return CreateErrorResponse("FilePath is required"); + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return CreateErrorResponse("Document not found"); + } + + var semanticModel = await document.GetSemanticModelAsync(); + if (semanticModel == null) + { + return CreateErrorResponse("Could not get semantic model"); + } + + return CreateSuccessResponse(message: "Semantic model retrieved successfully (not serializable, use specific queries)"); + } + + public async Task GetSolutionOverviewAsync(QueryRequest request) + { + var projects = Workspace?.CurrentSolution.Projects.ToList() ?? new List(); + var overview = new SolutionOverview + { + ProjectCount = projects.Count, + DocumentCount = projects.Sum(p => p.Documents.Count()), + TopLevelNamespaces = new List(), + Projects = new List() + }; + + var allNamespaces = new HashSet(); + + foreach (var project in projects) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + var projectNamespaces = new HashSet(); + var globalNamespace = compilation.GlobalNamespace; + + foreach (var ns in GetNamespaces(globalNamespace)) + { + var nsName = ns.ToDisplayString(); + if (!string.IsNullOrEmpty(nsName)) + { + allNamespaces.Add(nsName.Split('.').First()); + projectNamespaces.Add(nsName); + } + } + + overview.Projects.Add(new ProjectSummary + { + Name = project.Name, + FileCount = project.Documents.Count(), + TopNamespaces = projectNamespaces.Take(10).ToList() + }); + } + + overview.TopLevelNamespaces = allNamespaces.ToList(); + + return CreateSuccessResponse(overview); + } + + public async Task GetNamespaceTypesAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.SymbolName)) + { + return CreateErrorResponse("SymbolName (namespace name) is required"); + } + + var types = new List(); + + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + var namespaceSymbol = compilation.GetSymbolsWithName( + name => name == request.SymbolName, + SymbolFilter.Namespace + ).FirstOrDefault() as INamespaceSymbol; + + if (namespaceSymbol != null) + { + var namespaceTypes = namespaceSymbol.GetTypeMembers(); + foreach (var type in namespaceTypes) + { + types.Add(new NamespaceTypeInfo + { + Name = type.Name, + Kind = type.TypeKind.ToString(), + FullName = type.ToDisplayString(), + Summary = ExtractSummary(type.GetDocumentationCommentXml()) + }); + } + + return CreateSuccessResponse(types); + } + } + + return CreateErrorResponse($"Namespace '{request.SymbolName}' not found"); + } + + public async Task SearchCodeAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.SymbolName)) + { + return CreateErrorResponse("SymbolName (search pattern) is required"); + } + + string? scope = null; + request.Parameters?.TryGetValue("scope", out scope); + scope = scope ?? "all"; + var results = new List(); + + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + var symbols = compilation.GetSymbolsWithName( + name => System.Text.RegularExpressions.Regex.IsMatch(name, request.SymbolName), + SymbolFilter.All + ); + + foreach (var symbol in symbols) + { + // Filter by scope + if (scope != "all") + { + var symbolKind = symbol.Kind.ToString().ToLowerInvariant(); + if (scope == "methods" && symbolKind != "method") continue; + if (scope == "classes" && symbolKind != "namedtype") continue; + if (scope == "properties" && symbolKind != "property") continue; + } + + results.Add(await CreateSymbolInfoAsync(symbol)); + } + } + + return CreateSuccessResponse(results); + } + } +} diff --git a/RoslynBridge/Services/IRoslynQueryService.cs b/RoslynBridge/Services/IRoslynQueryService.cs new file mode 100644 index 0000000..08728e0 --- /dev/null +++ b/RoslynBridge/Services/IRoslynQueryService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using RoslynBridge.Models; + +namespace RoslynBridge.Services +{ + public interface IRoslynQueryService + { + Task InitializeAsync(); + Task ExecuteQueryAsync(QueryRequest request); + } +} diff --git a/RoslynBridge/Services/IWorkspaceProvider.cs b/RoslynBridge/Services/IWorkspaceProvider.cs new file mode 100644 index 0000000..802ab69 --- /dev/null +++ b/RoslynBridge/Services/IWorkspaceProvider.cs @@ -0,0 +1,9 @@ +using Microsoft.VisualStudio.LanguageServices; + +namespace RoslynBridge.Services +{ + public interface IWorkspaceProvider + { + VisualStudioWorkspace? Workspace { get; } + } +} diff --git a/RoslynBridge/Services/RefactoringService.cs b/RoslynBridge/Services/RefactoringService.cs new file mode 100644 index 0000000..270d032 --- /dev/null +++ b/RoslynBridge/Services/RefactoringService.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Rename; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Models; + +namespace RoslynBridge.Services +{ + public class RefactoringService : BaseRoslynService + { + public RefactoringService(AsyncPackage package, IWorkspaceProvider workspaceProvider) + : base(package, workspaceProvider) + { + } + + public async Task FormatDocumentAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath)) + { + return new QueryResponse { Success = false, Error = "FilePath is required" }; + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return new QueryResponse { Success = false, Error = "Document not found" }; + } + + var formattedDocument = await Formatter.FormatAsync(document); + var text = await formattedDocument.GetTextAsync(); + + // Apply the changes to the workspace + await Package.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (Workspace != null && Workspace.TryApplyChanges(formattedDocument.Project.Solution)) + { + return new QueryResponse + { + Success = true, + Message = "Document formatted successfully", + Data = new DocumentChangeInfo + { + FilePath = request.FilePath, + NewText = text.ToString() + } + }; + } + + return new QueryResponse { Success = false, Error = "Failed to apply formatting changes" }; + } + + public async Task OrganizeUsingsAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath)) + { + return new QueryResponse { Success = false, Error = "FilePath is required" }; + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return new QueryResponse { Success = false, Error = "Document not found" }; + } + + var root = await document.GetSyntaxRootAsync(); + if (root == null) + { + return new QueryResponse { Success = false, Error = "Could not get syntax root" }; + } + + // Remove unused usings and sort + var newRoot = root; + + // Get all using directives + var usings = root.DescendantNodes().OfType().ToList(); + if (usings.Any()) + { + // Sort usings alphabetically + var sortedUsings = usings.OrderBy(u => u.Name?.ToString()).ToList(); + + // Replace them in order + for (int i = 0; i < usings.Count; i++) + { + newRoot = newRoot.ReplaceNode(usings[i], sortedUsings[i]); + } + } + + var newDocument = document.WithSyntaxRoot(newRoot); + var formattedDocument = await Formatter.FormatAsync(newDocument); + + await Package.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (Workspace != null && Workspace.TryApplyChanges(formattedDocument.Project.Solution)) + { + var text = await formattedDocument.GetTextAsync(); + return new QueryResponse + { + Success = true, + Message = "Usings organized successfully", + Data = new DocumentChangeInfo + { + FilePath = request.FilePath, + NewText = text.ToString() + } + }; + } + + return new QueryResponse { Success = false, Error = "Failed to organize usings" }; + } + + public async Task RenameSymbolAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath) || !request.Line.HasValue || !request.Column.HasValue) + { + return new QueryResponse { Success = false, Error = "FilePath, Line, and Column are required" }; + } + + string? newName = null; + request.Parameters?.TryGetValue("newName", out newName); + + if (string.IsNullOrEmpty(newName)) + { + return new QueryResponse { Success = false, Error = "newName parameter is required" }; + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return new QueryResponse { Success = false, Error = "Document not found" }; + } + + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot?.FindToken(position).Parent; + var symbol = semanticModel?.GetSymbolInfo(node).Symbol; + + if (symbol == null) + { + return new QueryResponse { Success = false, Error = "Symbol not found at the specified location" }; + } + + try + { + var newSolution = await Renamer.RenameSymbolAsync( + document.Project.Solution, + symbol, + newName, + document.Project.Solution.Workspace.Options + ); + + var changes = newSolution.GetChanges(document.Project.Solution); + var changedDocs = new List(); + int totalChanges = 0; + + foreach (var projectChanges in changes.GetProjectChanges()) + { + foreach (var changedDocId in projectChanges.GetChangedDocuments()) + { + var oldDoc = document.Project.Solution.GetDocument(changedDocId); + var newDoc = newSolution.GetDocument(changedDocId); + + if (oldDoc != null && newDoc != null) + { + var newText = await newDoc.GetTextAsync(); + changedDocs.Add(new DocumentChangeInfo + { + FilePath = newDoc.FilePath, + NewText = newText.ToString() + }); + totalChanges++; + } + } + } + + await Package.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (Workspace != null && Workspace.TryApplyChanges(newSolution)) + { + return new QueryResponse + { + Success = true, + Message = $"Renamed '{symbol.Name}' to '{newName}' in {totalChanges} document(s)", + Data = new RenameResult + { + ChangedDocuments = changedDocs, + TotalChanges = totalChanges + } + }; + } + + return new QueryResponse { Success = false, Error = "Failed to apply rename changes" }; + } + catch (Exception ex) + { + return new QueryResponse { Success = false, Error = $"Rename failed: {ex.Message}" }; + } + } + + public async Task AddMissingUsingAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath) || !request.Line.HasValue || !request.Column.HasValue) + { + return new QueryResponse { Success = false, Error = "FilePath, Line, and Column are required" }; + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return new QueryResponse { Success = false, Error = "Document not found" }; + } + + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot?.FindToken(position).Parent; + + if (node == null || semanticModel == null) + { + return new QueryResponse { Success = false, Error = "Could not analyze position" }; + } + + // Get the symbol info + var symbolInfo = semanticModel.GetSymbolInfo(node); + if (symbolInfo.Symbol != null) + { + return new QueryResponse { Success = false, Error = "Symbol is already resolved" }; + } + + // Try to find the type in other namespaces + var compilation = semanticModel.Compilation; + var typeName = node.ToString(); + + INamedTypeSymbol? foundType = null; + foreach (var assembly in compilation.References) + { + var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(assembly) as IAssemblySymbol; + if (assemblySymbol != null) + { + foundType = FindTypeInAssembly(assemblySymbol.GlobalNamespace, typeName); + if (foundType != null) break; + } + } + + // Also search in current compilation + if (foundType == null) + { + foundType = FindTypeInAssembly(compilation.GlobalNamespace, typeName); + } + + if (foundType != null && syntaxRoot != null) + { + var namespaceToAdd = foundType.ContainingNamespace.ToDisplayString(); + var compilationUnit = syntaxRoot as CompilationUnitSyntax; + + if (compilationUnit != null) + { + var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceToAdd)) + .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed); + + var newCompilationUnit = compilationUnit.AddUsings(usingDirective); + var newDocument = document.WithSyntaxRoot(newCompilationUnit); + var formattedDocument = await Formatter.FormatAsync(newDocument); + + await Package.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (Workspace != null && Workspace.TryApplyChanges(formattedDocument.Project.Solution)) + { + return new QueryResponse + { + Success = true, + Message = $"Added using {namespaceToAdd};" + }; + } + } + } + + return new QueryResponse { Success = false, Error = "Could not find a suitable using directive to add" }; + } + + public async Task ApplyCodeFixAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath) || !request.Line.HasValue || !request.Column.HasValue) + { + return new QueryResponse { Success = false, Error = "FilePath, Line, and Column are required" }; + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return new QueryResponse { Success = false, Error = "Document not found" }; + } + + // Get diagnostics at the location + var semanticModel = await document.GetSemanticModelAsync(); + if (semanticModel == null) + { + return new QueryResponse { Success = false, Error = "Could not get semantic model" }; + } + + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var diagnostics = semanticModel.GetDiagnostics(); + + // Find diagnostics at the position + var relevantDiagnostics = diagnostics + .Where(d => d.Location.SourceSpan.Contains(position)) + .ToList(); + + if (!relevantDiagnostics.Any()) + { + return new QueryResponse { Success = false, Error = "No diagnostics found at the specified location" }; + } + + // For now, just return the available diagnostics + // Full code fix implementation would require loading all code fix providers + var diagnosticInfos = relevantDiagnostics.Select(d => new CodeFixInfo + { + DiagnosticId = d.Id, + Title = d.GetMessage(), + Description = d.Descriptor.Description.ToString() + }).ToList(); + + return new QueryResponse + { + Success = true, + Message = $"Found {diagnosticInfos.Count} diagnostic(s) at location", + Data = diagnosticInfos + }; + } + } +} diff --git a/RoslynBridge/Services/RoslynQueryService.cs b/RoslynBridge/Services/RoslynQueryService.cs new file mode 100644 index 0000000..5630796 --- /dev/null +++ b/RoslynBridge/Services/RoslynQueryService.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Constants; +using RoslynBridge.Models; + +namespace RoslynBridge.Services +{ + /// + /// Main orchestrator service that delegates queries to specialized services + /// + public class RoslynQueryService : IRoslynQueryService + { + private readonly AsyncPackage _package; + private readonly IWorkspaceProvider _workspaceProvider; + private readonly SymbolQueryService _symbolService; + private readonly DocumentQueryService _documentService; + private readonly DiagnosticsService _diagnosticsService; + private readonly RefactoringService _refactoringService; + + public RoslynQueryService(AsyncPackage package) + { + _package = package; + _workspaceProvider = new WorkspaceProvider(package); + + // Initialize specialized services + _symbolService = new SymbolQueryService(package, _workspaceProvider); + _documentService = new DocumentQueryService(package, _workspaceProvider); + _diagnosticsService = new DiagnosticsService(package, _workspaceProvider); + _refactoringService = new RefactoringService(package, _workspaceProvider); + } + + public async Task InitializeAsync() + { + await ((WorkspaceProvider)_workspaceProvider).InitializeAsync(); + } + + public async Task ExecuteQueryAsync(QueryRequest request) + { + try + { + if (_workspaceProvider.Workspace == null) + { + await InitializeAsync(); + } + + if (_workspaceProvider.Workspace?.CurrentSolution == null) + { + return new QueryResponse + { + Success = false, + Error = "No solution is currently open" + }; + } + + return request.QueryType?.ToLowerInvariant() switch + { + // Symbol queries + QueryTypes.GetSymbol => await _symbolService.GetSymbolInfoAsync(request), + QueryTypes.FindSymbol => await _symbolService.FindSymbolAsync(request), + QueryTypes.GetTypeMembers => await _symbolService.GetTypeMembersAsync(request), + QueryTypes.GetTypeHierarchy => await _symbolService.GetTypeHierarchyAsync(request), + QueryTypes.FindImplementations => await _symbolService.FindImplementationsAsync(request), + QueryTypes.GetCallHierarchy => await _symbolService.GetCallHierarchyAsync(request), + QueryTypes.GetSymbolContext => await _symbolService.GetSymbolContextAsync(request), + + // Document queries + QueryTypes.GetDocument => await _documentService.GetDocumentInfoAsync(request), + QueryTypes.GetProjects => await _documentService.GetProjectsAsync(request), + QueryTypes.GetSemanticModel => await _documentService.GetSemanticModelAsync(request), + QueryTypes.GetSyntaxTree => await _documentService.GetSyntaxTreeAsync(request), + QueryTypes.GetSolutionOverview => await _documentService.GetSolutionOverviewAsync(request), + QueryTypes.GetNamespaceTypes => await _documentService.GetNamespaceTypesAsync(request), + QueryTypes.SearchCode => await _documentService.SearchCodeAsync(request), + + // Diagnostics queries + QueryTypes.GetDiagnostics => await _diagnosticsService.GetDiagnosticsAsync(request), + QueryTypes.FindReferences => await _diagnosticsService.FindReferencesAsync(request), + + // Refactoring operations + QueryTypes.ApplyCodeFix => await _refactoringService.ApplyCodeFixAsync(request), + QueryTypes.FormatDocument => await _refactoringService.FormatDocumentAsync(request), + QueryTypes.RenameSymbol => await _refactoringService.RenameSymbolAsync(request), + QueryTypes.OrganizeUsings => await _refactoringService.OrganizeUsingsAsync(request), + QueryTypes.AddMissingUsing => await _refactoringService.AddMissingUsingAsync(request), + + _ => new QueryResponse + { + Success = false, + Error = $"Unknown query type: {request.QueryType}" + } + }; + } + catch (Exception ex) + { + return new QueryResponse + { + Success = false, + Error = ex.Message + }; + } + } + } +} diff --git a/RoslynBridge/Services/SymbolQueryService.cs b/RoslynBridge/Services/SymbolQueryService.cs new file mode 100644 index 0000000..122f959 --- /dev/null +++ b/RoslynBridge/Services/SymbolQueryService.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.VisualStudio.Shell; +using RoslynBridge.Models; + +namespace RoslynBridge.Services +{ + public class SymbolQueryService : BaseRoslynService + { + public SymbolQueryService(AsyncPackage package, IWorkspaceProvider workspaceProvider) + : base(package, workspaceProvider) + { + } + + public async Task GetSymbolInfoAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath)) + { + return CreateErrorResponse("FilePath is required"); + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return CreateErrorResponse("Document not found"); + } + + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + + if (semanticModel == null || syntaxRoot == null) + { + return CreateErrorResponse("Could not get semantic model"); + } + + if (request.Line.HasValue && request.Column.HasValue) + { + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot.FindToken(position).Parent; + + ISymbol? symbol = null; + if (node != null) + { + symbol = semanticModel.GetSymbolInfo(node).Symbol; + if (symbol == null) + { + // Fallback for declarations (class, method, property, field, local, parameter) + var declNode = node; + while (declNode != null && symbol == null) + { + symbol = semanticModel.GetDeclaredSymbol(declNode); + declNode = declNode.Parent; + } + } + } + + if (symbol != null) + { + var symbolInfo = await CreateSymbolInfoAsync(symbol); + return CreateSuccessResponse(symbolInfo); + } + } + + return CreateErrorResponse("Symbol not found"); + } + + public async Task FindSymbolAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.SymbolName)) + { + return CreateErrorResponse("SymbolName is required"); + } + + var symbols = new List(); + string? kind = null; + request.Parameters?.TryGetValue("kind", out kind); + + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + var matchingSymbols = compilation.GetSymbolsWithName( + name => name.IndexOf(request.SymbolName, StringComparison.OrdinalIgnoreCase) >= 0, + SymbolFilter.All + ); + + foreach (var symbol in matchingSymbols) + { + // Filter by kind if specified + if (!string.IsNullOrEmpty(kind) && + !symbol.Kind.ToString().Equals(kind, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + symbols.Add(await CreateSymbolInfoAsync(symbol)); + } + } + + return CreateSuccessResponse(symbols); + } + + public async Task GetTypeMembersAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.SymbolName)) + { + return CreateErrorResponse("SymbolName (type name) is required"); + } + + string? includeInheritedStr = null; + request.Parameters?.TryGetValue("includeInherited", out includeInheritedStr); + var includeInherited = includeInheritedStr == "true"; + + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + var typeSymbol = compilation.GetTypeByMetadataName(request.SymbolName) ?? + compilation.GetSymbolsWithName(request.SymbolName, SymbolFilter.Type).FirstOrDefault() as INamedTypeSymbol; + + if (typeSymbol != null) + { + var members = includeInherited + ? typeSymbol.GetMembers() + : typeSymbol.GetMembers().Where(m => m.ContainingType.Equals(typeSymbol, SymbolEqualityComparer.Default)); + + var memberInfos = members.Select(m => new MemberInfo + { + Name = m.Name, + Kind = m.Kind.ToString(), + ReturnType = (m as IMethodSymbol)?.ReturnType.ToDisplayString() ?? + (m as IPropertySymbol)?.Type.ToDisplayString() ?? + (m as IFieldSymbol)?.Type.ToDisplayString(), + Signature = m.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + Documentation = m.GetDocumentationCommentXml(), + Modifiers = GetModifiers(m), + Accessibility = m.DeclaredAccessibility.ToString(), + IsStatic = m.IsStatic, + IsAbstract = m.IsAbstract, + IsVirtual = m.IsVirtual, + IsOverride = m.IsOverride + }).ToList(); + + return CreateSuccessResponse(memberInfos); + } + } + + return CreateErrorResponse($"Type '{request.SymbolName}' not found"); + } + + public async Task GetTypeHierarchyAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.SymbolName)) + { + return CreateErrorResponse("SymbolName (type name) is required"); + } + + string? direction = null; + request.Parameters?.TryGetValue("direction", out direction); + direction = direction ?? "both"; + + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + var typeSymbol = compilation.GetTypeByMetadataName(request.SymbolName) ?? + compilation.GetSymbolsWithName(request.SymbolName, SymbolFilter.Type).FirstOrDefault() as INamedTypeSymbol; + + if (typeSymbol != null) + { + var hierarchy = new TypeHierarchyInfo + { + TypeName = typeSymbol.Name, + FullName = typeSymbol.ToDisplayString(), + BaseTypes = new List(), + Interfaces = typeSymbol.Interfaces.Select(i => i.ToDisplayString()).ToList(), + DerivedTypes = new List() + }; + + // Get base types + if (direction == "up" || direction == "both") + { + var baseType = typeSymbol.BaseType; + while (baseType != null && baseType.SpecialType != SpecialType.System_Object) + { + hierarchy.BaseTypes.Add(baseType.ToDisplayString()); + baseType = baseType.BaseType; + } + } + + // Get derived types + if (direction == "down" || direction == "both") + { + var derivedTypes = await SymbolFinder.FindDerivedClassesAsync(typeSymbol, Workspace.CurrentSolution, true); + hierarchy.DerivedTypes = derivedTypes.Select(t => t.ToDisplayString()).ToList(); + } + + return CreateSuccessResponse(hierarchy); + } + } + + return CreateErrorResponse($"Type '{request.SymbolName}' not found"); + } + + public async Task FindImplementationsAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.SymbolName) && string.IsNullOrEmpty(request.FilePath)) + { + return CreateErrorResponse("Either SymbolName or FilePath with Line/Column is required"); + } + + ISymbol? targetSymbol = null; + + // Find by name + if (!string.IsNullOrEmpty(request.SymbolName)) + { + foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty()) + { + var compilation = await project.GetCompilationAsync(); + if (compilation == null) continue; + + targetSymbol = compilation.GetTypeByMetadataName(request.SymbolName) ?? + compilation.GetSymbolsWithName(request.SymbolName, SymbolFilter.Type).FirstOrDefault(); + if (targetSymbol != null) break; + } + } + // Find by location + else if (!string.IsNullOrEmpty(request.FilePath) && request.Line.HasValue && request.Column.HasValue) + { + var document = FindDocument(request.FilePath); + + if (document != null) + { + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot?.FindToken(position).Parent; + targetSymbol = semanticModel?.GetSymbolInfo(node).Symbol; + } + } + + if (targetSymbol == null) + { + return CreateErrorResponse("Symbol not found"); + } + + var implementations = new List(); + + if (targetSymbol is INamedTypeSymbol namedType && (namedType.TypeKind == TypeKind.Interface || namedType.IsAbstract)) + { + var implementers = await SymbolFinder.FindImplementationsAsync(namedType, Workspace.CurrentSolution); + foreach (var impl in implementers) + { + implementations.Add(await CreateSymbolInfoAsync(impl)); + } + } + + return CreateSuccessResponse(implementations); + } + + public async Task GetCallHierarchyAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath) || !request.Line.HasValue || !request.Column.HasValue) + { + return CreateErrorResponse("FilePath, Line, and Column are required"); + } + + string? direction = null; + request.Parameters?.TryGetValue("direction", out direction); + direction = direction ?? "callers"; + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return CreateErrorResponse("Document not found"); + } + + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot?.FindToken(position).Parent; + var symbol = semanticModel?.GetSymbolInfo(node).Symbol; + + if (symbol == null) + { + return CreateErrorResponse("Symbol not found"); + } + + var calls = new List(); + + if (direction == "callers") + { + var callers = await SymbolFinder.FindCallersAsync(symbol, Workspace.CurrentSolution); + foreach (var caller in callers) + { + foreach (var location in caller.Locations) + { + calls.Add(new CallInfo + { + CallerName = caller.CallingSymbol.Name, + CallerType = caller.CallingSymbol.ContainingType?.Name, + Location = new LocationInfo + { + FilePath = location.SourceTree?.FilePath, + StartLine = location.GetLineSpan().StartLinePosition.Line + 1, + StartColumn = location.GetLineSpan().StartLinePosition.Character, + EndLine = location.GetLineSpan().EndLinePosition.Line + 1, + EndColumn = location.GetLineSpan().EndLinePosition.Character + } + }); + } + } + } + + var result = new CallHierarchyInfo + { + SymbolName = symbol.ToDisplayString(), + Calls = calls + }; + + return CreateSuccessResponse(result); + } + + public async Task GetSymbolContextAsync(QueryRequest request) + { + if (string.IsNullOrEmpty(request.FilePath) || !request.Line.HasValue || !request.Column.HasValue) + { + return CreateErrorResponse("FilePath, Line, and Column are required"); + } + + var document = FindDocument(request.FilePath); + + if (document == null) + { + return CreateErrorResponse("Document not found"); + } + + var semanticModel = await document.GetSemanticModelAsync(); + var syntaxRoot = await document.GetSyntaxRootAsync(); + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines[request.Line.Value - 1].Start + request.Column.Value; + var node = syntaxRoot?.FindToken(position).Parent; + + if (node == null || semanticModel == null) + { + return CreateErrorResponse("Could not analyze position"); + } + + var symbol = semanticModel.GetSymbolInfo(node).Symbol; + var containingMethod = node.AncestorsAndSelf().OfType().FirstOrDefault(); + var containingClass = node.AncestorsAndSelf().OfType().FirstOrDefault(); + + var context = new SymbolContextInfo + { + ContainingClass = containingClass?.Identifier.Text, + ContainingMethod = containingMethod?.Identifier.Text, + ContainingNamespace = semanticModel.GetDeclaredSymbol(containingClass)?.ContainingNamespace?.ToDisplayString(), + SymbolAtPosition = symbol?.ToDisplayString(), + LocalVariables = new List(), + Parameters = new List() + }; + + // Get local variables + if (containingMethod != null) + { + var dataFlow = semanticModel.AnalyzeDataFlow(containingMethod); + if (dataFlow.Succeeded) + { + context.LocalVariables = dataFlow.VariablesDeclared.Select(v => v.Name).ToList(); + } + + // Get parameters + var methodSymbol = semanticModel.GetDeclaredSymbol(containingMethod) as IMethodSymbol; + if (methodSymbol != null) + { + context.Parameters = methodSymbol.Parameters.Select(p => $"{p.Type.Name} {p.Name}").ToList(); + } + } + + return CreateSuccessResponse(context); + } + } +} diff --git a/RoslynBridge/Services/WorkspaceProvider.cs b/RoslynBridge/Services/WorkspaceProvider.cs new file mode 100644 index 0000000..867474b --- /dev/null +++ b/RoslynBridge/Services/WorkspaceProvider.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.Shell; + +namespace RoslynBridge.Services +{ + public class WorkspaceProvider : IWorkspaceProvider + { + private readonly AsyncPackage _package; + private VisualStudioWorkspace? _workspace; + + public VisualStudioWorkspace? Workspace => _workspace; + + public WorkspaceProvider(AsyncPackage package) + { + _package = package; + } + + public async Task InitializeAsync() + { + await _package.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // Get the workspace through MEF + var componentModel = await _package.GetServiceAsync(typeof(SComponentModel)) as IComponentModel; + if (componentModel != null) + { + _workspace = componentModel.GetService(); + } + } + } +} diff --git a/RoslynBridge/source.extension.vsixmanifest b/RoslynBridge/source.extension.vsixmanifest new file mode 100644 index 0000000..fa9f2ce --- /dev/null +++ b/RoslynBridge/source.extension.vsixmanifest @@ -0,0 +1,32 @@ + + + + + Claude Roslyn Bridge + Visual Studio extension that provides access to Roslyn APIs through an HTTP bridge for Claude integration. + https://github.com/yourusername/ClaudeRoslynBridge + README.txt + Roslyn, Claude, AI, Code Analysis + + + + amd64 + + + amd64 + + + amd64 + + + + + + + + + + + + +