Closed MagielBruntink closed 11 months ago
Documentation on how to get this going is very sparse. This is what the Java Debug Server project provides: https://github.com/microsoft/java-debug#usage-with-eclipsejdtls
This is my current non-functional attempt based on that and some inspection of lsp-java.el
and dap-java.el
:
(when (package-installed-p 'dape)
(require 'dape)
(add-to-list 'dape-configs
`(jdtls
modes (java-ts-mode java-mode)
command "jdtls"
command-args ()
:type "java"
:request "launch"
:command "vscode.java.startDebugSession"
:cwd dape-cwd-fn
:program dape-find-file-buffer-default
:initializationOptions (:bundles "c:/Tools/Java/java-debug/com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-0.50.0.jar"))))
The Java Debug Server has been built in c:/Tools/Java/java-debug
without issue and the necessary plugin jar is present at that path.
Running M-x dape
on a buffer in java-ts-mode then results in dape starting but with errors:
*dape-debug*
[info] Starting new single session
[info] Process started ("jdtls")
[io] Sending:
(:arguments (:clientID "dape" :adapterID "java" :pathFormat "path" :linesStartAt1 t :columnsStartAt1 t :supportsRunInTerminalRequest t :supportsProgressReporting t :supportsStartDebuggingRequest t) :type "request" :command "initialize" :seq 1)
[error] Timeout for reached for seq 1
*dape-processes*
WARNING: Using incubator modules: jdk.incubator.foreign, jdk.incubator.vector
nov. 12, 2023 9:36:38 P.M. org.apache.aries.spifly.BaseActivator log
INFO: Registered provider ch.qos.logback.classic.servlet.LogbackServletContainerInitializer of service jakarta.servlet.ServletContainerInitializer in bundle ch.qos.logback.classic
nov. 12, 2023 9:36:38 P.M. org.apache.aries.spifly.BaseActivator log
INFO: Registered provider ch.qos.logback.classic.spi.LogbackServiceProvider of service org.slf4j.spi.SLF4JServiceProvider in bundle ch.qos.logback.classic
nov. 12, 2023 9:36:39 P.M. org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer fireError
SEVERE: Unable to identify the input message.
com.google.gson.JsonParseException: Unable to identify the input message.
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.createMessage(MessageTypeAdapter.java:403)
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.read(MessageTypeAdapter.java:139)
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.read(MessageTypeAdapter.java:56)
at com.google.gson.Gson.fromJson(Gson.java:1227)
at com.google.gson.Gson.fromJson(Gson.java:1186)
at org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler.parseMessage(MessageJsonHandler.java:119)
at org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler.parseMessage(MessageJsonHandler.java:114)
at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.handleMessage(StreamMessageProducer.java:193)
at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen(StreamMessageProducer.java:94)
at org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run(ConcurrentMessageProcessor.java:113)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
Apparently the message sent to JDTLS by Dape is not as expected. I have no clue how to proceed from here.
PS. I have this JDTLS working perfectly fine with Eglot for normal LSP functionality.
After reading through the https://github.com/microsoft/java-debug#usage-with-eclipsejdtls , It says an [LSP client](https://langserver.org/) has to launch jdt.ls with initializationOptions
so instead of passing the bundle option from the dape i passed it from eglot. I use the following package for java https://github.com/yveszoundi/eglot-java in this i modified the initialization options
and then i have created a custom function called eglot-dap-activate
which will send the start debugger command
When i execute the above function, I got the following response
Based on the docs The response to this request will contain a port number on which the debug adapter is listening, and to which a client implementing the debug-adapter protocol can connect to.
so we can connect to this port from dape i guess ?? which i haven't tried it yet.
let's hack more on this and make it work for java @MagielBruntink :muscle:
Yes, I now also realize that we need eglot or another LSP client to first send this special initializationOptions
message to an instance of JDTLS, which will then be able to receive the Command to "vscode.java.startDebugSession". Then it will respond with a port on which dape can connect.
Rather tricky process...
I just started looking into it. It indeed seams cumbersome. I am willing to add to dape-config format to allow for this strange startup pattern to work, maybe som before hook or something. I think it would be preferable to have thedape
function be the start debugging function for all languages. Maybe init
which is executed with current dape-config before creating the connection to the server or somthing...
That's great news! I think with a few internals of eglot
we can do this. Let's try to create a minimal init sequence just using eglot
by looking at how eglot-java.el
pulls it off.
That's great news! I think with a few internals of eglot we can do this. Let's try to create a minimal init sequence just using eglot by looking at how eglot-java.el pulls it off.
Sounds like a good place to start. If anybody got a nice a concise example please share.
Here is some lisp to generate a working dape config for JDTLS Debug Server with the help of eglot. This is rudimentary and probably does not work with multiple main classes, missing project structure, and several edge cases.
In the following it is assumed eglot is already running on the Java project with the following eglot config applied (replace the jar file path with your own):
(add-to-list 'eglot-server-programs
'((java-mode java-ts-mode) .
("jdtls"
:initializationOptions
(:bundles ["c:/Tools/Java/java-debug/com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-0.50.0.jar"]))
))
Eval the following functions and then run M-: (add-to-list 'dape-configs (get-dape-jdtls-debug-config (current-buffer)))
while on a .java file in a properly defined project.
(defun get-dape-jdtls-debug-config (java-file-buffer)
(with-current-buffer java-file-buffer
(let* ((project (project-name (project-current)))
(server (eglot-current-server))
(main (plist-get (elt (eglot-execute-command server "vscode.java.resolveMainClass" project) 0) :mainClass))
(classpaths (elt (eglot-execute-command server "vscode.java.resolveClasspath" (vector main project)) 1))
(port (eglot-execute-command server "vscode.java.startDebugSession" nil)))
`(jdtls
modes (java-ts-mode java-mode)
host "localhost"
port ,port
:args ""
:type "java"
:request "launch"
:host "localhost"
:cwd dape-cwd-fn
:projectName ,project
:mainClass ,main
:stopOnEntry t
:console "dape"
:modulePaths [""]
:classPaths ,classpaths))))
Then running M-x dape
should work. Just run jdtls
as the adapter when asked and clean some of the pre-filled parameters that dape puts there.
@svaante the above would work with a user defined init function that gets the config template for the mode, and the current buffer, as parameters. The function would then be able to 'enrich' the config further with language specifics and dynamic values (such as the classpaths , mainClass, etc like above)
Thanks @MagielBruntink and @ThangaAyyanar!
I am not sure about the dape-config
format, went with fn
instead of init
.
Used the following configuration based on @MagielBruntink
(require 'eglot)
(setq dape--jdtls-java-debug-bundle "path/to/java-debug/com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin-0.50.0.jar")
(add-to-list 'eglot-server-programs
`((java-mode java-ts-mode) .
("jdtls"
:initializationOptions
(:bundles [,dape--jdtls-java-debug-bundle]))))
(add-to-list 'dape-configs
`(jdtls
modes (java-mode java-ts-mode)
fn (lambda (config)
(let* ((default-directory (project-root (project-current)))
(project (project-name (project-current)))
(server (eglot-current-server))
(main (plist-get (elt (eglot-execute-command server "vscode.java.resolveMainClass" project) 0) :mainClass))
(classpaths (elt (eglot-execute-command server "vscode.java.resolveClasspath" (vector main project)) 1))
(port (eglot-execute-command server "vscode.java.startDebugSession" nil)))
(append (list
'port port
:mainClass main
:projectName project
:classPaths classpaths)
config)))
:args ""
:type "java"
:request "launch"
:cwd dape-cwd-fn
:stopOnEntry t
:console "dape" ;; Don't know whats going on here
:modulePaths [""]))
Please get back to me if bf91567e5e81b15c5179609cb520ce8d3318ed54 works or if something can be improved.
Working great, thanks @svaante ! I'll see if the configuration could be further optimized (console eg. is set also in dap-java.el).
Then next on the list is the Java Unit Tests runner/debugger support :-) https://github.com/microsoft/vscode-java-test
Also: multiple main classes in nested Maven projects are a problem indeed. We could allow the user to pick one using completing read, or always select the file/class that the user opened dape on (sometimes there will be no main method).
@svaante One struggle is that commands like dape-restart
are sometimes run on a different buffer than where dape
was initially started, for example if point is within the dape REPL or info buffers.
For most debugger that's not an issue, but if we rely on eglot
like we do for jdtls
interaction, it is. On normal start we are in language mode buffer, which tells dape what config to use and allows us to fetch the relevant eglot server with (eglot-current-server)
.
However activating dape-restart
by doing r
in the REPL or clicking restart
in the info buffer with point not in the (java) language buffer causes the issue. Since it's not in a language buffer (eglot-current-server)
will return nil and break the rest of the config fn for jdtls.
A fix is perhaps to save the file for which dape was initially launched and expose that to the config fn?
I implemented a tentative fix for the above in the following. I store the launch-file
among the config and use it upon restarts to do the init correctly. Let me know if you find that a clean solution.
My dape-jdtls code is growing rapidly and will probably further expand with convenience functions for the tests run/debug functionality. Perhaps you are interested in a PR for extensions/dape-jtdls.el
? Or otherwise I should create a separate repo/package for this?
There is also some new code that asks the user to select the main class (with completion) :-)
(defun dape-jdtls-config-fn (config)
(with-current-buffer
(if (plist-get config 'launch-file)
(find-file (plist-get config 'launch-file))
(current-buffer))
(let* (
(default-directory (project-root (project-current)))
(server (eglot-current-server))
(entryPoints (eglot-execute-command server "vscode.java.resolveMainClass" (project-name (project-current))))
(selectedEntryPoint (dape-jdtls-select-entry-point entryPoints config))
(projectName (plist-get selectedEntryPoint :projectName))
(mainClass (plist-get selectedEntryPoint :mainClass))
(classpaths (elt (eglot-execute-command server "vscode.java.resolveClasspath" (vector mainClass projectName)) 1))
(port (eglot-execute-command server "vscode.java.startDebugSession" nil)))
(append (list
'port port
'launch-file (buffer-file-name (current-buffer))
:mainClass mainClass
:projectName projectName
:classPaths classpaths)
config))))
(defun dape-jdtls-select-entry-point (entryPoints config)
(let* ((separator "/")
(candidates (mapcar (lambda (entryPoint)
(s-concat (plist-get entryPoint :projectName) separator
(plist-get entryPoint :mainClass)))
(append entryPoints '())))
(user-input (s-concat (plist-get config :projectName) separator
(plist-get config :mainClass)))
(selected (completing-read "Select a main class: " candidates nil t
(unless (s-equals? user-input separator) user-input)))
(selected-split (s-split separator selected)))
(list :projectName (nth 0 selected-split)
:mainClass (nth 1 selected-split))))
VS Code has a Java Debug Server extension [1], built on JDTLS, that uses DAP. Would it be feasible to configure dape such that it can be used to debug Java programs using that same Debug Server? It seems dap-mode and nvim-dap have managed to create working configurations. Those are very elaborate and tend to be mixed with all kinds of other functionality.
I'm having trouble getting started with configuring dape for this case however. Are there any pointers?
[1] https://github.com/microsoft/java-debug