Subject: Eglot URI parsing bug when using clojure-lsp server

From: Danny Freeman <[email-masked]>
Date: Tue, 25 Oct 2022 17:44:22 -0400

Hello,

I am submitting 2 patches that were discussed over on eglot's github
issue tracker: https://github.com/joaotavora/eglot/issues/661

Only one of the patches is needed to solve the issue, but I wanted to
present both options.

The problem occurs when using `xref-find-defintions` while eglot is
managing a clojure buffer with clojure-lsp. If the symbol that
xref-find-definitions is activated on is defined in a jar file,
clojure-lsp by default will provide a location with a response like the 
following

(:jsonrpc "2.0" :id 14 :result
   (:uri 
"zipfile:///Users/andreperictavares/.m2/repository/org/clojure/tools.namespace/0.3.1/tools.namespace-0.3.1.jar::clojure/tools/namespace/find.clj" 
... ))

However, there is a clojure-lsp setting that the maintainers of 
clojure-lsp would like to make default that changes the URI format to 
send a response like this

(:jsonrpc "2.0" :id 14 :result
   (:uri 
"jar:file:///Users/andreperictavares/.m2/repository/org/clojure/tools.namespace/0.3.1/tools.namespace-0.3.1.jar!clojure/tools/namespace/find.clj" 
... )).

This jar format URI is a common thing used in the JVM world apparently. 
It is a URI that contains a nested URI. If eglot is provided this kind 
of URI it needs to parse it TWICE before eglot dispatches the path. If 
it's only parsed once then emacs ends up trying to navigate using 
something roughly equivalent to `(find-file "file:///path/to/jar")` 
which does not work. That is what the patch titled 
`0001-Parse-jar-scheme-URIs-in-eglot-correctly.patch` fixes.

The other patch forces clojure-lsp to use the `zipfile` scheme and 
avoids the need to parse the URI twice. I prefer the double parsing 
patch though, as I believe if other language servers for JVM languages 
use this jar URI scheme, they would also be able to benefit from the patch.

I have a very simple repository here that can be used to test the 
problem here: 
https://git.sr.ht/~dannyfreeman/eglot-xref-to-jar-repo/tree/main/item/src/user.clj#L4

With clojure and clojure-lsp installed, eglot activated in a buffer 
visiting that `user.clj` file, the issue can be recreated.

This is my first time attempting to contribute to emacs. I am actively 
going through the process of copyright assignment right now in case it 
is necessary.

Thank you,
Danny Freeman
From c20c0185929fbb3f5ca0101cab38721ddac412d6 Mon Sep 17 00:00:00 2001
From: dannyfreeman <[email-masked]>
Date: Tue, 25 Oct 2022 08:15:26 -0400
Subject: [PATCH] Initialize clojure-lsp with  the "zipfile" dependency-scheme
 option

When eglot is provided clojure dependencies that are located in external
jars, there are two formats they could be provided in.

The recommended format is "jar", which contains a jar URI like
"jar:file:///path/to/jar!/path/in/jar"
this URI contains a nested URI that eglot is not equipped to handle.

Setting this value to "zipfile" provides them in the following format
"zipfile:///path/to/jar::/path/in/jar"
which doesnt not contain any nested URIs.

This change ensures that we use zipfile so that the URI always correctly
has the scheme removed from the uri, and ends up being provided to xref
like "/path/to/jar::path/in/jar".
---
 lisp/progmodes/eglot.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index a28df6c2d5..c9d08de0d9 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -230,7 +230,7 @@ language-server/bin/php-language-server.php"))
                                 (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio"))))
                                 (dockerfile-mode . ("docker-langserver" "--stdio"))
                                 ((clojure-mode clojurescript-mode clojurec-mode)
-                                 . ("clojure-lsp"))
+                                 . ("clojure-lsp" :initializationOptions (:dependency-scheme "zipfile")))
                                 (csharp-mode . ("omnisharp" "-lsp"))
                                 (purescript-mode . ("purescript-language-server" "--stdio"))
                                 (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run"))
-- 
2.37.3

From f6e0a6d1ff557719b11cdf357fc24246c9da6a86 Mon Sep 17 00:00:00 2001
From: dannyfreeman <[email-masked]>
Date: Tue, 25 Oct 2022 08:22:20 -0400
Subject: [PATCH] Parse "jar" scheme URIs in eglot correctly

jar schemes contain nested URIs, like-this'
"jar:file:///home/user/some.jar!/path/in/jar" such that when eglot was
parsing these URIs provided by lsp servers, it ended up trying to
navigate to "file:///home/user/some.jar!/path/in/jar".

This change parses the URIs twice if the scheme is "jar", so that the
final parsed URI looks like "/home/user/some.jar!/path/in/jar" instead.

When providing this path to a jar to tools like xref, emacs will still
not correctly open the file within the jar, but this change makes that
work easier.
---
 lisp/progmodes/eglot.el | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index a28df6c2d5..549de5cba4 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -1501,12 +1501,22 @@ If optional MARKER, return a marker instead"
              (directory-file-name (file-local-name truepath))
              eglot--uri-path-allowed-chars))))
 
+(defun eglot--parse-uri (uri)
+  (url-unhex-string
+   (url-filename
+    (let ((url (url-generic-parse-url uri)))
+      (if (string= "jar" (url-type url))
+          ;; jar: URIs can contain a nested URI, so we need to parse twice.
+          ;; For example, `jar:file:///home/user/some.jar!/path/in/jar'
+          (url-generic-parse-url (url-filename url))
+        url)))))
+
 (defun eglot--uri-to-path (uri)
   "Convert URI to file path, helped by `eglot--current-server'."
   (when (keywordp uri) (setq uri (substring (symbol-name uri) 1)))
   (let* ((server (eglot-current-server))
          (remote-prefix (and server (eglot--trampish-p server)))
-         (retval (url-unhex-string (url-filename (url-generic-parse-url uri))))
+         (retval (eglot--parse-uri uri))
          ;; Remove the leading "/" for local MS Windows-style paths.
          (normalized (if (and (not remote-prefix)
                               (eq system-type 'windows-nt)
-- 
2.37.3