Rust-analyzer + emacs + multiple projects + changing features/target

Hi all,

I'm using emacs + lsp-mode + rust-analyzer and I have the following workflow:

The problem is that in emacs, the variables to control the set of features and the target with which rust-analyzer is started are global variables, so if I change them to restart rust-analyzer on project A, then open project B, rust-analyzer would start with the set of features and the target of project A.

Another related problem is that if I change the features/target of project B, then want to change those of project A, the current variables value reflect project B, not A.

What I would like:

  • Either a way to access the set of features and the target used by rust-analyzer within emacs. This way I could write a function to read those values from the server of the current buffer when I want to modify them.
  • Or a way to have those variables "server"-local which would probably be a new concept. Those variables would be shared between buffers that share the same rust-analyzer server.

EDIT 1: Note that a possible workaround would be to use one emacs server per project. But that's not very convenient.

Anybody experienced similar issues? Or has a solution?

Thanks!

2 Likes

For a whole bunch of reasons, whose details I do not care to remember, but among which were things that sound like what you describe, I switched to using a separate Emacs process per project, quite some time ago. My quality of life improved as a consequence.

Ok, I managed to find a solution. I just hold a mapping from project root directory to project configuration and use lsp-mode hooks to set/unset the global configuration based on the mapping when needed. I adapted my old functions to edit the mapping instead of the global variables. Here's the whole setup:

(custom-set-variables
 '(lsp-after-initialize-hook '(ia0-lsp-after-initialize-hook))
 '(lsp-workspace-folders-changed-functions
   '(ia0-lsp-workspace-folders-changed-functions)))

;; Maps directory(string) to variable(symbol) to value
(setq ia0-lsp (make-hash-table :test 'equal))
(defun ia0-lsp-get (k)
  (let ((h (gethash (lsp-workspace-root) ia0-lsp)))
    (or (if h (gethash k h)) (eval k))))
(defun ia0-lsp-put (k v)
  (let* ((w (lsp-workspace-root))
         (h (gethash w ia0-lsp)))
    (unless h
      (setq h (make-hash-table))
      (puthash w h ia0-lsp))
    (puthash k v h)))
(defun ia0-lsp-load (k)
  (set k (ia0-lsp-get k)))

(defun edit-lsp-rust-features ()
  "Edit lsp-rust-features and lsp-rust-no-default-features."
  (interactive)
  (let* ((old-features (ia0-lsp-get 'lsp-rust-features))
         (old-no-default (ia0-lsp-get 'lsp-rust-no-default-features)))
    (setq old-features (mapconcat 'identity old-features ","))
    (when old-no-default (setq old-features (concat "=" old-features)))
    (let ((new-features (read-string "Features? " old-features)))
    (setq new-no-default (string-prefix-p "=" new-features))
    (ia0-lsp-put 'lsp-rust-no-default-features new-no-default)
    (if new-no-default (aset new-features 0 ?,))
    (setq new-features (vconcat (split-string new-features "," t)))
    (ia0-lsp-put 'lsp-rust-features new-features))))

(defun edit-lsp-rust-target ()
  "Edit lsp-rust-analyzer-cargo-target."
  (interactive)
  (let* ((old-target (ia0-lsp-get 'lsp-rust-analyzer-cargo-target))
         (new-target (read-string "Target? " (or old-target ""))))
    (setq new-target (if (string= new-target "") nil new-target))
    (ia0-lsp-put 'lsp-rust-analyzer-cargo-target new-target)))

(defun ia0-lsp-workspace-folders-changed-functions (added removed)
  (ia0-lsp-load 'lsp-rust-no-default-features)
  (ia0-lsp-load 'lsp-rust-features)
  (ia0-lsp-load 'lsp-rust-analyzer-cargo-target))

(defun ia0-lsp-after-initialize-hook ()
  (setq lsp-rust-no-default-features nil)
  (setq lsp-rust-features [])
  (setq lsp-rust-analyzer-cargo-target nil))

I believe the official answer to "different projects have different values of variables" is dir locals which I indeed use to customize lsp-rust-analyzer-cargo-target for certain projects.

However, I haven't found a way to edit dir locals and update the values in all affected buffers, which is annoying because I have to edit the dir local, reopen one of the project's files, and then restart LSP every time I want to switch targets. I do this often enough that I wrote a function for it.

Exactly. I tried dir locals (it was suggested on the lsp-mode Discord thread where I ask the same question) but as you say it doesn't share the value between all buffers, it just reads it when a file is opened. So that's not a solution. This is why I implemented the hash map, because in that way the value is share among all buffers of the same project.

1 Like