diff --git a/src/CodingWithCalvin.MCPServer.Server/Program.cs b/src/CodingWithCalvin.MCPServer.Server/Program.cs index b1cece7..892bc70 100644 --- a/src/CodingWithCalvin.MCPServer.Server/Program.cs +++ b/src/CodingWithCalvin.MCPServer.Server/Program.cs @@ -97,7 +97,8 @@ static async Task RunServerAsync(string pipeName, string host, int port, string .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); var app = builder.Build(); diff --git a/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs b/src/CodingWithCalvin.MCPServer.Server/RpcClient.cs index e45bae6..37cfcd9 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) }; foreach (var toolType in toolTypes) { @@ -132,4 +132,20 @@ 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(); } 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/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/RpcContracts.cs b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs index 4875c6f..53843fc 100644 --- a/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs +++ b/src/CodingWithCalvin.MCPServer.Shared/RpcContracts.cs @@ -39,6 +39,22 @@ 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(); } /// diff --git a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs index 230b68b..0c355cd 100644 --- a/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/IVisualStudioService.cs @@ -35,4 +35,20 @@ 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(); } diff --git a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs index fc7789e..0561110 100644 --- a/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs +++ b/src/CodingWithCalvin.MCPServer/Services/RpcServer.cs @@ -192,4 +192,20 @@ 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(); } diff --git a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs index e986a89..188e2cf 100644 --- a/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs +++ b/src/CodingWithCalvin.MCPServer/Services/VisualStudioService.cs @@ -10,7 +10,6 @@ using EnvDTE; using EnvDTE80; using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; namespace CodingWithCalvin.MCPServer.Services; @@ -1041,4 +1040,442 @@ 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; + } }