Compare commits

...

234 Commits

Author SHA1 Message Date
2ff616fbd8 release 2.3.0 2025-05-24 23:06:32 -07:00
046822b512 Bump DB version to avoid error 2025-03-24 14:40:44 -07:00
cce9591c1c keep OLP data in node properties
Not all links to a node come from another node.  Persisting OLP data
allows us to build backlinks with information about these non-node
headings.

Revert: db4170a459.
2025-03-23 17:20:13 -07:00
db4170a459 (perf): Deprecate link :outline properties
As it happens, turning sexps into strings is one of the
computationally expensive steps in EmacSQL.

With an enormous number of links in the database, that's a lot
of (:outline nil) to stringify into "(:outline nil)".

Its only use was in org-roam-node-insert-section,
where the information is very cheap to reconstruct.
It's already in one of the other arguments!

This has passed unnoticed because org-roam-db-sync has other
performance tarpits, but it will probably be noticed eventually after
those get fixed.
2025-03-12 18:34:24 -07:00
0037daaf3e docs: update caching docs
Now that emacsql handles choosing an appropriate backend/connector, we
do not need to provide these instructions.  If there are issues, emacsql
has good error messages that tell the user what they need to do to get
things working.

In general:
  - If the user is on Emacs >= 29, emacsql will use the built-in sqlite
    functionality that comes with Emacs.
  - If the user is on Emacs < 29, emacsql will use a module-based
    connector that requires the user to have certain libraries available
    on their machine.  emacsql will tell the user what is needed if they
    do not have it already.

Ref: #2486
Close: #2502
Close: #2415
2025-02-18 09:22:30 -08:00
888b5d1a67 Set org-roam-directory to a non-existent path to ensure robust test
* In some rare case, the first expect of org-roam-file-p may return t
when running in some environment (e.g. during autopkgtest in Debian).
* Setting org-roam-directory to a non-existent path ensure that the
first check is always nil as expected.
2025-02-18 08:58:24 -08:00
ed272eaf56 Address compiler warnings
Depend on Org 9.6 for `org-fold-show-context'.
2025-02-18 08:56:35 -08:00
551ff3b17e Use org-fold-show-context instead of obsolete function alias
To avoid having to create a wrapper function and silence the byte-
compiler for both when using a pre-rename and post-rename Org, keep
it simple and just depend on Org 9.6.
2025-02-18 08:56:35 -08:00
719594dfc7 org-roam-db: Don't unnecessarily enable foreign keys support
Starting with EmacSQL v4.1.0, `emacsql-sqlite-open' takes care of that.
2025-02-18 08:56:35 -08:00
b4b8d8c0ee (fix): fix error message 2025-02-18 08:56:35 -08:00
0d51698839 Rely on emacsql-sqlite-open to pick the best available back-end
That function was added two years ago and the first release that
provided it was v4.0.0.  It automatically picks the best available
back-end, `emacsql-sqlite-builtin' or `emacsql-sqlite-module'.
In v4.0.0 it could also fall back to the legacy `emacsql-sqlite'.
The inferior third-party back-ends are no longer supported.

Emacsql v4.1.0, which will be releases in early December, removes
the legacy `emacsql-sqlite' back-end (which used a custom binary).
2025-02-18 08:56:35 -08:00
7dc76b708b fix(db): Org-roam does not store Org-ID w/ search option
add `:search-option` property to the `link` table when present.

Fix: #2495
Close: #2496
2025-02-17 13:51:29 -08:00
f3db974bcc fix #2425
(kill-whole-line) kills folded text if called from column 1
Make sure to unfold text before calling it --
Prevents data loss if user has decided to extract subtree on a folded org headline.
2025-02-17 13:36:45 -08:00
bb08be4740 refactor!: do not include time zone in org iso8601
org mode timestamps do not support time zones, so returning the
machine's local time zone is misleading.

Also, org-format-time-string is an obsolete alias of format-time-string.

Ref: cc2490a706
2025-02-17 12:48:43 -08:00
2490afe110 refactor: remove emacsql-sqlite backend option
This option is no longer supported in emacsql 4.0 and was throwing
linter errors in CI.

Ref: 7a79c2be3d
2025-02-17 12:48:43 -08:00
40b6c10d8a nit: formatting update
My editor reformatted some of the identation.  Also corrected a
docstring for org-roam-refile.
2025-02-17 12:48:43 -08:00
425d53d56d Use regexp match to replace hard-coded path equal test (#2497)
Hard-coded paths cause the tests to fail when building under different
environments, e.g. under Debian sbuild it fails with the following
errors:

,----
| ========================================
| org-roam-id-find finds the correct file node
| FAILED: Expected `(car location)' to be `equal' to `"/home/runner/work/org-roam/org-roam/tests/roam-files/foo.org"', but instead it was `"/build/reproducible-path/org-roam-2.2.2+git20250105.cad3518/tests/roam-files/foo.org"' which does not match because: (arrays-of-different-length 84 60 "/build/reproducible-path/org-roam-2.2.2+git20250105.cad3518/tests/roam-files/foo.org" "/home/runner/work/org-roam/org-roam/tests/roam-files/foo.org" first-mismatch-at 1).
|
| ========================================
| org-roam-id-find finds the correct heading node
| FAILED: Expected `(car location)' to be `equal' to `"/home/runner/work/org-roam/org-roam/tests/roam-files/family.org"', but instead it was `"/build/reproducible-path/org-roam-2.2.2+git20250105.cad3518/tests/roam-files/family.org"' which does not match because: (arrays-of-different-length 87 63 "/build/reproducible-path/org-roam-2.2.2+git20250105.cad3518/tests/roam-files/family.org" "/home/runner/work/org-roam/org-roam/tests/roam-files/family.org" first-mismatch-at 1).
|
| Ran 41 specs, 2 failed, in 980.31ms.
| buttercup-run failed: ""
`----
2025-01-11 10:55:35 -05:00
64e302c126 Depend on emacsql 4.0.0 (#2466)
See https://github.com/magit/emacsql/issues/113.  Emacsql 4.0.0 has
been released and should now be used in the package-requires header.
2025-01-10 21:52:11 -05:00
cad3518788 (fix): advise org-id-find rather than overwriting id link (#2432)
There have been recent changes in org-mode to improve `org-id-open' to
be able to apply search strings in id: links.  But, org-roam overrides
the `:follow` parameter for id: links to its own `org-roam-id-open'
function and therefore misses this.

The only change that `org-roam-id-open' makes is to try calling
`org-roam-id-find' before falling back on `org-id-find'.  This commit
uses advice to do this directly, thereby integrating better with
`org-id-open'.

There was some discussion on the org-mode list about adding a custom
variable to avoid using advice, but this accomplishes the result
without requiring changes in org:

https://list.orgmode.org/87jzlxjiuf.fsf@localhost/
2025-01-04 20:43:23 -08:00
2a630476b3 Take node as argument to #'org-roam-refile (#2388) 2024-10-07 19:04:46 +02:00
9fd7c87b5b Update org-roam.org (#2475)
Fix formatting for 2 code objects.
2024-09-17 19:40:37 +02:00
0b9fcbc97b (test): add org-roam-db-get-{scheduled|deadline}-time (#2465) 2024-07-16 22:35:22 +02:00
e415610b05 (test): add org-roam-db--file-hash (#2464) 2024-07-16 21:57:42 +02:00
3e186a8552 (fix): remove dead-code about org-roam-shield feature (#2462)
As described in https://github.com/org-roam/org-roam/issues/2348,
the `org-roam-shield-region` function is not called anymore.

The reverse function and associated face can also be removed.

Fix: #2348
2024-07-15 19:50:52 +02:00
0b2218706d (test): add org-roam-buffer-p (#2461) 2024-07-15 19:20:18 +02:00
84334b7e16 (test): add org-roam-file-p (#2460) 2024-07-15 18:37:15 +02:00
3c52d581ae (test): add org-roam-demote-entire-buffer (#2459) 2024-07-14 15:52:49 +02:00
fdd834d9bf (docs): explain org-roam-file-exclude-regexp (#2458) 2024-07-14 10:15:41 +02:00
8e6938a39d (docs): add missing versions dates and obsolete notice (#2456) 2024-07-08 20:50:41 +02:00
a753ec097d (test): add org-roam-alias-{add|remove} (#2455) 2024-07-08 19:29:24 +02:00
43a5362ada (test): add org-roam-node-from-{id|title-or-alias} (#2454) 2024-07-08 18:59:53 +02:00
a432539121 (test): add org-roam--get-titles (#2453) 2024-07-07 17:18:20 +02:00
1ce760ccc7 (test): add org-roam--buffer-promoteable-p (#2452) 2024-07-07 16:54:41 +02:00
76df9d1f3c (test): add org-roam-id-at-point (#2451) 2024-07-07 15:41:00 +02:00
74d714f789 (test): add org-roam-id-find (#2450) 2024-07-03 23:18:18 +02:00
6644cb27a9 (feat): allow a custom heading for a backlinks section (#2333)
Allow the user to set a custom heading for a backlinks section in the org-roam buffer
2024-07-03 20:56:42 +02:00
94b826d759 (feat): extract rg-command builder function from unlinked-references (#2449)
* (test): add "org-roam--list-files-search-globs"

* (feat): extract rg-command builder function from unlinked-references

Users can now advice this command to tweak the flag pased to ripgrep.
The current one have been expanded to long-form.

It's a follow up to the fix about shell quoting

* (docs): update CHANGELOG
2024-07-03 19:26:03 +02:00
edccf9be84 (fix): org-roam-directory having spaces and Unlinked References not working (#2411)
Co-authored-by: Vikram Mandyam <vicky08@gmail>
2024-07-03 18:38:15 +02:00
aa64cc9596 (chore): fix indent blocking ci lint execution (#2448)
This mix of tabs and spaces was introduced recently in:
2e94f55cc5
2024-07-02 17:56:56 +02:00
e93c77f6a5 (docs): add details about org-roam-protocol with Homebrew (#2441)
Add additional information for setting up org-roam-protocol on Mac OS when using emacs installed from Homebrew (see also https://org-roam.discourse.group/t/problem-with-org-roam-protocol/3473)
2024-07-01 19:50:40 +02:00
2d1c5d78e8 Link Martin Edström's knowledge base (#2394) 2024-07-01 19:46:07 +02:00
f8b40a3109 (docs): fix name of my-org-roam-show-backlink-p (#2447)
Fix: #2392
2024-07-01 18:33:56 +02:00
7fdc7150cc Fix some typos (#2430) 2024-07-01 08:39:57 +02:00
8667e44187 (docs): update org-protocol instructions (#2401) 2024-01-14 11:42:08 -08:00
2e94f55cc5 (node) add optional NOCASE parameter to org-roam-node-from-title-or-alias (#2403)
* (node) add NOCASE parameter to org-roam-node-from-title-or-alias

* fixed docstring of org-roam-node-from-title-or-alias.
2024-01-14 11:41:46 -08:00
5c06471c3a (node): org-roam-node-at-point: don't error in non-org buffers (#2329)
`=' assumes that both objects being compared are numbers. In some
buffers `outline-level' can return nil, so `=' returns an error. `eq'
does not assume this, but does return t when comparing two numbers.
2023-03-07 09:21:01 -08:00
b5436f3410 depend on snapshot of emacsql (#2327) 2023-03-05 09:20:26 -08:00
f7dc81e494 Fix org-fold-core-style in org-roam-buffer (#2325) 2023-03-05 09:20:07 -08:00
e73807efe1 Add customisable function for prompting when adding refs (#2317)
* (node): new custom `org-roam-ref-prompt-function'

Function for prompting when adding a ref.

* (node): use `org-roam-ref-prompt' in `org-roam-ref-add'

Prompt properly for ref.
2023-02-23 14:45:37 +08:00
1981dc3617 utils: descendant-of-p: Defend against nils (#2319)
This has an effect on find-file-hook for totally unrelated files,
through the callchain of `org-roam-db-autosync--setup-file-h ->
org-roam-file-p -> org-roam-descendant-of-p'.

From an org file, inside a project-el project, (find-file
"some/other/file") will intermittently reproduce this error:

   org-roam-descendant-of-p: Wrong type argument: arrayp, nil

If this fix seems inappropriate -- it could amount to surppressing a
valid error in routine calls -- then I would advise throwing an
explicit signal here, or finding a safer way to get the `path` inside
`org-roam-file-p'; the current means:

    (or file (buffer-file-name (buffer-base-buffer)))

.. can result in nil, which then bleeds errors into use elsewhere.

for posterity, here is the full calling context I'm referring to, from
`org-roam-file-p'.

  (let* ((path (or file (buffer-file-name (buffer-base-buffer))))
         (ext (when path (org-roam--file-name-extension path)))
         (ext (if (string= ext "gpg")
                  (org-roam--file-name-extension (file-name-sans-extension path))
                ext))
         (org-roam-dir-p (org-roam-descendant-of-p path
         org-roam-directory))
         ...
2023-02-23 14:43:47 +08:00
7d2f511251 (minor): fix lints on main (#2320) 2023-02-23 14:42:35 +08:00
74422df546 add discoverability support for age encrypted org files (#2302) 2022-12-31 13:22:30 -08:00
938c602faa (core): add org-roam-node-category (#2300) 2022-12-26 18:19:51 -08:00
45a6863e07 (completions): don't complete org-roam nodes in source blocks (#2292)
* don't complete org-roam nodes in source blocks

This fixes the following situation where
`org-roam-completion-everywhere = t` and you are working with org-babel
blocks in one of your org-roam nodes.

```
```

Before my nodes would be suggested, but after this change only valid
babel headers are suggested as desired.

Previously completions from org-roam nodes would be suggested

* fix lints

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2022-12-25 10:39:30 -08:00
cdad2ee7f6 (db): default to sqlite-builtin when available (#2299) 2022-12-25 10:25:55 -08:00
256fe73e7a (docs): update documentation on database-connectors (#2298)
Update docs to reflect sqlite-bulitin and sqlite-module
2022-12-24 10:50:01 -08:00
f9228ce319 (feat): add support for filtering backlinks. (#2247)
* Add support for filtering backlinks.

See #1043.

* Add documentation for backlinks filter
2022-12-04 19:55:47 -08:00
Eli
25c476791e (fix): update org-roam-unlinked-references-section check (#2254)
`org-roam-unlinked-references-section` would get nil org-roam-node-title
when create a headline node, which caused the issue #1625. This commit
add a check to make `org-roam-unlinked-references-section` work
correctly.
2022-12-03 09:50:01 -08:00
78ee5c6814 (fix):fill outline of link in properties (#2230) 2022-12-03 09:49:23 -08:00
apc
3add6748ae Minor typo in the commented text of the definition of org-roam-refile (#2263) 2022-12-02 00:17:10 -08:00
e418037991 (docs):orgmode.org/elpa has been shut down (#2258)
See https://list.orgmode.org/87blb3epey.fsf@gnu.org/ for details.
Removed it from the documentation in the manual to (try and) assist newcomers.
2022-12-02 00:16:42 -08:00
05f67901c6 (feat):support multi-line org titles (#2264)
Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2022-12-02 00:16:11 -08:00
c2e852e102 (feat)org-roam-tag-add: use tags separator as crm-separator (#2282)
Allows use of : in minibuffer to separate multiple selected tags.
2022-12-01 23:57:41 -08:00
4e6f934690 (fix)refs:support spaces in links (#2285)
Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2022-12-01 23:55:04 -08:00
d95d25615e (Docs): Fixed typos in the docs (#2256)
* Fix typo

* Fix typo

* Fix typo
2022-09-08 09:25:56 -07:00
7f453f3fff (fix)promote: promote all metadata to file level (#2246)
Without specifying `t`, it is only pushing the PROPERTIES drawer up  to
the file level, missing any other "planning information, clocking lines and
any kind of drawer." We want to promote all of this to the file level.
2022-08-03 21:37:36 -07:00
e435581215 (fix): remove use of deprecated org-font-lock-ensure (#2238) 2022-07-23 13:58:47 -07:00
nth
917a325dd9 (fix): links not displayed properly in org-roam-buffer (#2236)
Closes #2228.
2022-07-23 08:47:08 -07:00
c386761914 (fix): autoload org-roam-list-files (#2226) 2022-06-20 22:27:01 -07:00
171a8db32f (db)fix: file modification detection failing in some edge cases (#2221)
One example of an edge case:

- If a file has LF line endings,
- and is situated in a repository where .editorconfig asks for
  CRLF,
  (Such repositories exist, for example
  <https://github.com/obsidianmd/obsidian-docs/>.)
- and editorconfig-mode is enabled,

org-roam-db--file-hash would then return different results depending
on whether the file path is passed to it or not.

While this is a contrived edge case, there may be other cases like
this where the data from insert-file-contents-literally and the data
from find-file-noselect makes secure-hash return different values.

This commit removes the branch in org-roam-db--file-hash to compute
the hash based on buffer content, instead making it always require a
file path.

(This commit also allows org-roam-db-insert-file to take an optional
HASH argument, allowing it to avoid recalculating the hash.)

Since now we always compute the hash with the file on disk, the
special case for encrypted files is no longer needed.

This should have no impact on whether db is synchronized. The only
case when db--file-hash uses the "calculate in current buffer" path is
in db-insert-file; the only use of db-insert-file is in
db-update-file, which already calls db--file-hash with the file path,
and does not rely on the value from the current buffer.
2022-06-13 16:01:56 -07:00
bbac208fda (feat): org-roam-property-* code duplication removed (#2217) (#2218)
Co-authored-by: Chris Langhans <chris@langhans-coding.de>
2022-06-10 17:08:25 -07:00
fcefc1b1b4 (db)fix: FOREIGN KEY error in narrowed buffer (#2215)
When trying to save from a narrowed buffer, only the Roam nodes visible
in the narrowed buffer are written to the database.

This commit ensures that org-roam behaves the same whether the current
buffer is narrowed or not.
2022-06-08 09:21:13 -07:00
0cd9b9e6d3 (fix)completion: ensure unique ref candidates (#2208)
Add invisible id to candidate strings to ensure each candidate is
unique, and completing-read doesn't get confused about which one to open.

Fix #2207
2022-06-05 13:53:29 -07:00
83a0b3d464 (db)fix: org-roam-db-connector group (#2209) 2022-05-28 12:37:47 -07:00
ed7d4f0a2e (feat): support custom minibuffer matching function in org-roam-node-find (#2177) 2022-05-27 20:21:44 -07:00
32557afdbf (chore)ci: /s/master/main (#2204) 2022-05-25 14:19:11 -07:00
f144941dfb (fix)capture: respect blank lines in capture templates (#2203) 2022-05-25 11:57:17 -07:00
01843a6486 (chore): reapply #2178 (#2197) 2022-05-16 09:40:37 -07:00
1f51ec91d5 (node): fix org-roam-node-at-point check (#2195)
org-roam-node-at-point returned the wrong value if point-min is a
level-1 heading node. We make sure that everytime we go up one
heading, the outline-level changes (reduces by 1) to prevent this error.
2022-05-15 16:51:38 -07:00
2657f0b444 Fix org-roam-extract-subtree (#2191)
* Fix undefined incf.

Calling org-roam-extract-subtree failes with "Symbol’s function definition is void: incf"

org-roam.el includes cl-lib. org-roam--h1-count uses a bare incf call, which is undefined. Fix this by using cl-incf.

* Save buffer before promoting it.

After fixing incf in 8ec2e59e67, org-roam-extract-subtree still fails. 

The error is now: org-roam-db--file-hash: Opening input file: No such file or directory,...

The buffer with the newly created content is still unsaved at this time, so save it before calling (org-roam-promote-entire-buffer)

Caveat is we now save the buffer two times, before and after promoting it.
2022-05-12 09:34:53 -07:00
455f139d3e Revert "(fix)db: update atime on file access (#2174)"
This reverts commit b2d9543fa2.
2022-05-11 22:19:05 -07:00
b2d9543fa2 (fix)db: update atime on file access (#2174)
Previously we only updated the database on file save, but we need to
update the file's atime on file access, so we hook into `find-file-hook`
to do so. We only update the file atime if the file exists in the file table.
2022-05-11 15:39:49 -07:00
c0871c42be (fix) org-roam-file-p handle opening a buffer with no path (#2185)
reapply #2169
2022-05-07 17:19:48 -07:00
007e76725c (chore) fix version numbers (#2182) 2022-05-03 18:06:55 -07:00
5483e65d5a (fix)org-roam-file-p: don't exclude org-roam-directory (#2178)
* (fix)org-roam-file-p: don't exclude org-roam-directory

Don't exlude the org-roam-directory even if it matches
org-roam-file-exclude-regexp.

Should fix [#2165].

* Refactor PR #2178

Refactor PR #2178 to avoid recalculating (file-relative-name path
org-roam-directory) multiple timess.
2022-05-02 09:12:28 -07:00
b63ff2a7bb (chore): remove extraneous changelog line (#2172) 2022-04-24 18:44:58 -07:00
e8b4822a85 (perf)node-read: filter before map to candidate (#2168) 2022-04-24 17:12:22 -07:00
69116a4da4 v2.2.2 (#2171) 2022-04-24 17:05:27 -07:00
3014c63d50 (feat)log: init (#2170)
Create initial log module for working with Org logs
2022-04-24 16:57:13 -07:00
a073bcff5c (fix) org-roam-file-p handle opening a buffer with no path (#2169)
* (fix) org-roam-file-p handle opening a buffer with no path

Found this issue when opening a .epub file with nov.el (a package
that renders EPUB file) because nov.el doesn't store the filename
in buffer-file-name, opening it caused errors with org-roam-file-p.

* (minor): org-roam-file-p check if filename is non-nil first
2022-04-23 16:36:09 -07:00
b948cfbe37 (github): remove commit from issue template 2022-04-16 20:34:49 -07:00
3716817618 (utils): add sqlite-connector to diagnostics output (#2162) 2022-04-16 20:32:57 -07:00
608feed855 Update FUNDING.yml 2022-04-16 20:27:34 -07:00
6132155393 (db)fix: fix sqlite-builtin and sqlite-module (#2161) 2022-04-16 20:26:25 -07:00
d8985aa245 (node)refile: fixed org-roam-promote-entire-buffer structure errors (#2091) 2022-04-16 17:09:50 -07:00
61a544cebd (core): ignore org-attach-id-dir by default (#2160) 2022-04-16 14:49:03 -07:00
ddaf7ec10e (db): fix db sync for narrowed buffers (#2159)
* (db): fix db sync for narrowed buffers

* update changelog
2022-04-16 14:25:41 -07:00
8318da895d (db): support emacsql-sqlite-{builtin,module} (#2158)
* (db): support emacsql-sqlite-{builtin,module}

Add support for emacsql-sqlite-builtin and emacs-sqlite-module. Fixes #2146.

* update changelog
2022-04-16 14:16:39 -07:00
9eaf91b801 (fix)capture: Process fn capture templates before whitespace-content (#2157)
* [Fix #2156] Expand fn templates in fill-template before whitespace-content

* Update change log and adding tests related to #2157
2022-04-14 09:50:47 -07:00
3bb45afccb [Fix #2151] org-roam-preview-default-function doesn't copy next node (#2152) 2022-04-09 16:17:53 -07:00
fee008cdfb (docs): fix docs build (#2150)
debian packaging cannot use id links to build docs
2022-04-09 00:54:02 -07:00
36152590ad (feat)export: init (#2138) 2022-03-27 11:11:05 -07:00
a69968fc12 (chore): add changelog entry (#2136) 2022-03-27 10:42:23 -07:00
d71675fb47 (fix) unlinked-references: search symlinked directories (#2130)
Use the ripgrep '-L' option to follow directory symlinks.  This
matches the behavior of `org-roam--list-files-rg'.
2022-03-19 12:02:03 -07:00
3782e88d50 (release): v2.2.1 (#2126) 2022-03-14 23:37:29 -07:00
b4f14eebae (feat)capture: add org-roam-post-node-insert-hook (#2125)
* (feat): add org-roam-post-node-insert-hook

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2022-03-14 23:24:55 -07:00
cce6a05630 (fix)buffer: update defcustom for org-roam-mode-section-functions (#2124) 2022-03-13 09:04:55 -07:00
cc3689f30f (feat)buffer: pass args to org-mode-section-functions (#2123)
* (feat)buffer: pass args to org-mode-section-functions

* update docs
2022-03-12 21:24:11 -08:00
b179a5a1a6 (feat)buffer: add unique option for org-roam-backlinks-section (#2121)
Prior to this commit, when we would render backlinks in the
`org-buffer`, we would render each reference.  This meant that if a
source file referenced the node 5 times, there would be 5 backlink
references in the `org-buffer` (one for each position of the reference).

With this change, we add a parameter that specifies that backlink
sources should be unique.  In the above example, that would mean instead
of 5 backlinks we'd see 1.  And, as a concession, we'd use the lowest
position (e.g. the first reference in the source).

Closes #2119
2022-03-12 13:12:03 -08:00
9172001c11 (chore)readme: update link (#2120) 2022-03-11 21:49:24 -08:00
c51cadfe25 (fix)core: fix broken shell-based file listing methods (#2118)
Change #2025 broke the `org-roam--shell-command-files` filter that
affects all of the `org-roam-list-files-command` methods except
`elisp`.  This causes `org-roam--list-files` to iterate through the
variants executing their commands via `org-roam--shell-command-files`
but to not see any output, until it finally lands upon `elisp` and
gets some results.

The issue is that "#'s-present?" was replaced by

  (or (null s) (string= "" s))

effectively negating the test and converting it to "#'s-blank?".

This addresses that by negating the current test.
2022-03-10 16:07:39 -08:00
f6950a9820 (fix)capture: fill-template preserve whitespace content (#2117)
* (fix)capture: fill-template preserve whitespace content

Preserve the whitespace content given in the capture template, by
caching it and then appending it to the output template. For
Org-capture's purposes, we need to separately ensure that a newline is
present. Adds tests to the various helper functions to illustrate changes.

Addresses #2115
2022-03-10 09:46:53 -08:00
feb9179c9f (docs): add docs for org-protocol (#2116) 2022-03-09 22:11:05 -08:00
f50d6e7376 (fix): caching symlinked files (#2112)
* Revert "(fix): `org-roam-descendant-of-p` bug on Windows (#2089)"

This reverts commit 97a342fd3f.

Reason:

It seems that the `file-in-directory-p` function proposed in #2089 is provided only for
*physical directories*, does not work as we would like with *symlinked directories*.
It dereferences all the symlinks in its path, and previously seemed related paths
cease to be such.

Because of this, such notes are ignored and not indexed, although they
were indexed earlier.
2022-03-03 22:47:24 -08:00
65ea325071 (fix): place cursor after inserted link for new nodes (#2109)
Special care is taken not to move the point if the original point is no
longer the same (i.e. the user moves the cursor after calling the
capture, but before finalizing it). Fixes #2108.
2022-02-27 12:50:45 -08:00
62d311de22 (chore): org-map-entries -> org-map-region (#2107)
org-map-entries is affected by agenda, while org-map-region is not
2022-02-26 18:07:45 -08:00
c8a360afdd (fix)capture: don't update org-id-locations if file is unknown (#2103) 2022-02-24 09:11:20 -08:00
cebe77135a (fix)capture: always ask before deleting capture (#2100) 2022-02-20 16:51:41 -08:00
25d828c32e (chore): fix lints on master (#2099) 2022-02-20 13:14:58 -08:00
fd97c80a26 (chore): pin eldev install to 10.3 (#2098) 2022-02-20 10:40:54 -08:00
97a342fd3f (fix): org-roam-descendant-of-p bug on Windows (#2089)
On Windows, `file-truename` and `directory-file-name` downcase driver
label: "C:/" => "c:/", while `expand-file-name` keep the case
unchanged. If `org-roam-directory` use upper case driver label, `org-roam-descendant-of-p` will alway return `nil`. Fix by only using `file-truename` in the function.
2022-02-20 10:05:12 -08:00
d20480bb8d (fix)node: properly expand extraction file path (#2097)
Previously, the file-path of the new node in `org-roam-extract-subtree'
was incorrect in many circumstances.  Expanding w.r.t. the initial
prompt directory (ie, `org-roam-directory') fixes that
2022-02-19 19:12:53 -08:00
b163c900b8 (fix)capture: correctly update org-id-locations-file after capture (#2086) 2022-02-08 09:12:42 -08:00
0432b00485 (fix)buffer: buffer-toggle: don't destroy window if org-roam-node-toggle reuses window (#2082)
* (fix)buffer: buffer-toggle: don't destroy buffer if reused

Closes #2077

* add changelog
2022-02-06 14:20:24 -08:00
ccfa97ec3a (feat): ensuring that :ref info capture in all cases (#2079)
Let's assume we're evaluating the following region:

```elisp
(org-roam-capture-
     :node (org-roam-node-create :title "Org Roam Homepage")
     :info '(:ref "https://orgroam.com")
     :props '(:immediate-finish nil))
```

Prior to this commit, if you did not require "org-roam-protocol" then
`org-roam-capture-` would ignore the `:info '(:ref
"https://orgroam.com")` parameter.  Once you required
"org-roam-protocol" then the callback hooks that process `:info`'s
`:ref` will fire regardless of whether it comes from org-roam-protocol.

After this commit, you no longer need to require "org-roam-protocol" to
ensure that you capture an `:info`'s `:ref`.

Why is this useful?  When I'm reading through my elfeed RSS feed, I want
to capture an article.  I could use the org-roam-protocol to do this,
but that seems a bit unnecessary given that I'm already in Emacs.

Closes org-roam/org-roam#2078
2022-02-06 11:17:16 -08:00
86e102d990 (fix)dailies: prevent multiple "dailies/" subdir expansions (#2080) (#2080)
When dynamic binding `org-roam-directory` to the "dailies/"
subdirectory also bind `org-roam-dailies-directory` to "./" to prevent
invalid expansions in user-defined hook functions.

Fixes #2070
2022-02-06 11:16:33 -08:00
eed1df90f5 (feat)id: add org-roam-id module (#2072) 2022-01-30 22:54:28 -08:00
905564a7eb (docs)db: document "selecting deleted buffer" error (#2071) 2022-01-30 11:11:09 -08:00
9f7a4a0b02 (feat): allow specifying template keys in remaining org-roam-dailies capture/goto commands (#2065)
This is an extension of PR #2028 [1] by @astery, where the `keys`
argument was added to `org-roam-dailies--capture` `org-roam-dailies-capture-today`.

I extended this argument to also be available in other built-in dailies
functions, as I didn't see any reason why not and it results in my opinion in
more consistent behaviour.

The rationale is the same as in #2028: Allow users to add keybindings (or in my
case hydras) to calls of org-roam-dailies functions with specific template keys,
thus circumventing the selection screen.

So far it seems to work for me. I have only one pet-peeve about
the interface, if the user starts adding keys to all their goto-functions, they
might add

      (org-roam-dailies-goto-date nil "<key>")

Then the key-string will be interpreted as the value for `prefer-future` and
since it's non-nil, there won't be an error.

[1] https://github.com/org-roam/org-roam/pull/2028
2022-01-29 14:07:19 -08:00
aafe4114c2 (fix)node: make filter-fn for org-roam-node-random optional (#2063)
Also, make interactive argument actually apply for other-window
2022-01-25 21:54:06 -08:00
3e2716edf3 (fix)node: added DOUBLE ACUTE ACCENT for unicode normalization (#2060) 2022-01-21 15:50:52 -08:00
445e3594b2 (fix)dailies: remove f require (#2057) 2022-01-20 15:14:24 -08:00
6f5d65abd9 (breaking!)node: simplify default display-template (#2054)
Revert to a simplified `org-roam-node-display-template`, because on
non-vertical completion frameworks it looks and behaves strangely. To
restore the original default behaviour, set
`org-roam-node-display-template` in your Emacs configuration as such:

  (setq org-roam-node-display-template
        (concat "${title:*} "
                (propertize "${tags:10}" 'face 'org-tag)))
2022-01-20 11:19:41 -08:00
817d8036fb (fix)capture: return id in setup-target-location (#2052)
Fixes #2051.
2022-01-19 18:57:07 -08:00
c17310f0de (feat)org-roam-node-random: support filter-fn (#2050) 2022-01-19 14:23:57 -08:00
bf3ebe2121 (feat)capture: create ID before capture process (#2049)
* (feat)capture: create ID before capture process

This allows the ID to be used as part of the capture template. Addresses #2048.

* update changelog
2022-01-19 13:41:20 -08:00
47ad646d51 (docs)completions: document org-roam-node-display-template (#2047) 2022-01-18 15:56:47 -08:00
6263c3a956 (minor)dailies: fixed some wording (#2045) 2022-01-17 18:19:39 -08:00
69742c3d51 (feat)db: add customizability of extra element link parsing (#2042)
Adds two customizable variables: org-roam-db-extra-links-elements and
org-roam-db-extra-links-exclude-keys, which govern which elements are to
be considered for link parsing by Org-roam.

Also added sane defaults to org-roam-db-extra-links-exclude-keys.
2022-01-16 13:30:13 -08:00
5b15159a2c docs: symlink resolution with find-file-visit-truename
<p dir="auto"><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #1869.">Closes</span> <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="1004482400" data-permission-text="Title is private" data-url="https://github.com/org-roam/org-roam/issues/1869" data-hovercard-type="issue" data-hovercard-url="/org-roam/org-roam/issues/1869/hovercard" href="https://github.com/org-roam/org-roam/issues/1869">#1869</a></p>
2022-01-15 21:36:28 -08:00
c0c240b975 (chore): Drop f.el and s.el dependency (#2025)
Drops the f.el and implicit s.el dependency.

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2022-01-15 21:26:20 -08:00
001de3a874 (fix): fix completions width for Helm users (#2040) 2022-01-15 18:24:52 -08:00
6170cc9928 (release): bump 2021 -> 2022, 2.1.0 -> 2.2.0 2022-01-14 10:53:56 -08:00
cc95540135 (fix): hotfix changelog 2022-01-14 10:51:06 -08:00
24a683d58c (release): 2.2.0 (#2037) 2022-01-14 10:50:21 -08:00
86c9085363 (fix)core: make 'fd and 'fdfind backend work for listing files (#2021) 2022-01-11 15:05:27 -08:00
b67bccd6c2 (feat)dailies: add keys argument to org-roam-dailies-capture-today and org-roam-dailies--capture (#2028) 2022-01-11 14:13:40 -08:00
8b43093d1a (fix)org-roam-with-file: ensure local variables are respected (#2033)
When visiting a file, ensure that local variables are respected.
2022-01-12 03:38:44 +08:00
679ef6ef00 (fix)db: pragma foreign keys to work with sqlite3 (#2018)
* (fix):db: pragma foreign keys to work with sqlite3

This commit is follow-up for PR #2009 should fix issues #1927 & #1910.

Currently, `[:pragma (= foreign_keys ON)]` is present only in function
`org-roam-db--init`. This means the `foreign_keys` is turned on only when the db
file is created for the first time. Subsequent Emacs sessions won't turn it on
as the db file is already present and does not evaluate `org-roam-db--init`.

This PRAGMA needs to be turned on each database connection at runtime according
to sqlite's documentation (https://sqlite.org/foreignkeys.html#fk_enable)

```
Foreign key constraints are disabled by default (for backwards compatibility),
so must be enabled separately for each database connection
```

I have observed that on Windows only the Emacs session that initially creates
the Org-roam db file works correctly with the `sqlite3` option. Subsequent Emacs
sessions have the same "Unique constraint failed" error messages.

I have tested this patch on Windows and Ubuntu; both seem to work in the first
and subsequent Emacs sessions.

I am not 100% sure if the location of the PRAGMA is the best; use it as a
proof-of-cocenpt for the fix. Thank you.

* remove: [:pragma (= foreign_keys ON)] from org-roam-db--init

* fix: defconst org-roam--sqlite-available-p outputs error

When `org-roam-database-connector` is not `sqlite`, it outputs an unnecessary
error when Org-roam starts up as `defconst` evaluates
`(emacsql-sqlite-ensure-binary)`.

```
Org-roam initialization: (error "No EmacSQL SQLite binary available, aborting")
```

For other database connectors, this is not relevant.

* chore: rm org-roam--sqlite-available-p

As per our conversation, org-roam--sqlite-available-p not needed.
2022-01-02 14:03:30 +08:00
ee9a8d423e doc: Add mention of org-cite to bibliography (#2015)
* doc: Add mention of org-cite to bibliography

* regenerate texi

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2021-12-27 18:07:27 +08:00
d3c7d74329 (feat)db: allow [cite:@foo] links in ROAM_REFS (#2017) 2021-12-27 16:05:17 +08:00
3bf0a0a35d Use locate-user-emacs-file instead of expand-file-name. (#2016)
Then all advices on `locate-user-emacs-file` will work.
2021-12-27 14:37:46 +08:00
d0f17c6477 (docs): correct how to install gcc on Windows (#2013)
* docs: Correct how to install gcc on Windows

PATH needs to be manually set on Windows. Previously, it was explicitly
mentioned that it was unnecessary. More recent experience is that it's necessary.

* regenerate texi

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2021-12-23 17:22:02 +08:00
c90b2d68df fix(capture): fix whitespacing in org-roam-capture--fill-template (#2010) 2021-12-21 14:23:21 +08:00
91fd1083fe fix(db): fix support for sqlite3 connector (#2009)
Moving the PRAGMA foreign_keys invocation out of the transaction seems
to work now.
2021-12-21 13:42:57 +08:00
7ad5572741 (fix)db: fix node caching (#2006)
Previously node caching used org-map-entries: this only mapped over agenda
entries, hence skipping various nodes. Instead, we should be using
org-map-region, which maps over the entire file.
2021-12-15 16:48:44 +08:00
b6d59e2238 (minor): make the org-roam-node-read prompt configurable (#1993) 2021-12-14 16:05:08 +08:00
2c5f429b24 (chore): remove org-roam-v2-ack (#2005)
It's been long enough since the v2 release, removing the ack requirement
because it creates an annoying warning on native compilation.
2021-12-14 14:19:07 +08:00
7068d63e96 (docs) minor correction to #2002 (#2004)
* (docs): fix examples for .dir-locals.el

Two items:

1. The current example does not work; in order to call a function wihtin
`.dir-locals.el`, you should use `eval`.

  - [StackExchange](https://emacs.stackexchange.com/questions/21955/calling-functions-in-dir-locals-in-emacs)

  - [Working
example buried in an issue](https://github.com/org-roam/org-roam/issues/1527#issuecomment-933674233))

  - `(info "(emacs)Directory Variables")`

2. Add clear instruction to use an absolute path to define `org-roam-directory` in
this context (it's buried in this [issue](https://github.com/org-roam/org-roam/issues/1459#issuecomment-817259656).

* (docs)Fix the `eval` example for subdirectories.

Comment: https://github.com/org-roam/org-roam/pull/2002#issuecomment-991830879

The eval form that uses relative path sets the local variable *relative to the
org file*. This means the eval form woudl need to locate the relevant
`.dir-locals` file by traversing the directory tree upwards from the
file (lightly tested to wrok)

Using absolute path (the first example) should be fine without change (not tested)

* update texi

* (docs) minor correction to #2002

Correcting the example of .dir-locals using an absolute path to avoid confusion.
Lightly tested with a real absolute path in my system; works on my end with a
subdir.

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2021-12-13 17:44:37 +08:00
898295f4a0 (docs): fix examples for .dir-locals.el (#2002)
* (docs): fix examples for .dir-locals.el

Two items:

1. The current example does not work; in order to call a function wihtin
`.dir-locals.el`, you should use `eval`.

  - [StackExchange](https://emacs.stackexchange.com/questions/21955/calling-functions-in-dir-locals-in-emacs)

  - [Working
example buried in an issue](https://github.com/org-roam/org-roam/issues/1527#issuecomment-933674233))

  - `(info "(emacs)Directory Variables")`

2. Add clear instruction to use an absolute path to define `org-roam-directory` in
this context (it's buried in this [issue](https://github.com/org-roam/org-roam/issues/1459#issuecomment-817259656).

* (docs)Fix the `eval` example for subdirectories.

Comment: https://github.com/org-roam/org-roam/pull/2002#issuecomment-991830879

The eval form that uses relative path sets the local variable *relative to the
org file*. This means the eval form woudl need to locate the relevant
`.dir-locals` file by traversing the directory tree upwards from the
file (lightly tested to wrok)

Using absolute path (the first example) should be fine without change (not tested)

* update texi

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2021-12-13 13:12:38 +08:00
abe63b4360 (fix)node: org-roam-tag-remove respects tag argument (#1998) (#1999) 2021-12-06 13:07:36 +08:00
e992fc27e2 (feat)graph: allow customized graph links and access to the temporary filenames (#1988) 2021-11-30 19:52:40 +08:00
c3889b3b17 (fix)db: insert plain links into db (#1991) 2021-11-28 16:48:54 +08:00
8c3c216191 (hotfix)buffer: hotfix org-roam-buffer-postrender-functions hook 2021-11-26 15:13:54 +08:00
023af3ec32 (feat)buffer: add customization variables for buffer (#1990)
This commit adds 3 custom variables:

1. org-roam-buffer-postrender-functions

This list of functions are run within the Org-roam buffer after the
Org-roam buffer is rendered. For example, one can produce latex previews
for all content within the Org-roam buffer:

(add-hook
'org-roam-buffer-postrender-functions (lambda () (org--latex-preview-region
(point-min) (point-max))))

2. org-roam-preview-function

This is the function used to extract the content to populate the buffer.
It defaults to `org-roam-preview-default-function`, which extracts all
contents within the headline up to the next headline.

3. org-roam-preview-postprocess-functions

This is a list of functions run to post-process the content retrieved
from org-roam-preview-function. It can be used to strip additional
content from the buffer, or perform sentence-unwrapping.
2021-11-26 14:43:26 +08:00
67f10864df (fix)node: fix wrong string-glyph-* aliases (#1985) 2021-11-24 15:13:41 +08:00
c9efbe1dda (fix)db: fix org-roam-db-sync exiting early on unhandled errors (#1983)
When update-file fails, clear the file from the database and show an
error.
2021-11-23 23:47:00 +08:00
ae533fa309 (fix)node: fix org-roam-node-slug compatibility with Emacs 29.1 (#1982)
Emacs commit 3f096eb3405b2fce7c35366eb2dcf025dda55783 introduced
`string-glyph-compose` and `string-glyph-decompose`, autoloading these instead
of the (still existing) `ucs-normalize-NFC-region` and
`ucs-normalize-NFD-region`.

There are three cases:

- Emacs where these transitions have not happened yet (e.g. 27.1):
  `ucs-normalize-NFC-region` and `ucs-normalize-NFD-region` are still
  autoloaded, aliasing the new names to them will keep them usable
  and the code still works.
- Emacs where the new functions are defined (not yet released): the new names
  are autoloaded, no aliases are installed and the code still works.
- A (hypothetical?) Emacs where `string-glyph-compose` and
  `string-glyph-decompose` are renamed.  If `ucs-normalize-NFC-region` and
  `ucs-normalize-NFD-region` do not get their autoloaded status back, the
  aliasing will happen but the functions not autoloaded and the code will
  break in the same way as in #1981
2021-11-23 21:33:47 +08:00
e4188179f6 (fix)utils: fix org-roam-with-file changing the major-mode (#1980)
`org-roam-with-file` should not alter the major-mode if the
mode is already derived from org-mode.
2021-11-23 00:23:03 +08:00
91aae05810 (minor)capture: avoid surprises when STUFF contains the same key twice (#1961)
See also the discussion in #1961.  The summary is that if STUFF contained
the same key twice, `org-roam-capture--put' would favor the last setting in
STUFF while normally `plist-get' and `plist-put' use the first key/value pair
of a plist:

    (let ((org-capture-plist nil)
          (stuff (list :z 26 :z "ignored")))
      (apply 'org-roam-capture--put stuff)
      (list (plist-get stuff :z)
            (org-roam-capture--get :z)))
    ; old implementation => (26 "ignored")
    ; new implementation => Lisp error: wrong-number-of-arguments

In elisp, plists are defined as having no duplicate keys, but there are
no (costly?) runtime checks against this happening by accident.  OTOH,
`org-roam-capture--put' is only ever called with one key/value pair: we can
afford to restrict its interface to accept only one key/value pair and then
there can never be duplicate keys in its input.
2021-11-22 16:01:02 +08:00
a2e46db808 (feat)db: support org-ref v3 (#1977)
Org-ref v3 introduced a new citation syntax, and removed some functions
that org-roam had previously relied upon (e.g. for extracting keys from
multi-citations).

This commit introduces support on two fronts:

1. Add `org-roam-org-ref-path-to-keys` which converts a link path to a
list of keys. This supports both Org-ref v2 and v3.

2. Adds support for parsing multi-citations in refs.
2021-11-20 16:33:12 +08:00
d93423d4e1 fix(org-roam-db-map-links): goto-char added to point-min (#1969)
* fix(org-roam-db-map-links): goto-char added to point-min

* fix(org-roam-db-map-links): unnecessary point move removed

There's no need to move the point to the beginning of the buffer to
get the context of the link at point. See minimal working example
below.

,----
| (with-temp-buffer
|   (org-mode)
|   (insert "[[a]]")
|   (goto-char (point-min))
|   (pp (org-element-context)))
`----

,----
| (link
|  (:type "fuzzy" :path "a" :format bracket :raw-link "a" :application nil :search-option nil :begin 1 :end 6 :contents-begin nil :contents-end nil :post-blank 0 :parent
|         (paragraph
|          (:begin 1 :end 6 :contents-begin 1 :contents-end 6 :post-blank 0 :post-affiliated 1 :parent nil))))
`----

,----
| (with-temp-buffer
|   (org-mode)
|   (insert "[[a]]")
|   (pp (org-element-context)))
`----

,----
| (link
|  (:type "fuzzy" :path "a" :format bracket :raw-link "a" :application nil :search-option nil :begin 1 :end 6 :contents-begin nil :contents-end nil :post-blank 0 :parent
|         (paragraph
|          (:begin 1 :end 6 :contents-begin 1 :contents-end 6 :post-blank 0 :post-affiliated 1 :parent nil))))
`----
2021-11-14 17:13:43 +08:00
1af1639ee0 (feat)db: cache file title into files table (#1963)
adds a column `title` into the `files` table, and the `file-title` property to the node structure.
2021-11-12 23:47:34 +08:00
db573bbc4e (fix)node: fix org-roam-open-id-at-point not working in PROPERTIES drawer (#1964)
Update org-roam-open-id-at-point to use regexp instead of org-element so
the function works wherever the link is.
2021-11-12 15:42:39 +08:00
50cc4d6990 (minor)overlay: fix typos in file (#1960)
Fix typos in comments.
2021-11-11 20:01:23 +08:00
b96efbb444 (minor)chore: fix minor warnings by native compiler (#1959)
* Fix warnings reported by emacs (--with-native-compiler)
* Drive-by fix for org-roam-database-connector's :group
2021-11-11 20:00:07 +08:00
721f167563 (perf)utils: optimize org-roam-plist-map! (#1958)
The old implementation:

- did not handle duplicate keys well (the function would be applied to each value, but only the first value would be updated with the last value's result)
- was quadratic in the length of the input list (for each key, plist-put would search for the first occurrence of the key by going through all previous elements again)
- copied the input sequence even though this was not really needed
- did not provide a result value that could be useful, thus encouraging imperative programming
- did not need to be a macro, and the macro implementation was evaluating fn multiple times and causing warnings in the native compiler because k and v were not bound.
2021-11-11 19:59:36 +08:00
25bc3acce3 (fix)buffer: change preview-content logic (#1955)
Some commit in Org 9.5 changed the AST structure, so we our previous
preview content logic was borked and started showing the full contents
of the file. To be forward compatible we change the content preview
logic, and extract everything within the section.

Fixes #1934.
2021-11-09 22:32:40 +08:00
d8c7f7d4e7 (fix)db: refresh CATEGORY before writing to db (#1953)
Closes #1844.
2021-11-09 15:33:52 +08:00
01c45f9dca (minor)refactor: make prefix checking clearer (#1951) 2021-11-09 15:25:31 +08:00
239929045f (minor)db: filename title sans-extension (#1952)
Use the file name without extension when no TITLE is specified.
2021-11-09 14:15:58 +08:00
d26047e6f7 (minor)db: make check for property drawers case-insensitive (#1949) 2021-11-07 22:29:17 +08:00
36928d655a (fix)completions: fix same-line completions (#1948)
Make final regex group non-greedy, allowing same-line completions. Closes #1790 .
2021-11-06 21:03:36 +08:00
4a2b44252e (fix)db: don't consider links in ROAM_REFS as links (#1947)
Links within ROAM_REFS are no longer added as links into the database.
This prevents self-referencing. Links within other properties are still
valid.
2021-11-06 18:09:50 +08:00
3177b900e7 (perf)db: prefer org-entry-get over org-entry-properties (#1946)
Since we're only looking for a specific property, org-entry-get is much
faster. This should speed up link indexing significantly.
2021-11-06 17:11:38 +08:00
4d71fbdfe1 (fix)db: expand org-roam-directory name as directory (#1945)
Closes #1940
2021-11-06 17:03:54 +08:00
84f58cbc12 (perf)db: require optional libraries only once per function (#1944)
Require optional libraries 'org-ref and 'oc only once per function run:
org-roam-db-update-file and org-roam-db-sync will call these requires at
the top level instead of within the link mapper.
2021-11-06 15:03:04 +08:00
3e47f198c7 (fix)utils: org-roam-set-keyword skip over all drawers (#1931)
Previously org-roam-set-keyword was unable to handle the syntax of the
:LOGBOOK: drawer. We introduce `org-roam-end-of-meta-data` to move point
over all drawers, allowing us to set the keyword in the right place.
2021-11-01 14:39:50 +08:00
c789531e36 (chore)utils: standardize org-roam-property-add/remove fn signatures (#1930) 2021-10-30 16:21:43 +08:00
1b221a1d4a (fix):org-roam-refile: Don't try to refile a node into itself (#1928) 2021-10-29 16:07:25 +08:00
d0fd2c6959 (fix): protocol: set org-capture-link-is-already-stored (#1921)
Closes #1920
2021-10-27 21:28:01 +08:00
2c75b194d8 (fix)ref: fix org-roam-node-from-ref support for org-cite (#1919)
Support Elisp queries of form (org-roam-node-from-ref "@cite_key").
Closes #1918.
2021-10-22 16:15:58 +08:00
c8f8c3e876 (docs): added instructions on troubleshooting org-protocol (#1914)
Co-authored-by: Leo D'Angelo <ldangelo@mac.com>
2021-10-21 15:19:13 +08:00
852042436e (feat)db: support different sqlite3 libraries (#1907) 2021-10-17 19:13:03 +08:00
dafcf0dcf8 (feat)insert: add org-roam-node-formatted (#1909)
Format a node into a string using a templated string (e.g. "${title}")
or using a function.
2021-10-17 16:15:58 +08:00
2b6f8ce615 (perf)db: don't require (probably non-existent) 'oc feature for each org file (#1908)
Instead check if the (autoloaded) function `org-cite-insert' is bound.

This prevents a lot of unnecessary filesystem I/O when generating the
org roam database.
2021-10-16 17:49:26 +08:00
5651f4598c (chore)libraries: s/s-join/string-join/g (#1906)
Remove usages of s-join. s.el is currently pulled in implicitly by f.el.
2021-10-16 16:25:03 +08:00
e9299297f9 (fix)completion: annotation fixes (#1904)
* (fix)Formatter: Ensure that strings are always padded up to the field width

* (feat)Colorize tags by default with org-tag face
2021-10-14 00:24:39 +08:00
617e0021f5 (fix)completions: fix highlighting of formatted/truncated strings (#1895)
* (fix)org-roam-unlinked-references-section: Use truncate-string-ellipsis

The variable can be set to a unicode ellipsis.

* (fix)org-roam-node-read--format-entry: Fix highlighting for truncation

Fix #1801. The truncated part of the string is made invisible. Matching
on the whole string remains possible.

* (fix)org-roam-format-template: Preserve string properties like format

If the variable value carries properties itself, these properties
take precedence. Display templates can be properties with this change.

(setq org-roam-node-display-template
      (concat (propertize "${title:*}" 'face 'font-lock-keyword-face)
              " "
              (propertize "${tags:10}" 'face 'font-lock-constant-face)))
2021-10-13 19:13:45 +08:00
f80515ab5f (feat)completions: separate completion history (#1901)
* (fix) Use read-from-minibuffer instead of completing-read where appropriate

* (feat) Add separate node and ref completion histories

Using separate histories has the advantage that the history only contains node
names and no other strings from other unrelated minibuffer completions.
2021-10-13 12:47:19 +08:00
4af5ff662e Update documentation on migrating from v1 (#1900)
* Update documentation on migrating from v1

* update texi

Co-authored-by: Jethro Kuan <jethrokuan95@gmail.com>
2021-10-12 00:53:32 +08:00
73dfeb60cf (chore): update slack url (#1899) 2021-10-11 17:35:14 +08:00
5b933bd8ec (fix)completions: improve completion-at-point slightly (#1896)
* Allow capfs to run after org-roam-complete-everywhere. This is
  necessary since org-roam-complete-everywhere gets triggered on every
  word, even if no match has been found.
* Simpler hook setup. org-roam-complete-at-point is unnecessary, better
  add the capfs directly.
2021-10-11 13:57:24 +08:00
3a54182bb1 (fix)completion: minor fixes around completing-read (#1893)
* (fix)org-roam-node-default-sort: Add choice nil

* (fix)Ensure that annotation functions are compiled

* (fix)org-roam-node-read: Preserve sorting if sort-fn is non-nil

If the nodes are sorted by org-roam we don't want the completion UI to change
the order. In order to disable org-roam sorting set org-roam-node-default-sort
to nil. Then the sorting of the completion UI is used.

* (fix)org-roam-graph--build: Use ephermeral buffer name

Ephemeral buffers are hidden by default in the completion lists,
which is usually desired for background processes.

* (fix)capfs: Do not use completion-table-dynamic

completion-table-dynamic serves a different purpose. it should only be used if
the candidates are dynamically generated based on the input. This is not the
case for org-roam where we the list of nodes is already available right at the
beginning of the completion. This change should improve performance.
2021-10-10 19:37:57 +08:00
34243a0a90 (feat): allow org-roam-node-display-template to be a closure (#1891) 2021-10-09 21:36:20 +08:00
54d17cc50f (docs): add space so that list item displays correctly (#1880) 2021-10-03 14:02:40 +08:00
54b63db350 (fix)graph: fix default value of org-roam-graph-node-extra-config (#1884) 2021-10-03 13:57:55 +08:00
8f2c51ad21 (refactor)completions: simplify org-roam-node-read--format-entry (#1850)
This commit simplifies the code in org-roam-node-read--format-entry,
saving the entire processing of display properties when no
field-length is specified and thus no truncation needs to happen.
Since in this case, we don't truncate, the bug fixed in #1759 won't
reappear.

This has the beneficial side-effect that existing display properties
in the field-value, e.g., display properties that replace text by
images/icon, are not overridden by us when no field-length is
specified. Note that images anyway don't need truncation of the text
they replace.

This commit also fixes a wrong word in the comment.
2021-10-01 16:03:36 +08:00
3b93c83b23 (fix)db: Require 'oc to get org-cite functionality. (#1849) 2021-10-01 15:45:02 +08:00
a101c548c1 fix(org-roam-node-read): add aliases (#1883) 2021-10-01 15:44:09 +08:00
1b8ece0219 fix(org-roam-node-read): fix regression introduced in #1876 (#1882)
Fix `filter-fn` not working like its supposed to.
2021-10-01 15:09:10 +08:00
0ecbd3a104 fix(org-roam-extract-subtree): Use file-name-as-directory (#1871)
The current implementation doesn't cover the scenario where
org-roam-directory doesn't have a trailing slash. Because of this,
users are enforced to make org-roam-directory end with a trailing
slash.
2021-09-30 19:08:08 +08:00
66e10df943 (fix)dailies: stop asking for time, only date (#1877)
There is no reason `org-roam-dailies-capture-date' should ask for a
time as only a date is necessary.
2021-09-30 18:54:46 +08:00
d2fc4be73b optimize(core) linear->constant time filter-fn (#1876)
in org-roam-node-read, there is no need to check filter-fn for each node, move the branch up, O(n) to O(1)
2021-09-30 18:54:25 +08:00
17d8e84ea5 (docs): fix typo in README (#1842)
instuctions != instructions
2021-09-26 15:36:15 +08:00
777f969b50 (docs): fix typo in docs (#1834) 2021-09-26 15:35:53 +08:00
bc833a9ff4 (fix)capture: ignore file template upon capture goto. (#1858)
Currently, roam allows file template *nominally*, but, if one tries to set
up, it causes trouble.

`goto` functions, like `org-roam-dailies-goto-*`, tries to expand file
template with `org-roam-format-template`, which assumes template is
string. Hence, complains about it.

This commit suppress such malfunctions by do nothing for non-string template.
2021-09-26 15:00:53 +08:00
ec8d250f6c feat(org-roam-db-map-links): Consider links in properties drawers (#1868)
The current implementation uses org-element-map to iterate through all
links in the current buffer. This function doesn't consider links
within properties drawer. This implementation does consider such
links.
2021-09-26 14:44:16 +08:00
1795039ab9 (perf)db: wrap update-file in sql transaction (#1827)
This should lead to some performance gains on buffers with lots of
inserts required (e.g. when the buffer has a lot of links).
2021-09-01 19:43:19 +08:00
a8a36a420b (fix)org-roam-node-visit: pass correctly optional force parameter (#1821)
This function ignored the `force' optional argument because it always
quoted it, resulting in a non-nil value every time.

This makes the suggested code snippet in
https://github.com/org-roam/org-roam/issues/597#issuecomment-907743125
work as expected (not resetting point to the beginning of the "Index"
buffer on every invocation).
2021-08-30 02:30:04 +03:00
340215a16a (feat) core: support new org-mode citations (#1806)
Support caching the new Org 9.5 citations.

Because citations now has first-class support, and are treated
differently from links, they are now cached in their own table.

Org-ref citations, instead of being stored in the links table, are now
stored in the citations table instead.

To use a citation as a ROAM_REF, use the `@citeKey` syntax
2021-08-29 19:33:14 +08:00
941bd1f6b4 (fix)compat: resolve all the missing compile-time dependencies (#1820)
48a5d01726 was wrong, and the closure wasn't the cause. The source of
the issue stems from a missing compile-time dependency on subr-x.

Emacs 28 doesn't seem to require it for the needed functions anymore,
while older Emacsen still do. What even worse, the byte compiler doesn't
seem to complain about such thing on older Emacsen and the error message
produced at the run-time isn't helpful to identify the cause.

Fixes #1814.
2021-08-29 05:41:36 +03:00
48a5d01726 (fix)capture: refactor org-roam-capture--get-if-new-target-a (#1818)
It looks like Emacs' byte compiler is very naive and cannot properly
sometimes compile closures, which causes error like in #1814, at least
on Emacsen <28. One of the possible solutions is to write the code using
less abstractions in between, so the byte compiler would do the right
thing when arranging instructions.

Possibly addresses #1814.
2021-08-28 16:48:56 +03:00
0e6b93a253 (fix)db: prevent invalid refs from crashing db update (#1816)
Previously when ROAM_REFS is non-empty, but the refs are invalid, this
will cause an empty SQL expression and crash the db caching. This fixes
that scenario.
2021-08-28 13:42:30 +08:00
cfabe0ec38 (feat)capture: rename :if-new template property to :target (#1809)
This name will better reflect the current purpose of the property. At
the beginning of the v2 it would only trigger for the new nodes (hence
:if-new), but through the development it's no longer the case, though
the behavior various from each type of the target.

As for now, this will be soft deprecated and won't be strictly enforced
at the user.
2021-08-27 19:45:50 +08:00
ab87a08e17 (fix)db: prevent empty aliases sql expression (#1813) 2021-08-27 19:01:43 +08:00
714ea7a3fc (chore): upgrade dependency of magit-section to 3.0.0 (#1808)
Closes #1803.
2021-08-26 19:15:16 +08:00
4c4b024b49 (fix)capture: always trigger :if-new template for existing nodes (#1807)
Previously, if org-roam-capture- would be triggered for an existing node
it would just move the point to the beginning of the node, ignoring the
target from the :if-new part of the template.

This patch will now also run the :if-new part of the template for
existing nodes, just like it does right now in case of daily
templates (which happened to be never recognized as existing nodes
during the capture process).
2021-08-26 19:04:46 +08:00
5dde894a0c (fix)org-id: condition-case instead of unwind-protect the error handler
unwind-protect will still propagate the initial error, which isn't
desirable and can confuse both, the end-user and us when resolving
issues.

To keep the backtrace clean (in case of other errors), just
automatically detatch the advice once it ran in the error handling
branch. If the user change the file at the runtime, org-id shouldn't
fail with the error as long as it was handled at least once, and it
still will be handled by us during the next session. At the end of the
day it's a very niche edge case, which most of the users shouldn't ever
see.

Fixes #1805.
2021-08-26 02:29:03 +03:00
74a6fd598a (fix)org-id-locations-file: use :test instead of :type when recovering
The latter is incorrect construct for the make-hash-table.
2021-08-25 19:00:50 +03:00
e9ae19c01c (chore): adding changelog entry (#1800)
Related to #1693 and #1681
2021-08-23 14:34:50 +08:00
d25d477b4f (refactor): ensure dependencies are loaded for org-roam-utils.el (#1799)
If org-roam-utils.el is byte-compiled when org-macs is not loaded (`org-with-point-at` macro missing), error can appear about missing `org-with-point-at`.
2021-08-23 01:41:58 +03:00
04b7780ff9 fix: Do not skip invisible headings when getting node at point. (#1798) 2021-08-22 00:01:52 +08:00
858f531d96 (feat)buffer: optimize reflinks fetch (#1795)
Use SQL join instead of iterating over each ref and running another sql query.
2021-08-21 16:18:41 +08:00
39 changed files with 3627 additions and 1596 deletions

View File

@ -4,10 +4,13 @@
(elisp-lint-ignored-validators . ("byte-compile" "package-lint")) (elisp-lint-ignored-validators . ("byte-compile" "package-lint"))
(elisp-lint-indent-specs . ((describe . 1) (elisp-lint-indent-specs . ((describe . 1)
(it . 1) (it . 1)
(thread-first . 0)
(cl-flet . 1)
(cl-flet* . 1)
(org-element-map . defun) (org-element-map . defun)
(org-roam-dolist-with-progress . 2)
(org-roam-with-temp-buffer . 1) (org-roam-with-temp-buffer . 1)
(org-with-point-at . 1) (org-with-point-at . 1)
(magit-insert-section . defun) (magit-insert-section . defun)
(magit-section-case . 0) (magit-section-case . 0)
(->> . 1)
(org-roam-with-file . 2))))) (org-roam-with-file . 2)))))

2
.github/FUNDING.yml vendored
View File

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [jethrokuan, zaeph] github: [jethrokuan]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

View File

@ -39,5 +39,3 @@ Example:
### Environment ### Environment
<!-- Please M-x org-roam-diagnostics and paste results here --> <!-- Please M-x org-roam-diagnostics and paste results here -->
- Org-roam commit: https://github.com/jethrokuan/org-roam/commit/commithashhere

View File

@ -4,7 +4,7 @@ name: "Docs"
on: on:
push: push:
branches: branches:
- master - main
jobs: jobs:
build: build:

View File

@ -28,7 +28,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- master - main
jobs: jobs:
build: build:
@ -36,7 +36,11 @@ jobs:
strategy: strategy:
matrix: matrix:
emacs_version: emacs_version:
- 27.1 # REVIEW: we do not yet have the bootstrapping in place to test
# versions of emacs without built-in sqlite (pre 29.1)
# - 27.2
# - 28.2
- 29.4
- snapshot - snapshot
steps: steps:
- uses: purcell/setup-emacs@master - uses: purcell/setup-emacs@master
@ -46,7 +50,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Eldev - name: Install Eldev
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh run: curl -fsSL https://raw.github.com/org-roam/org-roam/master/github-eldev | sh
- name: Install dependencies - name: Install dependencies
run: make prepare run: make prepare

View File

@ -1,7 +1,176 @@
# Changelog # Changelog
## 2.1.0 ## 2.3.0 (2025-05-24)
* (perf)node-read: filter before map to candidate by @russmatney in https://github.com/org-roam/org-roam/pull/2168
* (chore): remove extraneous changelog line by @jethrokuan in https://github.com/org-roam/org-roam/pull/2172
* (fix)org-roam-file-p: don't exclude org-roam-directory by @toregilhk in https://github.com/org-roam/org-roam/pull/2178
* (chore) fix version numbers by @jethrokuan in https://github.com/org-roam/org-roam/pull/2182
* (fix) org-roam-file-p handle opening a buffer with no path by @jethrokuan in https://github.com/org-roam/org-roam/pull/2185
* (fix)db: update atime on file access by @jethrokuan in https://github.com/org-roam/org-roam/pull/2174
* Fix org-roam-extract-subtree by @ralfdoering in https://github.com/org-roam/org-roam/pull/2191
* (node): fix org-roam-node-at-point check by @jethrokuan in https://github.com/org-roam/org-roam/pull/2195
* (chore): reapply #2178 by @jethrokuan in https://github.com/org-roam/org-roam/pull/2197
* Solved issue #2192 (Respect blank lines in capture templates) by @alopezrivera in https://github.com/org-roam/org-roam/pull/2203
* (chore)ci: /s/master/main by @jethrokuan in https://github.com/org-roam/org-roam/pull/2204
* support custom minibuffer matching function by @cuttlefisch in https://github.com/org-roam/org-roam/pull/2177
* (db)fix: org-roam-db-connector group by @jethrokuan in https://github.com/org-roam/org-roam/pull/2209
* ensure unique candidates in `org-roam-ref-find` by @bdarcus in https://github.com/org-roam/org-roam/pull/2208
* (db)fix: FOREIGN KEY error in narrowed buffer by @psii in https://github.com/org-roam/org-roam/pull/2215
* (feat): org-roam-property-* code duplication removed (#2217) by @clanghans in https://github.com/org-roam/org-roam/pull/2218
* (db)fix: file modification detection failing in some edge cases by @kisaragi-hiu in https://github.com/org-roam/org-roam/pull/2221
* (fix): autoload org-roam-list-files by @jethrokuan in https://github.com/org-roam/org-roam/pull/2226
* (fix): links not displayed properly in org-roam-buffer by @ntharim in https://github.com/org-roam/org-roam/pull/2236
* (fix): remove use of deprecated org-font-lock-ensure by @jethrokuan in https://github.com/org-roam/org-roam/pull/2238
* was missing the FULL parameter to `org-end-of-meta-data` - fixes #2242 by @jmay in https://github.com/org-roam/org-roam/pull/2246
* (Docs): Fixed typos in the docs by @hnvy in https://github.com/org-roam/org-roam/pull/2256
* Fix space chars in roam refs by @hwiorn in https://github.com/org-roam/org-roam/pull/2285
* org-roam-tag-add: use tags separator as crm-separator by @Hugo-Heagren in https://github.com/org-roam/org-roam/pull/2282
* Support multi-line org titles by @FelixBrendel in https://github.com/org-roam/org-roam/pull/2264
* (Docs): orgmode.org/elpa has been shut down by @aviad in https://github.com/org-roam/org-roam/pull/2258
* Minor typo in the commented text of the definition of `org-roam-refile` by @apc in https://github.com/org-roam/org-roam/pull/2263
* fix outline of link in properties by @bhuztez in https://github.com/org-roam/org-roam/pull/2230
* (fix): update `org-roam-unlinked-references-section` check by @Elilif in https://github.com/org-roam/org-roam/pull/2254
* Add support for filtering backlinks. by @swflint in https://github.com/org-roam/org-roam/pull/2247
* (docs): update documentation on database-connectors by @jethrokuan in https://github.com/org-roam/org-roam/pull/2298
* (db): default to sqlite-builtin when possible by @jethrokuan in https://github.com/org-roam/org-roam/pull/2299
* don't complete org-roam nodes in source blocks by @ParetoOptimalDev in https://github.com/org-roam/org-roam/pull/2292
* (core): add org-roam-node-category by @jethrokuan in https://github.com/org-roam/org-roam/pull/2300
* add discoverability support for age encrypted org files by @anticomputer in https://github.com/org-roam/org-roam/pull/2302
* (minor): fix lints on main by @jethrokuan in https://github.com/org-roam/org-roam/pull/2320
* utils: descendant-of-p: Defend against nils by @qzdl in https://github.com/org-roam/org-roam/pull/2319
* Add customisable function for prompting when adding refs by @Hugo-Heagren in https://github.com/org-roam/org-roam/pull/2317
* Fix org-fold-core-style in org-roam-buffer by @hwiorn in https://github.com/org-roam/org-roam/pull/2325
* depend on snapshot of emacsql by @jethrokuan in https://github.com/org-roam/org-roam/pull/2327
* (node): org-roam-node-at-point: don't error in non-org buffers by @Hugo-Heagren in https://github.com/org-roam/org-roam/pull/2329
* (node) add optional NOCASE parameter to org-roam-node-from-title-or-alias by @nuthub in https://github.com/org-roam/org-roam/pull/2403
* (docs): update org-protocol instructions by @benthamite in https://github.com/org-roam/org-roam/pull/2401
* Fix some typos by @feltcat in https://github.com/org-roam/org-roam/pull/2430
* (docs): fix name of my-org-roam-show-backlink-p by @Delapouite in https://github.com/org-roam/org-roam/pull/2447
* Link Martin Edström's knowledge base by @meedstrom in https://github.com/org-roam/org-roam/pull/2394
* Update org-roam.org by @marcosbodio in https://github.com/org-roam/org-roam/pull/2441
* (chore): fix indent blocking ci lint execution by @Delapouite in https://github.com/org-roam/org-roam/pull/2448
* Fix Issue #2410 - Unlinked References doesnt work if the org-roam-directory has a space in it. by @vikram-mandyam in https://github.com/org-roam/org-roam/pull/2411
* (test): add "org-roam--list-files-search-globs" by @Delapouite in https://github.com/org-roam/org-roam/pull/2449
* Backlink heading by @toregilhk in https://github.com/org-roam/org-roam/pull/2333
* (test): add org-roam-id-find by @Delapouite in https://github.com/org-roam/org-roam/pull/2450
* (test): add org-roam-id-at-point by @Delapouite in https://github.com/org-roam/org-roam/pull/2451
* (test): add org-roam--buffer-promoteable-p by @Delapouite in https://github.com/org-roam/org-roam/pull/2452
* (test): add org-roam--get-titles by @Delapouite in https://github.com/org-roam/org-roam/pull/2453
* (test): add org-roam-node-from-{id|title-or-alias} by @Delapouite in https://github.com/org-roam/org-roam/pull/2454
* (test): add org-roam-alias-{add|remove} by @Delapouite in https://github.com/org-roam/org-roam/pull/2455
* (docs): add missing versions dates and obsolete notice by @Delapouite in https://github.com/org-roam/org-roam/pull/2456
* (docs): explain org-roam-file-exclude-regexp by @adamoudad in https://github.com/org-roam/org-roam/pull/2458
* (test): add org-roam-demote-entire-buffer by @Delapouite in https://github.com/org-roam/org-roam/pull/2459
* (test): add org-roam-file-p by @Delapouite in https://github.com/org-roam/org-roam/pull/2460
* (test): add org-roam-buffer-p by @Delapouite in https://github.com/org-roam/org-roam/pull/2461
* (fix): remove dead-code about org-roam-shield feature by @Delapouite in https://github.com/org-roam/org-roam/pull/2462
* (test): add org-roam-db--file-hash by @Delapouite in https://github.com/org-roam/org-roam/pull/2464
* (test): add org-roam-db-get-{scheduled|deadline}-time by @Delapouite in https://github.com/org-roam/org-roam/pull/2465
* Doc formatting: 2 fixes by @kevinrineer in https://github.com/org-roam/org-roam/pull/2475
* Take org-roam-node as argument to #'org-roam-refile by @pestctrl in https://github.com/org-roam/org-roam/pull/2388
* Compatibility with latest org-id version: advise org-id-find rather than overwriting id link by @ricklupton in https://github.com/org-roam/org-roam/pull/2432
* Depend on emacsql 4.0.0 by @bcc32 in https://github.com/org-roam/org-roam/pull/2466
* Use regexp match to replace hard-coded path equal test by @manphiz in https://github.com/org-roam/org-roam/pull/2497
* Align sqlite integration with emacsql 4.0 by @dustinfarris in https://github.com/org-roam/org-roam/pull/2503
* fix #2425 Prevent data loss when user has called org-roam-extract-subtree on folded org headline by @akashpal-21 in https://github.com/org-roam/org-roam/pull/2444
* Rely on emacsql-sqlite-open to pick the best available back-end by @tarsius in https://github.com/org-roam/org-roam/pull/2486
* Set org-roam-directory to a non-existent path to ensure robust test by @manphiz in https://github.com/org-roam/org-roam/pull/2499
* (perf): Deprecate link :outline properties by @meedstrom in https://github.com/org-roam/org-roam/pull/2509
* Bump DB version to avoid error by @meedstrom in https://github.com/org-roam/org-roam/pull/2514
## 2.2.2 (2022-04-25)
### Breaking
### Added ### Added
- [#2138](https://github.com/org-roam/org-roam/pull/2138) export: add new module
- [#2170](https://github.com/org-roam/org-roam/pull/2170) log: add new module for working with org logs
- [#2158](https://github.com/org-roam/org-roam/pull/2158) db: support emacsql-sqlite-builtin and emacsql-sqlite-module
- [#2160](https://github.com/org-roam/org-roam/pull/2160) core: support a list of `org-roam-file-exclude-regexp`
### Removed
### Fixed
- [#2091](https://github.com/org-roam/org-roam/pull/2091) node: fix org-roam-promote-entire-buffer structural errors
- [#2130](https://github.com/org-roam/org-roam/pull/2130) buffer: unlinked-references section now also searches within symlinked directories
- [#2152](https://github.com/org-roam/org-roam/pull/2152) org-roam-preview-default-function: doesn't copy copy content of next heading node when current node's content is empty
- [#2159](https://github.com/org-roam/org-roam/pull/2159) db: fix db syncs on narrowed buffers
- [#2156](https://github.com/org-roam/org-roam/pull/2157) capture: templates with functions are handled correctly to avoid signaling `char-or-string-p`
### Changed
- [#2160](https://github.com/org-roam/org-roam/pull/2160) core: ignore files in `org-attach-id-dir` by default
## 2.2.1 (2022-03-15)
### Breaking
- [#2054](https://github.com/org-roam/org-roam/pull/2054) node: simplify default `org-roam-node-display-template`.
This was done so completions work fine by default on all completion systems. To restore the tabular vertical completion interface, set this in your configuration:
```emacs-lisp
(setq org-roam-node-display-template
(concat "${title:*} "
(propertize "${tags:10}" 'face 'org-tag)))
```
### Added
- [#2042](https://github.com/org-roam/org-roam/pull/2042) db: add `org-roam-db-extra-links-elements` and `org-roam-db-extra-links-exclude-keys` for fine-grained control over additional link parsing
- [#2049](https://github.com/org-roam/org-roam/pull/2049) capture: allow ID to be used as part of `org-roam-capture-templates`
- [#2050](https://github.com/org-roam/org-roam/pull/2050) core: add `FILTER-FN` to `org-roam-node-random`
- [#2065](https://github.com/org-roam/org-roam/pull/2065) dailies: add `keys` argument to the remaining dailies functions `org-roam-dailies-goto-yesterday`/`-today`/`-tomorrow`/`-date` and `org-roam-dailies-capture-yesterday`/`-tomorrow`/`-date` to give the abilty to get into a capture buffer bypassing the selection screen in all dailies commands. Extension of #2055
- [#2079](https://github.com/org-roam/org-roam/pull/2079) capture: ensure that `:ref` info captured in all cases.
- [#2121](https://github.com/org-roam/org-roam/pull/2121) buffer: add unique option to `org-roam-backlinks-section`
### Removed
### Fixed
- [#2086](https://github.com/org-roam/org-roam/pull/2086) capture: correctly update org-id-locations on capture
- [#2082](https://github.com/org-roam/org-roam/pull/2082) buffer: don't destroy window if `org-roam-node-toggle` reuses window
- [#2080](https://github.com/org-roam/org-roam/pull/2080) dailies: prevent multiple "dailies/" subdir expansions
- [#2055](https://github.com/org-roam/org-roam/pull/2055) dailies: removed stray f require, which was causing require and compilation errors
- [#2117](https://github.com/org-roam/org-roam/pull/2117) capture: preserve trailing whitespace content in capture templates
### Changed
- [#2060](https://github.com/org-roam/org-roam/pull/2060) node: added double acute accent normalization for Unicode characters in titles
- [#2040](https://github.com/org-roam/org-roam/pull/2040) completions: fix completions display-width for Helm users
- [#2025](https://github.com/org-roam/org-roam/pull/2025) chore: removed the dependencies on f.el and s.el
- [#2109](https://github.com/org-roam/org-roam/pull/2109) capture: `org-roam-node-insert` places cursor after inserted link where appropriate
- [#2123](https://github.com/org-roam/org-roam/pull/2123), [#2124](https://github.com/org-roam/org-roam/pull/2124) buffer: `org-roam-mode-section-functions` renamed to `org-roam-mode-sections`, supports passing args into the section-rendering function
## 2.2.0 (2022-01-14)
### Added
- [#1806](https://github.com/org-roam/org-roam/pull/1806), [#2017](https://github.com/org-roam/org-roam/pull/2017) db: support caching and usage of Org 9.5's in-built citations
- [#1963](https://github.com/org-roam/org-roam/pull/1963) db: cache file title into files table
- [#1977](https://github.com/org-roam/org-roam/pull/1977) db: support Org-ref v3 citations
- [#1907](https://github.com/org-roam/org-roam/pull/1907), [#2009](https://github.com/org-roam/org-roam/pull/2009), [#2018](https://github.com/org-roam/org-roam/pull/2018) db: support sqlite3
- [#2028](https://github.com/org-roam/org-roam/pull/2028) dailies: add `keys` argument to `org-roam-dailies-capture-today` and `org-roam-dailies--capture` functions to give the abilty to get into a capture buffer bypassing the selection screen
### Removed
### Changed
- [#1795](https://github.com/org-roam/org-roam/pull/1795) buffer: optimized reflinks fetch
- [#1809](https://github.com/org-roam/org-roam/pull/1809) capture: the mandatory `:if-new` property of each capture template is now renamed to `:target`
- [#1829](https://github.com/org-roam/org-roam/pull/1829) perf: file sql updates are now wrapped in a transaction
- [#1877](https://github.com/org-roam/org-roam/pull/1877) dailies: stop asking for time, only date
- [#1949](https://github.com/org-roam/org-roam/pull/1949) db: check for property drawers are now case-insensitive
### Fixed
- [#1798](https://github.com/org-roam/org-roam/pull/1798) org-roam-node-at-point: do not skip invisible headings
- [#1807](https://github.com/org-roam/org-roam/pull/1807) capture: always trigger `:if-new` template for existing nodes
- [#1813](https://github.com/org-roam/org-roam/pull/1813) db: prevent empty ROAM_ALIASES from crashing db updates
- [#1816](https://github.com/org-roam/org-roam/pull/1816) db: prevent invalid ROAM_REFS from crashing db updates
- [#1893](https://github.com/org-roam/org-roam/pull/1893), [#1896](https://github.com/org-roam/org-roam/pull/1896), [#1901](https://github.com/org-roam/org-roam/pull/1901), [#1895](https://github.com/org-roam/org-roam/pull/1895), [#1904](https://github.com/org-roam/org-roam/pull/1904) completions: various mini-buffer related completion fixes
- [#1931](https://github.com/org-roam/org-roam/pull/1931) utils: org-roam-set-keyword now skips over all drawers
- [#1947](https://github.com/org-roam/org-roam/pull/1947) db: links in ROAM_REFS are no longer considered as links
- [#1948](https://github.com/org-roam/org-roam/pull/1948) completions: fix same-line completions
- [#1953](https://github.com/org-roam/org-roam/pull/1953) db: refresh CATEGORY before writing to db
- [#1946](https://github.com/org-roam/org-roam/pull/1946), [#1946](https://github.com/org-roam/org-roam/pull/1946), [#1958](https://github.com/org-roam/org-roam/pull/1958) various performance improvements
- [#1980](https://github.com/org-roam/org-roam/pull/1980) utils: fix org-roam-with-file changing the minor-mode
- [#2016](https://github.com/org-roam/org-roam/pull/2016) db: fix node caching being affected by agenda variables
- [#2033](https://github.com/org-roam/org-roam/pull/2023) db: respect local variables during db parsing
## 2.1.0 (2021-08-20)
### Added
- [#1693](https://github.com/org-roam/org-roam/pull/1693) added `filter-fn` and `templates` parameter to `org-roam-capture`, `org-roam-node-find`, and `org-roam-node-insert`
- [#1709](https://github.com/org-roam/org-roam/pull/1709) added ability to specify default value in org-roam capture templates - [#1709](https://github.com/org-roam/org-roam/pull/1709) added ability to specify default value in org-roam capture templates
- [#1710](https://github.com/org-roam/org-roam/pull/1710) added `org-roam-extract-subtree` - [#1710](https://github.com/org-roam/org-roam/pull/1710) added `org-roam-extract-subtree`
- [#1720](https://github.com/org-roam/org-roam/pull/1720) added `org-roam-db-update-on-save` - [#1720](https://github.com/org-roam/org-roam/pull/1720) added `org-roam-db-update-on-save`
@ -29,7 +198,10 @@
- [#1713](https://github.com/org-roam/org-roam/pull/1713) capture: check for file-existence before template insertion - [#1713](https://github.com/org-roam/org-roam/pull/1713) capture: check for file-existence before template insertion
- [#1725](https://github.com/org-roam/org-roam/pull/1725) db: always compute hash of encrypted file to prevent re-processing of encrypted files - [#1725](https://github.com/org-roam/org-roam/pull/1725) db: always compute hash of encrypted file to prevent re-processing of encrypted files
## 2.0.0 ## 2.0.0 (2021-07-17)
A few symbols have been marked as obsolete. Have a look at the content of [org-roam-compat.el](https://github.com/org-roam/org-roam/blob/f819720c510185af713522c592833ec9f2934251/org-roam-compat.el#L159)
### Added ### Added
- [#1396](https://github.com/org-roam/org-roam/pull/1396) add option to choose between prepending, appending, and omitting `roam_tags` in file completion - [#1396](https://github.com/org-roam/org-roam/pull/1396) add option to choose between prepending, appending, and omitting `roam_tags` in file completion
- [#1270](https://github.com/org-roam/org-roam/pull/1270) capture: create OLP if it does not exist. Removes need for OLP setup in `:head`. - [#1270](https://github.com/org-roam/org-roam/pull/1270) capture: create OLP if it does not exist. Removes need for OLP setup in `:head`.
@ -56,7 +228,7 @@
- [#1409](https://github.com/org-roam/org-roam/issues/1398) prevent inclusion of non-org-roam files in `org-roam-dailies--list-files` - [#1409](https://github.com/org-roam/org-roam/issues/1398) prevent inclusion of non-org-roam files in `org-roam-dailies--list-files`
- [#1542](https://github.com/org-roam/org-roam/issues/1542) fix files not excluded when `org-roam-list-files-commands` is nil - [#1542](https://github.com/org-roam/org-roam/issues/1542) fix files not excluded when `org-roam-list-files-commands` is nil
## 1.2.3 (13-11-2020) ## 1.2.3 (2020-11-13)
Primarily a stabilization and bug-fix release. Primarily a stabilization and bug-fix release.
@ -87,7 +259,7 @@ Org-roam-dailies has also been revamped to include new features, see [this video
- [#1233](https://github.com/org-roam/org-roam/issues/1233) fixes bug where descriptive file links become plain links during update for relative paths - [#1233](https://github.com/org-roam/org-roam/issues/1233) fixes bug where descriptive file links become plain links during update for relative paths
- [#1252](https://github.com/org-roam/org-roam/issues/1252) respect original link type during automatic replacement - [#1252](https://github.com/org-roam/org-roam/issues/1252) respect original link type during automatic replacement
## 1.2.2 (06-10-2020) ## 1.2.2 (2020-10-06)
In this release we support fuzzy links of the form `[[roam:Title]]`, `[[roam:*Headline]]` and `[[roam:Title*Headline]]`. Completion for these fuzzy links is supported via `completion-at-point`. In this release we support fuzzy links of the form `[[roam:Title]]`, `[[roam:*Headline]]` and `[[roam:Title*Headline]]`. Completion for these fuzzy links is supported via `completion-at-point`.
@ -125,7 +297,7 @@ This change requires you to set `org-roam-directory` to the resolved path of a f
- [#1057](https://github.com/org-roam/org-roam/pull/1057) Improve performance of database builds by preventing generation of LaTeX and image previews. - [#1057](https://github.com/org-roam/org-roam/pull/1057) Improve performance of database builds by preventing generation of LaTeX and image previews.
- [#1103](https://github.com/org-roam/org-roam/pull/1103) Fix long build-times for Windows on files with URL links - [#1103](https://github.com/org-roam/org-roam/pull/1103) Fix long build-times for Windows on files with URL links
## 1.2.1 (27-07-2020) ## 1.2.1 (2020-07-27)
This release consisted of a big deal of refactoring and bug fixes. Notably, we fixed several catastrophic failures on db builds with bad setups (#854), and modularized tag and title extractions. This release consisted of a big deal of refactoring and bug fixes. Notably, we fixed several catastrophic failures on db builds with bad setups (#854), and modularized tag and title extractions.
@ -158,7 +330,7 @@ We also added some new features that had been a long time coming:
- [#894](https://github.com/org-roam/org-roam/pull/894) Perform link fixes on all Org-roam files - [#894](https://github.com/org-roam/org-roam/pull/894) Perform link fixes on all Org-roam files
- [#952](https://github.com/org-roam/org-roam/pull/952) Cache `${foo}` template variables so they do not need to be re-entered twice - [#952](https://github.com/org-roam/org-roam/pull/952) Cache `${foo}` template variables so they do not need to be re-entered twice
## 1.2.0 (12-06-2020) ## 1.2.0 (2020-06-12)
In this release, we improved the linking process by achieving feature parity between links to files and links to headlines. Before, we used the `file:foo::*bar` format to link to the headline `bar` in file `foo`, but this was prone to breakage upon renaming the file or modifying the headline. This is not the case anymore. Now, we use `org-id` to create IDs for those headlines, which are then stored in our database to compute the relationships and jump around. Note that this will work even if youre not using `org-id` in your global configuration for Org-mode. In this release, we improved the linking process by achieving feature parity between links to files and links to headlines. Before, we used the `file:foo::*bar` format to link to the headline `bar` in file `foo`, but this was prone to breakage upon renaming the file or modifying the headline. This is not the case anymore. Now, we use `org-id` to create IDs for those headlines, which are then stored in our database to compute the relationships and jump around. Note that this will work even if youre not using `org-id` in your global configuration for Org-mode.
@ -186,7 +358,7 @@ We also add `org-roam-unlinked-references`, which naively finds text that could
- [#759](https://github.com/org-roam/org-roam/pull/759), [#760](https://github.com/org-roam/org-roam/pull/760) Tags are only added to the tags table if there are actually tags present - [#759](https://github.com/org-roam/org-roam/pull/759), [#760](https://github.com/org-roam/org-roam/pull/760) Tags are only added to the tags table if there are actually tags present
- [#700](https://github.com/org-roam/org-roam/pull/700), [#733](https://github.com/org-roam/org-roam/pull/733) Allow symlinks within the `org-roam` directory - [#700](https://github.com/org-roam/org-roam/pull/700), [#733](https://github.com/org-roam/org-roam/pull/733) Allow symlinks within the `org-roam` directory
## 1.1.1 (18-05-2020) ## 1.1.1 (2020-05-18)
In this release, we added two new features: In this release, we added two new features:
@ -226,7 +398,7 @@ As usual, this release comes with a multitude of bug-fixes and refactorings.
- [#606](https://github.com/org-roam/org-roam/pull/606) Added `org-roam-dev.el` for developer coding standards - [#606](https://github.com/org-roam/org-roam/pull/606) Added `org-roam-dev.el` for developer coding standards
- [#622](https://github.com/org-roam/org-roam/pull/622) Online documentation now points to the Org-based documentation - [#622](https://github.com/org-roam/org-roam/pull/622) Online documentation now points to the Org-based documentation
## 1.1.0 (21-04-2020) ## 1.1.0 (2020-04-21)
To the average user, this release is mainly a bugfix release with additional options to customize. However, the changes made to the source is significant. Most notably, in this release: To the average user, this release is mainly a bugfix release with additional options to customize. However, the changes made to the source is significant. Most notably, in this release:
@ -264,7 +436,7 @@ In the coming months, you can expect work on bigger projects (e.g. revamping the
- [#363](https://github.com/org-roam/org-roam/pull/363), [#473](https://github.com/org-roam/org-roam/pull/473) Modularize org-roam features. - [#363](https://github.com/org-roam/org-roam/pull/363), [#473](https://github.com/org-roam/org-roam/pull/473) Modularize org-roam features.
- [#497](https://github.com/org-roam/org-roam/pull/497) Simplify `org-roam--list-files` implementation - [#497](https://github.com/org-roam/org-roam/pull/497) Simplify `org-roam--list-files` implementation
## 1.0.0 (23-03-2020) ## 1.0.0 (2020-03-23)
Org-roam is now on MELPA! We have squashed most of the bugs, and Org-roam has Org-roam is now on MELPA! We have squashed most of the bugs, and Org-roam has
been stable for the most part. been stable for the most part.
@ -283,7 +455,7 @@ been stable for the most part.
- [#293](https://github.com/org-roam/org-roam/pull/293) Fix capture templates not working as expected for `org-roam-find-file` - [#293](https://github.com/org-roam/org-roam/pull/293) Fix capture templates not working as expected for `org-roam-find-file`
- [#275](https://github.com/org-roam/org-roam/pull/275) Fix database rebuild when `org-roam-directory` is set locally - [#275](https://github.com/org-roam/org-roam/pull/275) Fix database rebuild when `org-roam-directory` is set locally
## 1.0.0-rc1 (06-03-2020) ## 1.0.0-rc1 (2020-03-06)
This is a pre-release before the push to MELPA. It contains large This is a pre-release before the push to MELPA. It contains large
internal changes, with little user-facing changes. Most notably, the internal changes, with little user-facing changes. Most notably, the

View File

@ -39,7 +39,7 @@ detailed information, please read the [manual][docs].
### Using `package.el` ### Using `package.el`
<details> <details>
<summary>Toggle instuctions</summary> <summary>Toggle instructions</summary>
You can install `org-roam` from [MELPA](https://melpa.org/) or [MELPA You can install `org-roam` from [MELPA](https://melpa.org/) or [MELPA
Stable](https://stable.melpa.org/) using `package.el`: Stable](https://stable.melpa.org/) using `package.el`:
@ -47,35 +47,11 @@ Stable](https://stable.melpa.org/) using `package.el`:
``` ```
M-x package-install RET org-roam RET M-x package-install RET org-roam RET
``` ```
Here's a very basic sample for configuration of `org-roam` using `use-package`:
```emacs-lisp
(use-package org-roam
:ensure t
:custom
(org-roam-directory (file-truename "/path/to/org-files/"))
:bind (("C-c n l" . org-roam-buffer-toggle)
("C-c n f" . org-roam-node-find)
("C-c n g" . org-roam-graph)
("C-c n i" . org-roam-node-insert)
("C-c n c" . org-roam-capture)
;; Dailies
("C-c n j" . org-roam-dailies-capture-today))
:config
(org-roam-db-autosync-mode)
;; If using org-roam-protocol
(require 'org-roam-protocol))
```
Note that the `file-truename` function is only necessary when you use symbolic
link to `org-roam-directory`. Org-roam won't automatically resolve symbolic link
to the directory.
</details> </details>
### Using `straight.el` ### Using `straight.el`
<details> <details>
<summary>Toggle instuctions</summary> <summary>Toggle instructions</summary>
Installation from MELPA or MELPA Stable using `straight.el`: Installation from MELPA or MELPA Stable using `straight.el`:
@ -115,7 +91,7 @@ next sample will get you there:
### Using Doom Emacs ### Using Doom Emacs
<details> <details>
<summary>Toggle instuctions</summary> <summary>Toggle instructions</summary>
Doom's `:lang org` module comes with support for `org-roam`, but it's not Doom's `:lang org` module comes with support for `org-roam`, but it's not
enabled by default. To activate it pass `+roam2` flag to `org` module in your enabled by default. To activate it pass `+roam2` flag to `org` module in your
@ -177,9 +153,8 @@ dependencies. These include:
- dash - dash
- f - f
- s - s
- org (9.4 is the minimal required version!) - org (9.6 is the minimum required version!)
- emacsql - emacsql
- emacsql-sqlite
- magit-section - magit-section
- filenotify-recursive - filenotify-recursive
@ -198,6 +173,33 @@ Org-roam also comes with `.texi` files to integrate with Emacs' built-in Info
system. Read the manual to find more details for how to install them manually. system. Read the manual to find more details for how to install them manually.
</details> </details>
## Configuration
Here's a very basic sample for configuration of `org-roam` using `use-package`:
```emacs-lisp
(use-package org-roam
:ensure t
:custom
(org-roam-directory (file-truename "/path/to/org-files/"))
:bind (("C-c n l" . org-roam-buffer-toggle)
("C-c n f" . org-roam-node-find)
("C-c n g" . org-roam-graph)
("C-c n i" . org-roam-node-insert)
("C-c n c" . org-roam-capture)
;; Dailies
("C-c n j" . org-roam-dailies-capture-today))
:config
;; If you're using a vertical completion framework, you might want a more informative completion interface
(setq org-roam-node-display-template (concat "${title:*} " (propertize "${tags:10}" 'face 'org-tag)))
(org-roam-db-autosync-mode)
;; If using org-roam-protocol
(require 'org-roam-protocol))
```
Note that the `file-truename` function is only necessary when you use symbolic
link to `org-roam-directory`. Org-roam won't automatically resolve symbolic link
to the directory.
## Getting Started ## Getting Started
[David Wilson](https://github.com/daviwil) of [System [David Wilson](https://github.com/daviwil) of [System
@ -225,8 +227,9 @@ it has not already been addressed on [GitHub][issues] or on
- [Jethro Kuan](https://braindump.jethro.dev/) - [Jethro Kuan](https://braindump.jethro.dev/)
([Source](https://github.com/jethrokuan/braindump/tree/master/org)) ([Source](https://github.com/jethrokuan/braindump/tree/master/org))
- [Alexey Shmalko](https://braindump.rasen.dev/) - [Alexey Shmalko](https://www.alexeyshmalko.com/)
- [Sidharth Arya](https://sidhartharya.github.io/braindump/index.html) - [Sidharth Arya](https://sidhartharya.github.io/braindump/index.html)
- [Martin Edström](https://edstrom.dev/)
## Contributing ## Contributing
@ -249,6 +252,6 @@ General Public License, Version 3.
[release]: https://github.com/org-roam/org-roam/releases [release]: https://github.com/org-roam/org-roam/releases
[docs]: https://www.orgroam.com/manual.html [docs]: https://www.orgroam.com/manual.html
[discourse]: https://org-roam.discourse.group/ [discourse]: https://org-roam.discourse.group/
[slack]: https://join.slack.com/t/orgroam/shared_invite/zt-deoqamys-043YQ~s5Tay3iJ5QRI~Lxg [slack]: https://join.slack.com/t/orgroam/shared_invite/zt-wuoize1z-x3UyQnQ0WHF0RhuEQ2NLnQ
[issues]: https://github.com/org-roam/org-roam/issues [issues]: https://github.com/org-roam/org-roam/issues
[faq]: https://www.orgroam.com/manual.html#FAQ [faq]: https://www.orgroam.com/manual.html#FAQ

View File

@ -95,7 +95,7 @@
<ul> <ul>
<li>Read our documentation within Emacs, or on the <a href="https://www.orgroam.com/manual.html">Online Manual</a></li> <li>Read our documentation within Emacs, or on the <a href="https://www.orgroam.com/manual.html">Online Manual</a></li>
<li>Participate in our forum discussions on <a href="https://org-roam.discourse.group">Discourse</a></li> <li>Participate in our forum discussions on <a href="https://org-roam.discourse.group">Discourse</a></li>
<li>Chat with us on <a href="https://join.slack.com/t/orgroam/shared_invite/zt-deoqamys-043YQ~s5Tay3iJ5QRI~Lxg">Slack</a></li> <li>Chat with us on <a href="https://join.slack.com/t/orgroam/shared_invite/zt-wuoize1z-x3UyQnQ0WHF0RhuEQ2NLnQ">Slack</a></li>
</ul> </ul>
</div> </div>
</section> </section>
@ -151,7 +151,7 @@
<div class="row"> <div class="row">
<a <a
class="content footer-links" class="content footer-links"
href="https://join.slack.com/t/orgroam/shared_invite/zt-deoqamys-043YQ~s5Tay3iJ5QRI~Lxg" href="https://join.slack.com/t/orgroam/shared_invite/zt-wuoize1z-x3UyQnQ0WHF0RhuEQ2NLnQ"
>Slack</a >Slack</a
> >
</div> </div>

View File

@ -1,23 +1,23 @@
#+title: Org-roam User Manual #+title: Org-roam User Manual
#+author: Jethro Kuan #+author: Jethro Kuan
#+email: jethrokuan95@gmail.com #+email: jethrokuan95@gmail.com
#+date: 2020-2021 #+date: 2020-2025
#+language: en #+language: en
#+texinfo_deffn: t #+texinfo_deffn: t
#+texinfo_dir_category: Emacs #+texinfo_dir_category: Emacs
#+texinfo_dir_title: Org-roam: (org-roam). #+texinfo_dir_title: Org-roam: (org-roam).
#+texinfo_dir_desc: Roam Research for Emacs. #+texinfo_dir_desc: Roam Research for Emacs.
#+subtitle: for version 2.1.0 #+subtitle: for version 2.3.0
#+options: H:4 num:3 toc:nil creator:t ':t #+options: H:4 num:3 toc:nil creator:t ':t
#+property: header-args :eval never #+property: header-args :eval never
#+texinfo: @noindent #+texinfo: @noindent
This manual is for Org-roam version 2.1.0. This manual is for Org-roam version 2.3.0.
#+BEGIN_QUOTE #+BEGIN_QUOTE
Copyright (C) 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> Copyright (C) 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
You can redistribute this document and/or modify it under the terms of the GNU You can redistribute this document and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either General Public License as published by the Free Software Foundation, either
@ -134,7 +134,7 @@ A slip-box requires a method for quickly capturing ideas. These are called
*fleeting notes*: they are simple reminders of information or ideas that will *fleeting notes*: they are simple reminders of information or ideas that will
need to be processed later on, or trashed. This is typically accomplished using need to be processed later on, or trashed. This is typically accomplished using
~org-capture~ (see info:org#Capture), or using Org-roam's daily notes ~org-capture~ (see info:org#Capture), or using Org-roam's daily notes
functionality (see [[*Org-roam Dailies][Org-roam Dailies]]). This provides a central inbox for collecting functionality (see [[*org-roam-dailies][org-roam-dailies]]). This provides a central inbox for collecting
thoughts, to be processed later into permanent notes. thoughts, to be processed later into permanent notes.
*Permanent notes* *Permanent notes*
@ -178,18 +178,7 @@ archives to =package-archives=:
#+END_SRC #+END_SRC
Org-roam also depends on a recent version of Org, which can be obtained in Org's Org-roam also depends on a recent version of Org, which can be obtained in Org's
package repository (see info:org#Installation). To use Org's ELPA archive: package repository (see info:org#Installation).
#+BEGIN_SRC emacs-lisp
(add-to-list 'package-archives '("org" . "https://orgmode.org/elpa/") t)
#+END_SRC
Once you have added your preferred archive, you need to update the
local package list using:
#+BEGIN_EXAMPLE
M-x package-refresh-contents RET
#+END_EXAMPLE
Once you have done that, you can install Org-roam and its dependencies Once you have done that, you can install Org-roam and its dependencies
using: using:
@ -235,7 +224,6 @@ dependencies that it requires. These include:
- s - s
- org - org
- emacsql - emacsql
- emacsql-sqlite
- magit-section - magit-section
You can install this manually as well, or get the latest version from MELPA. You You can install this manually as well, or get the latest version from MELPA. You
@ -279,44 +267,6 @@ file:
install-info /path/to/my/info/files/org-roam.info /path/to/my/info/files/dir install-info /path/to/my/info/files/org-roam.info /path/to/my/info/files/dir
#+end_src #+end_src
** Installation Troubleshooting
*** C Compiler
Org-roam relies on an Emacs package called ~emacsql~ and ~emacsql-sqlite~ to
work with the ~sqlite~ database. Both of them should be installed automatically
in your Emacs environment as a prerequisite for Org-roam when you install it.
~emacsql-sqlite~ requires a C compiler (e.g. ~gcc~ or ~clang~) to be present in
your computer. How to install a C compiler depends on the OS that you use.
- For Windows:
There are various ways to install one, depending on how you have installed
Emacs. If you use Emacs within a Cygwin or MinGW environment, then you should
install a compiler using their respective package manager.
If you have installed your Emacs from the [[https://www.gnu.org/software/emacs/][GNU Emacs website]], then the easiest way
is to use [[https://www.msys2.org/][MSYS2]] as at the time of this writing:
1. Use the installer in the official website and install MSYS2
2. Run MSYS2
3. In the command-line tool, type the following and answer "Y" to proceed:
#+BEGIN_SRC bash
pacman -S gcc
#+END_SRC
Note that you do not need to manually set the PATH for MSYS2; the
installer automatically takes care of it for you.
4. Open Emacs and call ~M-x org-roam-db-autosync-mode~
This will automatically start compiling ~emacsql-sqlite~; you should see a
message in minibuffer. It may take a while until compilation completes. Once
complete, you should see a new file ~emacsql-sqlite.exe~ created in a subfolder
named ~sqlite~ under ~emacsql-sqlite~ installation folder. It's typically in
your Emacs configuration folder like this:
~/.config/emacs/elpa/emacsql-sqlite-20190727.1710/sqlite~
* Getting Started * Getting Started
** The Org-roam Node ** The Org-roam Node
@ -373,7 +323,12 @@ For this tutorial, create an empty directory, and set ~org-roam-directory~:
#+END_SRC #+END_SRC
The ~file-truename~ function is only necessary when you use symbolic links The ~file-truename~ function is only necessary when you use symbolic links
inside ~org-roam-directory~: Org-roam does not resolve symbolic links. inside ~org-roam-directory~: Org-roam does not resolve symbolic links. One can
however instruct Emacs to always resolve symlinks, at a performance cost:
#+begin_src emacs-lisp
(setq find-file-visit-truename t)
#+end_src
Next, we setup Org-roam to run functions on file changes to maintain cache Next, we setup Org-roam to run functions on file changes to maintain cache
consistency. This is achieved by running ~M-x org-roam-db-autosync-mode~. To consistency. This is achieved by running ~M-x org-roam-db-autosync-mode~. To
@ -417,7 +372,59 @@ brought through the node creation process.
One can also conveniently insert links via the completion-at-point functions One can also conveniently insert links via the completion-at-point functions
Org-roam provides (see [[*Completion][Completion]]). Org-roam provides (see [[*Completion][Completion]]).
** Customizing Node Completions
Node selection is achieved via the ~completing-read~ interface, typically
through ~org-roam-node-read~. The presentation of these nodes are governed by
~org-roam-node-display-template~.
- Variable: org-roam-node-display-template
Configures display formatting for Org-roam node.
Patterns of form "${field-name:length}" are interpolated based
on the current node.
Each "field-name" is replaced with the return value of each
corresponding accessor function for org-roam-node, e.g.
"${title}" will be interpolated by the result of
org-roam-node-title. You can also define custom accessors using
cl-defmethod. For example, you can define:
(cl-defmethod org-roam-node-my-title ((node org-roam-node))
(concat "My " (org-roam-node-title node)))
and then reference it here or in the capture templates as
"${my-title}".
"length" is an optional specifier and declares how many
characters can be used to display the value of the corresponding
field. If it's not specified, the field will be inserted as is,
i.e. it won't be aligned nor trimmed. If it's an integer, the
field will be aligned accordingly and all the exceeding
characters will be trimmed out. If it's "*", the field will use
as many characters as possible and will be aligned accordingly.
A closure can also be assigned to this variable in which case the
closure is evaluated and the return value is used as the
template. The closure must evaluate to a valid template string.
If you're using a vertical completion framework, such as Ivy and Selectrum,
Org-roam supports the generation of an aligned, tabular completion interface.
For example, to include a column for tags up to 10 character widths wide, one
can set ~org-roam-node-display-template~ as such:
#+begin_src emacs-lisp
(setq org-roam-node-display-template
(concat "${title:*} "
(propertize "${tags:10}" 'face 'org-tag)))
#+end_src
* Customizing Node Caching * Customizing Node Caching
** How to cache
Org-roam uses a SQLite database to perform caching. This integration is
managed by the [[https://github.com/magit/emacsql][emacsql]] library. It should "just work".
** What to cache ** What to cache
By default, all nodes (any headline or file with an ID) are cached by Org-roam. By default, all nodes (any headline or file with an ID) are cached by Org-roam.
@ -438,12 +445,41 @@ property to a non-nil value. For example:
One can also set ~org-roam-db-node-include-function~. For example, to exclude One can also set ~org-roam-db-node-include-function~. For example, to exclude
all headlines with the ~ATTACH~ tag from the Org-roam database, one can set: all headlines with the ~ATTACH~ tag from the Org-roam database, one can set:
#+begin_src org #+begin_src emacs-lisp
(setq org-roam-db-node-include-function (setq org-roam-db-node-include-function
(lambda () (lambda ()
(not (member "ATTACH" (org-get-tags))))) (not (member "ATTACH" (org-get-tags)))))
#+end_src #+end_src
Org-roam relied on the obtained Org AST for the buffer to parse links. However,
links appearing in some places (e.g. within property drawers) are not considered
by the Org AST to be links. Therefore, Org-roam takes special care of
additionally trying to process these links. Use
~org-roam-db-extra-links-elements~ to specify which additional Org AST element
types to consider.
- Variable: org-roam-db-extra-links-elements
The list of Org element types to include for parsing by Org-roam.
By default, when parsing Org's AST, links within keywords and
property drawers are not parsed as links. Sometimes however, it
is desirable to parse and cache these links (e.g. hiding links in
a property drawer).
Additionally, one may want to ignore certain keys from being excluded within
property drawers. For example, we would not want ~ROAM_REFS~ links to be
self-referential. Hence, to exclude specific keys, we use
~org-roam-db-extra-links-exclude-keys~.
- Variable: org-roam-db-extra-links-exclude-keys
Keys to ignore when mapping over links.
The car of the association list is the Org element type (e.g. keyword). The
cdr is a list of case-insensitive strings to exclude from being treated as
links.
** When to cache ** When to cache
By default, Org-roam is eager in caching: each time an Org-roam file is modified By default, Org-roam is eager in caching: each time an Org-roam file is modified
@ -510,10 +546,10 @@ There are currently 3 provided widget types:
- Unlinked references :: View nodes that contain text that match the nodes - Unlinked references :: View nodes that contain text that match the nodes
title/alias but are not linked title/alias but are not linked
To configure what sections are displayed in the buffer, set ~org-roam-mode-section-functions~. To configure what sections are displayed in the buffer, set ~org-roam-mode-sections~.
#+begin_src emacs-lisp #+begin_src emacs-lisp
(setq org-roam-mode-section-functions (setq org-roam-mode-sections
(list #'org-roam-backlinks-section (list #'org-roam-backlinks-section
#'org-roam-reflinks-section #'org-roam-reflinks-section
;; #'org-roam-unlinked-references-section ;; #'org-roam-unlinked-references-section
@ -522,6 +558,29 @@ To configure what sections are displayed in the buffer, set ~org-roam-mode-secti
Note that computing unlinked references may be slow, and has not been added in by default. Note that computing unlinked references may be slow, and has not been added in by default.
For each section function, you can pass args along to modify its behaviour. For
example, if you want to render unique sources for backlinks (and also keep
rendering reference links), set ~org-roam-mode-sections~ as follows:
#+begin_src emacs-lisp
(setq org-roam-mode-sections
'((org-roam-backlinks-section :unique t)
org-roam-reflinks-section))
#+end_src
The backlinks section ~org-roam-backlinks-section~ also supports a
predicate to filter backlinks, ~:show-backlink-p~. This can be used
as follows:
#+begin_src emacs-lisp
(defun my-org-roam-show-backlink-p (backlink)
(not (member "daily" (org-roam-node-tags (org-roam-backlink-source-node backlink)))))
(setq org-roam-mode-sections
'((org-roam-backlinks-section :unique t :show-backlink-p my-org-roam-show-backlink-p)
org-roam-reflinks-section))
#+end_src
** Configuring the Org-roam buffer display ** Configuring the Org-roam buffer display
Org-roam does not control how the pop-up buffer is displayed: this is left to Org-roam does not control how the pop-up buffer is displayed: this is left to
@ -629,7 +688,7 @@ With the above example, if another node links to https://www.google.com/, it
will show up as a “reference backlink”. will show up as a “reference backlink”.
These keys also come in useful for when taking website notes, using the These keys also come in useful for when taking website notes, using the
~roam-ref~ protocol (see [[*Org-roam Protocol][Roam Protocol]]). ~roam-ref~ protocol (see [[*org-roam-protocol][org-roam-protocol]]).
You may assign multiple refs to a single node, for example when you want You may assign multiple refs to a single node, for example when you want
multiple papers in a series to share the same note, or an article has a citation multiple papers in a series to share the same note, or an article has a citation
@ -646,6 +705,59 @@ Org-roam also provides some functions to add or remove refs.
Remove a ref from the node at point. Remove a ref from the node at point.
* Citations
Since version 9.5, Org has first-class support for citations. Org-roam supports
the caching of both these in-built citations (of form ~[cite:@key]~) and [[https://github.com/jkitchin/org-ref][org-ref]]
citations (of form cite:key).
Org-roam attempts to load both the ~org-ref~ and ~org-cite~ package when
indexing files, so no further setup from the user is required for citation
support.
** Using the Cached Information
It is common to use take reference notes for academic papers. To designate the
node to be the canonical node for the academic paper, we can use its unique
citation key:
#+begin_src org
,* Probabilistic Robotics
:PROPERTIES:
:ID: 51b7b82c-bbb4-4822-875a-ed548cffda10
:ROAM_REFS: @thrun2005probabilistic
:END:
#+end_src
or
#+begin_src org
,* Probabilistic Robotics
:PROPERTIES:
:ID: 51b7b82c-bbb4-4822-875a-ed548cffda10
:ROAM_REFS: [cite:@thrun2005probabilistic]
:END:
#+end_src
for ~org-cite~, or:
#+begin_src org
,* Probabilistic Robotics
:PROPERTIES:
:ID: 51b7b82c-bbb4-4822-875a-ed548cffda10
:ROAM_REFS: cite:thrun2005probabilistic
:END:
#+end_src
for ~org-ref~.
When another node has a citation for that key, we can see it using the
~Reflinks~ section of the Org-roam buffer.
Extension developers may be interested in retrieving the citations within their
notes. This information can be found within the ~citation~ table of the Org-roam
database.
* Completion * Completion
Completions for Org-roam are provided via ~completion-at-point~. Org-roam Completions for Org-roam are provided via ~completion-at-point~. Org-roam
@ -679,7 +791,7 @@ Similarly, the completion candidates are the titles and aliases for all Org-roam
nodes, and upon choosing a candidate a ~roam:Title~ link will be inserted nodes, and upon choosing a candidate a ~roam:Title~ link will be inserted
linking to the node of choice. linking to the node of choice.
This is disable by default. To enable it, set ~org-roam-completion-everywhere~ This is disabled by default. To enable it, set ~org-roam-completion-everywhere~
to ~t~: to ~t~:
#+begin_src emacs-lisp #+begin_src emacs-lisp
@ -698,7 +810,7 @@ extension in your Org-roam capture templates. For example:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(setq org-roam-capture-templates '(("d" "default" plain "%?" (setq org-roam-capture-templates '(("d" "default" plain "%?"
:if-new (file+head "${slug}.org.gpg" :target (file+head "${slug}.org.gpg"
"#+title: ${title}\n") "#+title: ${title}\n")
:unnarrowed t))) :unnarrowed t)))
#+end_src #+end_src
@ -707,13 +819,95 @@ Note that the Org-roam database stores metadata information in plain-text
(headline text, for example), so if this information is private to you then you (headline text, for example), so if this information is private to you then you
should also ensure the database is encrypted. should also ensure the database is encrypted.
* Org-roam Protocol * The Templating System
Org-roam extends the ~org-capture~ system, providing a smoother note-taking
experience. However, these extensions mean Org-roam capture templates are
incompatible with ~org-capture~ templates.
Org-roam's templates are specified by ~org-roam-capture-templates~. Just like
~org-capture-templates~, ~org-roam-capture-templates~ can contain multiple
templates. If ~org-roam-capture-templates~ only contains one template, there
will be no prompt for template selection.
** Template Walkthrough
To demonstrate the additions made to org-capture templates. Here, we explain
the default template, reproduced below. You will find most of the elements
of the template are similar to ~org-capture~ templates.
#+BEGIN_SRC emacs-lisp
(("d" "default" plain "%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n")
:unnarrowed t))
#+END_SRC
1. The template has short key ~"d"~. If you have only one template, org-roam
automatically chooses this template for you.
2. The template is given a description of ~"default"~.
3. ~plain~ text is inserted. Other options include Org headings via
~entry~.
4. Notice that the ~target~ that's usually in Org-capture templates is missing
here.
5. ~"%?"~ is the template inserted on each call to ~org-roam-capture-~.
This template means don't insert any content, but place the cursor here.
6. ~:target~ is a compulsory specification in the Org-roam capture template. The
first element of the list indicates the type of the target, the second
element indicates the location of the captured node, and the rest of the
elements indicate prefilled template that will be inserted and the position
of the point will be adjusted for. The latter behavior varies from type to
type of the capture target.
7. ~:unnarrowed t~ tells org-capture to show the contents for the whole file,
rather than narrowing to just the entry. This is part of the Org-capture
templates.
See the ~org-roam-capture-templates~ documentation for more details and
customization options.
** Org-roam Template Expansion
Org-roam's template definitions also extend org-capture's template syntax, to
allow prefilling of strings. We have seen a glimpse of this in [[*Template Walkthrough][Template
Walkthrough]].
Org-roam provides the ~${foo}~ syntax for substituting variables with known
strings. ~${foo}~'s substitution is performed as follows:
1. If ~foo~ is a function, ~foo~ is called with the current node as its
argument.
2. Else if ~org-roam-node-foo~ is a function, ~foo~ is called with the current node
as its argument. The ~org-roam-node-~ prefix defines many of Org-roam's node
accessors such as ~org-roam-node-title~ and ~org-roam-node-level~.
3. Else look up ~org-roam-capture--info~ for ~foo~. This is an internal variable
that is set before the capture process begins.
4. If none of the above applies, read a string using ~completing-read~.
a. Org-roam also provides the ~${foo=default_val}~ syntax, where if a default
value is provided, will be the initial value for the ~foo~ key during
minibuffer completion.
One can check the list of available keys for nodes by inspecting the
~org-roam-node~ struct. At the time of writing, it is:
#+begin_src emacs-lisp
(cl-defstruct (org-roam-node (:constructor org-roam-node-create)
(:copier nil))
"A heading or top level file with an assigned ID property."
file file-hash file-atime file-mtime
id level point todo priority scheduled deadline title properties olp
tags aliases refs)
#+end_src
This makes ~${file}~, ~${file-hash}~ etc. all valid substitutions.
* Extensions
** org-roam-protocol
Org-roam provides extensions for capturing content from external applications Org-roam provides extensions for capturing content from external applications
such as the browser, via ~org-protocol~. Org-roam extends ~org-protocol~ with 2 such as the browser, via ~org-protocol~. Org-roam extends ~org-protocol~ with 2
protocols: the ~roam-node~ and ~roam-ref~ protocols. protocols: the ~roam-node~ and ~roam-ref~ protocols.
** Installation *** Installation
To enable Org-roam's protocol extensions, simply add the following to your init To enable Org-roam's protocol extensions, simply add the following to your init
file: file:
@ -723,9 +917,18 @@ file:
#+END_SRC #+END_SRC
We also need to set up ~org-protocol~: the instructions for setting up We also need to set up ~org-protocol~: the instructions for setting up
~org-protocol~ are reproduced below. ~org-protocol~ are reproduced here.
*** Linux On a high-level, external calls are passed to Emacs via ~emacsclient~.
~org-protocol~ intercepts these and runs custom actions based on the protocols
registered. Hence, to use ~org-protocol~, once must:
1. launch the ~emacsclient~ process
2. Register ~org-protocol://~ as a valid scheme-handler
The instructions for the latter for each operating system is detailed below.
**** Linux
For Linux users, create a desktop application in For Linux users, create a desktop application in
~~/.local/share/applications/org-protocol.desktop~: ~~/.local/share/applications/org-protocol.desktop~:
@ -766,7 +969,7 @@ make the new policy take effect.
See [[https://www.chromium.org/administrators/linux-quick-start][here]] for more info on the ~/etc/opt/chrome/policies/managed~ directory and See [[https://www.chromium.org/administrators/linux-quick-start][here]] for more info on the ~/etc/opt/chrome/policies/managed~ directory and
[[https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExternalProtocolDialogShowAlwaysOpenCheckbox][here]] for information on the ~ExternalProtocolDialogShowAlwaysOpenCheckbox~ policy. [[https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExternalProtocolDialogShowAlwaysOpenCheckbox][here]] for information on the ~ExternalProtocolDialogShowAlwaysOpenCheckbox~ policy.
*** Mac OS **** Mac OS
For Mac OS, we need to create our own application. For Mac OS, we need to create our own application.
1. Launch Script Editor 1. Launch Script Editor
@ -776,7 +979,7 @@ For Mac OS, we need to create our own application.
on open location this_URL on open location this_URL
set EC to "/usr/local/bin/emacsclient --no-wait " set EC to "/usr/local/bin/emacsclient --no-wait "
set filePath to quoted form of this_URL set filePath to quoted form of this_URL
do shell script EC & filePath do shell script EC & filePath & " &> /dev/null &"
tell application "Emacs" to activate tell application "Emacs" to activate
end open location end open location
#+end_src #+end_src
@ -821,7 +1024,45 @@ defaults write com.apple.LaunchServices/com.apple.launchservices.secure LSHandle
Then restart your computer. Then restart your computer.
*** Windows If you're using the [[https://formulae.brew.sh/formula/emacs][Emacs Homebrew formula]], you may need one of the following additional configurations:
a) Add option `-c` to `emacsclient` in the script, and start emacs from command line with `emacs --daemon`
#+begin_src emacs-lisp
on open location this_URL
set EC to "/usr/local/bin/emacsclient -c --no-wait "
set filePath to quoted form of this_URL
do shell script EC & filePath & " &> /dev/null &"
tell application "Emacs" to activate
end open location
#+end_src
b) Add `(server-start)` in .emacs (in this case you do not need option `-c` for `emacsclient` in the script, and you do not need to start emacs with `emacs --daemon`
***** Testing org-protocol
To test that you have the handler setup and registered properly from the command
line you can run:
#+begin_src bash
open org-protocol://roam-ref\?template=r\&ref=test\&title=this
#+end_src
If you get an error similar too this or the wrong handler is run:
#+begin_quote
No application knows how to open URL org-protocol://roam-ref?template=r&ref=test&title=this (Error Domain=NSOSStatusErrorDomain Code=-10814 "kLSApplicationNotFoundErr: E.g. no application claims the file" UserInfo={_LSLine=1489, _LSFunction=runEvaluator}).
#+end_quote
You may need to manually register your handler, like this:
#+begin_src bash
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -R -f /Applications/OrgProtocolClient.app
#+end_src
Here is a link to the lsregister command that is really useful: https://eclecticlight.co/2019/03/25/lsregister-a-valuable-undocumented-command-for-launchservices/
**** Windows
For Windows, create a temporary ~org-protocol.reg~ file: For Windows, create a temporary ~org-protocol.reg~ file:
#+BEGIN_SRC text #+BEGIN_SRC text
@ -846,13 +1087,13 @@ Windows, replace the last line with:
After executing the .reg file, the protocol is registered and you can delete the After executing the .reg file, the protocol is registered and you can delete the
file. file.
** The roam-node protocol *** The roam-node protocol
The roam-node protocol opens the node with ID specified by the ~node~ key (e.g. The roam-node protocol opens the node with ID specified by the ~node~ key (e.g.
~org-protocol://roam-node?node=node-id~). ~org-roam-graph~ uses this to make the ~org-protocol://roam-node?node=node-id~). ~org-roam-graph~ uses this to make the
graph navigable. graph navigable.
** The roam-ref protocol *** The roam-ref protocol
This protocol finds or creates a new note with a given ~ROAM_REFS~: This protocol finds or creates a new note with a given ~ROAM_REFS~:
@ -880,89 +1121,11 @@ or as a keybinding in ~qutebrowser~ in , using the ~config.py~ file (see
where ~template~ is the template key for a template in where ~template~ is the template key for a template in
~org-roam-capture-ref-templates~ (see [[*The Templating System][The Templating System]]). ~org-roam-capture-ref-templates~ (see [[*The Templating System][The Templating System]]).
* The Templating System ** org-roam-graph
Org-roam extends the ~org-capture~ system, providing a smoother note-taking
experience. However, these extensions mean Org-roam capture templates are
incompatible with ~org-capture~ templates.
Org-roam's templates are specified by ~org-roam-capture-templates~. Just like
~org-capture-templates~, ~org-roam-capture-templates~ can contain multiple
templates. If ~org-roam-capture-templates~ only contains one template, there
will be no prompt for template selection.
** Template Walkthrough
To demonstrate the additions made to org-capture templates. Here, we explain
the default template, reproduced below. You will find some most of the elements
of the template are similar to ~org-capture~ templates.
#+BEGIN_SRC emacs-lisp
(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n")
:unnarrowed t))
#+END_SRC
1. The template has short key ~"d"~. If you have only one template, org-roam
automatically chooses this template for you.
2. The template is given a description of ~"default"~.
3. ~plain~ text is inserted. Other options include Org headings via
~entry~.
4. Notice that the ~target~ that's usually in Org-capture templates is missing
here.
5. ~"%?"~ is the template inserted on each call to ~org-roam-capture-~.
This template means don't insert any content, but place the cursor here.
6. ~:if-new~ is a compulsory specification in the Org-roam capture template.
This indicates the location for the new node.
7. ~:unnarrowed t~ tells org-capture to show the contents for the whole file,
rather than narrowing to just the entry. This is part of the Org-capture
templates.
See the ~org-roam-capture-templates~ documentation for more details and
customization options.
** Org-roam Template Expansion
Org-roam's template definitions also extend org-capture's template syntax, to
allow prefilling of strings. We have seen a glimpse of this in [[*Template Walkthrough][Template
Walkthrough]].
Org-roam provides the ~${foo}~ syntax for substituting variables with known
strings. ~${foo}~'s substitution is performed as follows:
1. If ~foo~ is a function, ~foo~ is called with the current node as its
argument.
2. Else if ~org-roam-node-foo~ is a function, ~foo~ is called with the current node
as its argument. The ~org-roam-node-~ prefix defines many of Org-roam's node
accessors such as ~org-roam-node-title~ and ~org-roam-node-level~.
3. Else look up ~org-roam-capture--info~ for ~foo~. This is an internal variable
that is set before the capture process begins.
4. If none of the above applies, read a string using ~completing-read~.
a. Org-roam also provides the ~${foo=default_val}~ syntax, where if a default
value is provided, will be the initial value for the ~foo~ key during
minibuffer completion.
One can check the list of available keys for nodes by inspecting the
~org-roam-node~ struct. At the time of writing, it is:
#+begin_src emacs-lisp
(cl-defstruct (org-roam-node (:constructor org-roam-node-create)
(:copier nil))
"A heading or top level file with an assigned ID property."
file file-hash file-atime file-mtime
id level point todo priority scheduled deadline title properties olp
tags aliases refs)
#+end_src
This makes ~${file}~, ~${file-hash}~ etc. all valid substitutions.
* Graphing
Org-roam provides basic graphing capabilities to explore interconnections Org-roam provides basic graphing capabilities to explore interconnections
between notes, in ~org-roam-graph~. This is done by performing SQL queries and between notes, in ~org-roam-graph~. This is done by performing SQL queries and
generating images using [[https://graphviz.org/][Graphviz]]. The graph can also be navigated: see [[*Org-roam Protocol][Roam generating images using [[https://graphviz.org/][Graphviz]]. The graph can also be navigated: see [[*org-roam-protocol][org-roam-protocol]].
Protocol]].
The entry point to graph creation is ~org-roam-graph~. The entry point to graph creation is ~org-roam-graph~.
@ -1003,7 +1166,7 @@ ARG may be any of the following values:
(org-roam-graph--open (concat "file://///wsl$/Ubuntu" file))))) (org-roam-graph--open (concat "file://///wsl$/Ubuntu" file)))))
#+END_SRC #+END_SRC
** Graph Options *** Graph Options
Graphviz provides many options for customizing the graph output, and Org-roam Graphviz provides many options for customizing the graph output, and Org-roam
supports some of them. See https://graphviz.gitlab.io/_pages/doc/info/attrs.html supports some of them. See https://graphviz.gitlab.io/_pages/doc/info/attrs.html
@ -1029,12 +1192,12 @@ for customizable options.
Extra options for edges in the graphviz output (The "E" attributes). Extra options for edges in the graphviz output (The "E" attributes).
Example: ~'(("dir" . "back"))~ Example: ~'(("dir" . "back"))~
* Org-roam Dailies ** org-roam-dailies
Org-roam provides journaling capabilities akin to Org-roam provides journaling capabilities akin to
Org-journal with ~org-roam-dailies~. Org-journal with ~org-roam-dailies~.
** Configuration *** Configuration
For ~org-roam-dailies~ to work, you need to define two variables: For ~org-roam-dailies~ to work, you need to define two variables:
@ -1054,13 +1217,13 @@ Here is a sane default configuration:
(setq org-roam-dailies-capture-templates (setq org-roam-dailies-capture-templates
'(("d" "default" entry '(("d" "default" entry
"* %?" "* %?"
:if-new (file+head "%<%Y-%m-%d>.org" :target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n")))) "#+title: %<%Y-%m-%d>\n"))))
#+end_src #+end_src
See [[*The Templating System][The Templating System]] for creating new templates. See [[*The Templating System][The Templating System]] for creating new templates.
** Usage *** Usage
~org-roam-dailies~ provides these interactive functions: ~org-roam-dailies~ provides these interactive functions:
@ -1078,7 +1241,7 @@ There are variants of those commands for ~-yesterday~ and ~-tomorrow~:
- Function: ~org-roam-dailies-capture-yesterday~ n &optional goto - Function: ~org-roam-dailies-capture-yesterday~ n &optional goto
Create an entry in the daily note for yesteday. Create an entry in the daily note for yesterday.
With numeric argument ~n~, use the daily note ~n~ days in the past. With numeric argument ~n~, use the daily note ~n~ days in the past.
@ -1114,6 +1277,18 @@ There are also commands which allow you to use Emacss ~calendar~ to find the
- Function: ~org-roam-dailies-goto-next-note~ - Function: ~org-roam-dailies-goto-next-note~
When in an daily-note, find the next one. When in an daily-note, find the next one.
** org-roam-export
Because Org-roam files are plain org files, they can be exported easily using
~org-export~ to a variety of formats, including ~html~ and ~pdf~. However,
Org-roam relies heavily on ID links, which Org's html export has poor support
of. To fix this, Org-roam provides a bunch of overrides to better support
export. To use them, simply run:
#+begin_src emacs-lisp
(require 'org-roam-export)
#+end_src
* Performance Optimization * Performance Optimization
** Garbage Collection ** Garbage Collection
@ -1187,7 +1362,7 @@ The Deft interface can slow down quickly when the number of files get huge.
[[https://github.com/bastibe/org-journal][Org-journal]] provides journaling capabilities to Org-mode. A lot of its [[https://github.com/bastibe/org-journal][Org-journal]] provides journaling capabilities to Org-mode. A lot of its
functionalities have been incorporated into Org-roam under the name functionalities have been incorporated into Org-roam under the name
[[*Org-roam Dailies][~org-roam-dailies~]]. It remains a good tool if you want to isolate your verbose [[*org-roam-dailies][~org-roam-dailies~]]. It remains a good tool if you want to isolate your verbose
journal entries from the ideas you would write on a scratchpad. journal entries from the ideas you would write on a scratchpad.
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
@ -1240,6 +1415,9 @@ documents (PDF, EPUB etc.) within Org-mode.
** Bibliography ** Bibliography
Org 9.5 added native citation and bibliography functionality, called "org-cite",
which org-roam supports.
[[https://github.com/org-roam/org-roam-bibtex][org-roam-bibtex]] offers tight integration between [[https://github.com/jkitchin/org-ref][org-ref]], [[https://github.com/tmalsburg/helm-bibtex][helm-bibtex]] and [[https://github.com/org-roam/org-roam-bibtex][org-roam-bibtex]] offers tight integration between [[https://github.com/jkitchin/org-ref][org-ref]], [[https://github.com/tmalsburg/helm-bibtex][helm-bibtex]] and
~org-roam~. This helps you manage your bibliographic notes under ~org-roam~. ~org-roam~. This helps you manage your bibliographic notes under ~org-roam~.
@ -1267,20 +1445,27 @@ variable using directory-local variables. This is what ~.dir-locals.el~ may
contain: contain:
#+BEGIN_SRC emacs-lisp #+BEGIN_SRC emacs-lisp
((nil . ((org-roam-directory . (expand-file-name ".")) ((nil . ((org-roam-directory . "/path/to/alt/org-roam-dir")
(org-roam-db-location . (expand-file-name "./org-roam.db"))))) (org-roam-db-location . "/path/to/alt/org-roam-dir/org-roam.db"))))
#+END_SRC
Note ~org-roam-directory~ and ~org-roam-db-location~ should be an absolute path, not relative.
Alternatively, use ~eval~ if you wish to call functions:
#+BEGIN_SRC emacs-lisp
((nil . ((eval . (setq-local
org-roam-directory (expand-file-name (locate-dominating-file
default-directory ".dir-locals.el"))))
(eval . (setq-local
org-roam-db-location (expand-file-name "org-roam.db"
org-roam-directory))))))
#+END_SRC #+END_SRC
All files within that directory will be treated as their own separate set of All files within that directory will be treated as their own separate set of
Org-roam files. Remember to run ~org-roam-db-sync~ from a file within Org-roam files. Remember to run ~org-roam-db-sync~ from a file within
that directory, at least once. that directory, at least once.
** How do I migrate from Roam Research?
Fabio has produced a command-line tool that converts markdown files exported
from Roam Research into Org-roam compatible markdown. More instructions are
provided [[https://github.com/fabioberger/roam-migration][in the repository]].
** How do I create a note whose title already matches one of the candidates? ** How do I create a note whose title already matches one of the candidates?
This situation arises when, for example, one would like to create a note titled This situation arises when, for example, one would like to create a note titled
@ -1301,28 +1486,110 @@ you don't want them to be (e.g. when tangling an Org file), check the value you
have set for ~org-id-link-to-org-use-id~: setting it to ~'create-if-interactive~ have set for ~org-id-link-to-org-use-id~: setting it to ~'create-if-interactive~
is a popular option. is a popular option.
* Migrating from Org-roam v1 ** How do I migrate from Roam Research?
Fabio has produced a command-line tool that converts markdown files exported
from Roam Research into Org-roam compatible markdown. More instructions are
provided [[https://github.com/fabioberger/roam-migration][in the repository]].
** How to migrate from Org-roam v1?
Those coming from Org-roam v1 will do well treating v2 as entirely new software. Those coming from Org-roam v1 will do well treating v2 as entirely new software.
V2 has a smaller core and fewer moving parts, while retaining the bulk of its V2 has a smaller core and fewer moving parts, while retaining the bulk of its
functionality. It is recommended to read the documentation above about nodes. functionality. It is recommended to read the documentation above about nodes.
It is still desirable to migrate notes collected in v1 to v2. To migrate your v1 It is still desirable to migrate notes collected in v1 to v2.
notes to v2, you may use the migration script provided in [[https://gist.github.com/jethrokuan/02f41028fb4a6f81787dc420fb99b6e4][this gist]], or [[https://gist.github.com/jethrokuan/02f41028fb4a6f81787dc420fb99b6e4#gistcomment-3737019][this To migrate your v1 notes to v2, use =M-x org-roam-migrate-wizard=.
gist]], the latter being better tested. [[https://d12frosted.io/posts/2021-06-11-path-to-org-roam-v2.html][This blog post]] provides a good overview of [[https://d12frosted.io/posts/2021-06-11-path-to-org-roam-v2.html][This blog post]]
what's new in v2 and how to migrate. provides a good overview of what's new in v2 and how to migrate.
Simply put, to migrate notes from v1 to v2, one must: Essentially, to migrate notes from v1 to v2, one must:
1. Add IDs to all existing notes. These are located in top-level property 1. Add IDs to all existing notes.
drawers (Although note that in v2, not all files need to have IDs) These are located in top-level property drawers
(Although note that in v2, not all files need to have IDs).
2. Update the Org-roam database to conform to the new schema. 2. Update the Org-roam database to conform to the new schema.
3. Replace ~#+ROAM_KEY~ into the ~ROAM_REFS~ property 3. Replace ~#+ROAM_KEY~ into the ~ROAM_REFS~ property
4. Replace ~#+ROAM_ALIAS~ into the ~ROAM_ALIASES~ property 4. Replace ~#+ROAM_ALIAS~ into the ~ROAM_ALIASES~ property
5. Move ~#+ROAM_TAGS~ into the ~#+FILETAGS~ property for file-level nodes, and 5. Move ~#+ROAM_TAGS~ into the ~#+FILETAGS~ property for file-level nodes,
the ~ROAM_TAGS~ property for headline nodes and the ~ROAM_TAGS~ property for headline nodes
6. Replace existing file links with ID links. 6. Replace existing file links with ID links.
** How do I publish my notes with an Internet-friendly graph?
The default graph builder creates a graph with an [[https://orgmode.org/worg/org-contrib/org-protocol.html][org-protocol]]
handler which is convenient when you're working locally but
inconvenient when you want to publish your notes for remote access.
Likewise, it defaults to displaying the graph in Emacs which has the
exact same caveats. This problem is solvable in the following way
using org-mode's native [[https://orgmode.org/manual/Publishing.html][publishing]] capability:
1. configure org-mode to publish your org-roam notes as a project.
2. create a function that overrides the default org-protocol link
creation function(=org-roam-default-link-builder=).
3. create a hook that's called at the end of graph creation to copy
the generated graph to the appropriate place.
The example code below is used to publish to a local directory where a
separate shell script copies the files to the remote site.
*** Configure org-mode for publishing
This has two steps:
1. Setting of a /roam/ project that publishes your notes.
2. Configuring the /sitemap.html/ generation.
3. Setting up =org-publish= to generate the graph.
This will require code like the following:
#+begin_src emacs-lisp
(defun roam-sitemap (title list)
(concat "#+OPTIONS: ^:nil author:nil html-postamble:nil\n"
"#+SETUPFILE: ./simple_inline.theme\n"
"#+TITLE: " title "\n\n"
(org-list-to-org list) "\nfile:sitemap.svg"))
(setq my-publish-time 0) ; see the next section for context
(defun roam-publication-wrapper (plist filename pubdir)
(org-roam-graph)
(org-html-publish-to-html plist filename pubdir)
(setq my-publish-time (cadr (current-time))))
(setq org-publish-project-alist
'(("roam"
:base-directory "~/roam"
:auto-sitemap t
:sitemap-function roam-sitemap
:sitemap-title "Roam notes"
:publishing-function roam-publication-wrapper
:publishing-directory "~/roam-export"
:section-number nil
:table-of-contents nil
:style "<link rel=\"stylesheet\" href=\"../other/mystyle.cs\" type=\"text/css\">")))
#+end_src
*** Overriding the default link creation function
The code below will generate a link to the generated html file instead
of the default org-protocol link.
#+begin_src emacs-lisp
(defun org-roam-custom-link-builder (node)
(let ((file (org-roam-node-file node)))
(concat (file-name-base file) ".html")))
(setq org-roam-graph-link-builder 'org-roam-custom-link-builder)
#+end_src
*** Copying the generated file to the export directory
The default behavior of =org-roam-graph= is to generate the graph and
display it in Emacs. There is an =org-roam-graph-generation-hook=
available that provides access to the file names so they can be copied
to the publishing directory. Example code follows:
#+begin_src emacs-lisp
(add-hook 'org-roam-graph-generation-hook
(lambda (dot svg) (if (< (- (cadr (current-time)) my-publish-time) 5)
(progn (copy-file svg "~/roam-export/sitemap.svg" 't)
(kill-buffer (file-name-nondirectory svg))
(setq my-publish-time 0)))))
#+end_src
* Developer's Guide to Org-roam * Developer's Guide to Org-roam
** Org-roam's Design Principle ** Org-roam's Design Principle
@ -1361,7 +1628,7 @@ queries on their Org files.
** Building Extensions and Advanced Customization of Org-roam ** Building Extensions and Advanced Customization of Org-roam
Because Org-roam's core functionality is small, it is possible and sometimes Because Org-roam's core functionality is small, it is possible and sometimes
desirable to build extensions on top of it. These extensions may one or more of desirable to build extensions on top of it. These extensions may use one or more of
the following functionalities: the following functionalities:
- Access to Org-roam's database - Access to Org-roam's database
@ -1468,7 +1735,7 @@ When GOTO is non-nil, go the note without creating an entry."
:END: :END:
#+BEGIN_QUOTE #+BEGIN_QUOTE
Copyright (C) 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> Copyright (C) 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
You can redistribute this document and/or modify it under the terms You can redistribute this document and/or modify it under the terms
of the GNU General Public License as published by the Free Software of the GNU General Public License as published by the Free Software

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,14 @@
;;; org-roam-dailies.el --- Daily-notes for Org-roam -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-dailies.el --- Daily-notes for Org-roam -*- coding: utf-8; lexical-binding: t; -*-
;;; ;;;
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Copyright © 2020 Leo Vivier <leo.vivier+dev@gmail.com> ;; Copyright © 2020 Leo Vivier <leo.vivier+dev@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Leo Vivier <leo.vivier+dev@gmail.com> ;; Leo Vivier <leo.vivier+dev@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org-roam "2.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org-roam "2.1"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -34,10 +34,9 @@
;; each file named after certain date and stored in `org-roam-dailies-directory'. ;; each file named after certain date and stored in `org-roam-dailies-directory'.
;; ;;
;; One can use dailies for various purposes, e.g. journaling, fleeting notes, ;; One can use dailies for various purposes, e.g. journaling, fleeting notes,
;; scratch notes and whatever else you can came up with. ;; scratch notes or whatever else you can think of.
;; ;;
;;; Code: ;;; Code:
(require 'f)
(require 'dash) (require 'dash)
(require 'org-roam) (require 'org-roam)
@ -62,7 +61,7 @@ This path is relative to `org-roam-directory'."
(defcustom org-roam-dailies-capture-templates (defcustom org-roam-dailies-capture-templates
`(("d" "default" entry `(("d" "default" entry
"* %?" "* %?"
:if-new (file+head "%<%Y-%m-%d>.org" :target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n"))) "#+title: %<%Y-%m-%d>\n")))
"Capture templates for daily-notes in Org-roam. "Capture templates for daily-notes in Org-roam.
Note that for daily files to show up in the calendar, they have to be of format Note that for daily files to show up in the calendar, they have to be of format
@ -92,7 +91,7 @@ See `org-roam-capture-templates' for the template documentation."
(function :tag "Template function"))) (function :tag "Template function")))
(plist :inline t (plist :inline t
;; Give the most common options as checkboxes ;; Give the most common options as checkboxes
:options (((const :format "%v " :if-new) :options (((const :format "%v " :target)
(choice :tag "Node location" (choice :tag "Node location"
(list :tag "File" (list :tag "File"
(const :format "" file) (const :format "" file)
@ -130,79 +129,103 @@ See `org-roam-capture-templates' for the template documentation."
;;; Commands ;;; Commands
;;;; Today ;;;; Today
;;;###autoload ;;;###autoload
(defun org-roam-dailies-capture-today (&optional goto) (defun org-roam-dailies-capture-today (&optional goto keys)
"Create an entry in the daily-note for today. "Create an entry in the daily-note for today.
When GOTO is non-nil, go the note without creating an entry." When GOTO is non-nil, go the note without creating an entry.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive "P") (interactive "P")
(org-roam-dailies--capture (current-time) goto)) (org-roam-dailies--capture (current-time) goto keys))
;;;###autoload ;;;###autoload
(defun org-roam-dailies-goto-today () (defun org-roam-dailies-goto-today (&optional keys)
"Find the daily-note for today, creating it if necessary." "Find the daily-note for today, creating it if necessary.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive) (interactive)
(org-roam-dailies-capture-today t)) (org-roam-dailies-capture-today t keys))
;;;; Tomorrow ;;;; Tomorrow
;;;###autoload ;;;###autoload
(defun org-roam-dailies-capture-tomorrow (n &optional goto) (defun org-roam-dailies-capture-tomorrow (n &optional goto keys)
"Create an entry in the daily-note for tomorrow. "Create an entry in the daily-note for tomorrow.
With numeric argument N, use the daily-note N days in the future. With numeric argument N, use the daily-note N days in the future.
With a `C-u' prefix or when GOTO is non-nil, go the note without With a `C-u' prefix or when GOTO is non-nil, go the note without
creating an entry." creating an entry.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive "p") (interactive "p")
(org-roam-dailies--capture (time-add (* n 86400) (current-time)) goto)) (org-roam-dailies--capture (time-add (* n 86400) (current-time)) goto keys))
;;;###autoload ;;;###autoload
(defun org-roam-dailies-goto-tomorrow (n) (defun org-roam-dailies-goto-tomorrow (n &optional keys)
"Find the daily-note for tomorrow, creating it if necessary. "Find the daily-note for tomorrow, creating it if necessary.
With numeric argument N, use the daily-note N days in the With numeric argument N, use the daily-note N days in the
future." future.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive "p") (interactive "p")
(org-roam-dailies-capture-tomorrow n t)) (org-roam-dailies-capture-tomorrow n t keys))
;;;; Yesterday ;;;; Yesterday
;;;###autoload ;;;###autoload
(defun org-roam-dailies-capture-yesterday (n &optional goto) (defun org-roam-dailies-capture-yesterday (n &optional goto keys)
"Create an entry in the daily-note for yesteday. "Create an entry in the daily-note for yesteday.
With numeric argument N, use the daily-note N days in the past. With numeric argument N, use the daily-note N days in the past.
When GOTO is non-nil, go the note without creating an entry." When GOTO is non-nil, go the note without creating an entry.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive "p") (interactive "p")
(org-roam-dailies-capture-tomorrow (- n) goto)) (org-roam-dailies-capture-tomorrow (- n) goto keys))
;;;###autoload ;;;###autoload
(defun org-roam-dailies-goto-yesterday (n) (defun org-roam-dailies-goto-yesterday (n &optional keys)
"Find the daily-note for yesterday, creating it if necessary. "Find the daily-note for yesterday, creating it if necessary.
With numeric argument N, use the daily-note N days in the With numeric argument N, use the daily-note N days in the
future." future.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive "p") (interactive "p")
(org-roam-dailies-capture-tomorrow (- n) t)) (org-roam-dailies-capture-tomorrow (- n) t keys))
;;;; Date ;;;; Date
;;;###autoload ;;;###autoload
(defun org-roam-dailies-capture-date (&optional goto prefer-future) (defun org-roam-dailies-capture-date (&optional goto prefer-future keys)
"Create an entry in the daily-note for a date using the calendar. "Create an entry in the daily-note for a date using the calendar.
Prefer past dates, unless PREFER-FUTURE is non-nil. Prefer past dates, unless PREFER-FUTURE is non-nil.
With a `C-u' prefix or when GOTO is non-nil, go the note without With a `C-u' prefix or when GOTO is non-nil, go the note without
creating an entry." creating an entry.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive "P") (interactive "P")
(let ((time (let ((org-read-date-prefer-future prefer-future)) (let ((time (let ((org-read-date-prefer-future prefer-future))
(org-read-date t t nil (if goto (org-read-date nil t nil (if goto
"Find daily-note: " "Find daily-note: "
"Capture to daily-note: "))))) "Capture to daily-note: ")))))
(org-roam-dailies--capture time goto))) (org-roam-dailies--capture time goto keys)))
;;;###autoload ;;;###autoload
(defun org-roam-dailies-goto-date (&optional prefer-future) (defun org-roam-dailies-goto-date (&optional prefer-future keys)
"Find the daily-note for a date using the calendar, creating it if necessary. "Find the daily-note for a date using the calendar, creating it if necessary.
Prefer past dates, unless PREFER-FUTURE is non-nil." Prefer past dates, unless PREFER-FUTURE is non-nil.
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(interactive) (interactive)
(org-roam-dailies-capture-date t prefer-future)) (org-roam-dailies-capture-date t prefer-future keys))
;;;; Navigation ;;;; Navigation
(defun org-roam-dailies-goto-next-note (&optional n) (defun org-roam-dailies-goto-next-note (&optional n)
@ -266,7 +289,7 @@ If FILE is not specified, use the current buffer's file-path."
(save-match-data (save-match-data
(and (and
(org-roam-file-p path) (org-roam-file-p path)
(f-descendant-of-p path directory))))) (org-roam-descendant-of-p path directory)))))
;;;###autoload ;;;###autoload
(defun org-roam-dailies-find-directory () (defun org-roam-dailies-find-directory ()
@ -300,11 +323,16 @@ Return (MONTH DAY YEAR) or nil if not an Org time-string."
;;; Capture implementation ;;; Capture implementation
(add-to-list 'org-roam-capture--template-keywords :override-default-time) (add-to-list 'org-roam-capture--template-keywords :override-default-time)
(defun org-roam-dailies--capture (time &optional goto) (defun org-roam-dailies--capture (time &optional goto keys)
"Capture an entry in a daily-note for TIME, creating it if necessary. "Capture an entry in a daily-note for TIME, creating it if necessary.
When GOTO is non-nil, go the note without creating an entry." When GOTO is non-nil, go the note without creating an entry.
(let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory)))
ELisp programs can set KEYS to a string associated with a template.
In this case, interactive selection will be bypassed."
(let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory))
(org-roam-dailies-directory "./"))
(org-roam-capture- :goto (when goto '(4)) (org-roam-capture- :goto (when goto '(4))
:keys keys
:node (org-roam-node-create) :node (org-roam-node-create)
:templates org-roam-dailies-capture-templates :templates org-roam-dailies-capture-templates
:props (list :override-default-time time))) :props (list :override-default-time time)))

View File

@ -0,0 +1,75 @@
;;; org-roam-export.el --- Org-roam org-export tweaks -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience
;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (org "9.6") (org-roam "2.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;;
;; This package provides the necessary changes required to make org-export work out-of-the-box.
;;
;; To enable it, run:
;;
;; (require 'org-roam-export)
;;
;; The key issue Org's export-to-html functionality has is that it does not respect the ID property, which
;; Org-roam relies heavily on. This patches the necessary function in ox-html to export ID links correctly,
;; pointing to the correct place.
;;
;;; Code:
(require 'ox-html)
(defun org-roam-export--org-html--reference (datum info &optional named-only)
"Org-roam's patch for `org-html--reference' to support ID link export.
See `org-html--reference' for DATUM, INFO and NAMED-ONLY."
(let* ((type (org-element-type datum))
(user-label
(org-element-property
(pcase type
((or `headline `inlinetask) :CUSTOM_ID)
((or `radio-target `target) :value)
(_ :name))
datum))
(user-label
(or user-label
(when-let ((path (org-element-property :ID datum)))
;; see `org-html-link' for why we use "ID-"
;; (search for "ID-" in ox-html.el)
(concat "ID-" path)))))
(cond
((and user-label
(or (plist-get info :html-prefer-user-labels)
(memq type '(headline inlinetask))))
user-label)
((and named-only
(not (memq type '(headline inlinetask radio-target target)))
(not user-label))
nil)
(t
(org-export-get-reference datum info)))))
(advice-add 'org-html--reference :override #'org-roam-export--org-html--reference)
(provide 'org-roam-export)
;;; org-roam-export.el ends here

View File

@ -1,12 +1,12 @@
;;; org-roam-graph.el --- Basic graphing functionality for Org-roam -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-graph.el --- Basic graphing functionality for Org-roam -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (org "9.4") (org-roam "2.1")) ;; Package-Requires: ((emacs "26.1") (org "9.6") (org-roam "2.1"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -81,7 +81,7 @@ Example:
("fillcolor" . "#EEEEEE") ("fillcolor" . "#EEEEEE")
("color" . "#C9C9C9") ("color" . "#C9C9C9")
("fontcolor" . "#0A97A6"))) ("fontcolor" . "#0A97A6")))
("https" . (("shape" . "rounded,filled") ("https" . (("style" . "rounded,filled")
("fillcolor" . "#EEEEEE") ("fillcolor" . "#EEEEEE")
("color" . "#C9C9C9") ("color" . "#C9C9C9")
("fontcolor" . "#0A97A6")))) ("fontcolor" . "#0A97A6"))))
@ -113,6 +113,25 @@ All other values including nil will have no effect."
(const :tag "no" nil)) (const :tag "no" nil))
:group 'org-roam) :group 'org-roam)
(defcustom org-roam-graph-link-builder 'org-roam-org-protocol-link-builder
"Function used to build the Org-roam graph links.
Given a node name, return a string to be used for the link fed to
the graph generation utility."
:type 'function
:group 'org-roam)
(defcustom org-roam-graph-generation-hook nil
"Functions to run after the graph has been generated.
Each function is called with two arguments: the filename
containing the graph generation tool, and the generated graph."
:type 'hook
:group 'org-roam)
(defun org-roam-org-protocol-link-builder (node)
"Default org-roam link builder. Generate an org-protocol link using NODE."
(concat "org-protocol://roam-node?node="
(url-hexify-string (org-roam-node-id node))))
;;; Interactive command ;;; Interactive command
;;;###autoload ;;;###autoload
(defun org-roam-graph (&optional arg node) (defun org-roam-graph (&optional arg node)
@ -147,13 +166,14 @@ CALLBACK is passed the graph file as its sole argument."
(temp-graph (make-temp-file "graph." nil (concat "." org-roam-graph-filetype)))) (temp-graph (make-temp-file "graph." nil (concat "." org-roam-graph-filetype))))
(org-roam-message "building graph") (org-roam-message "building graph")
(make-process (make-process
:name "*org-roam-graph--build-process*" :name "*org-roam-graph*"
:buffer "*org-roam-graph--build-process*" :buffer " *org-roam-graph*"
:command `(,org-roam-graph-executable ,temp-dot "-T" ,org-roam-graph-filetype "-o" ,temp-graph) :command `(,org-roam-graph-executable ,temp-dot "-T" ,org-roam-graph-filetype "-o" ,temp-graph)
:sentinel (when callback :sentinel (when callback
(lambda (process _event) (lambda (process _event)
(when (= 0 (process-exit-status process)) (when (= 0 (process-exit-status process))
(funcall callback temp-graph))))))) (progn (funcall callback temp-graph)
(run-hook-with-args 'org-roam-graph-generation-hook temp-dot temp-graph))))))))
(defun org-roam-graph--dot (&optional edges all-nodes) (defun org-roam-graph--dot (&optional edges all-nodes)
"Build the graphviz given the EDGES of the graph. "Build the graphviz given the EDGES of the graph.
@ -246,12 +266,11 @@ Handles both Org-roam nodes, and string nodes (e.g. urls)."
(org-roam-quote-string (org-roam-quote-string
(pcase org-roam-graph-shorten-titles (pcase org-roam-graph-shorten-titles
(`truncate (truncate-string-to-width title org-roam-graph-max-title-length nil nil "...")) (`truncate (truncate-string-to-width title org-roam-graph-max-title-length nil nil "..."))
(`wrap (s-word-wrap org-roam-graph-max-title-length title)) (`wrap (org-roam-word-wrap org-roam-graph-max-title-length title))
(_ title))))) (_ title)))))
(setq node-id (org-roam-node-id node) (setq node-id (org-roam-node-id node)
node-properties `(("label" . ,shortened-title) node-properties `(("label" . ,shortened-title)
("URL" . ,(concat "org-protocol://roam-node?node=" ("URL" . ,(funcall org-roam-graph-link-builder node))
(url-hexify-string (org-roam-node-id node))))
("tooltip" . ,(xml-escape-string title))))) ("tooltip" . ,(xml-escape-string title)))))
(setq node-id node (setq node-id node
node-properties (append `(("label" . ,(concat type ":" node))) node-properties (append `(("label" . ,(concat type ":" node)))

View File

@ -1,12 +1,12 @@
;;; org-roam-overlay.el --- Link overlay for [id:] links to Org-roam nodes -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-overlay.el --- Link overlay for [id:] links to Org-roam nodes -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (org "9.4") (org-roam "2.1")) ;; Package-Requires: ((emacs "26.1") (org "9.6") (org-roam "2.1"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -27,8 +27,8 @@
;;; Commentary: ;;; Commentary:
;; ;;
;; This extension provides allows to render [[id:]] links that don't have an ;; This extension allows to render [[id:]] links that don't have an associated
;; asscoiated descriptor with an overlay that displays the node's current title. ;; descriptor with an overlay that displays the node's current title.
;; ;;
;;; Code: ;;; Code:
(require 'org-roam) (require 'org-roam)

View File

@ -1,11 +1,11 @@
;;; org-roam-protocol.el --- Protocol handler for roam:// links -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-protocol.el --- Protocol handler for roam:// links -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (org "9.4") (org-roam "2.1")) ;; Package-Requires: ((emacs "26.1") (org "9.6") (org-roam "2.1"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -48,7 +48,7 @@
(defcustom org-roam-capture-ref-templates (defcustom org-roam-capture-ref-templates
'(("r" "ref" plain "%?" '(("r" "ref" plain "%?"
:if-new (file+head "${slug}.org" :target (file+head "${slug}.org"
"#+title: ${title}") "#+title: ${title}")
:unnarrowed t)) :unnarrowed t))
"The Org-roam templates used during a capture from the roam-ref protocol. "The Org-roam templates used during a capture from the roam-ref protocol.
@ -77,7 +77,7 @@ See `org-roam-capture-templates' for the template documentation."
(function :tag "Template function"))) (function :tag "Template function")))
(plist :inline t (plist :inline t
;; Give the most common options as checkboxes ;; Give the most common options as checkboxes
:options (((const :format "%v " :if-new) :options (((const :format "%v " :target)
(choice :tag "Node location" (choice :tag "Node location"
(list :tag "File" (list :tag "File"
(const :format "" file) (const :format "" file)
@ -141,12 +141,13 @@ It opens or creates a note with the given ref.
(plist-get info :ref))) (plist-get info :ref)))
:initial (or (plist-get info :body) "")) :initial (or (plist-get info :body) ""))
(raise-frame) (raise-frame)
(let ((org-capture-link-is-already-stored t))
(org-roam-capture- (org-roam-capture-
:keys (plist-get info :template) :keys (plist-get info :template)
:node (org-roam-node-create :title (plist-get info :title)) :node (org-roam-node-create :title (plist-get info :title))
:info (list :ref (plist-get info :ref) :info (list :ref (plist-get info :ref)
:body (plist-get info :body)) :body (plist-get info :body))
:templates org-roam-capture-ref-templates) :templates org-roam-capture-ref-templates))
nil) nil)
(defun org-roam-protocol-open-node (info) (defun org-roam-protocol-open-node (info)
@ -168,25 +169,6 @@ org-protocol://roam-node?node=uuid"
(push '("org-roam-node" :protocol "roam-node" :function org-roam-protocol-open-node) (push '("org-roam-node" :protocol "roam-node" :function org-roam-protocol-open-node)
org-protocol-protocol-alist) org-protocol-protocol-alist)
;;; Capture implementation
(add-hook 'org-roam-capture-preface-hook #'org-roam-protocol--try-capture-to-ref-h)
(defun org-roam-protocol--try-capture-to-ref-h ()
"Try to capture to an existing node that match the ref."
(when-let ((node (and (plist-get org-roam-capture--info :ref)
(org-roam-node-from-ref
(plist-get org-roam-capture--info :ref)))))
(set-buffer (org-capture-target-buffer (org-roam-node-file node)))
(goto-char (org-roam-node-point node))
(widen)
(org-roam-node-id node)))
(add-hook 'org-roam-capture-new-node-hook #'org-roam-protocol--insert-captured-ref-h)
(defun org-roam-protocol--insert-captured-ref-h ()
"Insert the ref if any."
(when-let ((ref (plist-get org-roam-capture--info :ref)))
(org-roam-ref-add ref)))
(provide 'org-roam-protocol) (provide 'org-roam-protocol)
;;; org-roam-protocol.el ends here ;;; org-roam-protocol.el ends here

11
github-eldev Executable file
View File

@ -0,0 +1,11 @@
#! /bin/sh
set -e
ELDEV_BIN_DIR=~/.local/bin
# `$GITHUB_PATH' is a magic file which contents is translated to environment variable `$PATH'.
echo "$ELDEV_BIN_DIR" >> $GITHUB_PATH
mkdir -p $ELDEV_BIN_DIR
curl -fsSL https://raw.githubusercontent.com/doublep/eldev/f111d19cda305e5e8fcb70a5675b87173041cb68/bin/eldev > $ELDEV_BIN_DIR/eldev
chmod a+x $ELDEV_BIN_DIR/eldev

View File

@ -1,12 +1,12 @@
;;; org-roam-capture.el --- Capture functionality -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-capture.el --- Capture functionality -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (emacsql "4.1.0") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -40,7 +40,7 @@
;;; Options ;;; Options
(defcustom org-roam-capture-templates (defcustom org-roam-capture-templates
'(("d" "default" plain "%?" '(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" :target (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n") "#+title: ${title}\n")
:unnarrowed t)) :unnarrowed t))
"Templates for the creation of new entries within Org-roam. "Templates for the creation of new entries within Org-roam.
@ -92,30 +92,41 @@ template The template for creating the capture item.
in order to get a template from a file, or dynamically in order to get a template from a file, or dynamically
from a function. from a function.
The template contains a compulsory :if-new property. This determines the The template contains a compulsory :target property. The :target property
location of the new node. The :if-new property contains a list, supporting contains a list, where:
the following options: - The first element indicates the type of the target.
- The second element indicates the location of the captured node.
- And the rest of the list indicate the prefilled template, that will be
inserted and the position of the point will be adjusted for.
This behavior varies from type to type.
The following options are supported for the :target property:
(file \"path/to/file\") (file \"path/to/file\")
The file will be created, and prescribed an ID. The file will be created, and prescribed an ID.
(file+head \"path/to/file\" \"head content\") (file+head \"path/to/file\" \"head content\")
The file will be created, prescribed an ID, and head content will be The file will be created, prescribed an ID, and head content will be
inserted into the file. inserted if the node is a newly captured one.
(file+olp \"path/to/file\" (\"h1\" \"h2\")) (file+olp \"path/to/file\" (\"h1\" \"h2\"))
The file will be created, prescribed an ID. The OLP (h1, h2) will be The file will be created, prescribed an ID. If the file doesn't contain
created, and the point placed after. the outline path (h1, h2), it will be automatically created. The point
will be adjusted to the last element in the OLP.
(file+head+olp \"path/to/file\" \"head content\" (\"h1\" \"h2\")) (file+head+olp \"path/to/file\" \"head content\" (\"h1\" \"h2\"))
The file will be created, prescribed an ID. Head content will be The file will be created, prescribed an ID. Head content will be
inserted at the start of the file. The OLP (h1, h2) will be created, inserted at the start of the file if the node is a newly captured one.
and the point placed after. If the file doesn't contain the outline path (h1, h2), it will be
automatically created. The point will be adjusted to the last element in
the OLP.
(file+datetree \"path/to/file\" day) (file+datetree \"path/to/file\" tree-type)
The file will be created, prescribed an ID. Head content will be The file will be created, prescribed an ID. A date based outline path
inserted at the start of the file. The datetree will be created, will be created for today's date. The tree-type can be one of the
available options are day, week, month. following symbols: day, week or month. The point will adjusted to the
last element in the tree. To prompt for date instead of using today's,
use the :time-prompt property.
(node \"title or alias or ID of an existing node\") (node \"title or alias or ID of an existing node\")
The point will be placed for an existing node, based on either, its The point will be placed for an existing node, based on either, its
@ -194,7 +205,8 @@ be replaced with content and expanded:
introduced with %[pathname] are expanded this way. Since this introduced with %[pathname] are expanded this way. Since this
happens after expanding non-interactive %-escapes, those can happens after expanding non-interactive %-escapes, those can
be used to fill the expression. be used to fill the expression.
%<...> The result of `format-time-string' on the ... format specification. %<...> The result of `format-time-string' on the ... format
specification.
%t Time stamp, date only. The time stamp is the current time, %t Time stamp, date only. The time stamp is the current time,
except when called from agendas with `\\[org-agenda-capture]' or except when called from agendas with `\\[org-agenda-capture]' or
with `org-capture-use-agenda-date' set. with `org-capture-use-agenda-date' set.
@ -289,7 +301,7 @@ streamlined user experience in Org-roam."
(function :tag "Template function"))) (function :tag "Template function")))
(plist :inline t (plist :inline t
;; Give the most common options as checkboxes ;; Give the most common options as checkboxes
:options (((const :format "%v " :if-new) :options (((const :format "%v " :target)
(choice :tag "Node location" (choice :tag "Node location"
(list :tag "File" (list :tag "File"
(const :format "" file) (const :format "" file)
@ -377,7 +389,7 @@ during the Org-roam capture process.")
This variable is populated dynamically, and is only non-nil This variable is populated dynamically, and is only non-nil
during the Org-roam capture process.") during the Org-roam capture process.")
(defconst org-roam-capture--template-keywords (list :if-new :id :link-description :call-location (defconst org-roam-capture--template-keywords (list :target :id :link-description :call-location
:region) :region)
"Keywords used in `org-roam-capture-templates' specific to Org-roam.") "Keywords used in `org-roam-capture-templates' specific to Org-roam.")
@ -395,6 +407,8 @@ TEMPLATES is a list of org-roam templates."
(mapcar (lambda (template) (mapcar (lambda (template)
(org-roam-capture--convert-template template props)) (org-roam-capture--convert-template template props))
(or templates org-roam-capture-templates))) (or templates org-roam-capture-templates)))
(_ (setf (org-roam-node-id node) (or (org-roam-node-id node)
(org-id-new))))
(org-roam-capture--node node) (org-roam-capture--node node)
(org-roam-capture--info info)) (org-roam-capture--info info))
(when (and (not keys) (when (and (not keys)
@ -433,36 +447,33 @@ the capture)."
"Get the value for KEYWORD from the `org-roam-capture-template'." "Get the value for KEYWORD from the `org-roam-capture-template'."
(plist-get (plist-get org-capture-plist :org-roam) keyword)) (plist-get (plist-get org-capture-plist :org-roam) keyword))
(defun org-roam-capture--put (&rest stuff) (defun org-roam-capture--put (prop value)
"Put properties from STUFF into the `org-roam-capture-template'." "Set property PROP to VALUE in the `org-roam-capture-template'."
(let ((p (plist-get org-capture-plist :org-roam))) (let ((p (plist-get org-capture-plist :org-roam)))
(while stuff
(setq p (plist-put p (pop stuff) (pop stuff))))
(setq org-capture-plist (setq org-capture-plist
(plist-put org-capture-plist :org-roam p)))) (plist-put org-capture-plist
:org-roam
(plist-put p prop value)))))
;;;; Capture target ;;;; Capture target
(defun org-roam-capture--prepare-buffer () (defun org-roam-capture--prepare-buffer ()
"Prepare the capture buffer for the current Org-roam based capture template. "Prepare the capture buffer for the current Org-roam based capture template.
This function will initialize and setup the capture buffer, This function will initialize and setup the capture buffer,
create the target node (`:if-new') if it doesn't exist, and place position the point to the current :target (and if necessary,
the point for further processing by `org-capture'. create it if it doesn't exist), and place the point for further
processing by `org-capture'.
Note: During the capture process this function is run by Note: During the capture process this function is run by
`org-capture-set-target-location', as a (function ...) based `org-capture-set-target-location', as a (function ...) based
capture target." capture target."
(let ((id (cond ((run-hook-with-args-until-success 'org-roam-capture-preface-hook)) (let ((id (cond ((run-hook-with-args-until-success 'org-roam-capture-preface-hook))
((and (org-roam-node-file org-roam-capture--node) (t (org-roam-capture--setup-target-location)))))
(org-roam-node-point org-roam-capture--node))
(set-buffer (org-capture-target-buffer (org-roam-node-file org-roam-capture--node)))
(goto-char (org-roam-node-point org-roam-capture--node))
(widen)
(org-roam-node-id org-roam-capture--node))
(t
(org-roam-capture--setup-target-location)))))
(org-roam-capture--adjust-point-for-capture-type) (org-roam-capture--adjust-point-for-capture-type)
(org-capture-put :template (let ((template (org-capture-get :template)))
(org-roam-capture--fill-template (org-capture-get :template))) (when (stringp template)
(org-capture-put
:template
(org-roam-capture--fill-template template))))
(org-roam-capture--put :id id) (org-roam-capture--put :id id)
(org-roam-capture--put :finalize (or (org-capture-get :finalize) (org-roam-capture--put :finalize (or (org-capture-get :finalize)
(org-roam-capture--get :finalize))))) (org-roam-capture--get :finalize)))))
@ -471,22 +482,17 @@ capture target."
"Initialize the buffer, and goto the location of the new capture. "Initialize the buffer, and goto the location of the new capture.
Return the ID of the location." Return the ID of the location."
(let (p new-file-p) (let (p new-file-p)
(pcase (or (org-roam-capture--get :if-new) (pcase (org-roam-capture--get-target)
(user-error "Template needs to specify `:if-new'"))
(`(file ,path) (`(file ,path)
(setq path (expand-file-name (setq path (org-roam-capture--target-truepath path)
(string-trim (org-roam-capture--fill-template path t)) new-file-p (org-roam-capture--new-file-p path))
org-roam-directory))
(setq new-file-p (org-roam-capture--new-file-p path))
(when new-file-p (org-roam-capture--put :new-file path)) (when new-file-p (org-roam-capture--put :new-file path))
(set-buffer (org-capture-target-buffer path)) (set-buffer (org-capture-target-buffer path))
(widen) (widen)
(setq p (goto-char (point-min)))) (setq p (goto-char (point-min))))
(`(file+olp ,path ,olp) (`(file+olp ,path ,olp)
(setq path (expand-file-name (setq path (org-roam-capture--target-truepath path)
(string-trim (org-roam-capture--fill-template path t)) new-file-p (org-roam-capture--new-file-p path))
org-roam-directory))
(setq new-file-p (org-roam-capture--new-file-p path))
(when new-file-p (org-roam-capture--put :new-file path)) (when new-file-p (org-roam-capture--put :new-file path))
(set-buffer (org-capture-target-buffer path)) (set-buffer (org-capture-target-buffer path))
(setq p (point-min)) (setq p (point-min))
@ -494,33 +500,27 @@ Return the ID of the location."
(goto-char m)) (goto-char m))
(widen)) (widen))
(`(file+head ,path ,head) (`(file+head ,path ,head)
(setq path (expand-file-name (setq path (org-roam-capture--target-truepath path)
(string-trim (org-roam-capture--fill-template path t)) new-file-p (org-roam-capture--new-file-p path))
org-roam-directory))
(setq new-file-p (org-roam-capture--new-file-p path))
(set-buffer (org-capture-target-buffer path)) (set-buffer (org-capture-target-buffer path))
(when new-file-p (when new-file-p
(org-roam-capture--put :new-file path) (org-roam-capture--put :new-file path)
(insert (org-roam-capture--fill-template head t))) (insert (org-roam-capture--fill-template head 'ensure-newline)))
(widen) (widen)
(setq p (goto-char (point-min)))) (setq p (goto-char (point-min))))
(`(file+head+olp ,path ,head ,olp) (`(file+head+olp ,path ,head ,olp)
(setq path (expand-file-name (setq path (org-roam-capture--target-truepath path)
(string-trim (org-roam-capture--fill-template path t)) new-file-p (org-roam-capture--new-file-p path))
org-roam-directory))
(setq new-file-p (org-roam-capture--new-file-p path))
(set-buffer (org-capture-target-buffer path)) (set-buffer (org-capture-target-buffer path))
(widen) (widen)
(when new-file-p (when new-file-p
(org-roam-capture--put :new-file path) (org-roam-capture--put :new-file path)
(insert (org-roam-capture--fill-template head t))) (insert (org-roam-capture--fill-template head 'ensure-newline)))
(setq p (point-min)) (setq p (point-min))
(let ((m (org-roam-capture-find-or-create-olp olp))) (let ((m (org-roam-capture-find-or-create-olp olp)))
(goto-char m))) (goto-char m)))
(`(file+datetree ,path ,tree-type) (`(file+datetree ,path ,tree-type)
(setq path (expand-file-name (setq path (org-roam-capture--target-truepath path))
(string-trim (org-roam-capture--fill-template path t))
org-roam-directory))
(require 'org-datetree) (require 'org-datetree)
(widen) (widen)
(set-buffer (org-capture-target-buffer path)) (set-buffer (org-capture-target-buffer path))
@ -574,13 +574,29 @@ Return the ID of the location."
;; caller. ;; caller.
(save-excursion (save-excursion
(goto-char p) (goto-char p)
(when-let* ((node org-roam-capture--node) (if-let ((id (org-entry-get p "ID")))
(id (org-roam-node-id node))) (setf (org-roam-node-id org-roam-capture--node) id)
(org-entry-put p "ID" id)) (org-entry-put p "ID" (org-roam-node-id org-roam-capture--node)))
(prog1 (prog1
(org-id-get-create) (org-id-get)
(run-hooks 'org-roam-capture-new-node-hook))))) (run-hooks 'org-roam-capture-new-node-hook)))))
(defun org-roam-capture--get-target ()
"Get the current capture :target for the capture template in use."
(or (org-roam-capture--get :target)
(user-error "Template needs to specify `:target'")))
(defun org-roam-capture--target-truepath (path)
"From PATH get the correct path to the current capture target and return it.
PATH is a string that can optionally contain templated text in
it."
(or (org-roam-node-file org-roam-capture--node)
(thread-first
path
(org-roam-capture--fill-template)
(string-trim)
(expand-file-name org-roam-directory))))
(defun org-roam-capture--new-file-p (path) (defun org-roam-capture--new-file-p (path)
"Return t if PATH is for a new file with no visiting buffer." "Return t if PATH is for a new file with no visiting buffer."
(not (or (file-exists-p path) (not (or (file-exists-p path)
@ -602,6 +618,7 @@ you can catch it with `condition-case'."
(org-with-wide-buffer (org-with-wide-buffer
(goto-char start) (goto-char start)
(dolist (heading olp) (dolist (heading olp)
(setq heading (org-roam-capture--fill-template heading))
(let ((re (format org-complex-heading-regexp-format (let ((re (format org-complex-heading-regexp-format
(regexp-quote heading))) (regexp-quote heading)))
(cnt 0)) (cnt 0))
@ -663,6 +680,24 @@ the current value of `point'."
(goto-char (org-entry-end-position)))))))) (goto-char (org-entry-end-position))))))))
(point)) (point))
;;; Capture implementation
(add-hook 'org-roam-capture-preface-hook #'org-roam-capture--try-capture-to-ref-h)
(defun org-roam-capture--try-capture-to-ref-h ()
"Try to capture to an existing node that match the ref."
(when-let ((node (and (plist-get org-roam-capture--info :ref)
(org-roam-node-from-ref
(plist-get org-roam-capture--info :ref)))))
(set-buffer (org-capture-target-buffer (org-roam-node-file node)))
(goto-char (org-roam-node-point node))
(widen)
(org-roam-node-id node)))
(add-hook 'org-roam-capture-new-node-hook #'org-roam-capture--insert-captured-ref-h)
(defun org-roam-capture--insert-captured-ref-h ()
"Insert the ref if any."
(when-let ((ref (plist-get org-roam-capture--info :ref)))
(org-roam-ref-add ref)))
;;;; Finalizers ;;;; Finalizers
(add-hook 'org-capture-prepare-finalize-hook #'org-roam-capture--install-finalize-h) (add-hook 'org-capture-prepare-finalize-hook #'org-roam-capture--install-finalize-h)
(defun org-roam-capture--install-finalize-h () (defun org-roam-capture--install-finalize-h ()
@ -672,14 +707,15 @@ the current value of `point'."
(defun org-roam-capture--finalize () (defun org-roam-capture--finalize ()
"Finalize the `org-roam-capture' process." "Finalize the `org-roam-capture' process."
(when-let ((region (org-roam-capture--get :region)))
(org-roam-unshield-region (car region) (cdr region)))
(if org-note-abort (if org-note-abort
(when-let ((new-file (org-roam-capture--get :new-file))) (when-let ((new-file (org-roam-capture--get :new-file))
(org-roam-message "Deleting file for aborted capture %s" new-file) (_ (yes-or-no-p "Delete file for aborted capture?")))
(when (find-buffer-visiting new-file) (when (find-buffer-visiting new-file)
(kill-buffer (find-buffer-visiting new-file))) (kill-buffer (find-buffer-visiting new-file)))
(delete-file new-file)) (delete-file new-file))
(when-let* ((buffer (plist-get org-capture-plist :buffer))
(file (buffer-file-name buffer)))
(org-id-add-location (org-roam-capture--get :id) file))
(when-let* ((finalize (org-roam-capture--get :finalize)) (when-let* ((finalize (org-roam-capture--get :finalize))
(org-roam-finalize-fn (intern (concat "org-roam-capture--finalize-" (org-roam-finalize-fn (intern (concat "org-roam-capture--finalize-"
(symbol-name finalize))))) (symbol-name finalize)))))
@ -702,20 +738,32 @@ This function is to be called in the Org-capture finalization process."
(buf (marker-buffer mkr))) (buf (marker-buffer mkr)))
(with-current-buffer buf (with-current-buffer buf
(when-let ((region (org-roam-capture--get :region))) (when-let ((region (org-roam-capture--get :region)))
(org-roam-unshield-region (car region) (cdr region))
(delete-region (car region) (cdr region)) (delete-region (car region) (cdr region))
(set-marker (car region) nil) (set-marker (car region) nil)
(set-marker (cdr region) nil)) (set-marker (cdr region) nil))
(let* ((id (org-roam-capture--get :id))
(description (org-roam-capture--get :link-description))
(link (org-link-make-string (concat "id:" id)
description)))
(if (eq (point) (marker-position mkr))
(insert link)
(org-with-point-at mkr (org-with-point-at mkr
(insert (org-link-make-string (concat "id:" (org-roam-capture--get :id)) (insert link)))
(org-roam-capture--get :link-description))))))) (run-hook-with-args 'org-roam-post-node-insert-hook
id
description)))))
;;;; Processing of the capture templates ;;;; Processing of the capture templates
(defun org-roam-capture--fill-template (template &optional org-capture-p) (defun org-roam-capture--fill-template (template &optional ensure-newline)
"Expand TEMPLATE and return it. "Expand TEMPLATE and return it.
It expands ${var} occurrences in TEMPLATE. When ORG-CAPTURE-P, It expands ${var} occurrences in TEMPLATE, and then runs
also run Org-capture's template expansion." org-capture's template expansion.
(funcall (if org-capture-p #'org-capture-fill-template #'identity) When ENSURE-NEWLINE, always ensure there's a newline behind."
(let* ((template (if (functionp template)
(funcall template)
template))
(template-whitespace-content (org-roam-whitespace-content template)))
(setq template
(org-roam-format-template (org-roam-format-template
template template
(lambda (key default-val) (lambda (key default-val)
@ -729,9 +777,20 @@ also run Org-capture's template expansion."
(funcall node-fn org-roam-capture--node)) (funcall node-fn org-roam-capture--node))
((plist-get org-roam-capture--info ksym) ((plist-get org-roam-capture--info ksym)
(plist-get org-roam-capture--info ksym)) (plist-get org-roam-capture--info ksym))
(t (let ((r (completing-read (format "%s: " key) nil nil nil default-val))) (t (let ((r (read-from-minibuffer (format "%s: " key) default-val)))
(plist-put org-roam-capture--info ksym r) (plist-put org-roam-capture--info ksym r)
r)))))))) r)))))))
;; WARNING:
;; `org-capture-fill-template' fills the template, but post-processes whitespace such that the resultant
;; template does not start with any whitespace, and only ends with a single newline
;;
;; Instead, we restore the whitespace in the original template.
(setq template (replace-regexp-in-string "[\n]*\\'" "" (org-capture-fill-template template)))
(when (and ensure-newline
(string-equal template-whitespace-content ""))
(setq template-whitespace-content "\n"))
(setq template (concat template template-whitespace-content))
template))
(defun org-roam-capture--convert-template (template &optional props) (defun org-roam-capture--convert-template (template &optional props)
"Convert TEMPLATE from Org-roam syntax to `org-capture-templates' syntax. "Convert TEMPLATE from Org-roam syntax to `org-capture-templates' syntax.

View File

@ -1,11 +1,11 @@
;;; org-roam-compat.el --- Backward compatibility code -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-compat.el --- Backward compatibility code -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1")) ;; Package-Requires: ((emacs "26.1"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -31,6 +31,8 @@
;; Emacsen and Org-roam versions. ;; Emacsen and Org-roam versions.
;; ;;
;;; Code: ;;; Code:
(require 'org-roam)
;;; Backports ;;; Backports
;; REVIEW Remove when 26.x support is dropped. This is exact the same as ;; REVIEW Remove when 26.x support is dropped. This is exact the same as
;; `directory-files-recursively' from Emacs 26, but with FOLLOW-SYMLINKS ;; `directory-files-recursively' from Emacs 26, but with FOLLOW-SYMLINKS
@ -99,22 +101,20 @@ recursion."
(advice-add #'org-id-add-location :around #'org-roam--handle-absent-org-id-locations-file-a) (advice-add #'org-id-add-location :around #'org-roam--handle-absent-org-id-locations-file-a)
(defun org-roam--handle-absent-org-id-locations-file-a (fn &rest args) (defun org-roam--handle-absent-org-id-locations-file-a (fn &rest args)
"Gracefully handle errors related to absence of `org-id-locations-file'. "Gracefully handle errors related to absence of `org-id-locations-file'.
FN is `org-id-locations-file' that comes from advice and ARGS are FN is `org-id-add-location' that comes from advice and ARGS are
passed to it." passed to it."
(let (result) (condition-case err
;; Use `unwind-protect' over `condition-case' because `org-id' can produce various other errors, but all (apply fn args)
;; of its errors are generic ones, so trapping all of them isn't a good idea and preserving the correct
;; backtrace is valuable.
(unwind-protect (setq result (apply fn args))
(unless result
(unless org-id-locations
;; Pre-allocate the hash table to avoid weird access related errors during the regeneration.
(setq org-id-locations (make-hash-table :type 'equal)))
;; `org-id' makes the assumption that `org-id-locations-file' will be stored in `user-emacs-directory' ;; `org-id' makes the assumption that `org-id-locations-file' will be stored in `user-emacs-directory'
;; which always exist if you have Emacs, so it uses `with-temp-file' to write to the file. However, ;; which always exist if you have Emacs, so it uses `with-temp-file' to write to the file. However, the
;; the users *do* change the path to this file and `with-temp-file' unable to create the file, if the ;; users *do* change the path to this file and `with-temp-file' unable to create the file, if the path to
;; path to it consists of directories that don't exist. We'll have to handle this ourselves. ;; it consists of directories that don't exist. We'll have to handle this ourselves.
(unless (file-exists-p (file-truename org-id-locations-file)) (error
(advice-remove 'org-id-add-location #'org-roam--handle-absent-org-id-locations-file-a)
(if (file-exists-p (file-truename org-id-locations-file))
(signal (car err) (cdr err))
;; Pre-allocate the hash table to avoid weird access related errors during the regeneration.
(or org-id-locations (setq org-id-locations (make-hash-table :test 'equal)))
;; If permissions allow that, try to create the user specified directory path to ;; If permissions allow that, try to create the user specified directory path to
;; `org-id-locations-file' ourselves. ;; `org-id-locations-file' ourselves.
(condition-case _err (condition-case _err
@ -153,8 +153,30 @@ identification (e.g. with \"ROAM_EXCLUDE\" property) as Org-roam
nodes." org-id-locations-file) nodes." org-id-locations-file)
(setq org-id-locations-file (setq org-id-locations-file
(expand-file-name ".orgids" (file-truename org-roam-directory))) (expand-file-name ".orgids" (file-truename org-roam-directory)))
(apply fn args)))))))
;;;; Deprecated :if-new capture template keyword
(with-eval-after-load 'org-roam-capture
(add-to-list 'org-roam-capture--template-keywords :if-new)
(let ((inhibit-warning-p t)) ; REVIEW Set this to nil close to next major release
(advice-add 'org-roam-capture--get-target :around #'org-roam-capture--get-if-new-target-a)
(defun org-roam-capture--get-if-new-target-a (fn &rest args)
"Get the current capture target using deprecated :if-new property."
(if-let ((target (org-roam-capture--get :if-new)))
(prog1 target
(unless inhibit-warning-p
(lwarn 'org-roam-capture :warning
(mapconcat
#'identity
["`:if-new' property is deprecated in favor of `:target'."
"This warning will popup once per each session. In order to get"
"rid of it, rename all the references to the `:if-new' property"
"in your capture templates to `:target'."]
"\n"))
;; Don't irritate the user too much. Displaying the warning once per session should be enough.
(setq inhibit-warning-p t)))
(apply fn args))))) (apply fn args)))))
result)))
;;; Obsolete aliases (remove after next major release) ;;; Obsolete aliases (remove after next major release)
(define-obsolete-function-alias (define-obsolete-function-alias
@ -199,6 +221,18 @@ nodes." org-id-locations-file)
'org-roam-dailies-find-date 'org-roam-dailies-find-date
'org-roam-dailies-goto-date "org-roam 2.0") 'org-roam-dailies-goto-date "org-roam 2.0")
(define-obsolete-function-alias
'org-roam-add-property
'org-roam-property-add "org-roam 2.1")
(define-obsolete-function-alias
'org-roam-remove-property
'org-roam-property-remove "org-roam 2.1")
(define-obsolete-variable-alias
'org-roam-mode-section-functions
'org-roam-mode-sections "org-roam 2.2.0")
;;; Obsolete functions ;;; Obsolete functions
(make-obsolete 'org-roam-get-keyword 'org-collect-keywords "org-roam 2.0") (make-obsolete 'org-roam-get-keyword 'org-collect-keywords "org-roam 2.0")

View File

@ -1,12 +1,12 @@
;;; org-roam-db.el --- Org-roam database API -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-db.el --- Org-roam database API -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (emacsql "4.1.0") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -31,10 +31,12 @@
;; ;;
;;; Code: ;;; Code:
(require 'org-roam) (require 'org-roam)
(require 'url-parse)
(require 'ol)
(defvar org-outline-path-cache) (defvar org-outline-path-cache)
;;; Options ;;; Options
(defcustom org-roam-db-location (expand-file-name "org-roam.db" user-emacs-directory) (defcustom org-roam-db-location (locate-user-emacs-file "org-roam.db")
"The path to file where the Org-roam database is stored. "The path to file where the Org-roam database is stored.
It is the user's responsibility to set this correctly, especially It is the user's responsibility to set this correctly, especially
@ -78,14 +80,35 @@ slow."
:type 'boolean :type 'boolean
:group 'org-roam) :group 'org-roam)
;;; Variables (defcustom org-roam-db-extra-links-elements '(node-property keyword)
(defconst org-roam-db-version 16) "The list of Org element types to include for parsing by Org-roam.
;; TODO Rename this By default, when parsing Org's AST, links within keywords and
(defconst org-roam--sqlite-available-p property drawers are not parsed as links. Sometimes however, it
(with-demoted-errors "Org-roam initialization: %S" is desirable to parse and cache these links (e.g. hiding links in
(emacsql-sqlite-ensure-binary) a property drawer)."
t)) :package-version '(org-roam . "2.2.0")
:group 'org-roam
:type '(set
(const :tag "keywords" keyword)
(const :tag "property drawers" node-property)))
(defcustom org-roam-db-extra-links-exclude-keys '((node-property . ("ROAM_REFS"))
(keyword . ("transclude")))
"Keys to ignore when mapping over links.
The car of the association list is the Org element type (e.g.
keyword). The cdr is a list of case-insensitive strings to
exclude from being treated as links.
For example, we use this to prevent self-referential links in
ROAM_REFS."
:package-version '(org-roam . "2.2.0")
:group 'org-roam
:type '(alist))
;;; Variables
(defconst org-roam-db-version 20)
(defvar org-roam-db--connection (make-hash-table :test #'equal) (defvar org-roam-db--connection (make-hash-table :test #'equal)
"Database connection to Org-roam database.") "Database connection to Org-roam database.")
@ -93,7 +116,7 @@ slow."
;;; Core Functions ;;; Core Functions
(defun org-roam-db--get-connection () (defun org-roam-db--get-connection ()
"Return the database connection, if any." "Return the database connection, if any."
(gethash (expand-file-name org-roam-directory) (gethash (expand-file-name (file-name-as-directory org-roam-directory))
org-roam-db--connection)) org-roam-db--connection))
(defun org-roam-db () (defun org-roam-db ()
@ -104,9 +127,8 @@ Performs a database upgrade when required."
(emacsql-live-p (org-roam-db--get-connection))) (emacsql-live-p (org-roam-db--get-connection)))
(let ((init-db (not (file-exists-p org-roam-db-location)))) (let ((init-db (not (file-exists-p org-roam-db-location))))
(make-directory (file-name-directory org-roam-db-location) t) (make-directory (file-name-directory org-roam-db-location) t)
(let ((conn (emacsql-sqlite org-roam-db-location))) (let ((conn (emacsql-sqlite-open org-roam-db-location)))
(set-process-query-on-exit-flag (emacsql-process conn) nil) (puthash (expand-file-name (file-name-as-directory org-roam-directory))
(puthash (expand-file-name org-roam-directory)
conn conn
org-roam-db--connection) org-roam-db--connection)
(when init-db (when init-db
@ -117,7 +139,7 @@ Performs a database upgrade when required."
((> version org-roam-db-version) ((> version org-roam-db-version)
(emacsql-close conn) (emacsql-close conn)
(user-error (user-error
"The Org-roam database was created with a newer Org-roam version. " "The Org-roam database was created with a newer Org-roam version. %s"
"You need to update the Org-roam package")) "You need to update the Org-roam package"))
((< version org-roam-db-version) ((< version org-roam-db-version)
(emacsql-close conn) (emacsql-close conn)
@ -145,6 +167,7 @@ The query is expected to be able to fail, in this situation, run HANDLER."
(defconst org-roam-db--table-schemata (defconst org-roam-db--table-schemata
'((files '((files
[(file :unique :primary-key) [(file :unique :primary-key)
title
(hash :not-null) (hash :not-null)
(atime :not-null) (atime :not-null)
(mtime :not-null)]) (mtime :not-null)])
@ -168,6 +191,13 @@ The query is expected to be able to fail, in this situation, run HANDLER."
alias] alias]
(:foreign-key [node-id] :references nodes [id] :on-delete :cascade))) (:foreign-key [node-id] :references nodes [id] :on-delete :cascade)))
(citations
([(node-id :not-null)
(cite-key :not-null)
(pos :not-null)
properties]
(:foreign-key [node-id] :references nodes [id] :on-delete :cascade)))
(refs (refs
([(node-id :not-null) ([(node-id :not-null)
(ref :not-null) (ref :not-null)
@ -195,7 +225,6 @@ The query is expected to be able to fail, in this situation, run HANDLER."
(defun org-roam-db--init (db) (defun org-roam-db--init (db)
"Initialize database DB with the correct schema and user version." "Initialize database DB with the correct schema and user version."
(emacsql-with-transaction db (emacsql-with-transaction db
(emacsql db "PRAGMA foreign_keys = ON")
(pcase-dolist (`(,table ,schema) org-roam-db--table-schemata) (pcase-dolist (`(,table ,schema) org-roam-db--table-schemata)
(emacsql db [:create-table $i1 $S2] table schema)) (emacsql db [:create-table $i1 $S2] table schema))
(pcase-dolist (`(,index-name ,table ,columns) org-roam-db--table-indices) (pcase-dolist (`(,index-name ,table ,columns) org-roam-db--table-indices)
@ -246,51 +275,93 @@ If FILE is nil, clear the current buffer."
file)) file))
;;;; Updating tables ;;;; Updating tables
(defun org-roam-db-insert-file ()
(defun org-roam-db--file-title ()
"In current Org buffer, get the title.
If there is no title, return the file name relative to
`org-roam-directory'."
(org-link-display-format
(or (string-join (cdr (assoc "TITLE" (org-collect-keywords '("title")))) " ")
(file-name-sans-extension (file-relative-name
(buffer-file-name (buffer-base-buffer))
org-roam-directory)))))
(defun org-roam-db-insert-file (&optional hash)
"Update the files table for the current buffer. "Update the files table for the current buffer.
If UPDATE-P is non-nil, first remove the file in the database." If UPDATE-P is non-nil, first remove the file in the database.
If HASH is non-nil, use that as the file's hash without recalculating it."
(let* ((file (buffer-file-name)) (let* ((file (buffer-file-name))
(file-title (org-roam-db--file-title))
(attr (file-attributes file)) (attr (file-attributes file))
(atime (file-attribute-access-time attr)) (atime (file-attribute-access-time attr))
(mtime (file-attribute-modification-time attr)) (mtime (file-attribute-modification-time attr))
(hash (org-roam-db--file-hash))) (hash (or hash (org-roam-db--file-hash file))))
(org-roam-db-query (org-roam-db-query
[:insert :into files [:insert :into files
:values $v1] :values $v1]
(list (vector file hash atime mtime))))) (list (vector file file-title hash atime mtime)))))
(defun org-roam-db-get-scheduled-time () (defun org-roam-db-get-scheduled-time ()
"Return the scheduled time at point in ISO8601 format." "Return the scheduled time at point in ISO8601 format."
(when-let ((time (org-get-scheduled-time (point)))) (when-let ((time (org-get-scheduled-time (point))))
(org-format-time-string "%FT%T%z" time))) (format-time-string "%FT%T" time)))
(defun org-roam-db-get-deadline-time () (defun org-roam-db-get-deadline-time ()
"Return the deadline time at point in ISO8601 format." "Return the deadline time at point in ISO8601 format."
(when-let ((time (org-get-deadline-time (point)))) (when-let ((time (org-get-deadline-time (point))))
(org-format-time-string "%FT%T%z" time))) (format-time-string "%FT%T" time)))
(defun org-roam-db-node-p () (defun org-roam-db-node-p ()
"Return t if headline at point is an Org-roam node, else return nil." "Return t if headline at point is an Org-roam node, else return nil."
(and (org-id-get) (and (org-id-get)
(not (cdr (assoc "ROAM_EXCLUDE" (org-entry-properties)))) (not (org-entry-get (point) "ROAM_EXCLUDE"))
(funcall org-roam-db-node-include-function))) (funcall org-roam-db-node-include-function)))
(defun org-roam-db-map-nodes (fns) (defun org-roam-db-map-nodes (fns)
"Run FNS over all nodes in the current buffer." "Run FNS over all nodes in the current buffer."
(org-with-point-at 1 (org-with-wide-buffer
(org-map-entries (org-map-region
(lambda () (lambda ()
(when (org-roam-db-node-p) (when (org-roam-db-node-p)
(dolist (fn fns) (dolist (fn fns)
(funcall fn))))))) (funcall fn))))
(point-min) (point-max))))
(defun org-roam-db-map-links (fns) (defun org-roam-db-map-links (fns)
"Run FNS over all links in the current buffer." "Run FNS over all links in the current buffer."
(org-with-point-at 1 (org-with-point-at 1
(org-element-map (org-element-parse-buffer) 'link (while (re-search-forward org-link-any-re nil :no-error)
(lambda (link) ;; `re-search-forward' let the cursor one character after the link, we need to go backward one char to
;; make the point be on the link.
(backward-char)
(let* ((begin (match-beginning 0))
(element (org-element-context))
(type (org-element-type element))
link)
(cond
;; Links correctly recognized by Org Mode
((eq type 'link)
(setq link element))
;; Links in property drawers and lines starting with #+. Recall that, as for Org Mode v9.4.4, the
;; org-element-type of links within properties drawers is "node-property" and for lines starting with
;; #+ is "keyword".
((and (member type org-roam-db-extra-links-elements)
(not (member-ignore-case (org-element-property :key element)
(cdr (assoc type org-roam-db-extra-links-exclude-keys))))
(setq link (save-excursion
(goto-char begin)
(save-match-data (org-element-link-parser)))))))
(when link
(dolist (fn fns) (dolist (fn fns)
(funcall fn link)))))) (funcall fn link)))))))
(defun org-roam-db-map-citations (info fns)
"Run FNS over all citations in the current buffer.
INFO is the org-element parsed buffer."
(org-element-map info 'citation-reference
(lambda (cite)
(dolist (fn fns)
(funcall fn cite)))))
(defun org-roam-db-insert-file-node () (defun org-roam-db-insert-file-node ()
"Insert the file-level node into the Org-roam cache." "Insert the file-level node into the Org-roam cache."
@ -299,19 +370,14 @@ If UPDATE-P is non-nil, first remove the file in the database."
(org-roam-db-node-p)) (org-roam-db-node-p))
(when-let ((id (org-id-get))) (when-let ((id (org-id-get)))
(let* ((file (buffer-file-name (buffer-base-buffer))) (let* ((file (buffer-file-name (buffer-base-buffer)))
(title (org-link-display-format (title (org-roam-db--file-title))
(or (cadr (assoc "TITLE" (org-collect-keywords '("title"))
#'string-equal))
(file-relative-name file org-roam-directory))))
(pos (point)) (pos (point))
(todo nil) (todo nil)
(priority nil) (priority nil)
(scheduled nil) (scheduled nil)
(deadline nil) (deadline nil)
(level 0) (level 0)
(aliases (org-entry-get (point) "ROAM_ALIASES"))
(tags org-file-tags) (tags org-file-tags)
(refs (org-entry-get (point) "ROAM_REFS"))
(properties (org-entry-properties)) (properties (org-entry-properties))
(olp nil)) (olp nil))
(org-roam-db-query! (org-roam-db-query!
@ -330,29 +396,8 @@ If UPDATE-P is non-nil, first remove the file in the database."
(mapcar (lambda (tag) (mapcar (lambda (tag)
(vector id (substring-no-properties tag))) (vector id (substring-no-properties tag)))
tags))) tags)))
(when aliases (org-roam-db-insert-aliases)
(org-roam-db-query (org-roam-db-insert-refs))))))
[:insert :into aliases
:values $v1]
(mapcar (lambda (alias)
(vector id alias))
(split-string-and-unquote aliases))))
(when refs
(setq refs (split-string-and-unquote refs))
(let (rows)
(dolist (ref refs)
(if (string-match org-link-plain-re ref)
(progn
(push (vector id (match-string 2 ref)
(match-string 1 ref)) rows))
(lwarn '(org-roam) :warning
"%s:%s\tInvalid ref %s, skipping..."
(buffer-file-name) (point) ref)))
(when rows
(org-roam-db-query
[:insert :into refs
:values $v1]
rows)))))))))
(cl-defun org-roam-db-insert-node-data () (cl-defun org-roam-db-insert-node-data ()
"Insert node data for headline at point into the Org-roam cache." "Insert node data for headline at point into the Org-roam cache."
@ -386,13 +431,14 @@ If UPDATE-P is non-nil, first remove the file in the database."
(defun org-roam-db-insert-aliases () (defun org-roam-db-insert-aliases ()
"Insert aliases for node at point into Org-roam cache." "Insert aliases for node at point into Org-roam cache."
(when-let ((node-id (org-id-get)) (when-let* ((node-id (org-id-get))
(aliases (org-entry-get (point) "ROAM_ALIASES"))) (aliases (org-entry-get (point) "ROAM_ALIASES"))
(aliases (split-string-and-unquote aliases)))
(org-roam-db-query [:insert :into aliases (org-roam-db-query [:insert :into aliases
:values $v1] :values $v1]
(mapcar (lambda (alias) (mapcar (lambda (alias)
(vector node-id alias)) (vector node-id alias))
(split-string-and-unquote aliases))))) aliases))))
(defun org-roam-db-insert-tags () (defun org-roam-db-insert-tags ()
"Insert tags for node at point into Org-roam cache." "Insert tags for node at point into Org-roam cache."
@ -411,39 +457,90 @@ If UPDATE-P is non-nil, first remove the file in the database."
(let (rows) (let (rows)
(dolist (ref refs) (dolist (ref refs)
(save-match-data (save-match-data
(if (string-match org-link-plain-re ref) (cond (;; @citeKey
(progn (string-prefix-p "@" ref)
(push (vector node-id (match-string 2 ref) (match-string 1 ref)) rows)) (push (vector node-id (substring ref 1) "cite") rows))
(;; [cite:@citeKey]
(string-prefix-p "[cite:" ref)
(condition-case nil
(let ((cite-obj (org-cite-parse-objects ref)))
(org-element-map cite-obj 'citation-reference
(lambda (cite)
(let ((key (org-element-property :key cite)))
(push (vector node-id key "cite") rows)))))
(error
(lwarn '(org-roam) :warning (lwarn '(org-roam) :warning
"%s:%s\tInvalid ref %s, skipping..." (buffer-file-name) (point) ref)))) "%s:%s\tInvalid cite %s, skipping..." (buffer-file-name) (point) ref))))
(;; https://google.com, cite:citeKey
;; Note: we use string-match here because it matches any link: e.g. [[cite:abc][abc]]
;; But this form of matching is loose, and can accept invalid links e.g. [[cite:abc]
(string-match org-link-any-re (org-link-encode ref '(#x20)))
(setq ref (org-link-encode ref '(#x20)))
(let ((ref-url (url-generic-parse-url (or (match-string 2 ref) (match-string 0 ref))))
(link-type ()) ;; clear url-type for backward compatible.
(path ()))
(setq link-type (url-type ref-url))
(setf (url-type ref-url) nil)
(setq path (org-link-decode (url-recreate-url ref-url)))
(if (and (boundp 'org-ref-cite-types)
(or (assoc link-type org-ref-cite-types)
(member link-type org-ref-cite-types)))
(dolist (key (org-roam-org-ref-path-to-keys path))
(push (vector node-id key link-type) rows))
(push (vector node-id path link-type) rows))))
(t
(lwarn '(org-roam) :warning
"%s:%s\tInvalid ref %s, skipping..." (buffer-file-name) (point) ref)))))
(when rows
(org-roam-db-query [:insert :into refs (org-roam-db-query [:insert :into refs
:values $v1] :values $v1]
rows)))) rows)))))
(defun org-roam-db-insert-link (link) (defun org-roam-db-insert-link (link)
"Insert link data for LINK at current point into the Org-roam cache." "Insert link data for LINK at current point into the Org-roam cache."
(save-excursion (save-excursion
(goto-char (org-element-property :begin link)) (goto-char (org-element-property :begin link))
(let ((type (org-element-property :type link)) (let* ((type (org-element-property :type link))
(path (org-element-property :path link)) (path (org-element-property :path link))
(option (and (string-match "::\\(.*\\)\\'" path)
(match-string 1 path)))
(path (if (not option) path
(substring path 0 (match-beginning 0))))
(source (org-roam-id-at-point))
(properties (list :outline (ignore-errors (properties (list :outline (ignore-errors
;; This can error if link is not under any headline ;; This can error if link is not under any headline
(org-get-outline-path 'with-self 'use-cache)))) (org-get-outline-path 'with-self 'use-cache))))
(source (org-roam-id-at-point))) (properties (if option (plist-put properties :search-option option)
properties)))
;; For Org-ref links, we need to split the path into the cite keys ;; For Org-ref links, we need to split the path into the cite keys
(when (and (boundp 'org-ref-cite-types)
(fboundp 'org-ref-split-and-strip-string)
(member type org-ref-cite-types))
(setq path (org-ref-split-and-strip-string path)))
(unless (listp path)
(setq path (list path)))
(when (and source path) (when (and source path)
(if (and (boundp 'org-ref-cite-types)
(or (assoc type org-ref-cite-types)
(member type org-ref-cite-types)))
(org-roam-db-query
[:insert :into citations
:values $v1]
(mapcar (lambda (k) (vector source k (point) properties))
(org-roam-org-ref-path-to-keys path)))
(org-roam-db-query (org-roam-db-query
[:insert :into links [:insert :into links
:values $v1] :values $v1]
(mapcar (lambda (p) (vector (point) source path type properties)))))))
(vector (point) source p type properties))
path)))))) (defun org-roam-db-insert-citation (citation)
"Insert data for CITATION at current point into the Org-roam cache."
(save-excursion
(goto-char (org-element-property :begin citation))
(let ((key (org-element-property :key citation))
(source (org-roam-id-at-point))
(properties (list :outline (ignore-errors
;; This can error if link is not under any headline
(org-get-outline-path 'with-self 'use-cache)))))
(when (and source key)
(org-roam-db-query
[:insert :into citations
:values $v1]
(vector source key (point) properties))))))
;;;; Fetching ;;;; Fetching
(defun org-roam-db--get-current-files () (defun org-roam-db--get-current-files ()
@ -454,35 +551,40 @@ If UPDATE-P is non-nil, first remove the file in the database."
(puthash (car row) (cadr row) ht)) (puthash (car row) (cadr row) ht))
ht)) ht))
(defun org-roam-db--file-hash (&optional file-path) (defun org-roam-db--file-hash (file-path)
"Compute the hash of FILE-PATH, a file or current buffer." "Compute the hash of FILE-PATH."
;; If it is a GPG encrypted file, we always want to compute the hash
;; for the GPG encrypted file (undecrypted)
(when (and (not file-path) (equal "gpg" (file-name-extension (buffer-file-name))))
(setq file-path (buffer-file-name)))
(if file-path
(with-temp-buffer (with-temp-buffer
(set-buffer-multibyte nil) (set-buffer-multibyte nil)
(insert-file-contents-literally file-path) (insert-file-contents-literally file-path)
(secure-hash 'sha1 (current-buffer))) (secure-hash 'sha1 (current-buffer))))
(org-with-wide-buffer
(secure-hash 'sha1 (current-buffer)))))
;;;; Synchronization ;;;; Synchronization
(defun org-roam-db-update-file (&optional file-path) (defun org-roam-db-update-file (&optional file-path no-require)
"Update Org-roam cache for FILE-PATH. "Update Org-roam cache for FILE-PATH.
If the file does not exist anymore, remove it from the cache. If the file does not exist anymore, remove it from the cache.
If the file exists, update the cache with information."
If the file exists, update the cache with information.
If NO-REQUIRE, don't require optional libraries. Set NO-REQUIRE
when the libraries are already required at some toplevel, e.g.
in `org-roam-db-sync'."
(setq file-path (or file-path (buffer-file-name (buffer-base-buffer)))) (setq file-path (or file-path (buffer-file-name (buffer-base-buffer))))
(let ((content-hash (org-roam-db--file-hash file-path)) (let ((content-hash (org-roam-db--file-hash file-path))
(db-hash (caar (org-roam-db-query [:select hash :from files (db-hash (caar (org-roam-db-query [:select hash :from files
:where (= file $s1)] file-path)))) :where (= file $s1)] file-path)))
info)
(unless (string= content-hash db-hash) (unless (string= content-hash db-hash)
(unless no-require
(org-roam-require '(org-ref oc)))
(org-roam-with-file file-path nil (org-roam-with-file file-path nil
(save-excursion (emacsql-with-transaction (org-roam-db)
(org-with-wide-buffer
(org-set-regexps-and-options 'tags-only) (org-set-regexps-and-options 'tags-only)
;; Org doesn't use this anymore, so we probably should stop too.
;; (org-refresh-category-properties)
(org-roam-db-clear-file) (org-roam-db-clear-file)
(org-roam-db-insert-file) (org-roam-db-insert-file content-hash)
(org-roam-db-insert-file-node) (org-roam-db-insert-file-node)
(setq org-outline-path-cache nil) (setq org-outline-path-cache nil)
(org-roam-db-map-nodes (org-roam-db-map-nodes
@ -491,8 +593,14 @@ If the file exists, update the cache with information."
#'org-roam-db-insert-tags #'org-roam-db-insert-tags
#'org-roam-db-insert-refs)) #'org-roam-db-insert-refs))
(setq org-outline-path-cache nil) (setq org-outline-path-cache nil)
(setq info (org-element-parse-buffer))
(org-roam-db-map-links (org-roam-db-map-links
(list #'org-roam-db-insert-link))))))) (list #'org-roam-db-insert-link))
(when (fboundp 'org-cite-insert)
(require 'oc) ;ensure feature is loaded
(org-roam-db-map-citations
info
(list #'org-roam-db-insert-citation)))))))))
;;;###autoload ;;;###autoload
(defun org-roam-db-sync (&optional force) (defun org-roam-db-sync (&optional force)
@ -502,6 +610,7 @@ If FORCE, force a rebuild of the cache from scratch."
(org-roam-db--close) ;; Force a reconnect (org-roam-db--close) ;; Force a reconnect
(when force (delete-file org-roam-db-location)) (when force (delete-file org-roam-db-location))
(org-roam-db) ;; To initialize the database, no-op if already initialized (org-roam-db) ;; To initialize the database, no-op if already initialized
(org-roam-require '(org-ref oc))
(let* ((gc-cons-threshold org-roam-db-gc-threshold) (let* ((gc-cons-threshold org-roam-db-gc-threshold)
(org-agenda-files nil) (org-agenda-files nil)
(org-roam-files (org-roam-list-files)) (org-roam-files (org-roam-list-files))
@ -514,18 +623,17 @@ If FORCE, force a rebuild of the cache from scratch."
(push file modified-files))) (push file modified-files)))
(remhash file current-files)) (remhash file current-files))
(emacsql-with-transaction (org-roam-db) (emacsql-with-transaction (org-roam-db)
(if (fboundp 'dolist-with-progress-reporter) (org-roam-dolist-with-progress (file (hash-table-keys current-files))
(dolist-with-progress-reporter (file (hash-table-keys current-files))
"Clearing removed files..." "Clearing removed files..."
(org-roam-db-clear-file file)) (org-roam-db-clear-file file))
(dolist (file (hash-table-keys current-files)) (org-roam-dolist-with-progress (file modified-files)
(org-roam-db-clear-file file)))
(if (fboundp 'dolist-with-progress-reporter)
(dolist-with-progress-reporter (file modified-files)
"Processing modified files..." "Processing modified files..."
(org-roam-db-update-file file)) (condition-case err
(dolist (file modified-files) (org-roam-db-update-file file 'no-require)
(org-roam-db-update-file file)))))) (error
(org-roam-db-clear-file file)
(lwarn 'org-roam :error "Failed to process %s with error %s, skipping..."
file (error-message-string err))))))))
;;;###autoload ;;;###autoload
(define-minor-mode org-roam-db-autosync-mode (define-minor-mode org-roam-db-autosync-mode

94
org-roam-id.el Normal file
View File

@ -0,0 +1,94 @@
;;; org-roam-id.el --- ID-related utilities for Org-roam -*- lexical-binding: t; -*-
;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience
;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;;
;; This module provides ID-related facilities using the Org-roam database.
;;
;;; Code:
(require 'org-id)
(defun org-roam-id-at-point ()
"Return the ID at point, if any.
Recursively traverses up the headline tree to find the
first encapsulating ID."
(org-with-wide-buffer
(org-back-to-heading-or-point-min t)
(while (and (not (org-roam-db-node-p))
(not (bobp)))
(org-roam-up-heading-or-point-min))
(when (org-roam-db-node-p)
(org-id-get))))
(defun org-roam-id-find (id &optional markerp)
"Return the location of the entry with the id ID using the Org-roam db.
The return value is a cons cell (file-name . position), or nil
if there is no entry with that ID.
With optional argument MARKERP, return the position as a new marker."
(cond
((symbolp id) (setq id (symbol-name id)))
((numberp id) (setq id (number-to-string id))))
(let ((node (org-roam-populate (org-roam-node-create :id id))))
(when-let ((file (org-roam-node-file node)))
(if markerp
(let ((buffer (or (find-buffer-visiting file)
(find-file-noselect file))))
(with-current-buffer buffer
(move-marker (make-marker) (org-roam-node-point node) buffer)))
(cons (org-roam-node-file node)
(org-roam-node-point node))))))
(defalias 'org-roam-id-open 'org-id-open
"Obsolete alias - use `org-id-open' directly.")
(advice-add 'org-id-find :before-until #'org-roam-id-find)
;;;###autoload
(defun org-roam-update-org-id-locations (&rest directories)
"Scan Org-roam files to update `org-id' related state.
This is like `org-id-update-id-locations', but will automatically
use the currently bound `org-directory' and `org-roam-directory'
along with DIRECTORIES (if any), where the lookup for files in
these directories will be always recursive.
Note: Org-roam doesn't have hard dependency on
`org-id-locations-file' to lookup IDs for nodes that are stored
in the database, but it still tries to properly integrates with
`org-id'. This allows the user to cross-reference IDs outside of
the current `org-roam-directory', and also link with \"id:\"
links to headings/files within the current `org-roam-directory'
that are excluded from identification in Org-roam as
`org-roam-node's, e.g. with \"ROAM_EXCLUDE\" property."
(interactive)
(cl-loop for dir in (cons org-roam-directory directories)
for org-roam-directory = dir
nconc (org-roam-list-files) into files
finally (org-id-update-id-locations files org-roam-verbose)))
(provide 'org-roam-id)
;;; org-roam-id.el ends here

53
org-roam-log.el Normal file
View File

@ -0,0 +1,53 @@
;;; org-roam-log.el --- Integrations with Org-log -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2022-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience
;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (emacsql "4.1.0") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Commentary:
;;
;; This module provides integrations with Org-log.
;;
;;; Code:
(require 'org-roam)
(defcustom org-roam-log-setup-hook nil
"Hook run when a log for an Org-roam file is setup."
:group 'org-roam
:type 'hook)
(defun org-roam-log-p ()
"Return t if the log buffer is for an Org-roam file, nil otherwise."
(and org-log-note-marker
(org-roam-file-p (buffer-file-name (marker-buffer org-log-note-marker)))))
(defun org-roam-log--setup ()
"Run hooks in `org-roam-log-setup-hook'."
(run-hooks 'org-roam-log-setup-hook))
(add-hook 'org-roam-log-setup-hook #'org-roam--register-completion-functions-h)
(add-hook 'org-log-buffer-setup-hook #'org-roam-log--setup)
(provide 'org-roam-log)
;;; org-roam-log.el ends here

View File

@ -1,12 +1,12 @@
;;; org-roam-migrate.el --- Migration utilities from v1 to v2 -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam-migrate.el --- Migration utilities from v1 to v2 -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (emacsql "4.1.0") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -34,48 +34,6 @@
;;; Code: ;;; Code:
(require 'org-roam) (require 'org-roam)
;;; v1 breaking warning
(defvar org-roam-v2-ack nil
"When set to t, won't display the annoying warning message about the upgrade.
Need to be set before the package is loaded, otherwise won't take
any affect.")
(unless org-roam-v2-ack
(lwarn 'org-roam :error "
------------------------------------
WARNING: You're now on Org-roam v2!
------------------------------------
You may have arrived here from a package upgrade. Please read the
wiki entry at
%s
for an overview of the major changes.
Notes taken in v1 are incompatible with v2, but you can upgrade
them to the v2 format via a simple command. To migrate your
notes, first make sure you're on at least Org 9.4 (check with
C-h v org-version) and set your org-roam-directory to your notes:
(setq org-roam-directory \"path/to/org/files\")
then, run:
M-x org-roam-migrate-wizard
If you wish to stay on v1, v1 is unfortunately not distributed on
MELPA. See org-roam/org-roam-v1 on GitHub on how to install v1.
If you've gone through the migration steps (if necessary), and
know what you're doing set `org-roam-v2-ack' to `t' to disable
this warning. You can do so by adding:
(setq org-roam-v2-ack t)
To your init file.
"
"https://github.com/org-roam/org-roam/wiki/Hitchhiker's-Rough-Guide-to-Org-roam-V2"))
;;; Migration wizard (v1 -> v2) ;;; Migration wizard (v1 -> v2)
;;;###autoload ;;;###autoload
(defun org-roam-migrate-wizard () (defun org-roam-migrate-wizard ()

View File

@ -1,12 +1,12 @@
;;; org-roam-mode.el --- Major mode for special Org-roam buffers -*- lexical-binding: t -*- ;;; org-roam-mode.el --- Major mode for special Org-roam buffers -*- lexical-binding: t -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (emacsql "4.1.0") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -39,12 +39,50 @@
(defvar org-ref-buffer-hacked) (defvar org-ref-buffer-hacked)
;;; Options ;;; Options
(defcustom org-roam-mode-section-functions (list #'org-roam-backlinks-section (defcustom org-roam-mode-sections (list #'org-roam-backlinks-section
#'org-roam-reflinks-section) #'org-roam-reflinks-section)
"Functions that insert sections in the `org-roam-mode' based buffers. "A list of sections for the `org-roam-mode' based buffers.
Each function is called with one argument, which is an Each section is a function that is passed the `org-roam-node'
`org-roam-node' for which the buffer will be constructed for. for which the section will be constructed as the first
Normally this node is `org-roam-buffer-current-node'." argument. Normally this node is `org-roam-buffer-current-node'.
The function may also accept other optional arguments. Each item
in the list is either:
1. A function, which is called only with the `org-roam-node' as the argument
2. A list, containing the function and the optional arguments.
For example, one can add
(org-roam-backlinks-section :unique t)
to the list to pass :unique t to the section-rendering function."
:group 'org-roam
:type `(repeat (choice (symbol :tag "Function")
(list :tag "Function with arguments"
(symbol :tag "Function")
(repeat :tag "Arguments" :inline t (sexp :tag "Arg"))))))
(defcustom org-roam-buffer-postrender-functions (list)
"Functions to run after the Org-roam buffer is rendered.
Each function accepts no arguments, and is run with the Org-roam
buffer as the current buffer."
:group 'org-roam
:type 'hook)
(defcustom org-roam-preview-function #'org-roam-preview-default-function
"The preview function to use to populate the Org-roam buffer.
The function takes no arguments, but the point is temporarily set
to the exact location of the backlink."
:group 'org-roam
:type 'function)
(defcustom org-roam-preview-postprocess-functions (list #'org-roam-strip-comments)
"A list of functions to postprocess the preview content.
Each function takes a single argument, the string for the preview
content, and returns the post-processed string. The functions are
applied in order of appearance in the list."
:group 'org-roam :group 'org-roam
:type 'hook) :type 'hook)
@ -144,7 +182,7 @@ This mode is used by special Org-roam buffers, such as persistent
`org-roam-buffer' and dedicated Org-roam buffers `org-roam-buffer' and dedicated Org-roam buffers
\(`org-roam-buffer-display-dedicated'), which render the \(`org-roam-buffer-display-dedicated'), which render the
information in a section-like manner (see information in a section-like manner (see
`org-roam-mode-section-functions'), with which the user can `org-roam-mode-sections'), with which the user can
interact with." interact with."
:group 'org-roam :group 'org-roam
(face-remap-add-relative 'header-line 'org-roam-header-line)) (face-remap-add-relative 'header-line 'org-roam-header-line))
@ -160,7 +198,8 @@ value shows the current node in the persistent `org-roam-buffer'.")
(defvar org-roam-buffer-current-directory nil (defvar org-roam-buffer-current-directory nil
"The `org-roam-directory' value of `org-roam-buffer-current-node'. "The `org-roam-directory' value of `org-roam-buffer-current-node'.
Set both, locally and globally in the same way as `org-roam-buffer-current-node'.") Set both, locally and globally in the same way as
`org-roam-buffer-current-node'.")
(put 'org-roam-buffer-current-directory 'permanent-local t) (put 'org-roam-buffer-current-directory 'permanent-local t)
@ -203,7 +242,15 @@ buffer."
(org-roam-node-title org-roam-buffer-current-node)) (org-roam-node-title org-roam-buffer-current-node))
(magit-insert-section (org-roam) (magit-insert-section (org-roam)
(magit-insert-heading) (magit-insert-heading)
(run-hook-with-args 'org-roam-mode-section-functions org-roam-buffer-current-node)) (dolist (section org-roam-mode-sections)
(pcase section
((pred functionp)
(funcall section org-roam-buffer-current-node))
(`(,fn . ,args)
(apply fn (cons org-roam-buffer-current-node args)))
(_
(user-error "Invalid `org-roam-mode-sections' specification")))))
(run-hooks 'org-roam-buffer-postrender-functions)
(goto-char 0))) (goto-char 0)))
(defun org-roam-buffer-set-header-line-format (string) (defun org-roam-buffer-set-header-line-format (string)
@ -263,7 +310,7 @@ To toggle its display use `org-roam-buffer-toggle' command.")
(pcase (org-roam-buffer--visibility) (pcase (org-roam-buffer--visibility)
('visible ('visible
(progn (progn
(delete-window (get-buffer-window org-roam-buffer)) (quit-window nil (get-buffer-window org-roam-buffer))
(remove-hook 'post-command-hook #'org-roam-buffer--redisplay-h))) (remove-hook 'post-command-hook #'org-roam-buffer--redisplay-h)))
((or 'exists 'none) ((or 'exists 'none)
(progn (progn
@ -272,7 +319,7 @@ To toggle its display use `org-roam-buffer-toggle' command.")
(define-inline org-roam-buffer--visibility () (define-inline org-roam-buffer--visibility ()
"Return the current visibility state of the persistent `org-roam-buffer'. "Return the current visibility state of the persistent `org-roam-buffer'.
Valid states are 'visible, 'exists and 'none." Valid states are `visible', `exists' and `none'."
(declare (side-effect-free t)) (declare (side-effect-free t))
(inline-quote (inline-quote
(cond (cond
@ -292,7 +339,7 @@ Has no effect when there's no `org-roam-node-at-point'."
(add-hook 'kill-buffer-hook #'org-roam-buffer--persistent-cleanup-h nil t))))) (add-hook 'kill-buffer-hook #'org-roam-buffer--persistent-cleanup-h nil t)))))
(defun org-roam-buffer--persistent-cleanup-h () (defun org-roam-buffer--persistent-cleanup-h ()
"Clean-up global state thats dedicated for the persistent `org-roam-buffer'." "Clean-up global state that's dedicated for the persistent `org-roam-buffer'."
(setq-default org-roam-buffer-current-node nil (setq-default org-roam-buffer-current-node nil
org-roam-buffer-current-directory nil)) org-roam-buffer-current-directory nil))
@ -394,90 +441,32 @@ In interactive calls OTHER-WINDOW is set with
(with-current-buffer buf (with-current-buffer buf
(widen) (widen)
(goto-char point)) (goto-char point))
(when (org-invisible-p) (org-show-context)) (when (org-invisible-p) (org-fold-show-context))
buf)) buf))
(defun org-roam-preview-get-contents (file point) (defun org-roam-preview-default-function ()
"Get preview content for FILE at POINT." "Return the preview content at point.
This function returns the all contents under the current
headline, up to the next headline."
(let ((beg (save-excursion
(org-roam-end-of-meta-data t)
(point)))
(end (save-excursion
(org-next-visible-heading 1)
(point))))
(string-trim (buffer-substring-no-properties beg end))))
(defun org-roam-preview-get-contents (file pt)
"Get preview content for FILE at PT."
(save-excursion (save-excursion
(org-roam-with-temp-buffer file (org-roam-with-temp-buffer file
(goto-char point)
(let ((elem (org-element-at-point)))
;; We want the parent element always
(while (org-element-property :parent elem)
(setq elem (org-element-property :parent elem)))
(pcase (car elem)
('headline ; show subtree
(org-roam-preview-get-entry-text (point-marker) most-positive-fixnum))
(_
(let ((begin (org-element-property :begin elem))
(end (org-element-property :end elem)))
(or (string-trim (buffer-substring-no-properties begin end))
(org-element-property :raw-value elem)))))))))
(defun org-roam-preview-get-entry-text (marker n-lines &optional indent)
"Extract entry text from MARKER, at most N-LINES lines.
This will ignore drawers etc, just get the text.
If INDENT is given, prefix every line with this string."
(let (txt ind)
(save-excursion
(with-current-buffer (marker-buffer marker)
(if (not (derived-mode-p 'org-mode))
(setq txt "")
(org-with-wide-buffer (org-with-wide-buffer
(goto-char marker) (goto-char pt)
(end-of-line 1) (let ((s (funcall org-roam-preview-function)))
(setq txt (buffer-substring (dolist (fn org-roam-preview-postprocess-functions)
(min (1+ (point)) (point-max)) (setq s (funcall fn s)))
(progn (outline-next-heading) (point)))) s)))))
(with-temp-buffer
(insert txt)
(goto-char (point-min))
(while (org-activate-links (point-max))
(goto-char (match-end 0)))
(goto-char (point-min))
(while (re-search-forward org-link-bracket-re (point-max) t)
(set-text-properties (match-beginning 0) (match-end 0)
nil))
(goto-char (point-min))
(while (re-search-forward org-drawer-regexp nil t)
(delete-region
(match-beginning 0)
(progn (re-search-forward
"^[ \t]*:END:.*\n?" nil 'move)
(point))))
(goto-char (point-min))
(goto-char (point-max))
(skip-chars-backward " \t\n")
(when (looking-at "[ \t\n]+\\'") (replace-match ""))
;; find and remove min common indentation
(goto-char (point-min))
(untabify (point-min) (point-max))
(setq ind (current-indentation))
(while (not (eobp))
(unless (looking-at "[ \t]*$")
(setq ind (min ind (current-indentation))))
(beginning-of-line 2))
(goto-char (point-min))
(while (not (eobp))
(unless (looking-at "[ \t]*$")
(move-to-column ind)
(delete-region (point-at-bol) (point)))
(beginning-of-line 2))
(goto-char (point-min))
(when indent
(while (and (not (eobp)) (re-search-forward "^" nil t))
(replace-match indent t t)))
(goto-char (point-min))
(while (looking-at "[ \t]*\n") (replace-match ""))
(goto-char (point-max))
(when (> (org-current-line)
n-lines)
(org-goto-line (1+ n-lines))
(backward-char 1))
(setq txt (buffer-substring (point-min) (point))))))))
txt))
;;;; Backlinks ;;;; Backlinks
(cl-defstruct (org-roam-backlink (:constructor org-roam-backlink-create) (cl-defstruct (org-roam-backlink (:constructor org-roam-backlink-create)
@ -493,14 +482,23 @@ If INDENT is given, prefix every line with this string."
(org-roam-populate (org-roam-backlink-target-node backlink))) (org-roam-populate (org-roam-backlink-target-node backlink)))
backlink) backlink)
(defun org-roam-backlinks-get (node) (cl-defun org-roam-backlinks-get (node &key unique)
"Return the backlinks for NODE." "Return the backlinks for NODE.
(let ((backlinks (org-roam-db-query
When UNIQUE is nil, show all positions where references are found.
When UNIQUE is t, limit to unique sources."
(let* ((sql (if unique
[:select :distinct [source dest pos properties]
:from links
:where (= dest $s1)
:and (= type "id")
:group :by source
:having (funcall min pos)]
[:select [source dest pos properties] [:select [source dest pos properties]
:from links :from links
:where (= dest $s1) :where (= dest $s1)
:and (= type "id")] :and (= type "id")]))
(org-roam-node-id node)))) (backlinks (org-roam-db-query sql (org-roam-node-id node))))
(cl-loop for backlink in backlinks (cl-loop for backlink in backlinks
collect (pcase-let ((`(,source-id ,dest-id ,pos ,properties) backlink)) collect (pcase-let ((`(,source-id ,dest-id ,pos ,properties) backlink))
(org-roam-populate (org-roam-populate
@ -516,16 +514,28 @@ Sorts by title."
(string< (org-roam-node-title (org-roam-backlink-source-node a)) (string< (org-roam-node-title (org-roam-backlink-source-node a))
(org-roam-node-title (org-roam-backlink-source-node b)))) (org-roam-node-title (org-roam-backlink-source-node b))))
(defun org-roam-backlinks-section (node) (cl-defun org-roam-backlinks-section (node &key (unique nil) (show-backlink-p nil)
"The backlinks section for NODE." (section-heading "Backlinks:"))
(when-let ((backlinks (seq-sort #'org-roam-backlinks-sort (org-roam-backlinks-get node)))) "The backlinks section for NODE.
When UNIQUE is nil, show all positions where references are found.
When UNIQUE is t, limit to unique sources.
When SHOW-BACKLINK-P is not null, only show backlinks for which
this predicate is not nil.
SECTION-HEADING is the string used as a heading for the backlink section."
(when-let ((backlinks (seq-sort #'org-roam-backlinks-sort (org-roam-backlinks-get node :unique unique))))
(magit-insert-section (org-roam-backlinks) (magit-insert-section (org-roam-backlinks)
(magit-insert-heading "Backlinks:") (magit-insert-heading section-heading)
(dolist (backlink backlinks) (dolist (backlink backlinks)
(when (or (null show-backlink-p)
(and (not (null show-backlink-p))
(funcall show-backlink-p backlink)))
(org-roam-node-insert-section (org-roam-node-insert-section
:source-node (org-roam-backlink-source-node backlink) :source-node (org-roam-backlink-source-node backlink)
:point (org-roam-backlink-point backlink) :point (org-roam-backlink-point backlink)
:properties (org-roam-backlink-properties backlink))) :properties (org-roam-backlink-properties backlink))))
(insert ?\n)))) (insert ?\n))))
;;;; Reflinks ;;;; Reflinks
@ -542,22 +552,27 @@ Sorts by title."
(defun org-roam-reflinks-get (node) (defun org-roam-reflinks-get (node)
"Return the reflinks for NODE." "Return the reflinks for NODE."
(let ((refs (org-roam-db-query [:select [ref] :from refs (let ((refs (org-roam-db-query [:select :distinct [refs:ref links:source links:pos links:properties]
:where (= node-id $s1)] :from refs
:left-join links
:where (= refs:node-id $s1)
:and (= links:dest refs:ref)
:union
:select :distinct [refs:ref citations:node-id
citations:pos citations:properties]
:from refs
:left-join citations
:where (= refs:node-id $s1)
:and (= citations:cite-key refs:ref)]
(org-roam-node-id node))) (org-roam-node-id node)))
links) links)
(pcase-dolist (`(,ref) refs) (pcase-dolist (`(,ref ,source-id ,pos ,properties) refs)
(pcase-dolist (`(,source-id ,pos ,properties) (org-roam-db-query
[:select [source pos properties]
:from links
:where (= dest $s1)]
ref))
(push (org-roam-populate (push (org-roam-populate
(org-roam-reflink-create (org-roam-reflink-create
:source-node (org-roam-node-create :id source-id) :source-node (org-roam-node-create :id source-id)
:ref ref :ref ref
:point pos :point pos
:properties properties)) links))) :properties properties)) links))
links)) links))
(defun org-roam-reflinks-sort (a b) (defun org-roam-reflinks-sort (a b)
@ -568,8 +583,8 @@ Sorts by title."
(defun org-roam-reflinks-section (node) (defun org-roam-reflinks-section (node)
"The reflinks section for NODE." "The reflinks section for NODE."
(when (org-roam-node-refs node) (when-let ((refs (org-roam-node-refs node))
(let* ((reflinks (seq-sort #'org-roam-reflinks-sort (org-roam-reflinks-get node)))) (reflinks (seq-sort #'org-roam-reflinks-sort (org-roam-reflinks-get node))))
(magit-insert-section (org-roam-reflinks) (magit-insert-section (org-roam-reflinks)
(magit-insert-heading "Reflinks:") (magit-insert-heading "Reflinks:")
(dolist (reflink reflinks) (dolist (reflink reflinks)
@ -577,7 +592,7 @@ Sorts by title."
:source-node (org-roam-reflink-source-node reflink) :source-node (org-roam-reflink-source-node reflink)
:point (org-roam-reflink-point reflink) :point (org-roam-reflink-point reflink)
:properties (org-roam-reflink-properties reflink))) :properties (org-roam-reflink-properties reflink)))
(insert ?\n))))) (insert ?\n))))
;;;; Grep ;;;; Grep
(defvar org-roam-grep-map (defvar org-roam-grep-map
@ -615,7 +630,7 @@ instead."
(forward-line (1- row))) (forward-line (1- row)))
(when col (when col
(forward-char (1- col)))) (forward-char (1- col))))
(when (org-invisible-p) (org-show-context)) (when (org-invisible-p) (org-fold-show-context))
buf)) buf))
;;;; Unlinked references ;;;; Unlinked references
@ -643,23 +658,28 @@ This is the ROW within FILE."
(end-of-line) (end-of-line)
(point))))) (point)))))
(defun org-roam-unlinked-references-section (node) (defun org-roam-unlinked-references--rg-command (titles)
"The unlinked references section for NODE. "Return the ripgrep command searching for TITLES."
References from FILE are excluded." (concat "rg --follow --only-matching --vimgrep --pcre2 --ignore-case "
(when (and (executable-find "rg") (mapconcat (lambda (glob) (concat "--glob " glob))
(not (string-match "PCRE2 is not available"
(shell-command-to-string "rg --pcre2-version"))))
(let* ((titles (cons (org-roam-node-title node)
(org-roam-node-aliases node)))
(rg-command (concat "rg -o --vimgrep -P -i "
(mapconcat (lambda (glob) (concat "-g " glob))
(org-roam--list-files-search-globs org-roam-file-extensions) (org-roam--list-files-search-globs org-roam-file-extensions)
" ") " ")
(format " '\\[([^[]]++|(?R))*\\]%s' " (format " '\\[([^[]]++|(?R))*\\]%s' "
(mapconcat (lambda (title) (mapconcat (lambda (title)
(format "|(\\b%s\\b)" (shell-quote-argument title))) (format "|(\\b%s\\b)" (shell-quote-argument title)))
titles "")) titles ""))
org-roam-directory)) (shell-quote-argument org-roam-directory)))
(defun org-roam-unlinked-references-section (node)
"The unlinked references section for NODE.
References from FILE are excluded."
(when (and (executable-find "rg")
(org-roam-node-title node)
(not (string-match "PCRE2 is not available"
(shell-command-to-string "rg --pcre2-version"))))
(let* ((titles (cons (org-roam-node-title node)
(org-roam-node-aliases node)))
(rg-command (org-roam-unlinked-references--rg-command titles))
(results (split-string (shell-command-to-string rg-command) "\n")) (results (split-string (shell-command-to-string rg-command) "\n"))
f row col match) f row col match)
(magit-insert-section (unlinked-references) (magit-insert-section (unlinked-references)
@ -672,14 +692,14 @@ References from FILE are excluded."
col (string-to-number (match-string 3 line)) col (string-to-number (match-string 3 line))
match (match-string 4 line)) match (match-string 4 line))
(when (and match (when (and match
(not (f-equal-p (org-roam-node-file node) f)) (not (file-equal-p (org-roam-node-file node) f))
(member (downcase match) (mapcar #'downcase titles))) (member (downcase match) (mapcar #'downcase titles)))
(magit-insert-section section (org-roam-grep-section) (magit-insert-section section (org-roam-grep-section)
(oset section file f) (oset section file f)
(oset section row row) (oset section row row)
(oset section col col) (oset section col col)
(insert (propertize (format "%s:%s:%s" (insert (propertize (format "%s:%s:%s"
(truncate-string-to-width (file-name-base f) 15 nil nil "...") (truncate-string-to-width (file-name-base f) 15 nil nil t)
row col) 'font-lock-face 'org-roam-dim) row col) 'font-lock-face 'org-roam-dim)
" " " "
(org-roam-fontify-like-in-org-mode (org-roam-fontify-like-in-org-mode

View File

@ -1,12 +1,12 @@
;;; org-roam-node.el --- Interfacing and interacting with nodes -*- lexical-binding: t; -*- ;;; org-roam-node.el --- Interfacing and interacting with nodes -*- lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.4") (magit-section "2.90.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -32,12 +32,12 @@
;; interactively. ;; interactively.
;; ;;
;;; Code: ;;; Code:
(require 'crm)
(require 'org-roam) (require 'org-roam)
;;; Options ;;; Options
;;;; Completing-read ;;;; Completing-read
(defcustom org-roam-node-display-template (defcustom org-roam-node-display-template "${title}"
"${title:*} ${tags:10}"
"Configures display formatting for Org-roam node. "Configures display formatting for Org-roam node.
Patterns of form \"${field-name:length}\" are interpolated based Patterns of form \"${field-name:length}\" are interpolated based
on the current node. on the current node.
@ -60,9 +60,13 @@ field. If it's not specified, the field will be inserted as is,
i.e. it won't be aligned nor trimmed. If it's an integer, the i.e. it won't be aligned nor trimmed. If it's an integer, the
field will be aligned accordingly and all the exceeding field will be aligned accordingly and all the exceeding
characters will be trimmed out. If it's \"*\", the field will use characters will be trimmed out. If it's \"*\", the field will use
as many characters as possible and will be aligned accordingly." as many characters as possible and will be aligned accordingly.
A closure can also be assigned to this variable in which case the
closure is evaluated and the return value is used as the
template. The closure must evaluate to a valid template string."
:group 'org-roam :group 'org-roam
:type 'string) :type '(string function))
(defcustom org-roam-node-annotation-function #'org-roam-node-read--annotation (defcustom org-roam-node-annotation-function #'org-roam-node-read--annotation
"This function used to attach annotations for `org-roam-node-read'. "This function used to attach annotations for `org-roam-node-read'.
@ -72,10 +76,22 @@ It takes a single argument NODE, which is an `org-roam-node' construct."
(defcustom org-roam-node-default-sort 'file-mtime (defcustom org-roam-node-default-sort 'file-mtime
"Default sort order for Org-roam node completions." "Default sort order for Org-roam node completions."
:type '(choice (const :tag "file-mtime" file-mtime) :type '(choice
(const :tag "none" nil)
(const :tag "file-mtime" file-mtime)
(const :tag "file-atime" file-atime)) (const :tag "file-atime" file-atime))
:group 'org-roam) :group 'org-roam)
(defcustom org-roam-node-formatter nil
"The link description for node insertion.
If a function is provided, the function should take a single
argument, an `org-roam-node', and return a string.
If a string is provided, it is a template string expanded by
`org-roam-node--format-entry'."
:group 'org-roam
:type '(string function))
(defcustom org-roam-node-template-prefixes (defcustom org-roam-node-template-prefixes
'(("tags" . "#") '(("tags" . "#")
("todo" . "t:")) ("todo" . "t:"))
@ -93,7 +109,15 @@ has the entry (\"tags\" . \"#\"), these will appear as
(defcustom org-roam-ref-annotation-function #'org-roam-ref-read--annotation (defcustom org-roam-ref-annotation-function #'org-roam-ref-read--annotation
"This function used to attach annotations for `org-roam-ref-read'. "This function used to attach annotations for `org-roam-ref-read'.
It takes a single argument REF, which is a propertized string.") It takes a single argument REF, which is a propertized string."
:group 'org-roam
:type '(function))
(defcustom org-roam-ref-prompt-function nil
"Function to prompt for ref strings in `org-roam-ref-add'.
Should take no arguments, prompt the user, and return a string."
:group 'org-roam
:type 'function)
;;;; Completion-at-point ;;;; Completion-at-point
(defcustom org-roam-completion-everywhere nil (defcustom org-roam-completion-everywhere nil
@ -114,18 +138,36 @@ It takes a single argument REF, which is a propertized string.")
:type 'boolean) :type 'boolean)
(defcustom org-roam-extract-new-file-path "%<%Y%m%d%H%M%S>-${slug}.org" (defcustom org-roam-extract-new-file-path "%<%Y%m%d%H%M%S>-${slug}.org"
"The file path to use when a node is extracted to its own file." "The file path template to use when a node is extracted to its own file.
This path is relative to `org-roam-directory'."
:group 'org-roam :group 'org-roam
:type 'string) :type 'string)
(defvar org-roam-node-history nil
"Minibuffer history of nodes.")
(defvar org-roam-ref-history nil
"Minibuffer history of refs.")
;;; Definition ;;; Definition
(cl-defstruct (org-roam-node (:constructor org-roam-node-create) (cl-defstruct (org-roam-node (:constructor org-roam-node-create)
(:copier nil)) (:copier nil))
"A heading or top level file with an assigned ID property." "A heading or top level file with an assigned ID property."
file file-hash file-atime file-mtime file file-title file-hash file-atime file-mtime
id level point todo priority scheduled deadline title properties olp id level point todo priority scheduled deadline title properties olp
tags aliases refs) tags aliases refs)
;; Shim `string-glyph-compose' and `string-glyph-decompose' for Emacs versions that do not have it.
;; The functions were introduced in emacs commit 3f096eb3405b2fce7c35366eb2dcf025dda55783 and the
;; (original) functions behind them aren't autoloaded anymore.
(dolist (sym.replace
'((string-glyph-compose . ucs-normalize-NFC-string)
(string-glyph-decompose . ucs-normalize-NFD-string)))
(let ((emacs-29-symbol (car sym.replace))
(previous-implementation (cdr sym.replace)))
(unless (fboundp emacs-29-symbol)
(defalias emacs-29-symbol previous-implementation))))
(cl-defmethod org-roam-node-slug ((node org-roam-node)) (cl-defmethod org-roam-node-slug ((node org-roam-node))
"Return the slug of NODE." "Return the slug of NODE."
(let ((title (org-roam-node-title node)) (let ((title (org-roam-node-title node))
@ -140,6 +182,7 @@ It takes a single argument REF, which is a propertized string.")
776 ; U+0308 COMBINING DIAERESIS 776 ; U+0308 COMBINING DIAERESIS
777 ; U+0309 COMBINING HOOK ABOVE 777 ; U+0309 COMBINING HOOK ABOVE
778 ; U+030A COMBINING RING ABOVE 778 ; U+030A COMBINING RING ABOVE
779 ; U+030B COMBINING DOUBLE ACUTE ACCENT
780 ; U+030C COMBINING CARON 780 ; U+030C COMBINING CARON
795 ; U+031B COMBINING HORN 795 ; U+031B COMBINING HORN
803 ; U+0323 COMBINING DOT BELOW 803 ; U+0323 COMBINING DOT BELOW
@ -151,14 +194,12 @@ It takes a single argument REF, which is a propertized string.")
816 ; U+0330 COMBINING TILDE BELOW 816 ; U+0330 COMBINING TILDE BELOW
817 ; U+0331 COMBINING MACRON BELOW 817 ; U+0331 COMBINING MACRON BELOW
))) )))
(cl-flet* ((nonspacing-mark-p (char) (cl-flet* ((nonspacing-mark-p (char) (memq char slug-trim-chars))
(memq char slug-trim-chars)) (strip-nonspacing-marks (s) (string-glyph-compose
(strip-nonspacing-marks (s) (apply #'string
(ucs-normalize-NFC-string (seq-remove #'nonspacing-mark-p
(apply #'string (seq-remove #'nonspacing-mark-p (string-glyph-decompose s)))))
(ucs-normalize-NFD-string s))))) (cl-replace (title pair) (replace-regexp-in-string (car pair) (cdr pair) title)))
(cl-replace (title pair)
(replace-regexp-in-string (car pair) (cdr pair) title)))
(let* ((pairs `(("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric (let* ((pairs `(("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric
("__*" . "_") ;; remove sequential underscores ("__*" . "_") ;; remove sequential underscores
("^_" . "") ;; remove starting underscore ("^_" . "") ;; remove starting underscore
@ -166,6 +207,20 @@ It takes a single argument REF, which is a propertized string.")
(slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs))) (slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs)))
(downcase slug))))) (downcase slug)))))
(cl-defmethod org-roam-node-formatted ((node org-roam-node))
"Return a formatted string for NODE."
(pcase org-roam-node-formatter
((pred functionp)
(funcall org-roam-node-formatter node))
((pred stringp)
(org-roam-node--format-entry (org-roam-node--process-display-format org-roam-node-formatter) node))
(_
(org-roam-node-title node))))
(cl-defmethod org-roam-node-category ((node org-roam-node))
"Return the category for NODE."
(cdr (assoc-string "CATEGORY" (org-roam-node-properties node))))
;;; Nodes ;;; Nodes
;;;; Getters ;;;; Getters
(defun org-roam-node-at-point (&optional assert) (defun org-roam-node-at-point (&optional assert)
@ -180,9 +235,12 @@ populated."
(magit-section-up) (magit-section-up)
(org-roam-node-at-point))) (org-roam-node-at-point)))
(t (org-with-wide-buffer (t (org-with-wide-buffer
(org-back-to-heading-or-point-min) (while (not (or (org-roam-db-node-p)
(while (and (not (org-roam-db-node-p)) (bobp)
(not (bobp))) (eq (funcall outline-level)
(save-excursion
(org-roam-up-heading-or-point-min)
(funcall outline-level)))))
(org-roam-up-heading-or-point-min)) (org-roam-up-heading-or-point-min))
(when-let ((id (org-id-get))) (when-let ((id (org-id-get)))
(org-roam-populate (org-roam-populate
@ -199,17 +257,21 @@ Return nil if a node with ID does not exist."
id)) 0) id)) 0)
(org-roam-populate (org-roam-node-create :id id)))) (org-roam-populate (org-roam-node-create :id id))))
(defun org-roam-node-from-title-or-alias (s) (defun org-roam-node-from-title-or-alias (s &optional nocase)
"Return an `org-roam-node' for the node with title or alias S. "Return an `org-roam-node' for the node with title or alias S.
Return nil if the node does not exist. Return nil if the node does not exist.
Throw an error if multiple choices exist." Throw an error if multiple choices exist.
If NOCASE is non-nil, the query is case insensitive. It is case sensitive otherwise."
(let ((matches (seq-uniq (let ((matches (seq-uniq
(append (append
(org-roam-db-query [:select [id] :from nodes (org-roam-db-query (vconcat [:select [id] :from nodes
:where (= title $s1)] :where (= title $s1)]
(if nocase [ :collate NOCASE ]))
s) s)
(org-roam-db-query [:select [node-id] :from aliases (org-roam-db-query (vconcat [:select [node-id] :from aliases
:where (= alias $s1)] :where (= alias $s1)]
(if nocase [ :collate NOCASE ]))
s))))) s)))))
(cond (cond
((seq-empty-p matches) ((seq-empty-p matches)
@ -223,9 +285,15 @@ Throw an error if multiple choices exist."
"Return an `org-roam-node' from REF reference. "Return an `org-roam-node' from REF reference.
Return nil if there's no node with such REF." Return nil if there's no node with such REF."
(save-match-data (save-match-data
(when (string-match org-link-plain-re ref) (let (type path)
(let ((type (match-string 1 ref)) (cond
(path (match-string 2 ref))) ((string-match org-link-plain-re ref)
(setq type (match-string 1 ref)
path (match-string 2 ref)))
((string-prefix-p "@" ref)
(setq type "cite"
path (substring ref 1))))
(when (and type path)
(when-let ((id (caar (org-roam-db-query (when-let ((id (caar (org-roam-db-query
[:select [nodes:id] [:select [nodes:id]
:from refs :from refs
@ -242,14 +310,15 @@ Return nil if there's no node with such REF."
Uses the ID, and fetches remaining details from the database. Uses the ID, and fetches remaining details from the database.
This can be quite costly: avoid, unless dealing with very few This can be quite costly: avoid, unless dealing with very few
nodes." nodes."
(when-let ((node-info (car (org-roam-db-query [:select [file level pos todo priority (when-let ((node-info (car (org-roam-db-query [:select [
file level pos todo priority
scheduled deadline title properties olp] scheduled deadline title properties olp]
:from nodes :from nodes
:where (= id $s1) :where (= id $s1)
:limit 1] :limit 1]
(org-roam-node-id node))))) (org-roam-node-id node)))))
(pcase-let* ((`(,file ,level ,pos ,todo ,priority ,scheduled ,deadline ,title ,properties ,olp) node-info) (pcase-let* ((`(,file ,level ,pos ,todo ,priority ,scheduled ,deadline ,title ,properties ,olp) node-info)
(`(,atime ,mtime) (car (org-roam-db-query [:select [atime mtime] (`(,atime ,mtime ,file-title) (car (org-roam-db-query [:select [atime mtime title]
:from files :from files
:where (= file $s1)] :where (= file $s1)]
file))) file)))
@ -263,6 +332,7 @@ nodes."
:where (= node-id $s1)] :where (= node-id $s1)]
(org-roam-node-id node))))) (org-roam-node-id node)))))
(setf (org-roam-node-file node) file (setf (org-roam-node-file node) file
(org-roam-node-file-title node) file-title
(org-roam-node-file-atime node) atime (org-roam-node-file-atime node) atime
(org-roam-node-file-mtime node) mtime (org-roam-node-file-mtime node) mtime
(org-roam-node-level node) level (org-roam-node-level node) level
@ -285,6 +355,7 @@ nodes."
"SELECT "SELECT
id, id,
file, file,
filetitle,
\"level\", \"level\",
todo, todo,
pos, pos,
@ -304,6 +375,7 @@ FROM
SELECT SELECT
id, id,
file, file,
filetitle,
\"level\", \"level\",
todo, todo,
pos, pos,
@ -334,6 +406,7 @@ FROM
nodes.olp as olp, nodes.olp as olp,
files.atime as atime, files.atime as atime,
files.mtime as mtime, files.mtime as mtime,
files.title as filetitle,
tags.tag as tags, tags.tag as tags,
aliases.alias as aliases, aliases.alias as aliases,
'(' || group_concat(RTRIM (refs.\"type\", '\"') || ':' || LTRIM(refs.ref, '\"'), ' ') || ')' as refs '(' || group_concat(RTRIM (refs.\"type\", '\"') || ':' || LTRIM(refs.ref, '\"'), ' ') || ')' as refs
@ -346,13 +419,15 @@ FROM
GROUP BY id, tags ) GROUP BY id, tags )
GROUP BY id"))) GROUP BY id")))
(cl-loop for row in rows (cl-loop for row in rows
append (pcase-let* ((`(,id ,file ,level ,todo ,pos ,priority ,scheduled ,deadline append (pcase-let* ((`(
,id ,file ,file-title ,level ,todo ,pos ,priority ,scheduled ,deadline
,title ,properties ,olp ,atime ,mtime ,tags ,aliases ,refs) ,title ,properties ,olp ,atime ,mtime ,tags ,aliases ,refs)
row) row)
(all-titles (cons title aliases))) (all-titles (cons title aliases)))
(mapcar (lambda (temp-title) (mapcar (lambda (temp-title)
(org-roam-node-create :id id (org-roam-node-create :id id
:file file :file file
:file-title file-title
:file-atime atime :file-atime atime
:file-mtime mtime :file-mtime mtime
:level level :level level
@ -362,6 +437,7 @@ GROUP BY id")))
:scheduled scheduled :scheduled scheduled
:deadline deadline :deadline deadline
:title temp-title :title temp-title
:aliases aliases
:properties properties :properties properties
:olp olp :olp olp
:tags tags :tags tags
@ -369,19 +445,40 @@ GROUP BY id")))
all-titles))))) all-titles)))))
;;;; Finders ;;;; Finders
(defun org-roam-node-find-noselect (node &optional force) (defun org-roam-node-marker (node)
"Navigate to the point for NODE, and return the buffer. "Get the marker for NODE."
If NODE is already visited, this won't automatically move the (let* ((file (org-roam-node-file node))
point to the beginning of the NODE, unless FORCE is non-nil." (buffer (or (find-buffer-visiting file)
(unless (org-roam-node-file node) (find-file-noselect file))))
(user-error "Node does not have corresponding file")) (with-current-buffer buffer
(let ((buf (find-file-noselect (org-roam-node-file node)))) (move-marker (make-marker) (org-roam-node-point node) buffer))))
(with-current-buffer buf
(defun org-roam-node-open (node &optional cmd force)
"Go to the node NODE.
CMD is the command used to display the buffer. If not provided,
`org-link-frame-setup' is respected. Assumes that the node is
fully populated, with file and point. If NODE is already visited,
this won't automatically move the point to the beginning of the
NODE, unless FORCE is non-nil."
(interactive (list (org-roam-node-at-point) current-prefix-arg))
(org-mark-ring-push)
(let ((m (org-roam-node-marker node))
(cmd (or cmd
(cdr
(assq
(cdr (assq 'file org-link-frame-setup))
'((find-file . switch-to-buffer)
(find-file-other-window . switch-to-buffer-other-window)
(find-file-other-frame . switch-to-buffer-other-frame))))
'switch-to-buffer-other-window)))
(if (not (equal (current-buffer) (marker-buffer m)))
(funcall cmd (marker-buffer m)))
(when (or force (when (or force
(not (equal (org-roam-node-id node) (not (equal (org-roam-node-id node)
(org-roam-id-at-point)))) (org-roam-id-at-point))))
(goto-char (org-roam-node-point node)))) (goto-char m))
buf)) (move-marker m nil))
(org-fold-show-context))
(defun org-roam-node-visit (node &optional other-window force) (defun org-roam-node-visit (node &optional other-window force)
"From the current buffer, visit NODE. Return the visited buffer. "From the current buffer, visit NODE. Return the visited buffer.
@ -393,16 +490,13 @@ If NODE is already visited, this won't automatically move the
point to the beginning of the NODE, unless FORCE is non-nil. In point to the beginning of the NODE, unless FORCE is non-nil. In
interactive calls FORCE always set to t." interactive calls FORCE always set to t."
(interactive (list (org-roam-node-at-point t) current-prefix-arg t)) (interactive (list (org-roam-node-at-point t) current-prefix-arg t))
(let ((buf (org-roam-node-find-noselect node 'force)) (org-roam-node-open node (if other-window
(display-buffer-fn (if other-window
#'switch-to-buffer-other-window #'switch-to-buffer-other-window
#'pop-to-buffer-same-window))) #'pop-to-buffer-same-window)
(funcall display-buffer-fn buf) force))
(when (org-invisible-p) (org-show-context))
buf))
;;;###autoload ;;;###autoload
(cl-defun org-roam-node-find (&optional other-window initial-input filter-fn &key templates) (cl-defun org-roam-node-find (&optional other-window initial-input filter-fn pred &key templates)
"Find and open an Org-roam node by its title or alias. "Find and open an Org-roam node by its title or alias.
INITIAL-INPUT is the initial input for the prompt. INITIAL-INPUT is the initial input for the prompt.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node', FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
@ -411,7 +505,7 @@ If OTHER-WINDOW, visit the NODE in another window.
The TEMPLATES, if provided, override the list of capture templates (see The TEMPLATES, if provided, override the list of capture templates (see
`org-roam-capture-'.)" `org-roam-capture-'.)"
(interactive current-prefix-arg) (interactive current-prefix-arg)
(let ((node (org-roam-node-read initial-input filter-fn))) (let ((node (org-roam-node-read initial-input filter-fn pred)))
(if (org-roam-node-file node) (if (org-roam-node-file node)
(org-roam-node-visit node other-window) (org-roam-node-visit node other-window)
(org-roam-capture- (org-roam-capture-
@ -420,70 +514,89 @@ The TEMPLATES, if provided, override the list of capture templates (see
:props '(:finalize find-file))))) :props '(:finalize find-file)))))
;;;###autoload ;;;###autoload
(defun org-roam-node-random (&optional other-window) (defun org-roam-node-random (&optional other-window filter-fn)
"Find and open a random Org-roam node. "Find and open a random Org-roam node.
With prefix argument OTHER-WINDOW, visit the node in another With prefix argument OTHER-WINDOW, visit the node in another
window instead." window instead.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out."
(interactive current-prefix-arg) (interactive current-prefix-arg)
(let ((random-row (seq-random-elt (org-roam-db-query [:select [id file pos] :from nodes])))) (org-roam-node-visit
(org-roam-node-visit (org-roam-node-create :id (nth 0 random-row) (cdr (seq-random-elt (org-roam-node-read--completions filter-fn)))
:file (nth 1 random-row) other-window))
:point (nth 2 random-row))
other-window)))
;;;; Completing-read interface ;;;; Completing-read interface
(defun org-roam-node-read (&optional initial-input filter-fn sort-fn require-match) (defun org-roam-node-read (&optional initial-input filter-fn sort-fn require-match prompt)
"Read and return an `org-roam-node'. "Read and return an `org-roam-node'.
INITIAL-INPUT is the initial minibuffer prompt value. INITIAL-INPUT is the initial minibuffer prompt value.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node', FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out. and when nil is returned the node will be filtered out.
SORT-FN is a function to sort nodes. See `org-roam-node-read-sort-by-file-mtime' SORT-FN is a function to sort nodes. See `org-roam-node-read-sort-by-file-mtime'
for an example sort function. for an example sort function.
If REQUIRE-MATCH, the minibuffer prompt will require a match." If REQUIRE-MATCH, the minibuffer prompt will require a match.
(let* ((nodes (org-roam-node-read--completions)) PROMPT is a string to show at the beginning of the mini-buffer, defaulting to \"Node: \""
(nodes (cl-remove-if-not (lambda (n) (let* ((nodes (org-roam-node-read--completions filter-fn sort-fn))
(if filter-fn (funcall filter-fn (cdr n)) t)) nodes)) (prompt (or prompt "Node: "))
(sort-fn (or sort-fn
(when org-roam-node-default-sort
(intern (concat "org-roam-node-read-sort-by-"
(symbol-name org-roam-node-default-sort))))))
(_ (when sort-fn (setq nodes (seq-sort sort-fn nodes))))
(node (completing-read (node (completing-read
"Node: " prompt
(lambda (string pred action) (lambda (string pred action)
(if (eq action 'metadata) (if (eq action 'metadata)
'(metadata `(metadata
(annotation-function . (lambda (title) ;; Preserve sorting in the completion UI if a sort-fn is used
,@(when sort-fn
'((display-sort-function . identity)
(cycle-sort-function . identity)))
(annotation-function
. ,(lambda (title)
(funcall org-roam-node-annotation-function (funcall org-roam-node-annotation-function
(get-text-property 0 'node title)))) (get-text-property 0 'node title))))
(category . org-roam-node)) (category . org-roam-node))
(complete-with-action action nodes string pred))) (complete-with-action action nodes string pred)))
nil require-match initial-input))) nil require-match initial-input 'org-roam-node-history)))
(or (cdr (assoc node nodes)) (or (cdr (assoc node nodes))
(org-roam-node-create :title node)))) (org-roam-node-create :title node))))
(defvar org-roam-node-read--cached-display-format nil) (defun org-roam-node-read--completions (&optional filter-fn sort-fn)
(defun org-roam-node-read--completions ()
"Return an alist for node completion. "Return an alist for node completion.
The car is the displayed title or alias for the node, and the cdr The car is the displayed title or alias for the node, and the cdr
is the `org-roam-node'. is the `org-roam-node'.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out.
SORT-FN is a function to sort nodes. See `org-roam-node-read-sort-by-file-mtime'
for an example sort function.
The displayed title is formatted according to `org-roam-node-display-template'." The displayed title is formatted according to `org-roam-node-display-template'."
(setq org-roam-node-read--cached-display-format nil) (let* ((template (org-roam-node--process-display-format org-roam-node-display-template))
(let ((nodes (org-roam-node-list))) (nodes (org-roam-node-list))
(mapcar #'org-roam-node-read--to-candidate nodes))) (nodes (if filter-fn
(cl-remove-if-not
(lambda (n) (funcall filter-fn n))
nodes)
nodes))
(nodes (mapcar (lambda (node)
(org-roam-node-read--to-candidate node template)) nodes))
(sort-fn (or sort-fn
(when org-roam-node-default-sort
(intern (concat "org-roam-node-read-sort-by-"
(symbol-name org-roam-node-default-sort))))))
(nodes (if sort-fn (seq-sort sort-fn nodes)
nodes)))
nodes))
(defun org-roam-node-read--to-candidate (node) (defun org-roam-node-read--to-candidate (node template)
"Return a minibuffer completion candidate given NODE." "Return a minibuffer completion candidate given NODE.
(let ((candidate-main (org-roam-node-read--format-entry node (1- (frame-width))))) TEMPLATE is the processed template used to format the entry."
(let ((candidate-main (org-roam-node--format-entry
template
node
(1- (if (bufferp (current-buffer))
(window-width) (frame-width))))))
(cons (propertize candidate-main 'node node) node))) (cons (propertize candidate-main 'node node) node)))
(defun org-roam-node-read--format-entry (node width) (defun org-roam-node--format-entry (template node &optional width)
"Formats NODE for display in the results list. "Formats NODE for display in the results list.
WIDTH is the width of the results list. WIDTH is the width of the results list.
Uses `org-roam-node-display-template' to format the entry." TEMPLATE is the processed template used to format the entry."
(pcase-let ((`(,tmpl . ,tmpl-width) (pcase-let ((`(,tmpl . ,tmpl-width) template))
(org-roam-node-read--process-display-format org-roam-node-display-template)))
(org-roam-format-template (org-roam-format-template
tmpl tmpl
(lambda (field _default-val) (lambda (field _default-val)
@ -508,26 +621,28 @@ Uses `org-roam-node-display-template' to format the entry."
((not field-width) ((not field-width)
field-width) field-width)
((string-equal field-width "*") ((string-equal field-width "*")
(- width tmpl-width)) (if width
(- width tmpl-width)
tmpl-width))
((>= (string-to-number field-width) 0) ((>= (string-to-number field-width) 0)
(string-to-number field-width)))) (string-to-number field-width))))
;; Setting the display (which would be padded out to the field length) for an (when field-width
;; empty string results in an empty string and misalignment for candidates that (let* ((truncated (truncate-string-to-width field-value field-width 0 ?\s))
;; don't have some field. This uses the actual display string, made of spaces (tlen (length truncated))
;; when the field-value is "" so that we actually take up space. (len (length field-value)))
(let ((display-string (if field-width (if (< tlen len)
(truncate-string-to-width field-value field-width 0 ?\s) ;; Make the truncated part of the string invisible. If strings
field-value))) ;; are pre-propertized with display or invisible properties, the
(if (equal field-value "") ;; formatting may get messed up. Ideally, truncated strings are
display-string ;; not preformatted with these properties. Face properties are
;; Remove properties from the full candidate string, otherwise the display ;; allowed without restriction.
;; formatting with pre-prioritized field-values gets messed up. (put-text-property tlen len 'invisible t field-value)
(propertize (substring-no-properties field-value) 'display display-string)))))))) ;; If the string wasn't truncated, but padded, use this string instead.
(setq field-value truncated))))
field-value)))))
(defun org-roam-node-read--process-display-format (format) (defun org-roam-node--process-display-format (format)
"Pre-calculate minimal widths needed by the FORMAT string." "Pre-calculate minimal widths needed by the FORMAT string."
(or org-roam-node-read--cached-display-format
(setq org-roam-node-read--cached-display-format
(let* ((fields-width 0) (let* ((fields-width 0)
(string-width (string-width
(string-width (string-width
@ -539,11 +654,12 @@ Uses `org-roam-node-display-template' to format the entry."
(string-to-number (string-to-number
(or (cadr (split-string field ":")) (or (cadr (split-string field ":"))
""))))))))) "")))))))))
(cons format (+ fields-width string-width)))))) (cons format (+ fields-width string-width))))
(defun org-roam-node-read-sort-by-file-mtime (completion-a completion-b) (defun org-roam-node-read-sort-by-file-mtime (completion-a completion-b)
"Sort files such that files modified more recently are shown first. "Sort files such that files modified more recently are shown first.
COMPLETION-A and COMPLETION-B are items in the form of (node-title org-roam-node-struct)" COMPLETION-A and COMPLETION-B are items in the form of
\(node-title org-roam-node-struct)"
(let ((node-a (cdr completion-a)) (let ((node-a (cdr completion-a))
(node-b (cdr completion-b))) (node-b (cdr completion-b)))
(time-less-p (org-roam-node-file-mtime node-b) (time-less-p (org-roam-node-file-mtime node-b)
@ -551,7 +667,8 @@ COMPLETION-A and COMPLETION-B are items in the form of (node-title org-roam-node
(defun org-roam-node-read-sort-by-file-atime (completion-a completion-b) (defun org-roam-node-read-sort-by-file-atime (completion-a completion-b)
"Sort files such that files accessed more recently are shown first. "Sort files such that files accessed more recently are shown first.
COMPLETION-A and COMPLETION-B are items in the form of (node-title org-roam-node-struct)" COMPLETION-A and COMPLETION-B are items in the form of
\(node-title org-roam-node-struct)"
(let ((node-a (cdr completion-a)) (let ((node-a (cdr completion-a))
(node-b (cdr completion-b))) (node-b (cdr completion-b)))
(time-less-p (org-roam-node-file-atime node-b) (time-less-p (org-roam-node-file-atime node-b)
@ -583,15 +700,19 @@ The INFO, if provided, is passed to the underlying `org-roam-capture-'."
(setq region-text (org-link-display-format (buffer-substring-no-properties beg end))))) (setq region-text (org-link-display-format (buffer-substring-no-properties beg end)))))
(node (org-roam-node-read region-text filter-fn)) (node (org-roam-node-read region-text filter-fn))
(description (or region-text (description (or region-text
(org-roam-node-title node)))) (org-roam-node-formatted node))))
(if (org-roam-node-id node) (if (org-roam-node-id node)
(progn (progn
(when region-text (when region-text
(delete-region beg end) (delete-region beg end)
(set-marker beg nil) (set-marker beg nil)
(set-marker end nil)) (set-marker end nil))
(let ((id (org-roam-node-id node)))
(insert (org-link-make-string (insert (org-link-make-string
(concat "id:" (org-roam-node-id node)) (concat "id:" id)
description))
(run-hook-with-args 'org-roam-post-node-insert-hook
id
description))) description)))
(org-roam-capture- (org-roam-capture-
:node node :node node
@ -600,31 +721,10 @@ The INFO, if provided, is passed to the underlying `org-roam-capture-'."
:props (append :props (append
(when (and beg end) (when (and beg end)
(list :region (cons beg end))) (list :region (cons beg end)))
(list :insert-at (point-marker) (list :link-description description
:link-description description
:finalize 'insert-link)))))) :finalize 'insert-link))))))
(deactivate-mark))) (deactivate-mark)))
(add-hook 'org-roam-find-file-hook #'org-roam-open-id-with-org-roam-db-h)
(defun org-roam-open-id-with-org-roam-db-h ()
"Try to open \"id:\" links at point by querying them to the database."
(add-hook 'org-open-at-point-functions #'org-roam-open-id-at-point nil t))
(defun org-roam-open-id-at-point ()
"Try to navigate \"id:\" link to find and visit node with an assigned ID.
Assumes that the cursor was put where the link is."
(let* ((context (org-element-context))
(type (org-element-property :type context))
(id (org-element-property :path context)))
(when (string= type "id")
(let ((node (org-roam-populate (org-roam-node-create :id id))))
(cond
((org-roam-node-file node)
(org-mark-ring-push)
(org-roam-node-visit node nil 'force)
t)
(t nil))))))
;;;;; [roam:] link ;;;;; [roam:] link
(org-link-set-parameters "roam" :follow #'org-roam-link-follow-link) (org-link-set-parameters "roam" :follow #'org-roam-link-follow-link)
(defun org-roam-link-follow-link (title-or-alias) (defun org-roam-link-follow-link (title-or-alias)
@ -676,7 +776,7 @@ Assumes that the cursor was put where the link is."
;;;;;; Completion-at-point interface ;;;;;; Completion-at-point interface
(defconst org-roam-bracket-completion-re (defconst org-roam-bracket-completion-re
"\\[\\[\\(\\(?:roam:\\)?\\)\\([^z-a]*\\)]]" "\\[\\[\\(\\(?:roam:\\)?\\)\\([^z-a]*?\\)]]"
"Regex for completion within link brackets. "Regex for completion within link brackets.
We use this as a substitute for `org-link-bracket-re', because We use this as a substitute for `org-link-bracket-re', because
`org-link-bracket-re' requires content within the brackets for a match.") `org-link-bracket-re' requires content within the brackets for a match.")
@ -685,13 +785,12 @@ We use this as a substitute for `org-link-bracket-re', because
"Complete \"roam:\" link at point to an existing Org-roam node." "Complete \"roam:\" link at point to an existing Org-roam node."
(let (roam-p start end) (let (roam-p start end)
(when (org-in-regexp org-roam-bracket-completion-re 1) (when (org-in-regexp org-roam-bracket-completion-re 1)
(setq roam-p (not (string-blank-p (match-string 1))) (setq roam-p (not (or (org-in-src-block-p)
(string-blank-p (match-string 1))))
start (match-beginning 2) start (match-beginning 2)
end (match-end 2)) end (match-end 2))
(list start end (list start end
(completion-table-dynamic (org-roam--get-titles)
(lambda (_)
(funcall #'org-roam--get-titles)))
:exit-function :exit-function
(lambda (str &rest _) (lambda (str &rest _)
(delete-char (- 0 (length str))) (delete-char (- 0 (length str)))
@ -709,25 +808,27 @@ outside of the bracket syntax for links (i.e. \"[[roam:|]]\"),
hence \"everywhere\"." hence \"everywhere\"."
(when (and org-roam-completion-everywhere (when (and org-roam-completion-everywhere
(thing-at-point 'word) (thing-at-point 'word)
(not (org-in-src-block-p))
(not (save-match-data (org-in-regexp org-link-any-re)))) (not (save-match-data (org-in-regexp org-link-any-re))))
(let ((bounds (bounds-of-thing-at-point 'word))) (let ((bounds (bounds-of-thing-at-point 'word)))
(list (car bounds) (cdr bounds) (list (car bounds) (cdr bounds)
(completion-table-dynamic (org-roam--get-titles)
(lambda (_)
(funcall #'org-roam--get-titles)))
:exit-function :exit-function
(lambda (str _status) (lambda (str _status)
(delete-char (- (length str))) (delete-char (- (length str)))
(insert "[[roam:" str "]]")))))) (insert "[[roam:" str "]]"))
;; Proceed with the next completion function if the returned titles
(defun org-roam-complete-at-point () ;; do not match. This allows the default Org capfs or custom capfs
"Try get completion candidates at point using `org-roam-completion-functions'." ;; of lower priority to run.
(run-hook-with-args-until-success 'org-roam-completion-functions)) :exclusive 'no))))
(add-hook 'org-roam-find-file-hook #'org-roam--register-completion-functions-h) (add-hook 'org-roam-find-file-hook #'org-roam--register-completion-functions-h)
(add-hook 'org-roam-indirect-buffer-hook #'org-roam--register-completion-functions-h)
(defun org-roam--register-completion-functions-h () (defun org-roam--register-completion-functions-h ()
"Setup `org-roam-completion-functions' for `completion-at-point'." "Setup `org-roam-completion-functions' for `completion-at-point'."
(add-hook 'completion-at-point-functions #'org-roam-complete-at-point nil t)) (dolist (f org-roam-completion-functions)
(add-hook 'completion-at-point-functions f nil t)))
;;;; Editing ;;;; Editing
(defun org-roam-demote-entire-buffer () (defun org-roam-demote-entire-buffer ()
@ -741,7 +842,8 @@ Any tags declared on #+FILETAGS: are transferred to tags on the new top heading.
Any top level properties drawers are incorporated into the new heading." Any top level properties drawers are incorporated into the new heading."
(interactive) (interactive)
(org-with-point-at 1 (org-with-point-at 1
(org-map-entries 'org-do-demote) (org-map-region #'org-do-demote
(point-min) (point-max))
(insert "* " (insert "* "
(org-roam--get-keyword "title") (org-roam--get-keyword "title")
"\n") "\n")
@ -750,35 +852,58 @@ Any top level properties drawers are incorporated into the new heading."
(org-roam-erase-keyword "title") (org-roam-erase-keyword "title")
(org-roam-erase-keyword "filetags"))) (org-roam-erase-keyword "filetags")))
(defun org-roam--h1-count ()
"Count level-1 headings in the current file."
(let ((h1-count 0))
(org-with-wide-buffer
(org-map-region (lambda ()
(if (= (org-current-level) 1)
(cl-incf h1-count)))
(point-min) (point-max))
h1-count)))
(defun org-roam--buffer-promoteable-p ()
"Verify that this buffer is promoteable:
There is a single level-1 heading
and no extra content before the first heading."
(and
(= (org-roam--h1-count) 1)
(org-with-point-at 1 (org-at-heading-p))))
(defun org-roam-promote-entire-buffer () (defun org-roam-promote-entire-buffer ()
"Promote the current buffer. "Promote the current buffer.
Converts a file containing a headline node at the top to a file Converts a file containing a single level-1 headline node to a file
node." node."
(interactive) (interactive)
(unless (org-roam--buffer-promoteable-p)
(user-error "Cannot promote: multiple root headings or there is extra file-level text"))
(org-with-point-at 1 (org-with-point-at 1
(org-map-entries (lambda ()
(when (> (org-outline-level) 1)
(org-do-promote))))
(let ((title (nth 4 (org-heading-components))) (let ((title (nth 4 (org-heading-components)))
(tags (nth 5 (org-heading-components)))) (tags (org-get-tags)))
(beginning-of-line) (org-fold-show-all)
(kill-line 1) (kill-whole-line)
(org-roam-set-keyword "title" title) (org-roam-end-of-meta-data t)
(when tags (org-roam-set-keyword "filetags" tags))))) (insert "#+title: " title "\n")
(when tags (org-roam-tag-add tags))
(org-map-region #'org-promote (point-min) (point-max))
(org-roam-db-update-file))))
;;;###autoload ;;;###autoload
(defun org-roam-refile () (defun org-roam-refile (node)
"Refile node at point to an Org-roam node. "Refile node at point to an org-roam NODE.
If region is active, then use it instead of the node at point." If region is active, then use it instead of the node at point."
(interactive) (interactive
(list (org-roam-node-read nil nil nil 'require-match)))
(let* ((regionp (org-region-active-p)) (let* ((regionp (org-region-active-p))
(region-start (and regionp (region-beginning))) (region-start (and regionp (region-beginning)))
(region-end (and regionp (region-end))) (region-end (and regionp (region-end)))
(node (org-roam-node-read nil nil nil 'require-match))
(file (org-roam-node-file node)) (file (org-roam-node-file node))
(nbuf (or (find-buffer-visiting file) (nbuf (or (find-buffer-visiting file)
(find-file-noselect file))) (find-file-noselect file)))
level reversed) level reversed)
(if (equal (org-roam-node-at-point) node)
(user-error "Target is the same as current node")
(if regionp (if regionp
(progn (progn
(org-kill-new (buffer-substring region-start region-end)) (org-kill-new (buffer-substring region-start region-end))
@ -816,18 +941,18 @@ If region is active, then use it instead of the node at point."
(if (buffer-file-name) (if (buffer-file-name)
(delete-file (buffer-file-name))) (delete-file (buffer-file-name)))
(set-buffer-modified-p nil) (set-buffer-modified-p nil)
;; In this was done during capture, abort the capture process. ;; If this was done during capture, abort the capture process.
(when (and org-capture-mode (when (and org-capture-mode
(buffer-base-buffer (current-buffer))) (buffer-base-buffer (current-buffer)))
(org-capture-kill)) (org-capture-kill))
(kill-buffer (current-buffer))))) (kill-buffer (current-buffer))))))
;;;###autoload ;;;###autoload
(defun org-roam-extract-subtree () (defun org-roam-extract-subtree ()
"Convert current subtree at point to a node, and extract it into a new file." "Convert current subtree at point to a node, and extract it into a new file."
(interactive) (interactive)
(save-excursion (save-excursion
(org-back-to-heading-or-point-min) (org-back-to-heading-or-point-min t)
(when (bobp) (user-error "Already a top-level node")) (when (bobp) (user-error "Already a top-level node"))
(org-id-get-create) (org-id-get-create)
(save-buffer) (save-buffer)
@ -845,55 +970,25 @@ If region is active, then use it instead of the node at point."
(funcall fn node)) (funcall fn node))
((fboundp node-fn) ((fboundp node-fn)
(funcall node-fn node)) (funcall node-fn node))
(t (let ((r (completing-read (format "%s: " key) nil nil nil default-val))) (t (let ((r (read-from-minibuffer (format "%s: " key) default-val)))
(plist-put template-info ksym r) (plist-put template-info ksym r)
r))))))) r)))))))
(file-path (read-file-name "Extract node to: " org-roam-directory template nil template))) (file-path
(expand-file-name
(read-file-name "Extract node to: "
(file-name-as-directory org-roam-directory) template nil template)
org-roam-directory)))
(when (file-exists-p file-path) (when (file-exists-p file-path)
(user-error "%s exists. Aborting" file-path)) (user-error "%s exists. Aborting" file-path))
(org-cut-subtree) (org-cut-subtree)
(save-buffer) (save-buffer)
(with-current-buffer (find-file-noselect file-path) (with-current-buffer (find-file-noselect file-path)
(org-paste-subtree) (org-paste-subtree)
(while (> (org-current-level) 1) (org-promote-subtree))
(save-buffer)
(org-roam-promote-entire-buffer) (org-roam-promote-entire-buffer)
(save-buffer))))) (save-buffer)))))
;;; IDs
;;;; Getters
(defun org-roam-id-at-point ()
"Return the ID at point, if any.
Recursively traverses up the headline tree to find the
first encapsulating ID."
(org-with-wide-buffer
(org-back-to-heading-or-point-min)
(while (and (not (org-roam-db-node-p))
(not (bobp)))
(org-roam-up-heading-or-point-min))
(when (org-roam-db-node-p)
(org-id-get))))
;;;###autoload
(defun org-roam-update-org-id-locations (&rest directories)
"Scan Org-roam files to update `org-id' related state.
This is like `org-id-update-id-locations', but will automatically
use the currently bound `org-directory' and `org-roam-directory'
along with DIRECTORIES (if any), where the lookup for files in
these directories will be always recursive.
Note: Org-roam doesn't have hard dependency on
`org-id-locations-file' to lookup IDs for nodes that are stored
in the database, but it still tries to properly integrates with
`org-id'. This allows the user to cross-reference IDs outside of
the current `org-roam-directory', and also link with \"id:\"
links to headings/files within the current `org-roam-directory'
that are excluded from identification in Org-roam as
`org-roam-node's, e.g. with \"ROAM_EXCLUDE\" property."
(interactive)
(cl-loop with files for dir in (cons org-roam-directory directories)
for org-roam-directory = dir
nconc (org-roam-list-files) into files
finally (org-id-update-id-locations files org-roam-verbose)))
;;; Refs ;;; Refs
;;;; Completing-read interface ;;;; Completing-read interface
(defun org-roam-ref-read (&optional initial-input filter-fn) (defun org-roam-ref-read (&optional initial-input filter-fn)
@ -908,13 +1003,12 @@ filtered out."
(ref (completing-read "Ref: " (ref (completing-read "Ref: "
(lambda (string pred action) (lambda (string pred action)
(if (eq action 'metadata) (if (eq action 'metadata)
'(metadata `(metadata
(annotation-function . (lambda (ref) (annotation-function
(funcall org-roam-ref-annotation-function . ,org-roam-ref-annotation-function)
ref)))
(category . org-roam-ref)) (category . org-roam-ref))
(complete-with-action action refs string pred))) (complete-with-action action refs string pred)))
nil t initial-input))) nil t initial-input 'org-roam-ref-history)))
(cdr (assoc ref refs)))) (cdr (assoc ref refs))))
(defun org-roam-ref-read--completions () (defun org-roam-ref-read--completions ()
@ -931,7 +1025,9 @@ The car is the ref, and the cdr is the corresponding node for the ref."
:file file :file file
:point pos :point pos
:title title))) :title title)))
(cons (propertize ref 'node node 'type type) (cons
(concat (propertize ref 'node node 'type type)
(propertize id 'invisible t))
node))))) node)))))
(defun org-roam-ref-read--annotation (ref) (defun org-roam-ref-read--annotation (ref)
@ -955,11 +1051,15 @@ and when nil is returned the node will be filtered out."
;;;; Editing ;;;; Editing
(defun org-roam-ref-add (ref) (defun org-roam-ref-add (ref)
"Add REF to the node at point." "Add REF to the node at point."
(interactive "sRef: ") (interactive `(,(if org-roam-ref-prompt-function
(funcall org-roam-ref-prompt-function)
(read-string "Ref: "))))
(let ((node (org-roam-node-at-point 'assert))) (let ((node (org-roam-node-at-point 'assert)))
(save-excursion (save-excursion
(goto-char (org-roam-node-point node)) (goto-char (org-roam-node-point node))
(org-roam-add-property ref "ROAM_REFS")))) (org-roam-property-add "ROAM_REFS" (if (member " " (string-to-list ref))
(concat "\"" ref "\"")
ref)))))
(defun org-roam-ref-remove (&optional ref) (defun org-roam-ref-remove (&optional ref)
"Remove a REF from the node at point." "Remove a REF from the node at point."
@ -967,7 +1067,7 @@ and when nil is returned the node will be filtered out."
(let ((node (org-roam-node-at-point 'assert))) (let ((node (org-roam-node-at-point 'assert)))
(save-excursion (save-excursion
(goto-char (org-roam-node-point node)) (goto-char (org-roam-node-point node))
(org-roam-remove-property "ROAM_REFS" ref)))) (org-roam-property-remove "ROAM_REFS" ref))))
;;; Tags ;;; Tags
;;;; Getters ;;;; Getters
@ -987,7 +1087,8 @@ and when nil is returned the node will be filtered out."
(defun org-roam-tag-add (tags) (defun org-roam-tag-add (tags)
"Add TAGS to the node at point." "Add TAGS to the node at point."
(interactive (interactive
(list (completing-read-multiple "Tag: " (org-roam-tag-completions)))) (list (let ((crm-separator "[ ]*:[ ]*"))
(completing-read-multiple "Tag: " (org-roam-tag-completions)))))
(let ((node (org-roam-node-at-point 'assert))) (let ((node (org-roam-node-at-point 'assert)))
(save-excursion (save-excursion
(goto-char (org-roam-node-point node)) (goto-char (org-roam-node-point node))
@ -1016,7 +1117,7 @@ and when nil is returned the node will be filtered out."
(org-make-tag-string (seq-difference current-tags tags #'string-equal)))) (org-make-tag-string (seq-difference current-tags tags #'string-equal))))
(let* ((current-tags (or (org-get-tags) (let* ((current-tags (or (org-get-tags)
(user-error "No tag to remove"))) (user-error "No tag to remove")))
(tags (completing-read-multiple "Tag: " current-tags))) (tags (or tags (completing-read-multiple "Tag: " current-tags))))
(org-set-tags (seq-difference current-tags tags #'string-equal)))) (org-set-tags (seq-difference current-tags tags #'string-equal))))
tags))) tags)))
@ -1034,7 +1135,7 @@ and when nil is returned the node will be filtered out."
(let ((node (org-roam-node-at-point 'assert))) (let ((node (org-roam-node-at-point 'assert)))
(save-excursion (save-excursion
(goto-char (org-roam-node-point node)) (goto-char (org-roam-node-point node))
(org-roam-add-property alias "ROAM_ALIASES")))) (org-roam-property-add "ROAM_ALIASES" alias))))
(defun org-roam-alias-remove (&optional alias) (defun org-roam-alias-remove (&optional alias)
"Remove an ALIAS from the node at point." "Remove an ALIAS from the node at point."
@ -1042,7 +1143,7 @@ and when nil is returned the node will be filtered out."
(let ((node (org-roam-node-at-point 'assert))) (let ((node (org-roam-node-at-point 'assert)))
(save-excursion (save-excursion
(goto-char (org-roam-node-point node)) (goto-char (org-roam-node-point node))
(org-roam-remove-property "ROAM_ALIASES" alias)))) (org-roam-property-remove "ROAM_ALIASES" alias))))
(provide 'org-roam-node) (provide 'org-roam-node)

View File

@ -1,12 +1,12 @@
;;; org-roam-utils.el --- Utilities for Org-roam -*- lexical-binding: t; -*- ;;; org-roam-utils.el --- Utilities for Org-roam -*- lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.4")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -32,6 +32,13 @@
;; ;;
;;; Code: ;;; Code:
(require 'org-roam)
(defun org-roam-require (libs)
"Require LIBS."
(dolist (lib libs)
(require lib nil 'noerror)))
;;; String utilities ;;; String utilities
;; TODO Refactor this. ;; TODO Refactor this.
(defun org-roam-replace-string (old new s) (defun org-roam-replace-string (old new s)
@ -45,20 +52,78 @@
(org-roam-replace-string "\\" "\\\\") (org-roam-replace-string "\\" "\\\\")
(org-roam-replace-string "\"" "\\\""))) (org-roam-replace-string "\"" "\\\"")))
(defun org-roam-word-wrap (len s)
"If S is longer than LEN, wrap the words with newlines."
(declare (side-effect-free t))
(save-match-data
(with-temp-buffer
(insert s)
(let ((fill-column len))
(fill-region (point-min) (point-max)))
(buffer-substring (point-min) (point-max)))))
(defun org-roam-string-equal (s1 s2)
"Return t if S1 and S2 are equal.
Like `string-equal', but case-insensitive."
(and (= (length s1) (length s2))
(or (string-equal s1 s2)
(string-equal (downcase s1) (downcase s2)))))
(defun org-roam-whitespace-content (s)
"Return the whitespace content at the end of S."
(with-temp-buffer
(insert s)
(skip-chars-backward " \t\n")
(buffer-substring-no-properties
(point) (point-max))))
(defun org-roam-strip-comments (s)
"Strip Org comments from string S."
(with-temp-buffer
(insert s)
(goto-char (point-min))
(while (not (eobp))
(if (org-at-comment-p)
(delete-region (line-beginning-position)
(progn (forward-line) (point)))
(forward-line)))
(buffer-string)))
;;; List utilities ;;; List utilities
(defmacro org-roam-plist-map! (fn plist) (defun org-roam-plist-map! (fn plist)
"Map FN over PLIST, modifying it in-place." "Map FN over PLIST, modifying it in-place and returning it.
(declare (indent 1)) FN must take two arguments: the key and the value."
(let ((plist-var (make-symbol "plist")) (let ((plist-index plist))
(k (make-symbol "k")) (while plist-index
(v (make-symbol "v"))) (let ((key (pop plist-index)))
`(let ((,plist-var (copy-sequence ,plist))) (setf (car plist-index) (funcall fn key (car plist-index))
(while ,plist-var plist-index (cdr plist-index)))))
(setq ,k (pop ,plist-var)) plist)
(setq ,v (pop ,plist-var))
(setq ,plist (plist-put ,plist ,k (funcall ,fn ,k ,v))))))) (defmacro org-roam-dolist-with-progress (spec msg &rest body)
"Loop over a list and report progress in the echo area.
Like `dolist-with-progress-reporter', but falls back to `dolist'
if the function does not yet exist.
Evaluate BODY with VAR bound to each car from LIST, in turn.
Then evaluate RESULT to get return value, default nil.
MSG is a progress reporter object or a string. In the latter
case, use this string to create a progress reporter.
SPEC is a list, as per `dolist'."
(declare (indent 2))
(if (fboundp 'dolist-with-progress-reporter)
`(dolist-with-progress-reporter ,spec ,msg ,@body)
`(dolist ,spec ,@body)))
;;; File utilities ;;; File utilities
(defun org-roam-descendant-of-p (a b)
"Return t if A is descendant of B."
(unless (and a b (equal (file-truename a) (file-truename b)))
(string-prefix-p (replace-regexp-in-string "^\\([A-Za-z]\\):" 'downcase (expand-file-name b) t t)
(replace-regexp-in-string "^\\([A-Za-z]\\):" 'downcase (expand-file-name a) t t))))
(defmacro org-roam-with-file (file keep-buf-p &rest body) (defmacro org-roam-with-file (file keep-buf-p &rest body)
"Execute BODY within FILE. "Execute BODY within FILE.
If FILE is nil, execute BODY in the current buffer. If FILE is nil, execute BODY in the current buffer.
@ -66,6 +131,7 @@ Kills the buffer if KEEP-BUF-P is nil, and FILE is not yet visited."
(declare (indent 2) (debug t)) (declare (indent 2) (debug t))
`(let* (new-buf `(let* (new-buf
(auto-mode-alist nil) (auto-mode-alist nil)
(find-file-hook nil)
(buf (or (and (not ,file) (buf (or (and (not ,file)
(current-buffer)) ;If FILE is nil, use current buffer (current-buffer)) ;If FILE is nil, use current buffer
(find-buffer-visiting ,file) ; If FILE is already visited, find buffer (find-buffer-visiting ,file) ; If FILE is already visited, find buffer
@ -74,11 +140,12 @@ Kills the buffer if KEEP-BUF-P is nil, and FILE is not yet visited."
(find-file-noselect ,file)))) ; Else, visit FILE and return buffer (find-file-noselect ,file)))) ; Else, visit FILE and return buffer
res) res)
(with-current-buffer buf (with-current-buffer buf
(unless (equal major-mode 'org-mode) (unless (derived-mode-p 'org-mode)
(delay-mode-hooks (delay-mode-hooks
(let ((org-inhibit-startup t) (let ((org-inhibit-startup t)
(org-agenda-files nil)) (org-agenda-files nil))
(org-mode)))) (org-mode)
(hack-local-variables))))
(setq res (progn ,@body)) (setq res (progn ,@body))
(unless (and new-buf (not ,keep-buf-p)) (unless (and new-buf (not ,keep-buf-p))
(save-buffer))) (save-buffer)))
@ -125,14 +192,20 @@ value (possibly nil). Adapted from `s-format'."
(let ((v (progn (let ((v (progn
(set-match-data saved-match-data) (set-match-data saved-match-data)
(funcall replacer var default-val)))) (funcall replacer var default-val))))
(if v (format "%s" v) (signal 'org-roam-format-resolve md))) (if v
(format (apply #'propertize "%s" (text-properties-at 0 var)) v)
(signal 'org-roam-format-resolve md)))
(set-match-data replacer-match-data)))) (set-match-data replacer-match-data))))
template (if (functionp template)
(funcall template)
template)
;; Need literal to make sure it works ;; Need literal to make sure it works
t t) t t)
(set-match-data saved-match-data)))) (set-match-data saved-match-data))))
;;; Fontification ;;; Fontification
(defvar org-ref-buffer-hacked)
(defun org-roam-fontify-like-in-org-mode (s) (defun org-roam-fontify-like-in-org-mode (s)
"Fontify string S like in Org mode. "Fontify string S like in Org mode.
Like `org-fontify-like-in-org-mode', but supports `org-ref'." Like `org-fontify-like-in-org-mode', but supports `org-ref'."
@ -156,36 +229,10 @@ Like `org-fontify-like-in-org-mode', but supports `org-ref'."
(insert s) (insert s)
(let ((org-ref-buffer-hacked t)) (let ((org-ref-buffer-hacked t))
(org-mode) (org-mode)
(org-font-lock-ensure) (setq-local org-fold-core-style 'overlays)
(font-lock-ensure)
(buffer-string)))) (buffer-string))))
;;;; Shielding regions
(defface org-roam-shielded
'((t :inherit (warning)))
"Face for regions that are shielded (marked as read-only).
This face is used on the region target by org-roam-insertion
during an `org-roam-capture'."
:group 'org-roam-faces)
(defun org-roam-shield-region (beg end)
"Shield region against modifications.
BEG and END are markers for the beginning and end regions.
REGION must be a cons-cell containing the marker to the region
beginning and maximum values."
(add-text-properties beg end
'(font-lock-face org-roam-shielded
read-only t)
(marker-buffer beg)))
(defun org-roam-unshield-region (beg end)
"Unshield the shielded REGION.
BEG and END are markers for the beginning and end regions."
(let ((inhibit-read-only t))
(remove-text-properties beg end
'(font-lock-face org-roam-shielded
read-only t)
(marker-buffer beg))))
;;; Org-mode utilities ;;; Org-mode utilities
;;;; Motions ;;;; Motions
(defun org-roam-up-heading-or-point-min () (defun org-roam-up-heading-or-point-min ()
@ -221,6 +268,45 @@ If BOUND, scan up to BOUND bytes of the buffer."
(when (re-search-forward re bound t) (when (re-search-forward re bound t)
(buffer-substring-no-properties (match-beginning 1) (match-end 1)))))) (buffer-substring-no-properties (match-beginning 1) (match-end 1))))))
(defun org-roam-end-of-meta-data (&optional full)
"Like `org-end-of-meta-data', but supports file-level metadata.
When FULL is non-nil but not t, skip planning information,
properties, clocking lines and logbook drawers.
When optional argument FULL is t, skip everything above, and also
skip keywords."
(org-back-to-heading-or-point-min t)
(when (org-at-heading-p) (forward-line))
;; Skip planning information.
(when (looking-at-p org-planning-line-re) (forward-line))
;; Skip property drawer.
(when (looking-at org-property-drawer-re)
(goto-char (match-end 0))
(forward-line))
;; When FULL is not nil, skip more.
(when (and full (not (org-at-heading-p)))
(catch 'exit
(let ((end (save-excursion (outline-next-heading) (point)))
(re (concat "[ \t]*$" "\\|" org-clock-line-re)))
(while (not (eobp))
(cond ;; Skip clock lines.
((looking-at-p re) (forward-line))
;; Skip logbook drawer.
((looking-at-p org-logbook-drawer-re)
(if (re-search-forward "^[ \t]*:END:[ \t]*$" end t)
(forward-line)
(throw 'exit t)))
((looking-at-p org-drawer-regexp)
(if (re-search-forward "^[ \t]*:END:[ \t]*$" end t)
(forward-line)
(throw 'exit t)))
;; When FULL is t, skip keywords too.
((and (eq full t)
(looking-at-p org-keyword-regexp))
(forward-line))
(t (throw 'exit t))))))))
(defun org-roam-set-keyword (key value) (defun org-roam-set-keyword (key value)
"Set keyword KEY to VALUE. "Set keyword KEY to VALUE.
If the property is already set, it's value is replaced." If the property is already set, it's value is replaced."
@ -230,14 +316,13 @@ If the property is already set, it's value is replaced."
(if (string-blank-p value) (if (string-blank-p value)
(kill-whole-line) (kill-whole-line)
(replace-match (concat " " value) 'fixedcase nil nil 1)) (replace-match (concat " " value) 'fixedcase nil nil 1))
(while (and (not (eobp)) (org-roam-end-of-meta-data 'drawers)
(looking-at "^[#:]"))
(if (save-excursion (end-of-line) (eobp)) (if (save-excursion (end-of-line) (eobp))
(progn (progn
(end-of-line) (end-of-line)
(insert "\n")) (insert "\n"))
(forward-line) (forward-line)
(beginning-of-line))) (beginning-of-line))
(insert "#+" key ": " value "\n"))))) (insert "#+" key ": " value "\n")))))
(defun org-roam-erase-keyword (keyword) (defun org-roam-erase-keyword (keyword)
@ -252,6 +337,18 @@ If the property is already set, it's value is replaced."
;;;; Properties ;;;; Properties
(defun org-roam-add-property (val prop) (defun org-roam-add-property (val prop)
"Add VAL value to PROP property for the node at point. "Add VAL value to PROP property for the node at point.
Both, VAL and PROP are strings."
(org-roam-property-add prop val))
(defun org-roam-remove-property (prop &optional val)
"Remove VAL value from PROP property for the node at point.
Both VAL and PROP are strings.
If VAL is not specified, user is prompted to select a value."
(org-roam-property-remove prop val))
(defun org-roam-property-add (prop val)
"Add VAL value to PROP property for the node at point.
Both, VAL and PROP are strings." Both, VAL and PROP are strings."
(let* ((p (org-entry-get (point) prop)) (let* ((p (org-entry-get (point) prop))
(lst (when p (split-string-and-unquote p))) (lst (when p (split-string-and-unquote p)))
@ -260,7 +357,7 @@ Both, VAL and PROP are strings."
(org-set-property prop (combine-and-quote-strings lst)) (org-set-property prop (combine-and-quote-strings lst))
val)) val))
(defun org-roam-remove-property (prop &optional val) (defun org-roam-property-remove (prop &optional val)
"Remove VAL value from PROP property for the node at point. "Remove VAL value from PROP property for the node at point.
Both VAL and PROP are strings. Both VAL and PROP are strings.
@ -274,6 +371,16 @@ If VAL is not specified, user is prompted to select a value."
(org-delete-property prop)) (org-delete-property prop))
prop-to-remove)) prop-to-remove))
;;; Refs
(defun org-roam-org-ref-path-to-keys (path)
"Return a list of keys given an org-ref cite: PATH.
Accounts for both v2 and v3."
(cond ((fboundp 'org-ref-parse-cite-path)
(mapcar (lambda (cite) (plist-get cite :key))
(plist-get (org-ref-parse-cite-path path) :references)))
((fboundp 'org-ref-split-and-strip-string)
(org-ref-split-and-strip-string path))))
;;; Logs ;;; Logs
(defvar org-roam-verbose) (defvar org-roam-verbose)
(defun org-roam-message (format-string &rest args) (defun org-roam-message (format-string &rest args)
@ -341,8 +448,11 @@ See <https://github.com/raxod502/straight.el/issues/520>."
'("Doom" "Spacemacs" "N/A" "I don't know")) '("Doom" "Spacemacs" "N/A" "I don't know"))
(quit "N/A")))) (quit "N/A"))))
(insert (format "- Org: %s\n" (org-version nil 'full))) (insert (format "- Org: %s\n" (org-version nil 'full)))
(insert (format "- Org-roam: %s" (org-roam-version))))) (insert (format "- Org-roam: %s" (org-roam-version)))
(insert (format "- sqlite-connector: %s"
(if-let ((conn (org-roam-db--get-connection)))
(eieio-object-class conn)
"not connected")))))
(provide 'org-roam-utils) (provide 'org-roam-utils)
;;; org-roam-utils.el ends here ;;; org-roam-utils.el ends here

View File

@ -1,12 +1,12 @@
;;; org-roam.el --- A database abstraction layer for Org-mode -*- coding: utf-8; lexical-binding: t; -*- ;;; org-roam.el --- A database abstraction layer for Org-mode -*- coding: utf-8; lexical-binding: t; -*-
;; Copyright © 2020-2021 Jethro Kuan <jethrokuan95@gmail.com> ;; Copyright © 2020-2025 Jethro Kuan <jethrokuan95@gmail.com>
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/org-roam/org-roam ;; URL: https://github.com/org-roam/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 2.1.0 ;; Version: 2.3.0
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "2.90.1")) ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.6") (emacsql "4.1.0") (magit-section "3.0.0"))
;; This file is NOT part of GNU Emacs. ;; This file is NOT part of GNU Emacs.
@ -71,7 +71,6 @@
;; majority of them can be found at https://github.com/org-roam and MELPA. ;; majority of them can be found at https://github.com/org-roam and MELPA.
;; ;;
;;; Code: ;;; Code:
(require 'f)
(require 'dash) (require 'dash)
(require 'rx) (require 'rx)
@ -81,9 +80,13 @@
(require 'magit-section) (require 'magit-section)
(require 'emacsql) (require 'emacsql)
;; REVIEW: is this require needed?
;; emacsql-sqlite provides a common interface to an emacsql SQLite backend (e.g. emacs-sqlite-builtin)
;; not to be confused with a backend itself named emacsql-sqlite that existed in emacsql < 4.0.
(require 'emacsql-sqlite) (require 'emacsql-sqlite)
(require 'org) (require 'org)
(require 'org-attach) ; To set `org-attach-id-dir'
(require 'org-id) (require 'org-id)
(require 'ol) (require 'ol)
(require 'org-element) (require 'org-element)
@ -94,9 +97,6 @@
(eval-when-compile (eval-when-compile
(require 'subr-x)) (require 'subr-x))
(require 'org-roam-utils)
(require 'org-roam-compat)
;;; Options ;;; Options
(defgroup org-roam nil (defgroup org-roam nil
"A database abstraction layer for Org-mode." "A database abstraction layer for Org-mode."
@ -126,6 +126,12 @@ All Org files, at any level of nesting, are considered part of the Org-roam."
:group 'org-roam :group 'org-roam
:type 'hook) :type 'hook)
(defcustom org-roam-post-node-insert-hook nil
"Hook run when an Org-roam node is inserted as an Org link.
Each function takes two arguments: the id of the node, and the link description."
:group 'org-roam
:type 'hook)
(defcustom org-roam-file-extensions '("org") (defcustom org-roam-file-extensions '("org")
"List of file extensions to be included by Org-Roam. "List of file extensions to be included by Org-Roam.
While a file extension different from \".org\" may be used, the While a file extension different from \".org\" may be used, the
@ -134,9 +140,11 @@ responsibility to ensure that."
:type '(repeat string) :type '(repeat string)
:group 'org-roam) :group 'org-roam)
(defcustom org-roam-file-exclude-regexp nil (defcustom org-roam-file-exclude-regexp (list org-attach-id-dir)
"Files matching this regular expression are excluded from the Org-roam." "Files matching this regular expression or list of regular expressions are excluded from the Org-roam."
:type '(choice :type '(choice
(repeat
(string :tag "Regular expression matching files to ignore"))
(string :tag "Regular expression matching files to ignore") (string :tag "Regular expression matching files to ignore")
(const :tag "Include everything" nil)) (const :tag "Include everything" nil))
:group 'org-roam) :group 'org-roam)
@ -147,32 +155,42 @@ responsibility to ensure that."
'(find fd fdfind rg)) '(find fd fdfind rg))
"Commands that will be used to find Org-roam files. "Commands that will be used to find Org-roam files.
It should be a list of symbols or cons cells representing any of the following It should be a list of symbols or cons cells representing any of
supported file search methods. the following supported file search methods.
The commands will be tried in order until an executable for a command is found. The commands will be tried in order until an executable for a
The Elisp implementation is used if no command in the list is found. command is found. The Elisp implementation is used if no command
in the list is found.
`find' `find'
Use find as the file search method. Use find as the file search method.
Example command: Example command:
find /path/to/dir -type f \( -name \"*.org\" -o -name \"*.org.gpg\" \) find /path/to/dir -type f \
\( -name \"*.org\" -o -name \"*.org.gpg\" -name \"*.org.age\" \)
`fd' `fd'
Use fd as the file search method. Use fd as the file search method.
Example command: fd /path/to/dir/ --type file -e \".org\" -e \".org.gpg\" Example command:
fd /path/to/dir/ --type file -e \".org\" -e \".org.gpg\" -e \".org.age\"
`fdfind' `fdfind'
Same as `fd'. It's an alias that used in some OSes (e.g. Debian, Ubuntu) Same as `fd'. It's an alias that used in some OSes (e.g. Debian, Ubuntu)
`rg' `rg'
Use ripgrep as the file search method.
Example command: rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\"
By default, `executable-find' will be used to look up the path to the Use ripgrep as the file search method.
executable. If a custom path is required, it can be specified together with the Example command:
method symbol as a cons cell. For example: '(find (rg . \"/path/to/rg\"))." rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\" -g \"*.org.age\"
:type '(set (const :tag "find" find)
By default, `executable-find' will be used to look up the path to
the executable. If a custom path is required, it can be specified
together with the method symbol as a cons cell. For example:
\\='(find (rg . \"/path/to/rg\"))."
:type '(set
(const :tag "find" find)
(const :tag "fd" fd) (const :tag "fd" fd)
(const :tag "fdfind" fdfind) (const :tag "fdfind" fdfind)
(const :tag "rg" rg) (const :tag "rg" rg)
@ -187,19 +205,34 @@ FILE is an Org-roam file if:
- It's located somewhere under `org-roam-directory' - It's located somewhere under `org-roam-directory'
- It has a matching file extension (`org-roam-file-extensions') - It has a matching file extension (`org-roam-file-extensions')
- It doesn't match excluded regexp (`org-roam-file-exclude-regexp')" - It doesn't match excluded regexp (`org-roam-file-exclude-regexp')"
(when (or file (buffer-file-name (buffer-base-buffer)))
(let* ((path (or file (buffer-file-name (buffer-base-buffer)))) (let* ((path (or file (buffer-file-name (buffer-base-buffer))))
(ext (when path (org-roam--file-name-extension path))) (relative-path (file-relative-name path org-roam-directory))
(ext (if (string= ext "gpg") (ext (org-roam--file-name-extension path))
(ext (if (or (string= ext "gpg")
(string= ext "age"))
(org-roam--file-name-extension (file-name-sans-extension path)) (org-roam--file-name-extension (file-name-sans-extension path))
ext))) ext))
(org-roam-dir-p (org-roam-descendant-of-p path org-roam-directory))
(valid-file-ext-p (member ext org-roam-file-extensions))
(match-exclude-regexp-p
(cond
((not org-roam-file-exclude-regexp) nil)
((stringp org-roam-file-exclude-regexp)
(string-match-p org-roam-file-exclude-regexp relative-path))
((listp org-roam-file-exclude-regexp)
(let (is-match)
(dolist (exclude-re org-roam-file-exclude-regexp)
(setq is-match (or is-match (string-match-p exclude-re relative-path))))
is-match)))))
(save-match-data (save-match-data
(and (and
path path
(member ext org-roam-file-extensions) org-roam-dir-p
(not (and org-roam-file-exclude-regexp valid-file-ext-p
(string-match-p org-roam-file-exclude-regexp path))) (not match-exclude-regexp-p))))))
(f-descendant-of-p path (expand-file-name org-roam-directory))))))
;;;###autoload
(defun org-roam-list-files () (defun org-roam-list-files ()
"Return a list of all Org-roam files under `org-roam-directory'. "Return a list of all Org-roam files under `org-roam-directory'.
See `org-roam-file-p' for how each file is determined to be as See `org-roam-file-p' for how each file is determined to be as
@ -263,27 +296,29 @@ If no files are found, an empty list is returned."
(shell-command-to-string it) (shell-command-to-string it)
(ansi-color-filter-apply it) (ansi-color-filter-apply it)
(split-string it "\n") (split-string it "\n")
(seq-filter #'s-present? it))) (seq-filter (lambda (s)
(not (or (null s) (string= "" s)))) it)))
(defun org-roam--list-files-search-globs (exts) (defun org-roam--list-files-search-globs (exts)
"Given EXTS, return a list of search globs. "Given EXTS, return a list of search globs.
E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")" E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")"
(cl-loop for e in exts (cl-loop for e in exts
append (list (format "\"*.%s\"" e) append (list (format "\"*.%s\"" e)
(format "\"*.%s.gpg\"" e)))) (format "\"*.%s.gpg\"" e)
(format "\"*.%s.age\"" e))))
(defun org-roam--list-files-find (executable dir) (defun org-roam--list-files-find (executable dir)
"Return all Org-roam files under DIR, using \"find\", provided as EXECUTABLE." "Return all Org-roam files under DIR, using \"find\", provided as EXECUTABLE."
(let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions))
(names (s-join " -o " (mapcar (lambda (glob) (concat "-name " glob)) globs))) (names (string-join (mapcar (lambda (glob) (concat "-name " glob)) globs) " -o "))
(command (s-join " " `(,executable "-L" ,dir "-type f \\(" ,names "\\)")))) (command (string-join `(,executable "-L" ,dir "-type f \\(" ,names "\\)") " ")))
(org-roam--shell-command-files command))) (org-roam--shell-command-files command)))
(defun org-roam--list-files-fd (executable dir) (defun org-roam--list-files-fd (executable dir)
"Return all Org-roam files under DIR, using \"fd\", provided as EXECUTABLE." "Return all Org-roam files under DIR, using \"fd\", provided as EXECUTABLE."
(let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions))
(extensions (s-join " -e " (mapcar (lambda (glob) (substring glob 2 -1)) globs))) (extensions (string-join (mapcar (lambda (glob) (concat "-e " (substring glob 2 -1))) globs) " "))
(command (s-join " " `(,executable "-L" ,dir "--type file" ,extensions)))) (command (string-join `(,executable "-L" "--type file" ,extensions "." ,dir) " ")))
(org-roam--shell-command-files command))) (org-roam--shell-command-files command)))
(defalias 'org-roam--list-files-fdfind #'org-roam--list-files-fd) (defalias 'org-roam--list-files-fdfind #'org-roam--list-files-fd)
@ -291,15 +326,18 @@ E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")"
(defun org-roam--list-files-rg (executable dir) (defun org-roam--list-files-rg (executable dir)
"Return all Org-roam files under DIR, using \"rg\", provided as EXECUTABLE." "Return all Org-roam files under DIR, using \"rg\", provided as EXECUTABLE."
(let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions))
(command (s-join " " `(,executable "-L" ,dir "--files" (command (string-join `(
,@(mapcar (lambda (glob) (concat "-g " glob)) globs))))) ,executable "-L" ,dir "--files"
,@(mapcar (lambda (glob) (concat "-g " glob)) globs)) " ")))
(org-roam--shell-command-files command))) (org-roam--shell-command-files command)))
(declare-function org-roam--directory-files-recursively "org-roam-compat")
(defun org-roam--list-files-elisp (dir) (defun org-roam--list-files-elisp (dir)
"Return all Org-roam files under DIR, using Elisp based implementation." "Return all Org-roam files under DIR, using Elisp based implementation."
(let ((regex (concat "\\.\\(?:"(mapconcat (let ((regex (concat "\\.\\(?:"(mapconcat
#'regexp-quote org-roam-file-extensions #'regexp-quote org-roam-file-extensions
"\\|" )"\\)\\(?:\\.gpg\\)?\\'")) "\\|" )"\\)\\(?:\\.gpg\\|\\.age\\)?\\'"))
result) result)
(dolist (file (org-roam--directory-files-recursively dir regex nil nil t) result) (dolist (file (org-roam--directory-files-recursively dir regex nil nil t) result)
(when (and (file-readable-p file) (when (and (file-readable-p file)
@ -310,10 +348,14 @@ E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")"
(provide 'org-roam) (provide 'org-roam)
(cl-eval-when (load eval) (cl-eval-when (load eval)
(require 'org-roam-compat)
(require 'org-roam-utils)
(require 'org-roam-db) (require 'org-roam-db)
(require 'org-roam-node) (require 'org-roam-node)
(require 'org-roam-id)
(require 'org-roam-capture) (require 'org-roam-capture)
(require 'org-roam-mode) (require 'org-roam-mode)
(require 'org-roam-log)
(require 'org-roam-migrate)) (require 'org-roam-migrate))
;;; org-roam.el ends here ;;; org-roam.el ends here

View File

@ -0,0 +1,8 @@
:PROPERTIES:
:ID: 97bf31cf-dfee-45d8-87a5-2ae0dabc4734
:END:
#+title: Demoteable
* Demoteable h1
** Demoteable child

View File

@ -0,0 +1,19 @@
:PROPERTIES:
:ID: 998b2341-b7fe-434d-848c-5282c0727870
:END:
#+title: Family
* Grand-Parent
:PROPERTIES:
:ID: 77a90980-1994-464e-901f-7e3d3df07fd3
:END:
** Parent
:PROPERTIES:
:ID: 0fa5bb3e-3d8c-4966-8bc9-78d32e505d69
:END:
*** Child
:PROPERTIES:
:ID: 5fb4fdc5-b6d2-4f75-8d54-e60053e467ec
:END:

View File

@ -0,0 +1,3 @@
* Promoteable h1
** Promoteable child

View File

@ -0,0 +1,7 @@
:PROPERTIES:
:ROAM_REFS: "http://site.net/docs/01. introduction - hello world.html"
:ID: 5b9a7400-f59c-4ef9-acbb-045b69af98f1
:END:
#+title: ref with space
* Note
hello

View File

@ -0,0 +1,5 @@
:PROPERTIES:
:ID: 57ff3ce7-5bda-4825-8fca-c09f523e87ba
:ROAM_ALIASES: Batman
:END:
#+title: Bruce Wayne

View File

@ -0,0 +1,16 @@
:PROPERTIES:
:ID: 9a20ca6c-5555-41c9-a039-ac38bf59c7a9
:END:
#+title: With Times
* Scheduled heading
SCHEDULED: <2024-07-16 Tue>
:PROPERTIES:
:ID: a523c198-4cb4-44d2-909c-a0e3258089cd
:END:
* Deadline heading
DEADLINE: <2024-07-17 Tue>
:PROPERTIES:
:ID: 3ab84701-d1c1-463f-b5c6-715e6ff5a0bf
:END:

View File

@ -0,0 +1,63 @@
;;; test-org-roam-capture.el --- Tests for Org-roam -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'buttercup)
(require 'org-roam)
(describe "org-roam-capture--fill-template"
(it "fills template without newline"
(expect
(org-roam-capture--fill-template "foo")
:to-equal "foo"))
(it "fills template ensuring newline"
(expect
(org-roam-capture--fill-template "foo" 'ensure-newline)
:to-equal "foo\n"))
(it "fills template with newline"
(expect
(org-roam-capture--fill-template "foo\n")
:to-equal "foo\n"))
(it "fills template with two newlines"
(expect
(org-roam-capture--fill-template "foo\n\n")
:to-equal "foo\n\n")
(expect
(org-roam-capture--fill-template "foo\n\t\n")
:to-equal "foo\n\t\n"))
(it "fills template without deleting newlines in its body"
(expect
(org-roam-capture--fill-template "foo\n\n\nbar\n\n")
:to-equal "foo\n\n\nbar\n\n"))
(it "expands templates when it's a function"
(expect
(org-roam-capture--fill-template (lambda () "foo"))
:to-equal "foo")))
(provide 'test-org-roam-capture)
;;; test-org-roam-capture.el ends here

120
tests/test-org-roam-db.el Normal file
View File

@ -0,0 +1,120 @@
;;; test-org-roam-db.el --- Tests for Org-roam -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'buttercup)
(require 'org-roam)
(defvar root-directory default-directory)
(describe "org-roam-db-get-scheduled-time"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location)
(cd root-directory))
(it "should get scheduled time for current heading node"
(org-roam-id-open "a523c198-4cb4-44d2-909c-a0e3258089cd" nil)
(expect (org-roam-db-get-scheduled-time) :to-equal "2024-07-16T00:00:00")))
(describe "org-roam-db-get-deadline-time"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location)
(cd root-directory))
(it "should get deadline time for current heading node"
(org-roam-id-open "3ab84701-d1c1-463f-b5c6-715e6ff5a0bf" nil)
(expect (org-roam-db-get-deadline-time) :to-equal "2024-07-17T00:00:00")))
(describe "org-roam-db--file-hash"
(it "computes the SHA1 of file"
(expect (org-roam-db--file-hash "tests/roam-files/family.org")
:to-equal
"c4ebf8918c1533df72e4d182cbf1bbd90f776b3b")))
(describe "org-roam-db-sync"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location))
(it "has the correct number of files"
(expect (caar (org-roam-db-query [:select (funcall count) :from files]))
:to-equal
9))
(it "has the correct number of nodes"
(expect (caar (org-roam-db-query [:select (funcall count) :from nodes]))
:to-equal
12))
(it "has the correct number of links"
(expect (caar (org-roam-db-query [:select (funcall count) :from links]))
:to-equal
1))
(it "respects ROAM_EXCLUDE"
;; The excluded node has ID "53fadc75-f48e-461e-be06-44a1e88b2abe"
(expect (mapcar #'car (org-roam-db-query [:select id :from nodes]))
:to-have-same-items-as
'("884b2341-b7fe-434d-848c-5282c0727861"
"440795d0-70c1-4165-993d-aebd5eef7a24"
"5b9a7400-f59c-4ef9-acbb-045b69af98f1"
"0fa5bb3e-3d8c-4966-8bc9-78d32e505d69"
"5fb4fdc5-b6d2-4f75-8d54-e60053e467ec"
"77a90980-1994-464e-901f-7e3d3df07fd3"
"57ff3ce7-5bda-4825-8fca-c09f523e87ba"
"998b2341-b7fe-434d-848c-5282c0727870"
"a523c198-4cb4-44d2-909c-a0e3258089cd"
"3ab84701-d1c1-463f-b5c6-715e6ff5a0bf"
"9a20ca6c-5555-41c9-a039-ac38bf59c7a9"
"97bf31cf-dfee-45d8-87a5-2ae0dabc4734")))
(it "reads ref in quotes correctly"
(expect (mapcar #'car (org-roam-db-query [:select [ref] :from refs]))
:to-have-same-items-as
'("//site.net/docs/01. introduction - hello world.html"))))
(provide 'test-org-roam-db)
;;; test-org-roam-db.el ends here

79
tests/test-org-roam-id.el Normal file
View File

@ -0,0 +1,79 @@
;;; test-org-roam-id.el --- Tests for Org-roam -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'buttercup)
(require 'org-roam)
(defvar root-directory default-directory)
(describe "org-roam-id-at-point"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location)
(cd root-directory))
(it "returns the correct node ids"
(find-file "tests/roam-files/family.org" nil)
(expect (org-roam-id-at-point) :to-equal "998b2341-b7fe-434d-848c-5282c0727870")
(search-forward "Grand")
(expect (org-roam-id-at-point) :to-equal "77a90980-1994-464e-901f-7e3d3df07fd3")
(search-forward "Child")
(expect (org-roam-id-at-point) :to-equal "5fb4fdc5-b6d2-4f75-8d54-e60053e467ec")))
(describe "org-roam-id-find"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location))
(it "finds nothing for non-existing node"
(expect (org-roam-id-find "non-existing") :to-equal nil))
(it "finds the correct file node"
(let ((location (org-roam-id-find "884b2341-b7fe-434d-848c-5282c0727861")))
(expect (car location) :to-match ".*/tests/roam-files/foo.org")
(expect (cdr location) :to-equal 1)))
(it "finds the correct heading node"
(let ((location (org-roam-id-find "0fa5bb3e-3d8c-4966-8bc9-78d32e505d69")))
(expect (car location) :to-match ".*/tests/roam-files/family.org")
(expect (cdr location) :to-equal 156))))
(provide 'test-org-roam-id)
;;; test-org-roam-id.el ends here

View File

@ -0,0 +1,39 @@
;;; test-org-roam-mode.el --- Tests for Org-roam -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'buttercup)
(require 'org-roam)
(describe "org-roam-unlinked-references--rg-command"
(before-each
;; the space in the directory is on purpose
(setq org-roam-directory "/tmp/org roam"))
(it "returns the correct rg command for unlinked references"
(expect (org-roam-unlinked-references--rg-command '("foo" "bar"))
:to-equal
"rg --follow --only-matching --vimgrep --pcre2 --ignore-case --glob \"*.org\" --glob \"*.org.gpg\" --glob \"*.org.age\" '\\[([^[]]++|(?R))*\\]|(\\bfoo\\b)|(\\bbar\\b)' /tmp/org\\ roam")))
(provide 'test-org-roam-mode)
;;; test-org-roam-mode.el ends here

164
tests/test-org-roam-node.el Normal file
View File

@ -0,0 +1,164 @@
;;; test-org-roam-node.el --- Tests for Org-roam -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'buttercup)
(require 'org-roam)
(defvar root-directory default-directory)
(describe "org-roam-node-from-id"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location))
(it "returns nil for unknown id"
(expect (org-roam-node-from-id "non-existing") :to-equal nil))
(it "returns correct node from id"
(let ((node (org-roam-node-from-id "884b2341-b7fe-434d-848c-5282c0727861")))
(expect (org-roam-node-title node) :to-equal "Foo"))))
(describe "org-roam-node-from-title-or-alias"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location))
(it "returns nil for unknown title"
(expect (org-roam-node-from-title-or-alias "non-existing") :to-equal nil))
(it "returns correct node from title"
(let ((node (org-roam-node-from-title-or-alias "Foo")))
(expect (org-roam-node-title node) :to-equal "Foo")))
(it "returns correct node from alias"
(let ((node (org-roam-node-from-title-or-alias "Batman")))
(expect (org-roam-node-title node) :to-equal "Bruce Wayne")))
(it "returns correct node from alias with nocase"
(let ((node (org-roam-node-from-title-or-alias "batman" t)))
(expect (org-roam-node-title node) :to-equal "Bruce Wayne"))))
(describe "org-roam-demote-entire-buffer"
(after-each
(cd root-directory))
(it "demotes an entire org buffer"
(find-file "tests/roam-files/demoteable.org" nil)
(org-roam-demote-entire-buffer)
(expect (buffer-substring-no-properties (point) (point-max))
:to-equal "* Demoteable\n:PROPERTIES:\n:ID: 97bf31cf-dfee-45d8-87a5-2ae0dabc4734\n:END:\n\n** Demoteable h1\n\n*** Demoteable child\n")))
(describe "org-roam--h1-count"
(after-each
(cd root-directory))
(it "returns the correct number of level-1 headings"
(find-file "tests/roam-files/foo.org" nil)
(expect (org-roam--h1-count) :to-equal 0)
(cd root-directory)
(find-file "tests/roam-files/family.org" nil)
(expect (org-roam--h1-count) :to-equal 1)))
(describe "org-roam--buffer-promoteable-p"
(after-each
(cd root-directory))
(it "should check if buffer is promoteable"
(find-file "tests/roam-files/foo.org" nil)
(expect (org-roam--buffer-promoteable-p) :to-equal nil)
(cd root-directory)
(find-file "tests/roam-files/family.org" nil)
(expect (org-roam--buffer-promoteable-p) :to-equal nil)
(cd root-directory)
(find-file "tests/roam-files/promoteable.org" nil)
(expect (org-roam--buffer-promoteable-p) :to-equal t)))
(describe "org-roam--get-titles"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location))
(it "returns the list of titles and aliases"
(expect (org-roam--get-titles)
:to-have-same-items-as
`("Bar" "Batman" "Bruce Wayne" "Child" "Deadline heading" "Demoteable" "Family"
"Foo" "Grand-Parent" "Parent" "ref with space" "Scheduled heading" "With Times"))))
(describe "org-roam-alias"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location)
(cd root-directory))
(it "adds an alias to a node"
(cd root-directory)
(find-file "tests/roam-files/foo.org" nil)
(org-roam-alias-add "qux")
(expect (buffer-substring-no-properties (point) (point-max))
:to-equal ":PROPERTIES:\n:ID: 884b2341-b7fe-434d-848c-5282c0727861\n:ROAM_ALIASES: qux\n:END:\n#+title: Foo\n"))
(it "removes an alias from a node"
(cd root-directory)
(find-file "tests/roam-files/with-alias.org" nil)
(org-roam-alias-remove "Batman")
(expect (buffer-substring-no-properties (point) (point-max))
:to-equal ":PROPERTIES:\n:ID: 57ff3ce7-5bda-4825-8fca-c09f523e87ba\n:END:\n#+title: Bruce Wayne\n")))
(provide 'test-org-roam-node)
;;; test-org-roam-node.el ends here

View File

@ -0,0 +1,64 @@
;;; test-org-roam-utils.el --- Tests for Org-roam -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(require 'buttercup)
(require 'org-roam)
(describe "org-roam-whitespace-content"
(it "extracts whitespace correctly"
(expect
(org-roam-whitespace-content "foo")
:to-equal "")
(expect
(org-roam-whitespace-content "foo\n")
:to-equal "\n")
(expect
(org-roam-whitespace-content "foo\n\t\n")
:to-equal "\n\t\n")))
(describe "org-roam-db--file-title"
(it "supports normal titles"
(expect
(with-temp-buffer
(insert "#+title:normal title")
(org-roam-db--file-title))
:to-equal "normal title"))
(it "supports multi-line titles"
(expect
(with-temp-buffer
(insert "#+title: title:\n#+title: separated by newline")
(org-roam-db--file-title))
:to-equal "title: separated by newline"))
(it "supports file-name based titles"
(progn
(setq org-roam-directory temporary-file-directory
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org"))
(with-temp-buffer
(write-file (expand-file-name "test file.org" org-roam-directory))
(org-roam-db--file-title)))
:to-equal "test file"))
(provide 'test-org-roam-utils)
;;; test-org-roam-utils.el ends here

View File

@ -24,6 +24,28 @@
(require 'buttercup) (require 'buttercup)
(require 'org-roam) (require 'org-roam)
(defvar root-directory default-directory)
(describe "org-roam-file-p"
(it "checks if given file respects criteria"
(setq org-roam-directory "/non-existent")
(expect (org-roam-file-p "tests/roam-files/family.org") :to-equal nil)
(setq org-roam-directory (expand-file-name "tests/roam-files"))
(expect (org-roam-file-p "tests/roam-files/family.org") :to-equal t)
(expect (org-roam-file-p "tests/roam-files/markdown.md") :to-equal nil)
(setq org-roam-file-exclude-regexp (regexp-quote "family.org"))
(expect (org-roam-file-p "tests/roam-files/family.org") :to-equal nil)))
(describe "org-roam-buffer-p"
(it "checks if current buffer respects criteria"
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-file-exclude-regexp nil)
(find-file "tests/roam-files/family.org" nil)
(expect (org-roam-buffer-p) :to-equal t)
(cd root-directory)))
(describe "org-roam-list-files" (describe "org-roam-list-files"
(before-each (before-each
(setq org-roam-directory (expand-file-name "tests/roam-files") (setq org-roam-directory (expand-file-name "tests/roam-files")
@ -31,52 +53,29 @@
org-roam-file-extensions '("org") org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)) org-roam-file-exclude-regexp nil))
(after-all
(org-roam-db--close)
(delete-file org-roam-db-location))
(it "gets files correctly" (it "gets files correctly"
(expect (length (org-roam-list-files)) (expect (length (org-roam-list-files)) :to-equal 9))
:to-equal 3))
(it "respects org-roam-file-extensions" (it "respects org-roam-file-extensions"
(setq org-roam-file-extensions '("md")) (setq org-roam-file-extensions '("md"))
(expect (length (org-roam-list-files)) :to-equal 1) (expect (length (org-roam-list-files)) :to-equal 1)
(setq org-roam-file-extensions '("org" "md")) (setq org-roam-file-extensions '("org" "md"))
(expect (length (org-roam-list-files)) :to-equal 4)) (expect (length (org-roam-list-files)) :to-equal 10))
(it "respects org-roam-file-exclude-regexp" (it "respects org-roam-file-exclude-regexp"
(setq org-roam-file-exclude-regexp (regexp-quote "foo.org")) (setq org-roam-file-exclude-regexp (regexp-quote "foo.org"))
(expect (length (org-roam-list-files)) :to-equal 2))) (expect (length (org-roam-list-files)) :to-equal 8)))
(describe "org-roam-db-sync" (describe "org-roam--list-files-search-globs"
(before-all
(setq org-roam-directory (expand-file-name "tests/roam-files")
org-roam-db-location (expand-file-name "org-roam.db" temporary-file-directory)
org-roam-file-extensions '("org")
org-roam-file-exclude-regexp nil)
(org-roam-db-sync))
(after-all (it "returns the correct list of globs"
(org-roam-db--close) (expect (org-roam--list-files-search-globs org-roam-file-extensions)
(delete-file org-roam-db-location))
(it "has the correct number of files"
(expect (caar (org-roam-db-query [:select (funcall count) :from files]))
:to-equal
3))
(it "has the correct number of nodes"
(expect (caar (org-roam-db-query [:select (funcall count) :from nodes]))
:to-equal
2))
(it "has the correct number of links"
(expect (caar (org-roam-db-query [:select (funcall count) :from links]))
:to-equal
1))
(it "respects ROAM_EXCLUDE"
;; The excluded node has ID "53fadc75-f48e-461e-be06-44a1e88b2abe"
(expect (mapcar #'car (org-roam-db-query [:select id :from nodes]))
:to-have-same-items-as :to-have-same-items-as
'("884b2341-b7fe-434d-848c-5282c0727861" "440795d0-70c1-4165-993d-aebd5eef7a24")))) '("\"*.org\"" "\"*.org.gpg\"" "\"*.org.age\""))))
(provide 'test-org-roam) (provide 'test-org-roam)