-
Notifications
You must be signed in to change notification settings - Fork 22
Description
Hello! Thanks so much for maintaining rustic-mode
! I'm not 100% sure if this fix should be in rust-mode
or rustic-mode
, but I wanted to file it and discuss. Happy to move or re-file it later 😃
In short, I believe one of the Rust modes should configure how Emacs finds the current project because Eglot uses this to start rust-analyzer
.
When rust-analyzer
is started, it fails to discover crates if the Cargo workspace is nested more than one directly level inside a Git repository. As far as I can tell, this is because of find_cargo_toml_in_child_dir
, which says:
fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
// Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
The problem occurs when the Emacs and Cargo don't agree on where the project root is. Eglot relies entirely on project.el
to determine the project root (see joaotavora/eglot#909). The default backend for project.el
is project-vc-find-project
, which simply finds the version control root (.git
directory). When rust-analyzer
is started with the VCS root, it cannot find the workspace members and reports an error:
[eglot] Server reports (type=1): Failed to discover workspace.
Consider adding the `Cargo.toml` of the workspace to the [`linkedProjects`](https://rust-analyzer.github.io/book/configuration.html#linkedProjects) setting.
Failed to load workspaces.
I'll suggest that rustic-mode
fixes this by explicitly configuring how to find the Cargo root. I've been using the following configuration snippet for a few days now and it seems to do the job:
;;; --- Configure project.el to find Cargo project roots intelligently ---
(require 'cl-lib)
(defun mg/cargo-toml-is-workspace (dir)
"Return non-nil if DIR contains a workspace Cargo.toml file."
(let ((cargo-toml (file-name-concat dir "Cargo.toml")))
(when (file-exists-p cargo-toml)
(with-temp-buffer
(insert-file-contents-literally cargo-toml)
(goto-char (point-min))
(search-forward-regexp "^\\[workspace\\]" nil t)))))
(defun mg/project-find-rust-project (dir)
"Find a Rust project root, prioritizing workspaces."
(let ((ws-root (locate-dominating-file dir #'mg/cargo-toml-is-workspace)))
(cond
;; Priority 1: A Cargo workspace was found. Use it directly.
(ws-root (cons 'cargo-project ws-root))
;; Priority 2: No workspace, so find the nearest `Cargo.toml`.
((when-let ((cargo-dir (locate-dominating-file dir "Cargo.toml")))
(let* ((vcs-root (vc-root-dir))
(project-root
(if-let ((vcs-root-path (and vcs-root (file-name-as-directory vcs-root))))
;; A VCS root exists. Decide based on nesting depth.
(let* ((cargo-path (file-name-as-directory cargo-dir))
(relative-path (file-relative-name cargo-path vcs-root-path))
(depth (cl-count ?\/ relative-path)))
(if (> depth 1)
;; Deeper than 1 level: use the Cargo.toml dir.
cargo-dir
;; 0 or 1 level deep: use the VCS root.
vcs-root))
;; No VCS root found, so the Cargo.toml dir is the best we can do.
cargo-dir)))
(cons 'cargo-project project-root)))))))
;; Teach project.el how to extract the root from our 'cargo-project type.
(cl-defmethod project-root ((project (head cargo-project)))
(cdr project))
;; Add our new function to the front of `project-find-functions` to give it
;; priority over the default VCS-based discovery.
(add-to-list 'project-find-functions #'mg/project-find-rust-project)
The logic for finding the correct Cargo.toml
file could probably be improved: calling cargo metadata
and parsing out the workspace_root
is probably the right solution.
The steps I used to test this locally was:
mkdir -p project-root
cd project-root
git init
mkdir -p nested
cd nested
cargo new --lib foo
To test, open project-root/nested/foo/src/lib.rs
in Emacs and start Eglot.
Version Info:
- Rustic Version: 20250630.1332
- Emacs Version: 30.1 from Debian Testing
- Eglot Version: 1.17.30
- rust-analyzer Version: 1.89.0 (2948388 2025-08-04)