diff --git a/src/Runner.Worker/Dap/DapDebugSession.cs b/src/Runner.Worker/Dap/DapDebugSession.cs index e37b8542c..65697e274 100644 --- a/src/Runner.Worker/Dap/DapDebugSession.cs +++ b/src/Runner.Worker/Dap/DapDebugSession.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -139,6 +139,12 @@ namespace GitHub.Runner.Worker.Dap "next" => HandleNext(request), "setBreakpoints" => HandleSetBreakpoints(request), "setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request), + "completions" => HandleCompletions(request), + "stepIn" => CreateResponse(request, false, "Step In is not supported. Actions jobs debug at the step level — use 'next' to advance to the next step.", body: null), + "stepOut" => CreateResponse(request, false, "Step Out is not supported. Actions jobs debug at the step level — use 'continue' to resume.", body: null), + "stepBack" => CreateResponse(request, false, "Step Back is not yet supported.", body: null), + "reverseContinue" => CreateResponse(request, false, "Reverse Continue is not yet supported.", body: null), + "pause" => CreateResponse(request, false, "Pause is not supported. The debugger pauses automatically at step boundaries.", body: null), _ => CreateResponse(request, false, $"Unsupported command: {request.Command}", body: null) }; } @@ -195,7 +201,7 @@ namespace GitHub.Runner.Worker.Dap SupportsRestartFrame = false, SupportsGotoTargetsRequest = false, SupportsStepInTargetsRequest = false, - SupportsCompletionsRequest = false, + SupportsCompletionsRequest = true, SupportsModulesRequest = false, SupportsTerminateRequest = false, SupportTerminateDebuggee = false, @@ -466,6 +472,52 @@ namespace GitHub.Runner.Worker.Dap } } + private Response HandleCompletions(Request request) + { + var args = request.Arguments?.ToObject(); + var text = args?.Text ?? string.Empty; + + var items = new List(); + + // Offer DSL commands when the user is starting to type + if (string.IsNullOrEmpty(text) || "help".StartsWith(text, System.StringComparison.OrdinalIgnoreCase)) + { + items.Add(new CompletionItem + { + Label = "help", + Text = "help", + Detail = "Show available debug console commands", + Type = "function" + }); + } + if (string.IsNullOrEmpty(text) || "help(\"run\")".StartsWith(text, System.StringComparison.OrdinalIgnoreCase)) + { + items.Add(new CompletionItem + { + Label = "help(\"run\")", + Text = "help(\"run\")", + Detail = "Show help for the run command", + Type = "function" + }); + } + if (string.IsNullOrEmpty(text) || "run(".StartsWith(text, System.StringComparison.OrdinalIgnoreCase) + || text.StartsWith("run(", System.StringComparison.OrdinalIgnoreCase)) + { + items.Add(new CompletionItem + { + Label = "run(\"...\")", + Text = "run(\"", + Detail = "Execute a script (like a workflow run step)", + Type = "function" + }); + } + + return CreateResponse(request, true, body: new CompletionsResponseBody + { + Targets = items + }); + } + private Response HandleContinue(Request request) { Trace.Info("Continue command received"); diff --git a/src/Runner.Worker/Dap/DapMessages.cs b/src/Runner.Worker/Dap/DapMessages.cs index bf8685981..53cd7a436 100644 --- a/src/Runner.Worker/Dap/DapMessages.cs +++ b/src/Runner.Worker/Dap/DapMessages.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -885,6 +885,103 @@ namespace GitHub.Runner.Worker.Dap #endregion + #region Completions Request/Response + + /// + /// Arguments for 'completions' request. + /// + public class CompletionsArguments + { + /// + /// Returns completions in the scope of this stack frame. + /// + [JsonProperty("frameId", NullValueHandling = NullValueHandling.Ignore)] + public int? FrameId { get; set; } + + /// + /// One or more source lines. Typically this is the text users have typed + /// in the debug console (REPL). + /// + [JsonProperty("text")] + public string Text { get; set; } + + /// + /// The position within 'text' for which to determine the completion proposals. + /// It is measured in UTF-16 code units. + /// + [JsonProperty("column")] + public int Column { get; set; } + + /// + /// A line for which to determine the completion proposals. + /// If missing the first line of the text is assumed. + /// + [JsonProperty("line", NullValueHandling = NullValueHandling.Ignore)] + public int? Line { get; set; } + } + + /// + /// A completion item in the debug console. + /// + public class CompletionItem + { + /// + /// The label of this completion item. + /// + [JsonProperty("label")] + public string Label { get; set; } + + /// + /// If text is returned and not an empty string, then it is inserted instead + /// of the label. + /// + [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] + public string Text { get; set; } + + /// + /// A human-readable string with additional information about this item. + /// + [JsonProperty("detail", NullValueHandling = NullValueHandling.Ignore)] + public string Detail { get; set; } + + /// + /// The item's type. Typically the client uses this information to render the item + /// in the UI with an icon. + /// Values: 'method', 'function', 'constructor', 'field', 'variable', 'class', + /// 'interface', 'module', 'property', 'unit', 'value', 'enum', 'keyword', + /// 'snippet', 'text', 'color', 'file', 'reference', 'customcolor' + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + + /// + /// Start position (0-based) within 'text' that should be replaced + /// by the completion text. + /// + [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] + public int? Start { get; set; } + + /// + /// Length of the text that should be replaced by the completion text. + /// + [JsonProperty("length", NullValueHandling = NullValueHandling.Ignore)] + public int? Length { get; set; } + } + + /// + /// Response body for 'completions' request. + /// + public class CompletionsResponseBody + { + /// + /// The possible completions. + /// + [JsonProperty("targets")] + public List Targets { get; set; } = new List(); + } + + #endregion + #region Events ///