How can I use the Run/Debug button for my own VS Code extension’s command?

Summary

The user’s request concerns integrating a custom MicroPython extension with the VS Code Run/Debug UI, specifically the “play button” in the title bar. The root issue is a misunderstanding of VS Code’s architecture: the Run/Debug button is strictly tied to Debuggers (via debuggers contribution point) or Tasks. It is not a generic UI element for arbitrary commands.

While the user wants to trigger a “Run” action, their goal requires debugger contribution to appear in that specific dropdown. A true “Run without debugging” flow is actually a preLaunchTask combined with an “empty” debugger configuration.

Root Cause

The user is attempting to use a standard Command (commands contribution point) to populate the Run/Debug dropdown. The VS Code architecture prevents this. The dropdown populates based on the active context (i.e., the current language mode) and the available Debug Configurations defined in launch.json or contributed by extensions.

The confusion stems from the Python extension’s behavior, which utilizes a Composite Debugger approach:

  1. It registers a debugger (python) to appear in the dropdown.
  2. It uses preLaunchTask to execute the “Run” logic (running the python process) before the debugger attaches (or instead of attaching).

Why This Happens in Real Systems

Visual Studio Code maintains a strict separation of concerns between Commands (actions) and Debugging (runtime inspection).

  • UI Consistency: The Run/Debug button is reserved for workflows that support standard debugging lifecycles (Launch, Attach, Stop, Step). Allowing arbitrary commands would break the predictable behavior of the “Stop” button and the Debug Console.
  • Contribution Points: Extensions must explicitly declare support for a specific language via contributes.debuggers. Without this declaration, the extension is invisible to the Run/Debug menu for that file type.
  • Editor Context: The button reacts to editorLangId. If your extension doesn’t claim to be a debugger for the active file type (likely python or micropython), the button will default to other installed debuggers (like the Python extension) or the last used configuration.

Real-World Impact

  • User Experience Fragmentation: The user’s extension works via context menus but fails to provide the “native” feel of the built-in Python extension.
  • Workflow Inefficiency: Users cannot use standard keyboard shortcuts (F5) to run code on the microcontroller.
  • Adoption Barrier: Developers expect “Run/Debug” to work out of the box. If it requires manual launch.json editing, the extension feels “broken” or “non-standard” compared to official extensions.

Example or Code

To hook into the Run/Debug button, you must declare a debugger in package.json and provide a resolveDebugConfiguration method in your extension.

1. package.json Contribution

You must register a debugger named mpython. Even if you don’t support attaching to a running process, this makes you appear in the dropdown.

{
  "contributes": {
    "debuggers": [
      {
        "type": "mpython",
        "label": "MicroPython Run",
        "win": {},
        "osx": {},
        "linux": {},
        "configurationAttributes": {
          "launch": {
            "properties": {
              "program": {
                "type": "string",
                "description": "Path to the script to run."
              }
            }
          }
        },
        "initialConfigurations": [
          {
            "type": "mpython",
            "name": "Run on MicroPython",
            "request": "launch",
            "program": "${file}"
          }
        ]
      }
    ]
  }
}

2. extension.ts Implementation

You need to handle the resolveDebugConfiguration function. This is where you intercept the “Run” click.

vscode.debug.registerDebugConfigurationProvider('mpython', {
    resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult {
        // 1. Intercept the launch
        const editor = vscode.window.activeTextEditor;
        if (editor) {
            const localPath = editor.document.fileName;
            const port = config.port || '/dev/ttyUSB0'; // Get port from config or defaults

            // 2. Run your logic (Simulating the "Run")
            // Since VS Code expects a debugger to launch, but we don't have one,
            // we execute the command directly and return null to cancel the debug session start.
            vscode.commands.executeCommand('mpremote.run', { fsPath: localPath });

            // 3. Returning null prevents the "No debugger found" error
            return null; 
        }
        return config;
    }
});

How Senior Engineers Fix It

Senior engineers approach this by adhering to the “Debugger Wrapper” pattern.

  1. Abstract the Logic: Ensure the core logic (running code on the device) is decoupled from the UI trigger. The mpremote.run command is correct, but it needs to be callable from two places: the context menu and the debug provider.
  2. Use resolveDebugConfiguration: This is the entry point. When the user clicks the green arrow, this function is invoked.
  3. Fire-and-Forget Execution: Since a MicroPython script execution is usually a “run and finish” task (not an interactive debugging session with breakpoints), the correct behavior is to:
    • Execute the run command.
    • Return null immediately from the provider.
    • Why null? Returning null tells VS Code “I handled it, don’t look for a debugger.”
  4. Handle “Run Without Debugging”: If you want the “Pause/Stop” button to work (which is a UI state, not code), you must actually start a DebugSession using vscode.debug.startDebugging. However, for a simple “Flash and Run” tool, returning null after executing the command is the standard way to make the button act like a “Run” button rather than a “Debug” button.

Why Juniors Miss It

  • Mental Model of the Button: Juniors often view the Run/Debug button as a generic “Do Action” button (like a Form Submit button). They don’t realize it is tightly coupled to the Debug Adapter Protocol lifecycle.
  • Search Keywords: They search for “add command to run button” or “customize run menu.” They should be searching for “VS Code debug contribution point” or “custom debugger extension.”
  • Ignoring package.json: They focus heavily on commands and menus contributions and overlook the debuggers section, which is mandatory for this specific UI element.
  • Underestimating launch.json: They often try to force a button click without understanding that the button’s behavior is defined by the configurations found in launch.json or provided by the initialConfigurations contribution.