mfussenegger / nvim-dap

Debug Adapter Protocol client implementation for Neovim
GNU General Public License v3.0
5.09k stars 179 forks source link

Support string input for `args` field in `launch.json` #1267

Closed Pagliacii closed 2 weeks ago

Pagliacii commented 2 weeks ago

Problem Statement

The args field in launch.json doesn't accept a string value when using the ${input:programArgs} variable with the codelldb adapter. This behavior differs from VS Code's handling of the args field with other debug adapters, where a string input is typically split by whitespace into multiple arguments.

While not explicitly documented, VS Code generally supports both an array of strings and a single string (to be tokenized) for the args field with most debug adapters. However, when using the codelldb adapter, attempting to use a string input results in an error:

Error on launch: Could not parse launch configuration: invalid type: string "1 1", expected a sequence

This inconsistency prevents passing multiple arguments to the debugging program using a single input variable when using codelldb, which is especially useful for dynamic argument input. The behavior doesn't match the functionality available with other debug adapters in VS Code.

My launch.json configuration:

Click to expand launch.json ```json { "version": "0.2.0", "configurations": [ { "name": "Launch C++ Program", "type": "codelldb", "request": "launch", "program": "${workspaceFolder}/build/${fileBasenameNoExtension}.exe", "args": "${input:programArgs}", "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "preLaunchTask": "build", "initCommands": [ "log enable lldb default api" ] } ], "inputs": [ { "id": "programArgs", "type": "promptString", "description": "Enter program arguments (separated by spaces):" } ] } ```

References

Possible Solutions

A potential solution to this issue involves modifying the vscode.lua file in the DAP (Debug Adapter Protocol) extension. Here's the proposed change:

diff --git a/lua/dap/ext/vscode.lua b/lua/dap/ext/vscode.lua
index d256352..b073ff0 100644
--- a/lua/dap/ext/vscode.lua
+++ b/lua/dap/ext/vscode.lua
@@ -128,6 +128,10 @@ local function apply_inputs(config, inputs)
   local cache = {}
   for key, value in pairs(config) do
     result[key] = apply_input(inputs, value, cache)
+    if key == "args" and type(value) == "string" then
+      --- The args property is a string, so we need to split it up
+      result[key] = vim.split(result[key], " ")
+    end
   end
   return result
 end

This solution adds a check specifically for the args key in the configuration. When the args value is a string, it splits the string into an array using whitespace as the delimiter. This approach aims to handle cases where args is provided as a string, converting it to the array format expected by the codelldb adapter.

I have personally verified that this solution works for my specific use case with the codelldb adapter.

Limitations and Considerations

While this solution addresses the immediate issue, it's important to note that it may not cover all scenarios. Some potential limitations include:

  1. It doesn't handle arguments that contain spaces within them (e.g., quoted strings).
  2. It assumes that whitespace is always the correct delimiter for splitting arguments.
  3. It modifies the behavior for all debug adapters, which might not be desirable in some cases.

A more robust solution might involve:

  1. Implementing more sophisticated argument parsing (e.g., respecting quotes).
  2. Adding configuration options to enable/disable this behavior for specific adapters.
  3. Considering edge cases like empty strings or special characters in arguments.

Further testing and refinement would be necessary to ensure this solution works correctly across various use cases and debug adapters.

Considered Alternatives

No response


Claim

This problem description and proposed solution have been refined with the assistance of an AI language model. As the original author is not a native English speaker, the AI has helped to improve clarity and expression while maintaining the original intent and technical accuracy of the content. The possible solutions, including the code modification and discussion of its limitations, are based on the author's original ideas and implementation. The AI has assisted in organizing and articulating these ideas more clearly, but the technical content and verification of the solution's effectiveness come from the author's own experience and testing.

mfussenegger commented 2 weeks ago

This is out of scope for nvim-dap as it aims to remain agnostic to debug adapter specific properties. If one debug adapter required a string args property this would break down.

You can use enrich_config (see :h dap-adapter) or a on_config listener (:help dap-listeners-on_config) to pre-process configurations.

Diaoul commented 1 day ago

It seems that in the context of python, using a string as "args" is not supported, I'm having this error

Diaoul commented 1 day ago

I'd love to request/contribute a fix for that but I'm not sure where is the best place to do this @mfussenegger. How can I have a client with supportsArgsCanBeInterpretedByShell capability? Does it need to be implemented on debugpy's side?

Diaoul commented 22 hours ago

As much as I would want this upstream, I've implemented the suggestion by @mfussenegger for now.

Improvement suggestions welcome 🙏

  -- string parser with quote escaping capabilities
  -- inspired by http://lua-users.org/wiki/LpegRecipes
  --- @param input string
  --- @return table
  local function split(input)
    local lpeg = vim.lpeg

    local P, S, C, Cc, Ct = lpeg.P, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Ct

    --- @param id string
    --- @param patt vim.lpeg.Capture
    --- @return vim.lpeg.Pattern
    local function token(id, patt)
      return Ct(Cc(id) * C(patt))
    end

    local single_quoted = P("'") * ((1 - S("'\r\n\f\\")) + (P("\\") * 1)) ^ 0 * "'"
    local double_quoted = P('"') * ((1 - S('"\r\n\f\\')) + (P("\\") * 1)) ^ 0 * '"'

    local whitespace = token("whitespace", S("\r\n\f\t ") ^ 1)
    local word = token("word", (1 - S("' \r\n\f\t\"")) ^ 1)
    local string = token("string", single_quoted + double_quoted)

    local pattern = Ct((string + whitespace + word) ^ 0)

    local t = {}
    local tokens = lpeg.match(pattern, input)

    -- somehow, this did not work out
    if tokens == nil or type(tokens) == "integer" then
      return t
    end

    for _, tok in ipairs(tokens) do
      if tok[1] ~= "whitespace" then
        if tok[1] == "string" then
          -- cut off quotes and replace escaped quotes
          local v, _ = tok[2]:sub(2, -2):gsub("\\(['\"])", "%1")
          table.insert(t, v)
        else
          table.insert(t, tok[2])
        end
      end
    end

    return t
  end

  -- add debugpy capability to specify args as string
  dap.listeners.on_config["debugpy"] = function(config)
    -- copy the config and split the args if it is a string
    local c = {}

    for k, v in pairs(vim.deepcopy(config)) do
      if k == "args" and type(v) == "string" then
        c[k] = split(v)
      else
        c[k] = v
      end
    end

    return c
  end
end,
mfussenegger commented 15 hours ago

I'd accept a PR that adds just the split function to dap.utils see https://github.com/mfussenegger/nvim-dap/issues/629

Diaoul commented 14 hours ago

There you go