neoclide / coc.nvim

Nodejs extension host for vim & neovim, load extensions like VSCode and host language servers.
Other
24.36k stars 956 forks source link

Debug Adapter Protocol Support #322

Open tbo opened 5 years ago

tbo commented 5 years ago

Is your feature request related to a problem? Please describe.

I would like to have additional debugging support in coc.nvim. There are no debug extensions for neovim, that provide proper auto-completion, because this would require additional IDE level language support.

Describe the solution you'd like

VS Code integrates a multitude of debuggers via its Debug Adapter Protocol. They offer a node based sdk as a separate NPM Package. Additional info can be found here. coc.nvim would need to:

Describe alternatives you've considered

I checked two existing debugger extensions:

Additional context

All debug adapters known to me only exist as a vscode extension and in the case of Java Debug even require its LSP counterpart. I realize, that debugging is not coc.nvim's main focus, but if this can be implemented for neovim, then likely only on top of coc.nvim. I don't know any other plugin, that integrates the vscode ecosystem so well and also provides LSP-support.

chemzqm commented 5 years ago

Is possbile to execute command and send request to a running language server started by coc, so it's possible to make a coc extension for it.

puremourning commented 5 years ago

I would consider vimspector support for neovim, but only after I have made vimspector fully tested and stable (e.g. a "v1 version") on vim, as that's the platform I use. Contributions are of course welcome.

LinArcX commented 5 years ago

@chemzqm How can we write these kind of extensions? I can help you.

XVilka commented 5 years ago

@tbo just an update - there is an WIP PR to add support of Neovim in Vimspector by @romgrk.

tbo commented 5 years ago

@XVilka I checked it out, but the last few comments do not look promising.

puremourning commented 5 years ago

Better ref https://github.com/puremourning/vimspector/issues/29

tbo commented 5 years ago

@puremourning Thanks for reference. That actually looks quite promising.

LinArcX commented 4 years ago

Is possbile to execute command and send request to a running language server started by coc, so it's possible to make a coc extension for it.

@chemzqm Could you please help us how to create this kind of extension?

adelarsq commented 4 years ago

@chemzqm I'm interested in help too.

hariamoor-zz commented 4 years ago

Better ref neovim/neovim#11732. That way, it isn't bound to Coc and can be packaged with a stable release of Neovim one day.

puremourning commented 4 years ago

Vimspector neovim support is now fully functional and just needs testing.

hariamoor-zz commented 4 years ago

@chemzqm @LinArcX Would you two consider doing it in Lua and shipping it with Neovim? Like I said, I'd rather have it as a Lua plugin shipped with the editor than a Coc plugin, because that way, the functionality isn't bound to Coc.

tbo commented 4 years ago

@hariamoor What should this plugin provide, that isn't already provided by Vimspector? AFAIK auto-completion is the only thing missing and also the original reason I created this feature request here. I just saw, that @puremourning mentioned, that DAP has support for this as well (https://github.com/puremourning/vimspector/issues/52#issuecomment-537460921). So Coc might not be necessary for this after all. @puremourning please correct me, if I'm wrong.

puremourning commented 4 years ago

You’re right. Now that vimspector has neovim support (nearly).

Vimspector has cmdline completion (sort of) for evaluate and watch. And for the console prompt buffer, it’s possible manually/planned generally to make an omnifunc which would work with standard completion and/or YCM etc.

I suppose one thing that would help is coc could expose an equivalent to the command to that YCM does to allow starting the java debug adapter (which is a plugin to jdt.ls). In YCM user runs :YcmCompleter ExecuteCommand vscode.java.startDebugSession That’s an LSP command within the java server and a mechanism by which the plugin is discovered and loaded.

But other than that it seems out of scope for coc.

oblitum commented 4 years ago

@hariamoor this is coc issue tracker, for a possible coc debugging extension. It doesn't make sense to request coc author to write a standalone debugging interface, for NeoVim only, in Lua.

rockneverdies55 commented 4 years ago

So based on @puremourning's comment... currently is there a way to start debug session on java language server from within coc (vscode.java.startDebugSession)? Or a workaround?

dansomething commented 4 years ago

Here's an example:

function! JavaStartDebugCallback(err, port)
  execute "cexpr! 'Java debug started on port: " . a:port . "'"
  call vimspector#LaunchWithSettings({ "configuration": "Java Attach", "AdapterPort": a:port })
endfunction

function! JavaStartDebug()
  call CocActionAsync('runCommand', 'vscode.java.startDebugSession', function('JavaStartDebugCallback'))
endfunction

nmap <F1> :call JavaStartDebug()<CR>
rockneverdies55 commented 4 years ago

Thank you @dansomething. Here how it goes when I press F1 to invoke your function:

(1 of 1): Java debug started on port: null

Press ENTER or type command to continue

Enter value for debugPort: 8000

Here it opens complete empty vimspector window with asking for the port at the bottom again:

Enter port to connect to: 8000 Request for initialize aborted: Closing down

Debugging works flawlessly in vscode but can't get it to work in nvim. Not sure if it's caused by vimspector itself or coc is somehow preventing vscode-debugger and vimspector to talk to each other.

dansomething commented 4 years ago

Yeah, you'll need to get the Java Debug extension loaded into jdt.ls. Currently Vimspector won't do this for you. The vscode-java-debug plugin does it like this. See the jdt.ls extension docs for more info.

I've hacked to together an extension for coc.nvim to do this same thing. You're welcome to try it, but I can't make any guarantees for your setup. Being experimental, its currently not available as an npm package.

To install the coc-java-debug extension:

 CocInstall https://github.com/dansomething/coc-java-debug

To uninstall it

CocUninstall coc-java-debug
dansomething commented 4 years ago

Oh, here's the .vimspector.json I'm using with this setup to attached to an existing Java process that's waiting for a debug connection.

{
  "adapters": {
    "java-debug-server": {
      "name": "vscode-java",
      "port": "${AdapterPort}"
    }
  },
  "configurations": {
    "Java Attach": {
      "adapter": "java-debug-server",
      "configuration": {
        "request": "attach",
        "host": "127.0.0.1",
        "port": "5005"
      },
      "breakpoints": {
        "exception": {
          "caught": "N",
          "uncaught": "N"
        }
      }
    }
  }
}

See the Vimspector config for more info on this setup.

rockneverdies55 commented 4 years ago

Great. Thanks for the guidance. After hours of struggling... finally once your plugin was installed, it started to work like a charm.

Oh man this is plain awesome :slightly_smiling_face: With coc and vimspector, I don't ever need to touch my ide anymore. :+1:

dansomething commented 4 years ago

With coc and vimspector, I don't ever need to touch my ide anymore.

this!

Let's hope the process keeps on getting better and smoother thanks to all the hard work of these project maintainers.

puremourning commented 4 years ago

Currently Vimspector won't do this for you

correction : can't.

puremourning commented 4 years ago

@dansomething - if you're feeling generous, would you mind writing this up on the vimspector wiki? I really appreciate the work you put in to make that smooth and to help others to get it working too

dansomething commented 4 years ago

Sure, I'll give it a shot. And thanks for the clarification above. Its the responsibility of the code that starts jdt.ls to provide the config to enable the java-debug server extension.

dansomething commented 4 years ago

@puremourning I had some progress started on the readme for coc-java-debug so I just pushed that up. Feel free to copy/tweak or link to that for the vimspector wiki.

luisdavim commented 3 years ago

what about https://github.com/mfussenegger/nvim-dap and https://github.com/rcarriga/nvim-dap-ui ?

martin-braun commented 1 year ago

coc.nvim is the easier alternative to mason.nvim. I love it so much, but unfortunately, coc.nvim won't support nvim-dap unlike mason.nvim, leaving with no debugger. I was looking for any other DAP manager that can complement coc.nvim in that regard, preferably with nvim-dap integration, but I found none that is maintained. (dap-buddy would be it, if it was maintained.)

To install mason along coc.nvim feels redundant. Can anybody suggest a maintained alternative to dap-buddy?

luisdavim commented 1 year ago

It would be awesome if coc could support dap like it does lsp...

oblitum commented 1 year ago

@luisdavim what do you mean by "support dap like it does lsp"? LSP is a protocol, and DAP is another, so, how does former supports the latter?

luisdavim commented 1 year ago

They are different protocols, currently, coc supports one but not the other. What I'm saying is that coc should support both.

Nate-Wilkins commented 1 year ago

Is the request to replace vimspector in a coc plugin?

I just got into vimspector and haven't configured it all the way yet but came here thinking that coc might have a debugging interface that I wasn't aware of.

Frederick888 commented 1 year ago

OP probably meant integrating nvim-dap(-ui)/vimspector like vscode?

For example, using rust analyzer, there are run/debug commands (and virtual texts) for main function and tests atm. I just tried debugging one, and instead of starting my DAP client automatically, I got

[coc.nvim]: Error on notification "runCommand": Vim:E492: Not an editor command: TermdebugCommand /home/frederick/Programming/Rust/external-editor-revived/target/debug/deps/external_editor_revived-0ce839c598c3e140 util::tests --nocapture

I'm not sure if TermdebugCommand is something generic, but I think e.g. https://github.com/simrat39/rust-tools.nvim can do this for Rust at least.

Edit: Ah TermdebugCommand is a Neovim command :h terminal-debug. It's quite basic and just brings up an interactive gdb. Not sure how rust-tools.nvim handles this (it uses nvim-dap).

follyzx commented 1 year ago

I found the auto-completion feature can be achieved in the varies watch window of vimspector by introduce plug Youcompleteme. And I tried to modify the vimrc file to do the same thing by coc.vim but failed. Can this feature be realized in the future?

asmodeus812 commented 6 months ago

Here is something i have whipped up for java, if someone is interested, it integrates both CoC and native nvim lsp with nvim-dap (install coc-java-debug (needed to inject the debug jar bundles) and coc-java for coc, or configure init_options for nvim-jdtls with java-debug)

local HOTCODEREPLACE_TYPE = {
    END = "END",
    ERROR = "ERROR",
    WARNING = "WARNING",
    STARTING = "STARTING",
    BUILD_COMPLETE = "BUILD_COMPLETE",
}
local SETTINGS = {}

local function cache_settings()
    if vim.g.did_coc_loaded ~= nil then
        SETTINGS = vim.fn["coc#util#get_config"]("java")
    else
        SETTINGS = {}
    end
end
require("dap").listeners.before["event_hotcodereplace"]["jdtls"] = function(session, body)
    if body.changeType == HOTCODEREPLACE_TYPE.BUILD_COMPLETE then
        session:request("redefineClasses", nil, function(err)
            if err then
                vim.notify("Error during hot reload", vim.log.levels.WARN)
            else
                vim.notify("Applied hot code reload", vim.log.levels.WARN)
            end
        end)
    elseif body.message then
        vim.notify(body.message, vim.log.levels.WARN)
    end
end

local function execute_command(command, callback, bufnr)
    if vim.g.did_coc_loaded ~= nil then
        if not command.arguments then
            command.arguments = {}
        end
        if type(command.arguments) ~= "table" then
            command.arguments = { command.arguments }
        end
        table.insert(command.arguments, function(error, result)
            error = error ~= vim.NIL and { message = error } or nil
            callback(error, result, {
                name = "jdtls",
                config = {
                    settings = {
                        java = SETTINGS,
                    },
                },
            })
        end)
        vim.fn.CocActionAsync("runCommand", command.command, unpack(command.arguments))
    else
        local clients = {}
        for _, c in pairs(vim.lsp.get_active_clients({ bufnr = bufnr }) or {}) do
            local command_provider = c.server_capabilities.executeCommandProvider
            local commands = type(command_provider) == "table" and command_provider.commands or {}
            if vim.tbl_contains(commands, command.command) then
                table.insert(clients, c)
            end
        end
        if vim.tbl_count(clients) == 0 then
            vim.notify(string.format("Unable to find client that supports %s", command.command), vim.log.levels.WARN)
        else
            clients[1].request("workspace/executeCommand", command, function(err, resp)
                if callback then
                    callback(err, resp)
                elseif err then
                    vim.notify(string.format("Could not execute command action: %s", err.message), vim.log.levels.WARN)
                end
            end)
        end
    end
end

local function init_debug_session(cb, bufnr)
    execute_command({
        command = "vscode.java.startDebugSession",
    }, function(err, port)
        if err ~= nil then
            vim.notify(string.format("Unable to initiate java debug session %s", err.message), vim.log.levels.WARN)
        else
            cb(port)
        end
    end, bufnr)
end

local function fetch_java_executable(mainclass, project, cb, bufnr)
    if not project or #project == 0 then
        local binary = vim.env.JAVA_HOME and string.format("%s/%s", vim.fs.normalize(vim.env.JAVA_HOME), "bin/java") or "java"
        if vim.fn.executable(binary) then
            cb(binary)
        else
            vim.notify(string.format("Unable to resolve default system java executable for %s", mainclass), vim.log.levels.WARN)
        end
    else
        execute_command({
            command = "vscode.java.resolveJavaExecutable",
            arguments = { mainclass, project },
        }, function(err, java_exec)
            if err then
                vim.notify(
                    string.format("Unable to resolve java executable for %s/%s: %s", project, mainclass, err.message),
                    vim.log.levels.WARN
                )
            else
                cb(java_exec)
            end
        end, bufnr)
    end
end

local function fetch_needs_preview(mainclass, project, cb, bufnr)
    if not project or #project == 0 then
        cb("")
    else
        execute_command({
            command = "vscode.java.checkProjectSettings",
            arguments = vim.fn.json_encode({
                className = mainclass,
                projectName = project,
                inheritedOptions = true,
                expectedOptions = { ["org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures"] = "enabled" },
            }),
        }, function(err, use_preview)
            vim.print(err, use_preview, "preview")
            if err then
                vim.notify(
                    string.format("Unable to resolve preview feature for %s/%s: %s", project, mainclass, err.message),
                    vim.log.levels.WARN
                )
            else
                use_preview = use_preview and "--enable-preview" or ""
                cb(use_preview)
            end
        end, bufnr)
    end
end

local function fetch_main_definitions(cb, bufnr)
    execute_command({
        command = "vscode.java.resolveMainClass",
    }, function(err, mainclasses)
        if err then
            vim.notify(string.format("Unable to resolve any main project definitions", err.message), vim.log.levels.WARN)
        else
            cb(mainclasses)
        end
    end, bufnr)
end

local function fetch_project_paths(mainclass, project, cb, bufnr)
    if not project or #project == 0 then
        cb(nil, nil)
    else
        execute_command({
            command = "vscode.java.resolveClasspath",
            arguments = { mainclass, project },
        }, function(err, paths)
            if err then
                vim.notify(string.format("Unable to resolve classpath for %s/%s: %s", project, mainclass, err.message), vim.log.levels.WARN)
            else
                cb(paths[1], paths[2])
            end
        end, bufnr)
    end
end

local function fetch_dap_configs(callback)
    cache_settings()
    local bufnr = vim.api.nvim_get_current_buf()
    fetch_main_definitions(function(mainclasses)
        local configurations = {} -- clean config map
        if mainclasses == nil or #mainclasses == 0 then
            callback(configurations)
            return
        end
        local remaining = #mainclasses
        for _, mc in pairs(mainclasses) do
            local mainclass = mc.mainClass
            local project = mc.projectName
            fetch_java_executable(mainclass, project, function(java_exec)
                fetch_needs_preview(mainclass, project, function(use_preview)
                    fetch_project_paths(mainclass, project, function(module_paths, class_paths)
                        remaining = remaining - 1
                        if module_paths and class_paths then
                            -- insert new fresh config entry
                            table.insert(configurations, {
                                vmArgs = use_preview,
                                javaExec = java_exec,
                                projectName = project,
                                mainClass = mainclass,
                                classPaths = class_paths,
                                modulePaths = module_paths,
                            })
                        end
                        if remaining == 0 then
                            callback(configurations)
                        end
                    end, bufnr)
                end, bufnr)
            end, bufnr)
        end
    end, bufnr)
end

return function(opts)
    local function attempt_enrich_config(config, on_config)
        if not config.__template then
            -- flat config, already resolved, just enrich
            -- we have nothing more to do to this config
            opts.enricher(config, on_config)
        else
            -- config table here is a copy of the original,
            -- but the result will have the main class resolved
            -- we know it was 'template' but yet not resolved
            -- we resolve it and the template flag will be cleared
            -- within the default dap enricher later on
            fetch_dap_configs(function(configs)
                if #configs > 1 then
                    local list = {}
                    for i, c in ipairs(configs) do
                        table.insert(list, string.format("%s: %s", i, c.mainClass))
                    end
                    vim.ui.select(list, { prompt = "Select application entry point" }, function(selection)
                        if selection == nil or selection == "" then
                            return
                        end
                        local mainid = tonumber(selection:match("(%d+)")) -- get the config index
                        opts.enricher(vim.tbl_extend("force", config, configs[mainid]), on_config)
                    end)
                    vim.notify("Multiple entrypoints have been detected", vim.log.levels.WARN)
                else
                    opts.enricher(vim.tbl_extend("force", config, configs[1]), on_config)
                end
            end)
        end
    end

    return {
        adapter = function(callback, _)
            init_debug_session(function(port)
                callback({
                    type = "server",
                    enrich_config = attempt_enrich_config,
                    host = opts.server_host or "127.0.0.1",
                    port = opts.server_port or port,
                })
            end, vim.api.nvim_get_current_buf())
        end,
        configurations = {
            default_launch = {
                __template = true,
                type = "java",
                request = "launch",
                cwd = "${workspaceFolder}",
                console = "integratedTerminal",
                name = "Default launch (java)",
            },
        },
    }
end