Added Files
This commit is contained in:
20
.claude/settings.local.json
Normal file
20
.claude/settings.local.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
134
.claude/skills/roslyn-api/SKILL.md
Normal file
134
.claude/skills/roslyn-api/SKILL.md
Normal 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
33
.gitignore
vendored
Normal 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
35
AGENTS.md
Normal 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 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.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
21
RoslynBridge.sln
Normal 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
|
||||
47
RoslynBridge/ClaudeRoslynBridgePackage.cs
Normal file
47
RoslynBridge/ClaudeRoslynBridgePackage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
RoslynBridge/Constants/ServerConstants.cs
Normal file
43
RoslynBridge/Constants/ServerConstants.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
17
RoslynBridge/Models/CallHierarchyModels.cs
Normal file
17
RoslynBridge/Models/CallHierarchyModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
17
RoslynBridge/Models/DiagnosticModels.cs
Normal file
17
RoslynBridge/Models/DiagnosticModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
32
RoslynBridge/Models/DocumentModels.cs
Normal file
32
RoslynBridge/Models/DocumentModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
11
RoslynBridge/Models/LocationModels.cs
Normal file
11
RoslynBridge/Models/LocationModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
27
RoslynBridge/Models/ProjectModels.cs
Normal file
27
RoslynBridge/Models/ProjectModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
10
RoslynBridge/Models/RefactoringModels.cs
Normal file
10
RoslynBridge/Models/RefactoringModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
22
RoslynBridge/Models/RequestModels.cs
Normal file
22
RoslynBridge/Models/RequestModels.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
58
RoslynBridge/Models/SymbolModels.cs
Normal file
58
RoslynBridge/Models/SymbolModels.cs
Normal 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
1
RoslynBridge/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
See documentation for setup and instructions.
|
||||
134
RoslynBridge/RoslynBridge.csproj
Normal file
134
RoslynBridge/RoslynBridge.csproj
Normal 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>
|
||||
51
RoslynBridge/RoslynBridgePackage.cs
Normal file
51
RoslynBridge/RoslynBridgePackage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
217
RoslynBridge/Server/BridgeServer.cs
Normal file
217
RoslynBridge/Server/BridgeServer.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
184
RoslynBridge/Services/BaseRoslynService.cs
Normal file
184
RoslynBridge/Services/BaseRoslynService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
103
RoslynBridge/Services/DiagnosticsService.cs
Normal file
103
RoslynBridge/Services/DiagnosticsService.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
247
RoslynBridge/Services/DocumentQueryService.cs
Normal file
247
RoslynBridge/Services/DocumentQueryService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
RoslynBridge/Services/IRoslynQueryService.cs
Normal file
11
RoslynBridge/Services/IRoslynQueryService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
RoslynBridge/Services/IWorkspaceProvider.cs
Normal file
9
RoslynBridge/Services/IWorkspaceProvider.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.VisualStudio.LanguageServices;
|
||||
|
||||
namespace RoslynBridge.Services
|
||||
{
|
||||
public interface IWorkspaceProvider
|
||||
{
|
||||
VisualStudioWorkspace? Workspace { get; }
|
||||
}
|
||||
}
|
||||
341
RoslynBridge/Services/RefactoringService.cs
Normal file
341
RoslynBridge/Services/RefactoringService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
104
RoslynBridge/Services/RoslynQueryService.cs
Normal file
104
RoslynBridge/Services/RoslynQueryService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
396
RoslynBridge/Services/SymbolQueryService.cs
Normal file
396
RoslynBridge/Services/SymbolQueryService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
RoslynBridge/Services/WorkspaceProvider.cs
Normal file
32
RoslynBridge/Services/WorkspaceProvider.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
RoslynBridge/source.extension.vsixmanifest
Normal file
32
RoslynBridge/source.extension.vsixmanifest
Normal 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>
|
||||
Reference in New Issue
Block a user