switch project fix

This commit is contained in:
2026-02-18 11:35:19 +01:00
parent a8bccc3dea
commit b35e06abd6
4 changed files with 247 additions and 5 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.direnv/

207
AGENTS.md Normal file
View File

@@ -0,0 +1,207 @@
# AGENTS.md — org-roam-project
Guidance for AI coding agents working in this repository.
## Project overview
`org-roam-project` is an Emacs Lisp package that extends
[org-roam](https://www.orgroam.com/) with per-project databases using
Emacs' built-in `project.el`. Each project gets its own isolated notes
directory and SQLite database. The global zettelkasten is never touched.
**Language:** Emacs Lisp
**Dependencies:** Emacs >= 28.1, org-roam >= 2.2.0
**License:** GPL-3.0-or-later
### Repository layout
```
org-roam-project.el Main package source (~355 lines)
org-roam-project-test.el ERT test suite (~430 lines)
Makefile Build and test runner
flake.nix / shell.nix Nix dev environment
```
## Build commands
```sh
make all # Byte-compile + run tests
make compile # Byte-compile org-roam-project.el
make test # Run the full ERT test suite in batch mode
make clean # Remove .elc files
```
### Running a single test
```sh
emacs --batch \
-l ert \
-l org-roam \
--eval "(add-to-list 'load-path \".\")" \
-l org-roam-project \
-l org-roam-project-test \
--eval "(ert-run-tests-batch-and-exit '\"orp-test-NAME\")"
```
Replace `orp-test-NAME` with the test name (e.g. `orp-test-default-notes-subdir`).
The string is an ERT selector regexp matching test names.
### Dev environment
The project uses Nix flakes. Run `nix develop` (or let `direnv` activate
via `.envrc`) to get an Emacs with org-roam and all dev tools on `$PATH`.
## Code style guidelines
### Lexical binding
Every `.el` file MUST have `lexical-binding: t` in the first line:
```elisp
;;; file.el --- Description -*- lexical-binding: t; -*-
```
### File structure
Follow this order in every source file:
1. First line with description and `lexical-binding: t`
2. Copyright / Author / Version / Package-Requires / Keywords / URL
3. License boilerplate (GPL-3.0-or-later)
4. `;;; Commentary:` section
5. `;;; Code:` marker
6. `(require ...)` forms
7. `;;; Section` headers (three semicolons) grouping related code
8. `(provide 'feature)` at the end
9. `;;; file.el ends here` footer
### Namespace prefix
All symbols use the `org-roam-project` prefix:
- **Public:** `org-roam-project-<name>` (single dash)
- **Private:** `org-roam-project--<name>` (double dash)
- **Test public:** `orp-test-<name>`
- **Test private:** `orp-test--<name>`
Never introduce unprefixed top-level symbols.
### Requires / imports
- Place all `(require ...)` forms at the top of `;;; Code:`.
- Only hard runtime dependencies. No `eval-when-compile` unless truly
needed for macros.
- **Do not use `cl-lib` in production code.** Use only built-in Emacs
Lisp primitives (`let`, `let*`, `when`, `unless`, `cond`, `cons`,
`car`, `cdr`, `alist-get`, `dolist`, etc.). `cl-lib` is acceptable
in the test file only.
### Formatting
- **Line length:** ~80 columns. String literals may slightly exceed this.
- **Indentation:** Standard Emacs Lisp indentation (2-space aligned
`let` bindings, standard `defun`/`defmacro`/`cond` indentation).
- **Parentheses:** Closing parens always on the same line as the last
form — never on their own line.
- **Blank lines:** One blank line between top-level forms. Section
headers (`;;;`) get blank lines before and after.
- **Function references:** Always use sharp-quote: `#'function-name`.
- **`let` vs `let*`:** Use `let*` only when bindings depend on earlier
bindings in the same form. Use plain `let` otherwise.
### Docstrings
Every public and private function, macro, and variable must have a
docstring.
- First line: complete imperative sentence ("Return the context…").
- Arguments referenced in UPPER CASE: `"DIR defaults to…"`.
- Cross-reference other symbols with backtick-quote: `` `org-roam-project-init' ``.
- Document return values and nil-return conditions explicitly.
### Inline comments
- Use `;;` (two semicolons) on a line by itself above the code.
- Section headers use `;;;` (three semicolons).
- Avoid end-of-line comments in production code (acceptable in tests
for brief annotations).
### Defcustom conventions
```elisp
(defcustom org-roam-project-XXXX default-value
"Docstring."
:type 'TYPE
:group 'org-roam-project)
```
- Always specify `:type` and `:group`.
- If the variable is usable in `.dir-locals.el`, add a
`safe-local-variable` declaration with an `;;;###autoload` cookie:
```elisp
;;;###autoload
(put 'org-roam-project-XXXX 'safe-local-variable #'stringp)
```
### Autoloads
Place `;;;###autoload` on:
- All interactive commands
- The minor mode definition (`define-minor-mode`)
- `safe-local-variable` `put` forms
Never autoload internal (`--`) functions.
### Error handling
Two-tier pattern:
1. **Internal functions** return `nil` on failure — never signal errors.
Let callers decide behavior.
2. **Interactive commands** call `org-roam-project--require-context`
which signals `user-error` (not `error`) with actionable messages.
Always use `user-error` for user-facing errors, never bare `error`.
### Macros
- Always declare `(declare (indent 0) (debug t))` as the first form.
- Accept `&rest body`, splice with `,@body`.
- Use `unwind-protect` for cleanup in test macros.
### Dynamic scoping pattern
The core architectural pattern: temporarily rebind `org-roam-directory`
and `org-roam-db-location` via `let` — never mutate them with `setq`.
```elisp
(let ((org-roam-directory (car ctx))
(org-roam-db-location (cdr ctx)))
(org-roam-node-find))
```
## Testing conventions
- **Framework:** ERT (Emacs Regression Testing), built-in.
- **Test file:** `org-roam-project-test.el`
- **Test names:** `orp-test-<descriptive-name>` (no double dash).
- **Helpers/macros:** `orp-test--<name>` (double dash).
- Tests create real temporary Git repos via `git init` so `project.el`
recognizes them. Always clean up with `orp-test--cleanup`.
- Use `orp-test--with-project` or `orp-test--with-initialized-project`
macros to set up and tear down test fixtures.
- Test `user-error` conditions with `(should-error ... :type 'user-error)`.
- Tests use real org-roam (not mocked) to catch compatibility issues.
- `cl-lib` functions (`cl-remove-if`, `cl-find`, `cl-count`, etc.) are
allowed in tests.
## Section headers in test file
Tests are numbered in groups with decorative Unicode headers:
```elisp
;;; ─── 1. Customisation defaults ───────────────
```
Maintain this grouping style when adding new test sections.

View File

@@ -187,6 +187,20 @@ real org-roam database is opened globally."
(should (consp ctx))
(should (string-suffix-p "my-custom.db" (cdr ctx)))))))
(ert-deftest orp-test-context-via-directory-override ()
"Context is detected via `project-current-directory-override' (Emacs 29+).
Simulates the environment created by `project-switch-project': the
override is set to the target project while `default-directory' still
points elsewhere (e.g. the previously active project)."
(skip-unless (boundp 'project-current-directory-override))
(orp-test--with-initialized-project
(let ((project-current-directory-override test-root)
(default-directory "/")) ; deliberately wrong — not the project
(let ((ctx (org-roam-project--context)))
(should (consp ctx))
(should (string= (file-name-as-directory (car ctx))
(file-name-as-directory test-notes-dir)))))))
;;; ─── 5. org-roam-project--with-context (macro) ──────────────────────────────
(ert-deftest orp-test-with-context-binds-org-roam-vars ()
@@ -246,6 +260,19 @@ real org-roam database is opened globally."
(expand-file-name org-roam-project-db-filename
test-notes-dir))))))
(ert-deftest orp-test-require-context-via-directory-override ()
"require-context succeeds via `project-current-directory-override' (Emacs 29+).
Simulates `project-switch-project' setting the override while
`default-directory' still points to the previous project."
(skip-unless (boundp 'project-current-directory-override))
(orp-test--with-initialized-project
(let ((project-current-directory-override test-root)
(default-directory "/")) ; deliberately wrong — not the project
(let ((ctx (org-roam-project--require-context)))
(should (consp ctx))
(should (string= (file-name-as-directory (car ctx))
(file-name-as-directory test-notes-dir)))))))
;;; ─── 7. org-roam-project--add-to-gitignore ──────────────────────────────────
(ert-deftest orp-test-gitignore-no-file ()

View File

@@ -105,7 +105,11 @@ Falls back to the global value of VAR if no dir-local value is set."
(defun org-roam-project--context (&optional dir)
"Return the org-roam context for the project containing DIR.
DIR defaults to `default-directory'.
When DIR is nil, the project is detected via `project-current', which
respects `project-current-directory-override' (set by
`project-switch-project' on Emacs 29+) before falling back to
`default-directory'. Pass an explicit DIR only when the caller already
has a concrete filesystem path (e.g. the autosync advice).
Returns a cons cell (NOTES-DIR . DB-PATH) where NOTES-DIR is the
absolute path to the project's org-roam notes directory and DB-PATH
@@ -115,8 +119,9 @@ Returns nil if:
- DIR is not inside a project.el project.
- The notes directory does not exist and `org-roam-project-auto-create'
is nil."
(let* ((dir (expand-file-name (or dir default-directory)))
(project (project-current nil dir)))
(let* ((project (if dir
(project-current nil (expand-file-name dir))
(project-current nil))))
(when project
(let* ((root (project-root project))
(notes-subdir (org-roam-project--value-in-dir
@@ -150,10 +155,12 @@ Otherwise BODY executes with the global org-roam settings."
(defun org-roam-project--require-context ()
"Return the project context or signal a helpful error.
Respects `project-current-directory-override' (set by
`project-switch-project' on Emacs 29+) before falling back to
`default-directory'.
Signals `user-error' if there is no current project or the project's
notes directory has not been initialized."
(let* ((dir default-directory)
(project (project-current nil dir)))
(let* ((project (project-current nil)))
(unless project
(user-error "Not inside a project.el project"))
(let* ((root (project-root project))