Skip to content

Instantly share code, notes, and snippets.

@pesterhazy
Last active May 13, 2024 12:14
Show Gist options
  • Save pesterhazy/e8e445e6715f5d8bae3c62bc9db32469 to your computer and use it in GitHub Desktop.
Save pesterhazy/e8e445e6715f5d8bae3c62bc9db32469 to your computer and use it in GitHub Desktop.
Emacs, Monorepos, Eglot, Clojure-lsp and Project.el

This gist is for anyone who's trying to use emacs + eglot + monorepo (with Clojure or any other language).

When you open a file in a buffer, eglot needs to determine the scope or folder to run the language server in. By default, the folder eglot will pick as the assumed project root is the repo root (the ancestory directory containing .git).

But in a monorepo, that's rarely what you want. In a large repo, analyzing all the *.clj files with clojure-lsp could take a minute or longer. As a limiting case, imagine Google with its gargantuan monorepo. Analyzing all the source files would take an indefinite period of time.

So we need to scope eglot (and clojure-lsp) to a subfolder of the monorepo (e.g. /projects/foo). Eglot is built on top of project.el, a sort of built-in replacement for projectile, and uses its (project-current) function to determine the project root. (You can run M-x eval-expression (project-current) to see which folder it picks.)

Sadly project.el doesn't have an easy, built-in way to define what your repo root is (a strange omission for a project management tool.)

However, you can customize the behavior with elisp:

(require 'cl-extra)
(setq project-sentinels '("bb.edn" "deps.edn" "package.json" ".monorepo-project"))

(defun find-enclosing-project (dir)
  (locate-dominating-file
   dir
   (lambda (file)
     (and (file-directory-p file)
          (cl-some (lambda (sentinel)
                     (file-exists-p (expand-file-name sentinel file)))
                   project-sentinels)))))

(add-hook 'project-find-functions
          #'(lambda (d)
              (let ((dir (find-enclosing-project d)))
                (if dir (cons 'vc dir) nil))))

What this does is to instruct project.el (and thus, eglot) to pick the folder with a "dominating file" (in Emacs lingo a sentinel file found in the a file's parent directory, or its parent directory, or its parent directory etc) called deps.edn (or package.json etc) as the project root.

In Clojure, looking for deps.edn as a sentinel is not a bad assumption. Make sure to adapt the project-sentinels list to your own project.

h/t https://michael.stapelberg.ch/posts/2021-04-02-emacs-project-override/

@bplubell
Copy link

bplubell commented Nov 2, 2022

Thanks for posting this. This, and the linked post from Michael Stapelberg, were quite helpful though not exactly what was after - I wanted project.el to continue operating at the monorepo level (so I could find project files across all sub-projects) but have eglot use sub-directories when starting lsp servers.

It looks like recent versions of eglot have a variable, eglot-lsp-context, which is only set when it is looking for the project root. That means you can inspect the variable and only "change" the project root when eglot is starting an lsp server.

What I ended up with:

(defun project-find-shadow-cljs-edn (dir)
  (let ((override (locate-dominating-file dir "shadow-cljs.edn")))
    (if (and (boundp 'eglot-lsp-context) eglot-lsp-context override)
        (cons 'vc override)
      nil)))

(add-hook 'project-find-functions #'project-find-shadow-cljs-edn)

@pesterhazy
Copy link
Author

Thanks @bplubell, that's very helpful!

@pesterhazy
Copy link
Author

Looks like (cons 'vc override) doesn't work anymore – something must have changed in a dependency.

Now I'm using

(add-hook 'project-find-functions
          #'(lambda (d)
              (let ((dir (find-enclosing-project d)))
                (if dir (list 'vc 'Git  dir) nil))))

@pesterhazy
Copy link
Author

This can now be done more easily with the project-vc-extra-root-markers variable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment