Closed dannyfreeman closed 1 year ago
FWIW DAP handles this with a standard approach: a "Source" type and /source request. Clients are told that a source is not a "file on the filesystem" but a "source reference". Client does a "/source" request with the source reference and server returns the contents.
https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Source https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source
Ben Jackson @.***> writes:
FWIW DAP handles this with a standard approach: a "Source" type and /source request. Clients are told that a source is not a "file on the filesystem" but a "source reference". Client does a "/source" request with the source reference and server returns the contents.
https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Source https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source
Yeah, this seems a lot like my proposal number 2.i.
Something to consider with that is some users would want things like
textDocument/definition
to continue working in the newly opened source
file. If the server sends back some kind of unique identifier for the
source, I imagine it would be able to keep track of it and continue to
offer those capabilities. I know clojure-lsp does this when files are
opened as a jar
uri, and that jar
uri continues to be provided in
TextDocumentIdentifier
requests.
-- Danny Freeman
Digging through the open issues, this might be a duplicate of https://github.com/microsoft/language-server-protocol/issues/336
@dannyfreeman you are correct, is a dup of #336. The idea is that servers can register a content provider for a URL scheme and then the client has to call the method to fetch the content. It is comparable to what DAP does.
I have been working on one of the Emacs lsp clients and have run into an issue with various language servers for JVM languages. Most of them attempt to implement responses for the textDocuemnt/definition method and others like it, where the definition is not a plain source file that is readily available on the file system. Instead the definition is either in a source code file in .jar archive managed by a dependency tool like maven, or the "source" is a bytecode file contained in a .jar that must be extracted and decompiled for the user to make sense of it. (Java bytecode files are also referred to as
.class
files)The three JVM language servers I've investigated are
Each of them take a different approach to handling these types of dependencies, some of which require non-standard LSP extensions to use.
I will outline them here:
Metals
The Scala language server takes an approach that requires NO special client implementation. When a user attempts to navigate to a definition in a scala source file contained in a jar, the language server extract the source file into a temp directory contained within the project. This temp source file is then provided to client via the
Location
response as a simplefile://
URL. Something similar is done with definitions residing in .class files in jar archives: The class file is extracted and decompiled into a temporary directory under the projects, and theLocation
response contains afile://
URL pointing to the temp file.Clojure-lsp
Clojure-lsp does has 2 different behaviors.
I will describe them below, but more information can be found in this issue of the clojure-lsp repo: https://github.com/clojure-lsp/clojure-lsp/issues/1385
Clojure .clj source files
If the dependency is a Clojure source file contained in a .jar archive, then clojure-lsp will return a
Location
response that contains either ajar
URL (spec), or azipfile
URL (no official specification, but looks likezipfile:path/to/archive.jar::path/in/jar/to/source.clj
). The type of URL is controlled by a setting in the LSP server.When this happens, a client can do one of two things. They can either open the URL themselves if the clients have such capabilities. This involves extract the file from the jar archive (which is really just a zipfile with a different extension). Another option is the client can send a non standard request to the clojure-lsp server:
clojure/dependencyContents
. The method responds with the contents of the file, which the client can then display however it likes (perhaps a temporary buffer, saving to a file).Either way, this strategy requires some special knowledge on the client side and is unique to clojure-lsp.
This strategy does not extend to the other JVM language servers, even though it could if they used the standard jar URI format and all clients chose to open them themselves instead of using nonstandard methods like
clojure/dependencyContents
.Compiled java .class bytecode files
Clojure-lsp deals with these files the same way as the metals lsp server. It extracts and decompiles .class files when serving up responses to
textDocuemnt/definition
into a temp file under the current project. This tempfile is sent back in asfile://
URL in theLocation
response.jdt.ls
The java language server doesn't typically deal with plain source files in jars like Scala and Clojure language servers do. Instead there is one strategy it takes for these types of dependencies:
When a definition exists in a .class bytecode file within a jar archive, jdt.ls either returns an empty response, or it returns a URL of a bespoke format if a certain setting is enabled in the lsp server.
The bespoke URLs look like this:
They are not standard like the
jar:
scheme URLs that clojure-lsp uses, and are not really meant to be parsed by clients. This one also seems partially parsed, I've seen others in the wild that have a LOT more URL escaping. Instead it acts more like a token that must be passed back to the server using the non-standardjava/classFileContents
method for jdtls. The method responds with the contents of the buffer, very similar toclojure/dependencyContents
. Forcing the client to defer to the server to get the source is intentional, as there are MANY ways of decompiling a .class file, so the server must to be the source of truth if it is going to provide location information in the resulting decompiled file. If the clients tried to decompile it on their own then it is likely the location would not match up.More information about this can be found in this issue of jdt.ls: https://github.com/eclipse/eclipse.jdt.ls/issues/2322.
Looking for a final solution
I'm writing this issue to the spec because as someone who contributes to a lsp client, I would like to see one standardized way of handling these types of
Location
responses, which I would summarize as:What the best solution is for this? I do not know. The way I see things there are a couple strategies that could be taken:
textDocuemnt/definition
(and friends) request, the lsp server automatically "gets" the source files and extracts it to a temp file, and responds with afile:
URL.textDocuemnt/definition
response Instead of aLocation
response with aURI
key, some new response would tell the LSP client what server method to call, which would respond with:Location
response pointing to a temp filetextDocument/definition
response to ALWAYS return the contents of the dependency, with no extra round trip like suggestion number 2.I'm sure there are other solutions for this, and things I am not considering (like how it would work on language servers running on a remote machine, something I have little experience with). Putting something in the standard would eliminate a class of custom behavior I have observed among various JVM language servers and make the out of the box experience better without having to setup custom client code for every single language server that needs something like this.
I also have no doubt it would help other language servers as well outside the JVM ecosystem. I can imagine that language servers supporting CLR languages might also be able to take advantage of this when dealing with libraries distributed as DLLs (at least they were distributed as DLLs when I worked with C# years ago).