From 213b8e0b978a28305a1a3c9735114449962cfbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 16 Apr 2026 08:03:33 +0100 Subject: [PATCH] Eglot: un-canonicalize server URIs to project's root Some LSP servers prefer to canonicalize all document URIs to their "true name", chasing all fs symbolic links. If the user is visiting a project under a symlink, say ~/myprojects/foo which points to /data/true/projects/foo. When visiting a ~/myprojects/foo/f.c and trying to jump to a nearby file f.h via LSP 'xref-find-definitions', the path should probably be ~/myproject/foo/f.h, not /data/true/projects/foo/f.h. Even though Emacs can recognize that the two files are the same, 'buffer-file-name' is not. This confuses tools like C-x C-f, ibuffer, and ultimately the users themselves who probably oblivious to the true place of the project. After all they started Eglot under a syminked file. * lisp/progmodes/eglot.el (eglot-lsp-server): Add trueroot slot. (eglot-uri-to-path): Rework to consider trueroot and project root. --- lisp/progmodes/eglot.el | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf6fbc69ba2..6e7d0dcf7a1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1225,7 +1225,11 @@ object." :accessor eglot--saved-initargs) (semtok-cache :initform (make-hash-table :test #'equal) - :documentation "Map LSP token conses to face names.")) + :documentation "Map LSP token conses to face names.") + (trueroot + :initform nil + :documentation "Cached truename of the associated project root." + :accessor eglot--trueroot)) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -1235,19 +1239,27 @@ object." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let* ((server (eglot-current-server)) (remote-prefix (and server (eglot--trampish-p server))) + (root (and server (project-root (eglot--project server)))) + (trueroot (and root (eglot--trueroot server))) (url (url-generic-parse-url uri))) ;; Only parse file:// URIs, leave other URI untouched as ;; `file-name-handler-alist' should know how to handle them ;; (bug#58790). (if (string= "file" (url-type url)) - (let* ((retval (url-unhex-string (url-filename url))) + (let* ((unhexed (url-unhex-string (url-filename url))) ;; Remove the leading "/" for local MS Windows-style paths. (normalized (if (and (not remote-prefix) (eq system-type 'windows-nt) - (cl-plusp (length retval)) - (eq (aref retval 0) ?/)) - (w32-long-file-name (substring retval 1)) - retval))) + (cl-plusp (length unhexed)) + (eq (aref unhexed 0) ?/)) + (w32-long-file-name (substring unhexed 1)) + unhexed)) + ;; Make sure the final path is relative to project's root + ;; (found when the user `M-x eglot''ed), not trueroot. + (normalized (if trueroot + (replace-regexp-in-string (concat "^" trueroot) root + normalized) + normalized))) (concat remote-prefix normalized)) uri))) @@ -1833,6 +1845,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) + (setf (eglot--trueroot server) (file-truename (project-root project))) (setf (eglot--languages server) (cl-loop for m in managed-modes for l in language-ids collect (cons m l)))