diff --git a/README.md b/README.md index 69634a4..97d9f67 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,43 @@ | `build_cancel` | Cancel a running build | | `build_status` | Get current build status | +### 🧭 Navigation Tools + +| Tool | Description | +|------|-------------| +| `goto_definition` | Navigate to the definition of a symbol | +| `find_references` | Find all references to a symbol | +| `symbol_document` | Get all symbols defined in a document | +| `symbol_workspace` | Search for symbols across the solution | + +### 🐛 Debugger Tools + +| Tool | Description | +|------|-------------| +| `debugger_status` | Get current debugger state | +| `debugger_launch` | Start debugging (F5) | +| `debugger_launch_without_debugging` | Start without debugger (Ctrl+F5) | +| `debugger_continue` | Continue execution (F5) | +| `debugger_break` | Pause execution (Ctrl+Alt+Break) | +| `debugger_stop` | Stop debugging (Shift+F5) | +| `debugger_step_over` | Step over (F10) | +| `debugger_step_into` | Step into (F11) | +| `debugger_step_out` | Step out (Shift+F11) | +| `debugger_add_breakpoint` | Add a breakpoint at a file and line | +| `debugger_remove_breakpoint` | Remove a breakpoint | +| `debugger_list_breakpoints` | List all breakpoints | +| `debugger_get_locals` | Get local variables in current frame | +| `debugger_get_callstack` | Get the call stack | + +### 🔍 Diagnostics Tools + +| Tool | Description | +|------|-------------| +| `errors_list` | Read build errors, warnings, and messages from the Error List | +| `output_read` | Read content from an Output window pane | +| `output_write` | Write a message to an Output window pane | +| `output_list_panes` | List all available Output window panes | + ## đŸ› ī¸ Installation ### Visual Studio Marketplace @@ -103,20 +140,36 @@ Download the latest `.vsix` from the [Releases](https://github.com/CodingWithCal 2. Go to **Tools > MCP Server > Start Server** (or enable auto-start in settings) 3. The MCP server starts on `http://localhost:5050` -### 🤖 Configuring Claude Desktop +### 🤖 Configuring Claude Desktop & Claude Code -Add this to your Claude Desktop MCP settings: +Add this to your Claude Desktop or Claude Code MCP settings (preferred HTTP method): ```json { "mcpServers": { - "visual-studio": { + "visualstudio": { + "type": "http", + "url": "http://localhost:5050" + } + } +} +``` + +**Legacy SSE method** (deprecated, but still supported): + +```json +{ + "mcpServers": { + "visualstudio": { + "type": "sse", "url": "http://localhost:5050/sse" } } } ``` +> â„šī¸ **Note:** The HTTP method is the preferred standard. SSE (Server-Sent Events) is a legacy protocol and should only be used for backward compatibility. + ### âš™ī¸ Settings Configure the extension at **Tools > Options > MCP Server**: diff --git a/src/CodingWithCalvin.MCPServer.Server/CodingWithCalvin.MCPServer.Server.csproj b/src/CodingWithCalvin.MCPServer.Server/CodingWithCalvin.MCPServer.Server.csproj index 804839a..d814bd4 100644 --- a/src/CodingWithCalvin.MCPServer.Server/CodingWithCalvin.MCPServer.Server.csproj +++ b/src/CodingWithCalvin.MCPServer.Server/CodingWithCalvin.MCPServer.Server.csproj @@ -14,9 +14,9 @@ - + - + diff --git a/src/CodingWithCalvin.MCPServer.Server/Program.cs b/src/CodingWithCalvin.MCPServer.Server/Program.cs index b1cece7..ae9f87c 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Program.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Program.cs @@ -57,6 +57,7 @@ static async Task RunServerAsync(string pipeName, string host, int port, string serverName, string logLevel) { +#pragma warning disable VSTHRD103 // Console.Error.WriteLine is appropriate in console app context // Parse log level var msLogLevel = logLevel switch { @@ -97,7 +98,9 @@ static async Task RunServerAsync(string pipeName, string host, int port, string .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools() + .WithTools(); var app = builder.Build(); @@ -115,4 +118,5 @@ static async Task RunServerAsync(string pipeName, string host, int port, string await app.RunAsync(); Console.Error.WriteLine("Server shutdown complete"); +#pragma warning restore VSTHRD103 } diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index e45bae6..ba85cd1 100644 --- a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs +++ b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs @@ -65,7 +65,7 @@ public Task> GetAvailableToolsAsync() } var tools = new List(); - var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools) }; + var toolTypes = new[] { typeof(Tools.SolutionTools), typeof(Tools.DocumentTools), typeof(Tools.BuildTools), typeof(Tools.NavigationTools), typeof(Tools.DebuggerTools), typeof(Tools.DiagnosticsTools) }; foreach (var toolType in toolTypes) { @@ -96,8 +96,10 @@ public Task> GetAvailableToolsAsync() public Task ShutdownAsync() { +#pragma warning disable VSTHRD103 // Console.Error.WriteLine is fine in console app context Console.Error.WriteLine("Shutdown requested via RPC"); _shutdownCts.Cancel(); +#pragma warning restore VSTHRD103 return Task.CompletedTask; } @@ -132,4 +134,27 @@ public Task GoToDefinitionAsync(string path, int line, int col => Proxy.GoToDefinitionAsync(path, line, column); public Task FindReferencesAsync(string path, int line, int column, int maxResults = 100) => Proxy.FindReferencesAsync(path, line, column, maxResults); + + public Task GetDebuggerStatusAsync() => Proxy.GetDebuggerStatusAsync(); + public Task DebugLaunchAsync() => Proxy.DebugLaunchAsync(); + public Task DebugLaunchWithoutDebuggingAsync() => Proxy.DebugLaunchWithoutDebuggingAsync(); + public Task DebugContinueAsync() => Proxy.DebugContinueAsync(); + public Task DebugBreakAsync() => Proxy.DebugBreakAsync(); + public Task DebugStopAsync() => Proxy.DebugStopAsync(); + public Task DebugStepOverAsync() => Proxy.DebugStepOverAsync(); + public Task DebugStepIntoAsync() => Proxy.DebugStepIntoAsync(); + public Task DebugStepOutAsync() => Proxy.DebugStepOutAsync(); + + public Task DebugAddBreakpointAsync(string file, int line) => Proxy.DebugAddBreakpointAsync(file, line); + public Task DebugRemoveBreakpointAsync(string file, int line) => Proxy.DebugRemoveBreakpointAsync(file, line); + public Task> DebugGetBreakpointsAsync() => Proxy.DebugGetBreakpointsAsync(); + public Task> DebugGetLocalsAsync() => Proxy.DebugGetLocalsAsync(); + public Task> DebugGetCallStackAsync() => Proxy.DebugGetCallStackAsync(); + + public Task GetErrorListAsync(string? severity = null, int maxResults = 100) + => Proxy.GetErrorListAsync(severity, maxResults); + public Task ReadOutputPaneAsync(string paneIdentifier) => Proxy.ReadOutputPaneAsync(paneIdentifier); + public Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false) + => Proxy.WriteOutputPaneAsync(paneIdentifier, message, activate); + public Task> GetOutputPanesAsync() => Proxy.GetOutputPanesAsync(); } diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs new file mode 100644 index 0000000..89373af --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/DebuggerTools.cs @@ -0,0 +1,131 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Threading.Tasks; +using ModelContextProtocol.Server; + +namespace CodingWithCalvin.MCPServer.Server.Tools; + +[McpServerToolType] +public class DebuggerTools +{ + private readonly RpcClient _rpcClient; + private readonly JsonSerializerOptions _jsonOptions; + + public DebuggerTools(RpcClient rpcClient) + { + _rpcClient = rpcClient; + _jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + } + + [McpServerTool(Name = "debugger_status", ReadOnly = true)] + [Description("Get the current debugger state. Returns the mode (Design = not debugging, Run = executing, Break = paused at breakpoint/step), break reason, current source location (file, line, function), and debugged process name. Always succeeds regardless of debugger state.")] + public async Task GetDebuggerStatusAsync() + { + var status = await _rpcClient.GetDebuggerStatusAsync(); + return JsonSerializer.Serialize(status, _jsonOptions); + } + + [McpServerTool(Name = "debugger_launch", Destructive = false)] + [Description("Start debugging the current startup project (equivalent to F5). A solution must be open with a valid startup project configured. Use debugger_status to check the resulting state.")] + public async Task DebugLaunchAsync() + { + var success = await _rpcClient.DebugLaunchAsync(); + return success ? "Debugging started" : "Failed to start debugging (is a solution open with a startup project configured?)"; + } + + [McpServerTool(Name = "debugger_launch_without_debugging", Destructive = false)] + [Description("Start the current startup project without the debugger attached (equivalent to Ctrl+F5). The application runs normally without breakpoints or stepping. A solution must be open with a valid startup project configured.")] + public async Task DebugLaunchWithoutDebuggingAsync() + { + var success = await _rpcClient.DebugLaunchWithoutDebuggingAsync(); + return success ? "Started without debugging" : "Failed to start without debugging (is a solution open with a startup project configured?)"; + } + + [McpServerTool(Name = "debugger_continue", Destructive = false)] + [Description("Continue execution after a break (equivalent to F5 while paused). Only works when the debugger is in Break mode (paused at a breakpoint or after stepping). Use debugger_status to verify the debugger is in Break mode first.")] + public async Task DebugContinueAsync() + { + var success = await _rpcClient.DebugContinueAsync(); + return success ? "Execution continued" : "Cannot continue (debugger is not in Break mode)"; + } + + [McpServerTool(Name = "debugger_break", Destructive = false)] + [Description("Pause execution of the running program (equivalent to Ctrl+Alt+Break). Only works when the debugger is in Run mode. Use debugger_status to verify the debugger is in Run mode first.")] + public async Task DebugBreakAsync() + { + var success = await _rpcClient.DebugBreakAsync(); + return success ? "Execution paused" : "Cannot break (debugger is not in Run mode)"; + } + + [McpServerTool(Name = "debugger_stop", Destructive = true)] + [Description("Stop the current debugging session (equivalent to Shift+F5). Terminates the debugged process. Only works when a debugging session is active (Run or Break mode).")] + public async Task DebugStopAsync() + { + var success = await _rpcClient.DebugStopAsync(); + return success ? "Debugging stopped" : "Cannot stop (no active debugging session)"; + } + + [McpServerTool(Name = "debugger_step_over", Destructive = false)] + [Description("Step over the current statement (equivalent to F10). Executes the current line and stops at the next line in the same function. Only works when the debugger is in Break mode.")] + public async Task DebugStepOverAsync() + { + var success = await _rpcClient.DebugStepOverAsync(); + return success ? "Stepped over" : "Cannot step over (debugger is not in Break mode)"; + } + + [McpServerTool(Name = "debugger_step_into", Destructive = false)] + [Description("Step into the current statement (equivalent to F11). If the current line contains a function call, steps into that function. Only works when the debugger is in Break mode.")] + public async Task DebugStepIntoAsync() + { + var success = await _rpcClient.DebugStepIntoAsync(); + return success ? "Stepped into" : "Cannot step into (debugger is not in Break mode)"; + } + + [McpServerTool(Name = "debugger_step_out", Destructive = false)] + [Description("Step out of the current function (equivalent to Shift+F11). Continues execution until the current function returns, then breaks at the caller. Only works when the debugger is in Break mode.")] + public async Task DebugStepOutAsync() + { + var success = await _rpcClient.DebugStepOutAsync(); + return success ? "Stepped out" : "Cannot step out (debugger is not in Break mode)"; + } + + [McpServerTool(Name = "debugger_add_breakpoint", Destructive = false)] + [Description("Add a breakpoint at a specific file and line number. Works in any debugger mode (Design, Run, or Break). The file path must be absolute.")] + public async Task DebugAddBreakpointAsync(string path, int line) + { + var success = await _rpcClient.DebugAddBreakpointAsync(path, line); + return success ? $"Breakpoint added at {path}:{line}" : $"Failed to add breakpoint at {path}:{line}"; + } + + [McpServerTool(Name = "debugger_remove_breakpoint", Destructive = true)] + [Description("Remove a breakpoint at a specific file and line number. Returns whether a breakpoint was found and removed.")] + public async Task DebugRemoveBreakpointAsync(string path, int line) + { + var success = await _rpcClient.DebugRemoveBreakpointAsync(path, line); + return success ? $"Breakpoint removed from {path}:{line}" : $"No breakpoint found at {path}:{line}"; + } + + [McpServerTool(Name = "debugger_list_breakpoints", ReadOnly = true)] + [Description("List all breakpoints in the current solution. Returns file, line, column, function name, condition, enabled state, and hit count for each breakpoint.")] + public async Task DebugListBreakpointsAsync() + { + var breakpoints = await _rpcClient.DebugGetBreakpointsAsync(); + return JsonSerializer.Serialize(breakpoints, _jsonOptions); + } + + [McpServerTool(Name = "debugger_get_locals", ReadOnly = true)] + [Description("Get local variables in the current stack frame. Only works when the debugger is in Break mode. Returns name, value, type, and validity for each local variable.")] + public async Task DebugGetLocalsAsync() + { + var locals = await _rpcClient.DebugGetLocalsAsync(); + return JsonSerializer.Serialize(locals, _jsonOptions); + } + + [McpServerTool(Name = "debugger_get_callstack", ReadOnly = true)] + [Description("Get the call stack of the current thread. Only works when the debugger is in Break mode. Returns depth, function name, file name, line number, module, language, and return type for each frame.")] + public async Task DebugGetCallStackAsync() + { + var callStack = await _rpcClient.DebugGetCallStackAsync(); + return JsonSerializer.Serialize(callStack, _jsonOptions); + } +} diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/DiagnosticsTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/DiagnosticsTools.cs new file mode 100644 index 0000000..2de5f38 --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/DiagnosticsTools.cs @@ -0,0 +1,79 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Threading.Tasks; +using ModelContextProtocol.Server; + +namespace CodingWithCalvin.MCPServer.Server.Tools; + +[McpServerToolType] +public class DiagnosticsTools +{ + private readonly RpcClient _rpcClient; + private readonly JsonSerializerOptions _jsonOptions; + + public DiagnosticsTools(RpcClient rpcClient) + { + _rpcClient = rpcClient; + _jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + } + + [McpServerTool(Name = "errors_list", ReadOnly = true)] + [Description("Get errors, warnings, and messages from the Error List. Returns diagnostics with file, line, description, and severity. Filter by severity to focus on specific issues.")] + public async Task GetErrorListAsync( + [Description("Filter by severity: \"Error\", \"Warning\", \"Message\", or null for all. Case-insensitive.")] + string? severity = null, + [Description("Maximum number of items to return. Defaults to 100.")] + int maxResults = 100) + { + var result = await _rpcClient.GetErrorListAsync(severity, maxResults); + + // Always return the JSON result (includes debug info if TotalCount is 0) + return JsonSerializer.Serialize(result, _jsonOptions); + } + + [McpServerTool(Name = "output_read", ReadOnly = true)] + [Description("Read content from an Output window pane. Specify pane by GUID or well-known name (\"Build\", \"Debug\", \"General\"). Note: Some panes may not support reading due to VS API limitations.")] + public async Task ReadOutputPaneAsync( + [Description("Output pane identifier: GUID string or well-known name (\"Build\", \"Debug\", \"General\").")] + string paneIdentifier) + { + var result = await _rpcClient.ReadOutputPaneAsync(paneIdentifier); + + if (string.IsNullOrEmpty(result.Content)) + { + return $"Output pane '{paneIdentifier}' is empty or does not support reading"; + } + + return JsonSerializer.Serialize(result, _jsonOptions); + } + + [McpServerTool(Name = "output_write", Destructive = false, Idempotent = false)] + [Description("Write a message to an Output window pane. Custom panes are auto-created. System panes (Build, Debug) must already exist. Message is appended to existing content.")] + public async Task WriteOutputPaneAsync( + [Description("Output pane identifier: GUID string or name. Custom GUIDs/names will create new panes if needed.")] + string paneIdentifier, + [Description("Message to write. Appended to existing content.")] + string message, + [Description("Whether to activate (bring to front) the Output window. Defaults to false.")] + bool activate = false) + { + var success = await _rpcClient.WriteOutputPaneAsync(paneIdentifier, message, activate); + return success + ? $"Message written to output pane: {paneIdentifier}" + : $"Failed to write to output pane: {paneIdentifier}"; + } + + [McpServerTool(Name = "output_list_panes", ReadOnly = true)] + [Description("List available Output window panes. Returns well-known panes (Build, Debug, General) with their names and GUIDs.")] + public async Task GetOutputPanesAsync() + { + var panes = await _rpcClient.GetOutputPanesAsync(); + + if (panes.Count == 0) + { + return "No output panes available"; + } + + return JsonSerializer.Serialize(panes, _jsonOptions); + } +} diff --git a/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs b/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs index 8e9ef54..487c6af 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Tools/DocumentTools.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel; using System.Text.Json; using System.Threading.Tasks; @@ -65,10 +66,42 @@ public async Task CloseDocumentAsync( [McpServerTool(Name = "document_read", ReadOnly = true)] [Description("Read the contents of a document. If the document is open in VS, reads the current editor buffer (including unsaved changes); otherwise reads from disk.")] public async Task ReadDocumentAsync( - [Description("The full absolute path to the document. Supports forward slashes (/) or backslashes (\\).")] string path) + [Description("The full absolute path to the document. Supports forward slashes (/) or backslashes (\\).")] string path, + [Description("The line number to start reading from (1-based). Defaults to 1.")] int offset = 1, + [Description("Maximum number of lines to read. Defaults to 500. Use smaller values for large files.")] int limit = 500) { var content = await _rpcClient.ReadDocumentAsync(path); - return content ?? $"Could not read document: {path}"; + if (content == null) + { + return $"Could not read document: {path}"; + } + + var lines = content.Split('\n'); + var totalLines = lines.Length; + var startIndex = Math.Max(0, offset - 1); + var count = Math.Min(limit, totalLines - startIndex); + + if (startIndex >= totalLines) + { + return $"Offset {offset} is beyond end of file ({totalLines} lines)"; + } + + var selectedLines = new string[count]; + Array.Copy(lines, startIndex, selectedLines, 0, count); + + var result = new System.Text.StringBuilder(); + for (int i = 0; i < selectedLines.Length; i++) + { + result.AppendLine($"{startIndex + i + 1}\t{selectedLines[i].TrimEnd('\r')}"); + } + + var header = $"Lines {startIndex + 1}-{startIndex + count} of {totalLines}"; + if (startIndex + count < totalLines) + { + header += $" (use offset={startIndex + count + 1} to read more)"; + } + + return $"{header}\n{result}"; } [McpServerTool(Name = "document_write", Destructive = true)] diff --git a/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs b/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs new file mode 100644 index 0000000..0f6dd82 --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Shared/Models/DebuggerModels.cs @@ -0,0 +1,42 @@ +namespace CodingWithCalvin.MCPServer.Shared.Models; + +public class DebuggerStatus +{ + public string Mode { get; set; } = string.Empty; + public bool IsDebugging { get; set; } + public string LastBreakReason { get; set; } = string.Empty; + public string CurrentProcessName { get; set; } = string.Empty; + public string CurrentFile { get; set; } = string.Empty; + public int CurrentLine { get; set; } + public string CurrentFunction { get; set; } = string.Empty; +} + +public class BreakpointInfo +{ + public string File { get; set; } = string.Empty; + public int Line { get; set; } + public int Column { get; set; } + public string FunctionName { get; set; } = string.Empty; + public string Condition { get; set; } = string.Empty; + public bool Enabled { get; set; } + public int CurrentHits { get; set; } +} + +public class LocalVariableInfo +{ + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public bool IsValidValue { get; set; } +} + +public class CallStackFrameInfo +{ + public int Depth { get; set; } + public string FunctionName { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public int LineNumber { get; set; } + public string Module { get; set; } = string.Empty; + public string Language { get; set; } = string.Empty; + public string ReturnType { get; set; } = string.Empty; +} diff --git a/src/CodingWithCalvin.MCPServer.Shared/Models/DiagnosticsModels.cs b/src/CodingWithCalvin.MCPServer.Shared/Models/DiagnosticsModels.cs new file mode 100644 index 0000000..e67d188 --- /dev/null +++ b/src/CodingWithCalvin.MCPServer.Shared/Models/DiagnosticsModels.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace CodingWithCalvin.MCPServer.Shared.Models; + +public class ErrorListResult +{ + public List Items { get; set; } = new(); + public int TotalCount { get; set; } + public int ErrorCount { get; set; } + public int WarningCount { get; set; } + public int MessageCount { get; set; } + public bool Truncated { get; set; } +} + +public class ErrorItemInfo +{ + public string Severity { get; set; } = string.Empty; // "Error", "Warning", "Message" + public string Description { get; set; } = string.Empty; + public string ErrorCode { get; set; } = string.Empty; // e.g., "CS0103" + public string Project { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public int Line { get; set; } + public int Column { get; set; } +} + +public class OutputPaneInfo +{ + public string Name { get; set; } = string.Empty; + public string Guid { get; set; } = string.Empty; +} + +public class OutputReadResult +{ + public string Content { get; set; } = string.Empty; + public string PaneName { get; set; } = string.Empty; + public int LinesRead { get; set; } +} diff --git a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index 4875c6f..1928be6 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -39,6 +39,28 @@ public interface IVisualStudioRpc Task SearchWorkspaceSymbolsAsync(string query, int maxResults = 100); Task GoToDefinitionAsync(string path, int line, int column); Task FindReferencesAsync(string path, int line, int column, int maxResults = 100); + + Task GetDebuggerStatusAsync(); + Task DebugLaunchAsync(); + Task DebugLaunchWithoutDebuggingAsync(); + Task DebugContinueAsync(); + Task DebugBreakAsync(); + Task DebugStopAsync(); + Task DebugStepOverAsync(); + Task DebugStepIntoAsync(); + Task DebugStepOutAsync(); + + Task DebugAddBreakpointAsync(string file, int line); + Task DebugRemoveBreakpointAsync(string file, int line); + Task> DebugGetBreakpointsAsync(); + Task> DebugGetLocalsAsync(); + Task> DebugGetCallStackAsync(); + + // Diagnostics tools + Task GetErrorListAsync(string? severity = null, int maxResults = 100); + Task ReadOutputPaneAsync(string paneIdentifier); + Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false); + Task> GetOutputPanesAsync(); } /// diff --git a/src/CodingWithCalvin.MCPServer/CodingWithCalvin.MCPServer.csproj b/src/CodingWithCalvin.MCPServer/CodingWithCalvin.MCPServer.csproj index e35cc89..991f5f6 100644 --- a/src/CodingWithCalvin.MCPServer/CodingWithCalvin.MCPServer.csproj +++ b/src/CodingWithCalvin.MCPServer/CodingWithCalvin.MCPServer.csproj @@ -5,10 +5,13 @@ latest enable CodingWithCalvin.MCPServer + + $(NoWarn);VSTHRD002;VSTHRD003;VSTHRD010;VSTHRD110;VSSDK007 + diff --git a/src/CodingWithCalvin.MCPServer/Commands/ServerCommands.cs b/src/CodingWithCalvin.MCPServer/Commands/ServerCommands.cs index 3fd5913..64d0c59 100644 --- a/src/CodingWithCalvin.MCPServer/Commands/ServerCommands.cs +++ b/src/CodingWithCalvin.MCPServer/Commands/ServerCommands.cs @@ -154,6 +154,11 @@ private static void OnShowTools(object sender, EventArgs e) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + if (MCPServerPackage.Instance == null) + { + return; + } + if (MCPServerPackage.RpcServer == null || !MCPServerPackage.RpcServer.IsConnected) { VsShellUtilities.ShowMessageBox( diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 230b68b..4f32024 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -35,4 +35,25 @@ public interface IVisualStudioService Task SearchWorkspaceSymbolsAsync(string query, int maxResults = 100); Task GoToDefinitionAsync(string path, int line, int column); Task FindReferencesAsync(string path, int line, int column, int maxResults = 100); + + Task GetDebuggerStatusAsync(); + Task DebugLaunchAsync(); + Task DebugLaunchWithoutDebuggingAsync(); + Task DebugContinueAsync(); + Task DebugBreakAsync(); + Task DebugStopAsync(); + Task DebugStepOverAsync(); + Task DebugStepIntoAsync(); + Task DebugStepOutAsync(); + + Task DebugAddBreakpointAsync(string file, int line); + Task DebugRemoveBreakpointAsync(string file, int line); + Task> DebugGetBreakpointsAsync(); + Task> DebugGetLocalsAsync(); + Task> DebugGetCallStackAsync(); + + Task GetErrorListAsync(string? severity = null, int maxResults = 100); + Task ReadOutputPaneAsync(string paneIdentifier); + Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false); + Task> GetOutputPanesAsync(); } diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index fc7789e..7b50f18 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -192,4 +192,27 @@ public Task GoToDefinitionAsync(string path, int line, int col => _vsService.GoToDefinitionAsync(path, line, column); public Task FindReferencesAsync(string path, int line, int column, int maxResults = 100) => _vsService.FindReferencesAsync(path, line, column, maxResults); + + public Task GetDebuggerStatusAsync() => _vsService.GetDebuggerStatusAsync(); + public Task DebugLaunchAsync() => _vsService.DebugLaunchAsync(); + public Task DebugLaunchWithoutDebuggingAsync() => _vsService.DebugLaunchWithoutDebuggingAsync(); + public Task DebugContinueAsync() => _vsService.DebugContinueAsync(); + public Task DebugBreakAsync() => _vsService.DebugBreakAsync(); + public Task DebugStopAsync() => _vsService.DebugStopAsync(); + public Task DebugStepOverAsync() => _vsService.DebugStepOverAsync(); + public Task DebugStepIntoAsync() => _vsService.DebugStepIntoAsync(); + public Task DebugStepOutAsync() => _vsService.DebugStepOutAsync(); + + public Task DebugAddBreakpointAsync(string file, int line) => _vsService.DebugAddBreakpointAsync(file, line); + public Task DebugRemoveBreakpointAsync(string file, int line) => _vsService.DebugRemoveBreakpointAsync(file, line); + public Task> DebugGetBreakpointsAsync() => _vsService.DebugGetBreakpointsAsync(); + public Task> DebugGetLocalsAsync() => _vsService.DebugGetLocalsAsync(); + public Task> DebugGetCallStackAsync() => _vsService.DebugGetCallStackAsync(); + + public Task GetErrorListAsync(string? severity = null, int maxResults = 100) + => _vsService.GetErrorListAsync(severity, maxResults); + public Task ReadOutputPaneAsync(string paneIdentifier) => _vsService.ReadOutputPaneAsync(paneIdentifier); + public Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false) + => _vsService.WriteOutputPaneAsync(paneIdentifier, message, activate); + public Task> GetOutputPanesAsync() => _vsService.GetOutputPanesAsync(); } diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index e986a89..2d7abea 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -4,13 +4,20 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using CodingWithCalvin.MCPServer.Shared.Models; using CodingWithCalvin.Otel4Vsix; using EnvDTE; using EnvDTE80; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Editor; +using Microsoft.VisualStudio.Package; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Shell.TableManager; +using Microsoft.VisualStudio.Shell.TableControl; +using Microsoft.VisualStudio.Text.Editor; namespace CodingWithCalvin.MCPServer.Services; @@ -1041,4 +1048,840 @@ private static bool IsWordBoundary(string text, int start, int length) var afterOk = start + length >= text.Length || !char.IsLetterOrDigit(text[start + length]); return beforeOk && afterOk; } + + public async Task GetDebuggerStatusAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + var status = new DebuggerStatus + { + Mode = debugger.CurrentMode switch + { + dbgDebugMode.dbgDesignMode => "Design", + dbgDebugMode.dbgBreakMode => "Break", + dbgDebugMode.dbgRunMode => "Run", + _ => "Unknown" + }, + IsDebugging = debugger.CurrentMode != dbgDebugMode.dbgDesignMode, + LastBreakReason = debugger.LastBreakReason switch + { + dbgEventReason.dbgEventReasonNone => "None", + dbgEventReason.dbgEventReasonBreakpoint => "Breakpoint", + dbgEventReason.dbgEventReasonStep => "Step", + dbgEventReason.dbgEventReasonUserBreak => "UserBreak", + dbgEventReason.dbgEventReasonExceptionThrown => "ExceptionThrown", + dbgEventReason.dbgEventReasonExceptionNotHandled => "ExceptionNotHandled", + dbgEventReason.dbgEventReasonStopDebugging => "StopDebugging", + dbgEventReason.dbgEventReasonGo => "Go", + dbgEventReason.dbgEventReasonAttachProgram => "AttachProgram", + dbgEventReason.dbgEventReasonDetachProgram => "DetachProgram", + dbgEventReason.dbgEventReasonLaunchProgram => "LaunchProgram", + dbgEventReason.dbgEventReasonEndProgram => "EndProgram", + dbgEventReason.dbgEventReasonContextSwitch => "ContextSwitch", + _ => "Unknown" + } + }; + + try + { + if (debugger.DebuggedProcesses?.Count > 0) + { + var process = debugger.DebuggedProcesses.Item(1); + status.CurrentProcessName = process.Name; + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + + try + { + if (debugger.CurrentMode == dbgDebugMode.dbgBreakMode && debugger.CurrentStackFrame != null) + { + var frame = (EnvDTE90a.StackFrame2)debugger.CurrentStackFrame; + status.CurrentFunction = frame.FunctionName; + status.CurrentFile = frame.FileName; + status.CurrentLine = (int)frame.LineNumber; + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + + return status; + } + + public async Task DebugLaunchAsync() + { + using var activity = VsixTelemetry.Tracer.StartActivity("DebugLaunch"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + try + { + dte.ExecuteCommand("Debug.Start"); + return true; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public async Task DebugLaunchWithoutDebuggingAsync() + { + using var activity = VsixTelemetry.Tracer.StartActivity("DebugLaunchWithoutDebugging"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + + try + { + dte.ExecuteCommand("Debug.StartWithoutDebugging"); + return true; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public async Task DebugContinueAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode) + { + return false; + } + + try + { + debugger.Go(false); + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + public async Task DebugBreakAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + if (debugger.CurrentMode != dbgDebugMode.dbgRunMode) + { + return false; + } + + try + { + debugger.Break(false); + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + public async Task DebugStopAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + if (debugger.CurrentMode == dbgDebugMode.dbgDesignMode) + { + return false; + } + + try + { + debugger.Stop(false); + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + public async Task DebugStepOverAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode) + { + return false; + } + + try + { + debugger.StepOver(false); + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + public async Task DebugStepIntoAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode) + { + return false; + } + + try + { + debugger.StepInto(false); + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + public async Task DebugStepOutAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode) + { + return false; + } + + try + { + debugger.StepOut(false); + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + public async Task DebugAddBreakpointAsync(string file, int line) + { + using var activity = VsixTelemetry.Tracer.StartActivity("DebugAddBreakpoint"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + try + { + var normalizedPath = NormalizePath(file); + debugger.Breakpoints.Add(Function: "", File: normalizedPath, Line: line); + return true; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public async Task DebugRemoveBreakpointAsync(string file, int line) + { + using var activity = VsixTelemetry.Tracer.StartActivity("DebugRemoveBreakpoint"); + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + + try + { + var breakpoints = debugger.Breakpoints; + for (int i = breakpoints.Count; i >= 1; i--) + { + var bp = breakpoints.Item(i); + if (bp.File != null && PathsEqual(bp.File, file) && bp.FileLine == line) + { + bp.Delete(); + return true; + } + } + + return false; + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + return false; + } + } + + public async Task> DebugGetBreakpointsAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + var results = new List(); + + try + { + foreach (Breakpoint bp in debugger.Breakpoints) + { + try + { + results.Add(new BreakpointInfo + { + File = bp.File ?? string.Empty, + Line = bp.FileLine, + Column = bp.FileColumn, + FunctionName = bp.FunctionName ?? string.Empty, + Condition = bp.Condition ?? string.Empty, + Enabled = bp.Enabled, + CurrentHits = bp.CurrentHits + }); + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + + return results; + } + + public async Task> DebugGetLocalsAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + var results = new List(); + + if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode) + { + return results; + } + + try + { + var frame = debugger.CurrentStackFrame; + if (frame == null) + { + return results; + } + + foreach (Expression local in frame.Locals) + { + try + { + results.Add(new LocalVariableInfo + { + Name = local.Name, + Value = local.Value ?? string.Empty, + Type = local.Type ?? string.Empty, + IsValidValue = local.IsValidValue + }); + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + + return results; + } + + public async Task> DebugGetCallStackAsync() + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dte = await GetDteAsync(); + var debugger = (Debugger2)dte.Debugger; + var results = new List(); + + if (debugger.CurrentMode != dbgDebugMode.dbgBreakMode) + { + return results; + } + + try + { + var thread = debugger.CurrentThread; + if (thread == null) + { + return results; + } + + var depth = 0; + foreach (EnvDTE.StackFrame frame in thread.StackFrames) + { + try + { + var info = new CallStackFrameInfo + { + Depth = depth, + FunctionName = frame.FunctionName, + Module = frame.Module ?? string.Empty, + Language = frame.Language ?? string.Empty, + ReturnType = frame.ReturnType ?? string.Empty + }; + + if (frame is EnvDTE90a.StackFrame2 frame2) + { + info.FileName = frame2.FileName ?? string.Empty; + info.LineNumber = (int)frame2.LineNumber; + } + + results.Add(info); + depth++; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + + return results; + } + + public async Task GetErrorListAsync(string? severity = null, int maxResults = 100) + { + var result = new ErrorListResult(); + + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // Get the Error List service + var errorListService = ServiceProvider.GetService(typeof(SVsErrorList)); + if (errorListService == null) + { + result.Items.Add(new ErrorItemInfo + { + Description = "Error List service not available", + Severity = "Message" + }); + return result; + } + + // Cast to IErrorList to access the TableControl + IErrorList? errorList = errorListService as IErrorList; + if (errorList == null) + { + result.Items.Add(new ErrorItemInfo + { + Description = "Could not access Error List", + Severity = "Message" + }); + return result; + } + + IWpfTableControl tableControl = errorList.TableControl; + if (tableControl == null) + { + result.Items.Add(new ErrorItemInfo + { + Description = "Could not access Error List table control", + Severity = "Message" + }); + return result; + } + + int count = 0; + var severityFilter = severity?.ToLowerInvariant(); + + // Enumerate through error list entries + foreach (ITableEntryHandle entry in tableControl.Entries) + { + if (count >= maxResults) + break; + + try + { + // Get error properties from the table entry + string errorCode = ""; + string projectName = ""; + string text = ""; + string documentName = ""; + int line = 0; + int column = 0; + string severityStr = "Message"; + + // Extract all available properties + if (entry.TryGetValue(StandardTableKeyNames.ErrorCode, out object codeObj)) + { + errorCode = codeObj as string ?? ""; + } + + if (entry.TryGetValue(StandardTableKeyNames.ProjectName, out object projectObj)) + { + projectName = projectObj as string ?? ""; + } + + if (entry.TryGetValue(StandardTableKeyNames.Text, out object textObj)) + { + text = textObj as string ?? ""; + } + + if (entry.TryGetValue(StandardTableKeyNames.DocumentName, out object docObj)) + { + documentName = docObj as string ?? ""; + } + + // Get line number + if (entry.TryGetValue(StandardTableKeyNames.Line, out object lineObj) && lineObj is int lineInt) + { + line = lineInt; + } + + // Get column number + if (entry.TryGetValue(StandardTableKeyNames.Column, out object colObj) && colObj is int colInt) + { + column = colInt; + } + + // Get error severity + if (entry.TryGetValue(StandardTableKeyNames.ErrorSeverity, out object severityObj) && + severityObj is __VSERRORCATEGORY errorCategory) + { + severityStr = errorCategory switch + { + __VSERRORCATEGORY.EC_ERROR => "Error", + __VSERRORCATEGORY.EC_WARNING => "Warning", + __VSERRORCATEGORY.EC_MESSAGE => "Message", + _ => "Message" + }; + } + + // Apply severity filter if specified + if (!string.IsNullOrEmpty(severityFilter) && + !severityStr.Equals(severityFilter, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Add the error item to results + result.Items.Add(new ErrorItemInfo + { + FilePath = documentName, + Line = line, + Column = column, + Description = text, + Severity = severityStr, + ErrorCode = errorCode, + Project = projectName + }); + + count++; + + // Count by severity + if (severityStr == "Error") result.ErrorCount++; + else if (severityStr == "Warning") result.WarningCount++; + else result.MessageCount++; + } + catch (Exception itemEx) + { + VsixTelemetry.TrackException(itemEx); + } + } + + result.TotalCount = count; + + if (count == 0) + { + result.Items.Add(new ErrorItemInfo + { + Description = "No errors or warnings in the Error List. Build the project to populate the Error List.", + Severity = "Message" + }); + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + result.Items.Add(new ErrorItemInfo + { + Description = $"Error accessing Error List: {ex.Message}", + Severity = "Message" + }); + } + + return result; + } + + public async Task> GetOutputPanesAsync() + { + var panes = new List(); + + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var dte = await GetDteAsync(); + + if (dte.ToolWindows?.OutputWindow?.OutputWindowPanes != null) + { + // Enumerate all actual panes in the Output window + foreach (EnvDTE.OutputWindowPane pane in dte.ToolWindows.OutputWindow.OutputWindowPanes) + { + panes.Add(new OutputPaneInfo + { + Name = pane.Name, + Guid = pane.Guid ?? string.Empty // Custom panes may not have a GUID + }); + } + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + } + + return panes; + } + + public async Task ReadOutputPaneAsync(string paneIdentifier) + { + var result = new OutputReadResult { PaneName = paneIdentifier }; + + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var dte = await GetDteAsync(); + + if (dte.ToolWindows?.OutputWindow == null) + { + result.Content = "Output window not available"; + return result; + } + + // Find the matching pane by name (works for both well-known and custom panes) + EnvDTE.OutputWindowPane? targetPane = null; + + foreach (EnvDTE.OutputWindowPane outputPane in dte.ToolWindows.OutputWindow.OutputWindowPanes) + { + if (outputPane.Name == paneIdentifier) + { + targetPane = outputPane; + break; + } + } + + if (targetPane == null) + { + result.Content = $"Output pane '{paneIdentifier}' not found"; + return result; + } + + // Read the text using the documented approach: + // 1) Get the TextDocument + // 2) Get StartPoint and create EditPoint + // 3) Call GetText with EndPoint + try + { + // Check if TextDocument is available + if (targetPane.TextDocument == null) + { + result.Content = $"Output pane '{paneIdentifier}' is empty or not yet initialized. " + + "Trigger an action for this pane (e.g., start debugging, build, or write to it)."; + return result; + } + + try + { + EnvDTE.TextDocument textDoc = targetPane.TextDocument; + EnvDTE.EditPoint startPoint = textDoc.StartPoint.CreateEditPoint(); + EnvDTE.TextPoint endPoint = textDoc.EndPoint; + + string text = startPoint.GetText(endPoint); + result.Content = text; + return result; + } + catch (System.Runtime.InteropServices.COMException comEx) when (comEx.HResult == unchecked((int)0x80004005)) + { + // E_FAIL: TextDocument exists but is not accessible (pane not initialized) + result.Content = $"Output pane '{paneIdentifier}' is not yet initialized. " + + "Trigger an action for this pane to generate content."; + return result; + } + } + catch (Exception innerEx) + { + result.Content = $"Could not read TextDocument: {innerEx.Message}"; + return result; + } + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + result.Content = $"Error reading output pane: {ex.Message}"; + } + + return result; + } + + public async Task WriteOutputPaneAsync(string paneIdentifier, string message, bool activate = false) + { + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var outputWindow = ServiceProvider.GetService(typeof(SVsOutputWindow)) as IVsOutputWindow; + if (outputWindow == null) + { + return false; + } + + // Parse identifier as GUID or name + Guid paneGuid = Guid.Empty; + bool isCustomPane = false; + + if (Guid.TryParse(paneIdentifier, out var parsedGuid)) + { + paneGuid = parsedGuid; + isCustomPane = !IsWellKnownPane(paneGuid); + } + else + { + // Map well-known names to GUIDs + paneGuid = paneIdentifier.ToLowerInvariant() switch + { + "build" => VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid, + "debug" => VSConstants.OutputWindowPaneGuid.DebugPane_guid, + "general" => VSConstants.OutputWindowPaneGuid.GeneralPane_guid, + _ => Guid.NewGuid() // Create custom pane with new GUID + }; + + isCustomPane = paneIdentifier.ToLowerInvariant() switch + { + "build" or "debug" or "general" => false, + _ => true + }; + } + + // Try to get existing pane + var paneGuidRef = paneGuid; + int hr = outputWindow.GetPane(ref paneGuidRef, out IVsOutputWindowPane? pane); + + // If pane doesn't exist and it's a custom pane, create it + if (hr != 0 && isCustomPane) + { + hr = outputWindow.CreatePane(ref paneGuid, paneIdentifier, 1, 1); + if (hr != 0) + { + return false; + } + + // Get the newly created pane + paneGuidRef = paneGuid; + hr = outputWindow.GetPane(ref paneGuidRef, out pane); + if (hr != 0 || pane == null) + { + return false; + } + } + else if (hr != 0 || pane == null) + { + // System pane not found - don't create + return false; + } + + // Write message to pane + pane.OutputStringThreadSafe(message + "\r\n"); + + // Activate pane if requested + if (activate) + { + pane.Activate(); + } + + return true; + } + catch (Exception ex) + { + VsixTelemetry.TrackException(ex); + return false; + } + } + + private IVsOutputWindowPane? GetPaneByIdentifier(IVsOutputWindow outputWindow, string paneIdentifier) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (Guid.TryParse(paneIdentifier, out var paneGuid)) + { + var guidRef = paneGuid; + outputWindow.GetPane(ref guidRef, out var pane); + return pane; + } + + // Map well-known names + var guid = paneIdentifier.ToLowerInvariant() switch + { + "build" => VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid, + "debug" => VSConstants.OutputWindowPaneGuid.DebugPane_guid, + "general" => VSConstants.OutputWindowPaneGuid.GeneralPane_guid, + _ => Guid.Empty + }; + + if (guid != Guid.Empty) + { + var guidRef = guid; + outputWindow.GetPane(ref guidRef, out var pane); + return pane; + } + + return null; + } + + private bool IsWellKnownPane(Guid paneGuid) + { + return paneGuid == VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid || + paneGuid == VSConstants.OutputWindowPaneGuid.DebugPane_guid || + paneGuid == VSConstants.OutputWindowPaneGuid.GeneralPane_guid; + } }