purcell/envrc

Possible to propagate environment when jumping to definitions in libraries?

anderspapitto opened this issue · 2 comments

I have the following situation

  • I'm in a rust buffer, using doom emacs with lsp and envrc, and everything works together wonderfully. I happen to have the language server installed via my direnv-managed environment, rather than globally or managed by emacs/lsp.
  • If I use lsp to jump to the definition of a function in the stdlib (or in a 3rd party library), the definition is located in a file that's outside my project root, because it's not my code.
  • Therefore envrc does not propagate my environment for that file, and therefore my language server is not available to lsp.

I would like the behavior to be something similar to how envrc handles buffers like *Help*, i.e. I would like envrc to hook into jump-to-definition with something that says "if the place I jump to has no .envrc file, keep using the one that I was just in"

This is really out of scope for envrc to handle directly, but it's probably possible to hack something together.

something similar to how envrc handles buffers like Help, i.e. I would like envrc to hook into jump-to-definition

envrc doesn't "handle" this, exactly. Those buffers simply inherit default-directory from the calling buffer. If you wrap the jump function with an around advice, you can note the pre-jump default-directory, restore it in the post-jump buffer, and then trigger envrc and LSP as desired.

I wrote some not-so-elegant hacks to deal with this problem. It seems to work well so far.

(defun akira/xref-inherit-envrc (func &rest args)
  (if envrc-mode
      (cl-letf*
	  (((default-value 'process-environment) process-environment)
           ((default-value 'exec-path) exec-path))
	(let* ((marker (apply func args))
	       (new-buf (marker-buffer marker)))
	  (with-current-buffer new-buf
	    (setq-local process-environment (default-value 'process-environment))
	    (setq-local exec-path (default-value 'exec-path))
	    marker)))
    (apply func args)))

;; for `evil-ret', for example, with `evil-mode', when your cursor is above <vector> in #include <vector>,
;; you can press enter to direct access the file
(defun akira/lsp-document-link-inherit-envrc (func &rest args)
  (if (and envrc-mode
	   (equal "file"
		  (url-type (url-generic-parse-url (url-unhex-string (car args))))))
      (cl-letf*
	  (((default-value 'process-environment) process-environment)
	   ((default-value 'exec-path) exec-path)
	   (result (apply func args)))
	(setq-local process-environment (default-value 'process-environment))
	(setq-local exec-path (default-value 'exec-path))
	result)
    (apply func args)))

;; for the builtin gdb debugger of emacs
(defun akira/gud-display-line-inherit-envrc (func &rest args)
  ;; the caller calls `gud-display-line' in a buffer with `default-directory' set correctly, but doesn't
  ;; have `envrc-mode' enabled, thus we enable it before calling `gud-display-line'
  (unless envrc-mode
    (envrc-mode))
  (cl-letf*
      (((default-value 'process-environment) process-environment)
       ((default-value 'exec-path) exec-path)
       ;; the first parameter is the file-name
       (f-name (car args))
       (result (apply func args)))
    ;; `gud-display-line' create a buffer for the file, but doesn't seem to return the buffer instance
    ;; Also when the function finishes, the `(current-buffer)' is not the buffer newly created.
    ;; Thus I choose to use `get-file-buffer' to get access to the buffer.
    (with-current-buffer (get-file-buffer f-name)
      (setq-local process-environment (default-value 'process-environment))
      (setq-local exec-path (default-value 'exec-path)))
    result))

(advice-add #'xref-location-marker :around #'akira/xref-inherit-envrc)
(advice-add #'lsp--document-link-handle-target :around #'akira/lsp-document-link-inherit-envrc)
(advice-add #'gud-display-line :around #'akira/gud-display-line-inherit-envrc)

I'm using Emacs 29.