switch project fix
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.direnv/
|
||||
207
AGENTS.md
Normal file
207
AGENTS.md
Normal 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.
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user