Add completions support and friendly errors for unsupported commands

Completions (SupportsCompletionsRequest = true):
- Respond to DAP 'completions' requests with our DSL commands
  (help, help("run"), run(...)) so they appear in the debug
  console autocomplete across all DAP clients
- Add CompletionsArguments, CompletionItem, and
  CompletionsResponseBody to DapMessages

Friendly error messages for unsupported stepping commands:
- stepIn: explain that Actions debug at the step level
- stepOut: suggest using 'continue'
- stepBack/reverseContinue: note 'not yet supported'
- pause: explain automatic pausing at step boundaries

The DAP spec does not provide a capability to hide stepIn/stepOut
buttons (they are considered fundamental operations). The best
server-side UX is clear error messages when clients send them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Francesco Renzi
2026-03-12 11:38:20 +00:00
committed by GitHub
parent 860a919081
commit 8d6b38a428
2 changed files with 152 additions and 3 deletions

View File

@@ -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<CompletionsArguments>();
var text = args?.Text ?? string.Empty;
var items = new List<CompletionItem>();
// 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");

View File

@@ -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
/// <summary>
/// Arguments for 'completions' request.
/// </summary>
public class CompletionsArguments
{
/// <summary>
/// Returns completions in the scope of this stack frame.
/// </summary>
[JsonProperty("frameId", NullValueHandling = NullValueHandling.Ignore)]
public int? FrameId { get; set; }
/// <summary>
/// One or more source lines. Typically this is the text users have typed
/// in the debug console (REPL).
/// </summary>
[JsonProperty("text")]
public string Text { get; set; }
/// <summary>
/// The position within 'text' for which to determine the completion proposals.
/// It is measured in UTF-16 code units.
/// </summary>
[JsonProperty("column")]
public int Column { get; set; }
/// <summary>
/// A line for which to determine the completion proposals.
/// If missing the first line of the text is assumed.
/// </summary>
[JsonProperty("line", NullValueHandling = NullValueHandling.Ignore)]
public int? Line { get; set; }
}
/// <summary>
/// A completion item in the debug console.
/// </summary>
public class CompletionItem
{
/// <summary>
/// The label of this completion item.
/// </summary>
[JsonProperty("label")]
public string Label { get; set; }
/// <summary>
/// If text is returned and not an empty string, then it is inserted instead
/// of the label.
/// </summary>
[JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)]
public string Text { get; set; }
/// <summary>
/// A human-readable string with additional information about this item.
/// </summary>
[JsonProperty("detail", NullValueHandling = NullValueHandling.Ignore)]
public string Detail { get; set; }
/// <summary>
/// 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'
/// </summary>
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; }
/// <summary>
/// Start position (0-based) within 'text' that should be replaced
/// by the completion text.
/// </summary>
[JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)]
public int? Start { get; set; }
/// <summary>
/// Length of the text that should be replaced by the completion text.
/// </summary>
[JsonProperty("length", NullValueHandling = NullValueHandling.Ignore)]
public int? Length { get; set; }
}
/// <summary>
/// Response body for 'completions' request.
/// </summary>
public class CompletionsResponseBody
{
/// <summary>
/// The possible completions.
/// </summary>
[JsonProperty("targets")]
public List<CompletionItem> Targets { get; set; } = new List<CompletionItem>();
}
#endregion
#region Events
/// <summary>