Added Files

This commit is contained in:
AJ
2025-10-24 20:28:13 -04:00
commit b75fbbedb9
30 changed files with 2410 additions and 0 deletions

View File

@@ -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": []
}
}

View File

@@ -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

33
.gitignore vendored Normal file
View File

@@ -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/

35
AGENTS.md Normal file
View File

@@ -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 4space 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 1based; columns 0based. 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.

21
LICENSE Normal file
View File

@@ -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.

21
RoslynBridge.sln Normal file
View File

@@ -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

View File

@@ -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<ServiceProgressData> 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);
}
}
}

View File

@@ -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";
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace RoslynBridge.Models
{
public class CallHierarchyInfo
{
public string? SymbolName { get; set; }
public List<CallInfo>? Calls { get; set; }
}
public class CallInfo
{
public string? CallerName { get; set; }
public string? CallerType { get; set; }
public LocationInfo? Location { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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<string>? Usings { get; set; }
public List<string>? Classes { get; set; }
public List<string>? Interfaces { get; set; }
public List<string>? Enums { get; set; }
}
public class DocumentChangeInfo
{
public string? FilePath { get; set; }
public List<TextChangeInfo>? 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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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<string>? Documents { get; set; }
public List<string>? References { get; set; }
}
public class ProjectSummary
{
public string? Name { get; set; }
public int FileCount { get; set; }
public List<string>? TopNamespaces { get; set; }
}
public class SolutionOverview
{
public int ProjectCount { get; set; }
public int DocumentCount { get; set; }
public List<string>? TopLevelNamespaces { get; set; }
public List<ProjectSummary>? Projects { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace RoslynBridge.Models
{
public class RenameResult
{
public List<DocumentChangeInfo>? ChangedDocuments { get; set; }
public int TotalChanges { get; set; }
}
}

View File

@@ -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<string, string>? 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; }
}
}

View File

@@ -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<LocationInfo>? Locations { get; set; }
public string? Documentation { get; set; }
public List<string>? 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<string>? 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<string>? BaseTypes { get; set; }
public List<string>? Interfaces { get; set; }
public List<string>? 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<string>? LocalVariables { get; set; }
public List<string>? Parameters { get; set; }
}
}

1
RoslynBridge/README.txt Normal file
View File

@@ -0,0 +1 @@
See documentation for setup and instructions.

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MinimumVisualStudioVersion>17.0</MinimumVisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<SchemaVersion>2.0</SchemaVersion>
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<ProjectGuid>{B2C3D4E5-F6A7-4B5C-9D8E-0F1A2B3C4D5E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>RoslynBridge</RootNamespace>
<AssemblyName>RoslynBridge</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<GeneratePkgDefFile>true</GeneratePkgDefFile>
<UseCodebase>true</UseCodebase>
<IncludeAssemblyInVSIXContainer>true</IncludeAssemblyInVSIXContainer>
<IncludeDebugSymbolsInVSIXContainer>false</IncludeDebugSymbolsInVSIXContainer>
<IncludeDebugSymbolsInLocalVSIXDeployment>false</IncludeDebugSymbolsInLocalVSIXDeployment>
<CopyBuildOutputToOutputDirectory>true</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>true</CopyOutputSymbolsToOutputDirectory>
<StartAction>Program</StartAction>
<StartProgram Condition="'$(DevEnvDir)' != ''">$(DevEnvDir)devenv.exe</StartProgram>
<StartArguments>/rootsuffix Exp</StartArguments>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<!-- Main Package -->
<Compile Include="RoslynBridgePackage.cs" />
<!-- Constants -->
<Compile Include="Constants\ServerConstants.cs" />
<!-- Models -->
<Compile Include="Models\CallHierarchyModels.cs" />
<Compile Include="Models\DiagnosticModels.cs" />
<Compile Include="Models\DocumentModels.cs" />
<Compile Include="Models\LocationModels.cs" />
<Compile Include="Models\ProjectModels.cs" />
<Compile Include="Models\RefactoringModels.cs" />
<Compile Include="Models\RequestModels.cs" />
<Compile Include="Models\SymbolModels.cs" />
<!-- Server -->
<Compile Include="Server\BridgeServer.cs" />
<!-- Services -->
<Compile Include="Services\BaseRoslynService.cs" />
<Compile Include="Services\DiagnosticsService.cs" />
<Compile Include="Services\DocumentQueryService.cs" />
<Compile Include="Services\IRoslynQueryService.cs" />
<Compile Include="Services\IWorkspaceProvider.cs" />
<Compile Include="Services\RefactoringService.cs" />
<Compile Include="Services\RoslynQueryService.cs" />
<Compile Include="Services\SymbolQueryService.cs" />
<Compile Include="Services\WorkspaceProvider.cs" />
<None Include="source.extension.vsixmanifest">
<SubType>Designer</SubType>
</None>
<Content Include="README.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<IncludeInVSIX>true</IncludeInVSIX>
</Content>
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.SDK" Version="17.8.37221" ExcludeAssets="runtime">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="17.8.2369">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- Roslyn packages -->
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.8.0" />
<PackageReference Include="Microsoft.VisualStudio.LanguageServices" Version="4.8.0" />
<!-- Additional VS SDK packages -->
<PackageReference Include="Microsoft.VisualStudio.Shell.15.0" Version="17.8.37221" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Framework" Version="17.8.37221" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.8.14" />
<!-- HTTP server -->
<PackageReference Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != ''" />
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
</Project>

View File

@@ -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
{
/// <summary>
/// Visual Studio package that provides Roslyn API access via HTTP bridge
/// </summary>
[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<ServiceProgressData> 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);
}
}
}

View File

@@ -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<QueryRequest?> ParseRequestAsync(HttpListenerRequest request)
{
try
{
using var reader = new StreamReader(request.InputStream, request.ContentEncoding);
var requestBody = await reader.ReadToEndAsync();
return JsonSerializer.Deserialize<QueryRequest>(requestBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch
{
return null;
}
}
private async Task<QueryResponse> 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}");
}
}
}
}
}

View File

@@ -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<RoslynBridge.Models.SymbolInfo> 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<string>()
});
}
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<string> GetModifiers(ISymbol symbol)
{
var modifiers = new List<string>();
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("<summary>", StringComparison.Ordinal);
var summaryEnd = xmlDoc.IndexOf("</summary>", StringComparison.Ordinal);
if (summaryStart >= 0 && summaryEnd > summaryStart)
{
return xmlDoc.Substring(summaryStart + 9, summaryEnd - summaryStart - 9).Trim();
}
return null;
}
protected IEnumerable<INamespaceSymbol> 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
};
}
}
}

View File

@@ -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<QueryResponse> GetDiagnosticsAsync(QueryRequest request)
{
var diagnostics = new List<DiagnosticInfo>();
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<Project>())
{
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<QueryResponse> 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 };
}
}
}

View File

@@ -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<QueryResponse> 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<UsingDirectiveSyntax>()
.Select(u => u.Name?.ToString() ?? string.Empty).ToList(),
Classes = syntaxRoot.DescendantNodes().OfType<ClassDeclarationSyntax>()
.Select(c => c.Identifier.Text).ToList(),
Interfaces = syntaxRoot.DescendantNodes().OfType<InterfaceDeclarationSyntax>()
.Select(i => i.Identifier.Text).ToList(),
Enums = syntaxRoot.DescendantNodes().OfType<EnumDeclarationSyntax>()
.Select(e => e.Identifier.Text).ToList()
};
return CreateSuccessResponse(docInfo);
}
public async Task<QueryResponse> 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<QueryResponse> 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<QueryResponse> 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<QueryResponse> GetSolutionOverviewAsync(QueryRequest request)
{
var projects = Workspace?.CurrentSolution.Projects.ToList() ?? new List<Project>();
var overview = new SolutionOverview
{
ProjectCount = projects.Count,
DocumentCount = projects.Sum(p => p.Documents.Count()),
TopLevelNamespaces = new List<string>(),
Projects = new List<ProjectSummary>()
};
var allNamespaces = new HashSet<string>();
foreach (var project in projects)
{
var compilation = await project.GetCompilationAsync();
if (compilation == null) continue;
var projectNamespaces = new HashSet<string>();
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<QueryResponse> GetNamespaceTypesAsync(QueryRequest request)
{
if (string.IsNullOrEmpty(request.SymbolName))
{
return CreateErrorResponse("SymbolName (namespace name) is required");
}
var types = new List<NamespaceTypeInfo>();
foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty<Project>())
{
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<QueryResponse> 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<RoslynBridge.Models.SymbolInfo>();
foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty<Project>())
{
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);
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using RoslynBridge.Models;
namespace RoslynBridge.Services
{
public interface IRoslynQueryService
{
Task InitializeAsync();
Task<QueryResponse> ExecuteQueryAsync(QueryRequest request);
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.VisualStudio.LanguageServices;
namespace RoslynBridge.Services
{
public interface IWorkspaceProvider
{
VisualStudioWorkspace? Workspace { get; }
}
}

View File

@@ -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<QueryResponse> 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<QueryResponse> 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<UsingDirectiveSyntax>().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<QueryResponse> 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<DocumentChangeInfo>();
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<QueryResponse> 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<QueryResponse> 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
};
}
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
using RoslynBridge.Constants;
using RoslynBridge.Models;
namespace RoslynBridge.Services
{
/// <summary>
/// Main orchestrator service that delegates queries to specialized services
/// </summary>
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<QueryResponse> 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
};
}
}
}
}

View File

@@ -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<QueryResponse> 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<QueryResponse> FindSymbolAsync(QueryRequest request)
{
if (string.IsNullOrEmpty(request.SymbolName))
{
return CreateErrorResponse("SymbolName is required");
}
var symbols = new List<RoslynBridge.Models.SymbolInfo>();
string? kind = null;
request.Parameters?.TryGetValue("kind", out kind);
foreach (var project in Workspace?.CurrentSolution.Projects ?? Enumerable.Empty<Project>())
{
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<QueryResponse> 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<Project>())
{
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<QueryResponse> 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<Project>())
{
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<string>(),
Interfaces = typeSymbol.Interfaces.Select(i => i.ToDisplayString()).ToList(),
DerivedTypes = new List<string>()
};
// 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<QueryResponse> 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<Project>())
{
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<RoslynBridge.Models.SymbolInfo>();
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<QueryResponse> 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<CallInfo>();
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<QueryResponse> 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<MethodDeclarationSyntax>().FirstOrDefault();
var containingClass = node.AncestorsAndSelf().OfType<ClassDeclarationSyntax>().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<string>(),
Parameters = new List<string>()
};
// 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);
}
}
}

View File

@@ -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<VisualStudioWorkspace>();
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="RoslynBridge.b2c3d4e5-f6a7-4b5c-9d8e-0f1a2b3c4d5e" Version="1.0.0" Language="en-US" Publisher="Your Name" />
<DisplayName>Claude Roslyn Bridge</DisplayName>
<Description xml:space="preserve">Visual Studio extension that provides access to Roslyn APIs through an HTTP bridge for Claude integration.</Description>
<MoreInfo>https://github.com/yourusername/ClaudeRoslynBridge</MoreInfo>
<License>README.txt</License>
<Tags>Roslyn, Claude, AI, Code Analysis</Tags>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[17.0,18.0)">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
<InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[17.0,18.0)">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
<InstallationTarget Id="Microsoft.VisualStudio.Enterprise" Version="[17.0,18.0)">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
</Installation>
<Dependencies>
<Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.8,)" />
</Dependencies>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[17.0,18.0)" DisplayName="Visual Studio core editor" />
<Prerequisite Id="Microsoft.VisualStudio.Component.Roslyn.LanguageServices" Version="[17.0,18.0)" DisplayName="Roslyn Language Services" />
</Prerequisites>
<Assets>
<Asset Type="Microsoft.VisualStudio.VsPackage" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%;PkgdefProjectOutputGroup|" />
</Assets>
</PackageManifest>