Compare commits

...

32 Commits

Author SHA1 Message Date
cb029c4ce8 (release): org-roam v0.1.2 (#147) 2020-02-21 13:44:16 +08:00
571f65cebd (docs): Add graph setup documentation for MacOS (#146)
Thanks @naistran for instructions, and @leothelocust and @seandavi for
discussion and confirmation
2020-02-21 13:17:09 +08:00
316ad40b2c (feature): org-roam-show-graph: fallback to Emacs SVG viewer (#145) (#145) 2020-02-21 10:55:53 +08:00
b78b545d31 (feature): allow customization of graphviz node appearance (#132) 2020-02-20 17:43:30 +08:00
8523fb43b4 (feature): global org-roam-mode (#143)
Makes org-roam-mode a global minor mode. This mode adds an advice to find-file-function, which decides whether to turn on the local post-command-hook and after-save-hook.

It also advices delete-file and rename-file to ensure cache consistency. Also fixes a bug introduced with #142
2020-02-20 17:33:30 +08:00
dd4b1a97a1 (feature): Add advice to delete-file (#142)
Closes #119
2020-02-20 16:18:40 +08:00
3a8908f72a (feature): add a variable org-roam-new-file-directory (#141)
Fixes #140
2020-02-20 14:34:23 +08:00
ddaf855b5d (chore): some cleanups (#139)
* move org-roam-switch-to-buffer

* add org-roam-switch-to-buffer to docs

* Update changelog
2020-02-20 13:38:31 +08:00
f458c6caf6 (feature): add org-roam-switch-to-buffer (#138)
Implement a buffer-switching mechanism for org-roam files.

Closes #126
2020-02-20 13:23:32 +08:00
0346d3b16c (bugfix): handle nil file names in org-roam--org-roam-file-p (#136) 2020-02-20 11:45:12 +08:00
63754d1ccd (feature): make org-roam-insert work in org-capture buffers (#134)
In org-capture buffers, (buffer-file-name) will always return nil. But
we can get the name of the buffer that we are capturing into
via (buffer-base-buffer), then getting the path to the current file
works as expected.
2020-02-20 09:15:13 +08:00
05ada41710 (docs): add Spacemacs installation instructions (#133) 2020-02-20 08:48:47 +08:00
bdc13cd6bf (feature): update cache on file rename (#124) 2020-02-20 08:32:54 +08:00
2522b9d2fa (bugfi): check for buffer file in post-command-hook (#128)
If you delete a org-roam buffer it can still trigger an update. Need to make
sure that their exists a file for the buffer before we proceed
2020-02-20 00:38:12 +08:00
edbe34a1d9 (chore): add autoload to org-roam-mode (#121)
With this change it is no longer required to load org-roam before use.
2020-02-19 14:50:36 +08:00
570467b34b (internal): increase performance of post-command-hook (#122)
file-truename can be an expensive function on a slow filesystem like NFS. I Removed a lot of the unneeded code and refactored to improve performance. In my testing it took the execution time from 13ms per call to 2µs; over a 1000x speedup. This is important since post-command-hook is called with every character you type.
2020-02-19 02:39:22 +08:00
e84ab1de9a (chore): Add GitHub templates (#117) 2020-02-18 13:43:13 +08:00
996923f9d9 (feature): add jump to point from org-roam buffer (#99) 2020-02-18 11:41:13 +08:00
4a5531cde3 (fix): fix typo (#113)
this addresses the minor typo noted in #112
2020-02-18 10:12:36 +08:00
2d206134fd (feature): insert link as downcased title with org-roam-insert (#110) 2020-02-17 23:15:27 +08:00
883eed0a5e (change): open file links in org-roam buffer in org-roam-last-window (#108)
Addresses #30 , thanks @l3kn
2020-02-17 21:46:48 +08:00
659babf922 (bugfix): force a cache update on making a new file (#107) 2020-02-17 19:02:46 +08:00
424de1f0cb (docs): update changelog (#106) 2020-02-17 16:01:46 +08:00
618b7f6124 (tests): add tests for org-roam-insert (#105) 2020-02-17 15:48:54 +08:00
ce305af319 (breaking): change org-roam-file-format to a function (#103)
This allows for more flexible naming of files. Now filename defaults
to yyyymmddhhmmss_title_here.org. Also, remove
`org-use-timestamp-as-filename`, and change it to
`org-roam-filename-noconfirm` to better describe what it is doing.s
2020-02-17 13:50:40 +08:00
58590a0e9d (bugfix): fix org-roam--find-file picking up temporary files (#98)
Fixes #96
2020-02-17 12:07:08 +08:00
9ae03532da (tests): test org-roam--build-cache-async (#92) 2020-02-16 20:31:09 +08:00
159b64b538 (feature): add support for file encryption by default (#90)
This is controlled by variable org-roam-encrypt-files
2020-02-16 18:31:51 +08:00
8ec3b441d1 (docs): document deft-title patching (#89) 2020-02-16 14:29:58 +08:00
f048a6b866 (bugfix): org-roam--org-roam-file-p: add a missed file-truename (#88) 2020-02-16 11:33:31 +08:00
9aba7ee094 (feature): support encrypted org files (#87)
Authored by @chip2n
2020-02-16 03:04:54 +08:00
ba91fc41a7 (bugfix): fix org-roam--parse-content incorrect :to computation (#86)
* fix org-roam--parse-content incorrect :to computation

org-roam--parse-content always computed the to-path relative to the
org-roam-directory, when it should be relative to the file-path in
question. Fixes #81.

* Add to changelog
2020-02-16 00:41:25 +08:00
21 changed files with 862 additions and 240 deletions

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,30 @@
---
name: Bug Report
about: Something's not working.
title: ''
labels: ''
assignees: 'jethrokuan'
---
### Description
#### Steps to Reproduce
<!--
Example:
1. Load Emacs
2. Run `org-roam--build-cache-async`
3. Run `org-roam-find-file`
...
-->
#### Expected Results
<!-- Example: File A is there -->
#### Actual Results
<!-- Example: File A is missing -->
### Versions
- Emacs (`C-h v emacs-version`): vX.X.X
- Org-roam commit: https://github.com/jethrokuan/org-roam/commit/commithashhere

View File

@ -0,0 +1,16 @@
---
name: Feature Request
about: Create a feature request to improve Org-roam
title: ''
labels: 'enhancement'
assignees: 'jethrokuan'
---
### Brief Abstract
### Long Description
### Proposed Implementation (if any)
### Please check the following:
- [ ] No similar feature requests

1
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1 @@
###### Motivation for this change

1
.gitignore vendored
View File

@ -1 +1,2 @@
/.sandbox/
**/*.elc

View File

@ -1,5 +1,43 @@
# Changelog
## 0.1.2 (2020-02-21)
### Breaking Changes
* [#143][gh-143] `org-roam-mode` is now a global mode. The installation instructions have changed accordingly.
* [#103][gh-103] Change `org-roam-file-format` to a function: `org-roam-file-name-function` to allow more flexible file name customizaton. Also changes `org-roam-use-timestamp-as-filename` to `org-roam-filename-noconfirm` to better describe what it does.
### New Features
* [#145][gh-145] `org-roam-show-graph`: Fallback to Emacs SVG viewer
* [#141][gh-141] add variable `org-roam-new-file-directory` for new Org-roam files
* [#138][gh-138] add `org-roam-switch-to-buffer`
* [#124][gh-124], [#141][gh-141] Maintain cache consistency on file rename and delete
* [#87][gh-87], [#90][gh-90] Support encrypted Org files
* [#110][gh-110] Add prefix to `org-roam-insert`, for inserting titles down-cased
* [#99][gh-99] Add keybinding so that `<return>` or `mouse-1` in the backlinks buffer visits the source file of the backlink at point
### Changes
* [#108][gh-108] Locally overwrite the link following behaviour in the org-roam-buffer to open files in the same window `org-roam` was called from
### Bugfixes
* [#86][gh-86] Fix `org-roam--parse-content` incorrect `:to` computation for nested files
* [#98][gh-98] Fix `org-roam--find-file` picking up temporary files
* [#136][gh-136] Misc bugfixes
### Internal
* [#122][gh-122], [#128][gh-128] Improve performance of post-command-hook
* [#92][gh-92], [#105][gh-105] Add tests for core functionality
### New Contributors
* [@frigge](https://github.com/frigge)
* [@juergenhoetzel](https://github.com/juergenhoetzel)
* [@chip2n](https://github.com/chip2n)
* [@l3kn](https://github.com/l3kn)
* [@jdormit](https://github.com/jdormit)
* [@herbertjones](https://github.com/herbertjones)
* [@CeleritasCelery](https://github.com/CeleritasCelery)
* [@daniel-koudouna](https://github.com/daniel-koudouna)
## 0.1.1 (2020-02-15)
Mostly a documentation/cleanup release.
@ -31,6 +69,24 @@ Mostly a documentation/cleanup release.
[gh-75]: https://github.com/jethrokuan/org-roam/pull/75
[gh-78]: https://github.com/jethrokuan/org-roam/pull/78
[gh-82]: https://github.com/jethrokuan/org-roam/pull/82
[gh-86]: https://github.com/jethrokuan/org-roam/pull/86
[gh-87]: https://github.com/jethrokuan/org-roam/pull/87
[gh-90]: https://github.com/jethrokuan/org-roam/pull/90
[gh-92]: https://github.com/jethrokuan/org-roam/pull/92
[gh-98]: https://github.com/jethrokuan/org-roam/pull/98
[gh-99]: https://github.com/jethrokuan/org-roam/pull/99
[gh-103]: https://github.com/jethrokuan/org-roam/pull/103
[gh-105]: https://github.com/jethrokuan/org-roam/pull/105
[gh-108]: https://github.com/jethrokuan/org-roam/pull/108
[gh-110]: https://github.com/jethrokuan/org-roam/pull/110
[gh-122]: https://github.com/jethrokuan/org-roam/pull/122
[gh-124]: https://github.com/jethrokuan/org-roam/pull/124
[gh-128]: https://github.com/jethrokuan/org-roam/pull/128
[gh-136]: https://github.com/jethrokuan/org-roam/pull/136
[gh-138]: https://github.com/jethrokuan/org-roam/pull/138
[gh-141]: https://github.com/jethrokuan/org-roam/pull/141
[gh-142]: https://github.com/jethrokuan/org-roam/pull/142
[gh-143]: https://github.com/jethrokuan/org-roam/pull/143
# Local Variables:
# eval: (auto-fill-mode -1)

View File

@ -43,22 +43,20 @@ The recommended method is using use-package and straight, or a similar package m
(use-package org-roam
:after org
:hook
((org-mode . org-roam-mode)
(after-init . org-roam--build-cache-async) ;; optional!
)
(after-init . org-roam-mode)
:straight (:host github :repo "jethrokuan/org-roam" :branch "develop")
:custom
(org-roam-directory "/path/to/org-files/")
:bind
("C-c n l" . org-roam)
("C-c n t" . org-roam-today)
("C-c n f" . org-roam-find-file)
("C-c n i" . org-roam-insert)
("C-c n g" . org-roam-show-graph))
:bind (:map org-roam-mode-map
(("C-c n l" . org-roam)
("C-c n f" . org-roam-find-file)
("C-c n g" . org-roam-show-graph))
:map org-mode-map
(("C-c n i" . org-roam-insert))))
```
For more detailed installation instructions, please see [the
installation
For more detailed installation instructions (including instructions for
Spacemacs users), please see [the installation
documentation](https://org-roam.readthedocs.io/en/develop/installation/).
## Knowledge Bases using Org-Roam
@ -74,7 +72,7 @@ A changelog is being maintained [here](CHANGELOG.md)
To report bugs and suggest new feature use the issue tracker. If you
have some code which you would like to be merged, then open a pull
request. Please also see [CONTRIBUTING.md](CONTRIBUTING.md).
request. Please also see [CONTRIBUTING.md](.github/CONTRIBUTING.md).
## License

View File

@ -66,24 +66,20 @@ Org files in all of its main commands (`org-roam-insert`,
`org-roam-find-file`). Hence, having any unique file name is a decent
option, and the default workflow uses the timestamp as the filename.
The format of the filename is specified by the string
`org-roam-file-format`, which defaults to `"%Y%m%d%H%M%S"`. To see
valid specifications, see the help (`C-h f`) for `format-time-string`.
The format of the filename is controlled by the function
`org-roam-file-name-function`, which defaults to a format like
`YYYYMMDDHHMMSS_title_here.org`. You may choose to define your own
function to change this.
There are several reasons for keeping filenames meaningful. For
example, one may wish to publish the Org files, and some publishing
methods such as Org-publish use the file names as slugs for the URLs.
If you wish to maintain manual control of filenames, set
`org-roam-use-timestamp-as-filename` to `nil`:
If you wish to be prompted to change the file name on creation, set
`org-roam-filename-noconfirm` to `nil`:
```emacs-lisp
(setq org-roam-use-timestamp-as-filename nil)
(setq org-roam-filename-noconfirm nil)
```
When this setting is turned off, the user is instead manually prompted
for a filename. It is then the user's responsibility to ensure that
the file names are unique.
It is then the user's responsibility to ensure that the file names are
unique.
### Autopopulating Titles
@ -94,6 +90,15 @@ typically near the top of the file. The option
attribute is automatically inserted into the files created via
Org-roam commands. Setting it to `nil` will disable this behaviour.
### Encryption
Encryption (via GPG) can be enabled for all new files by setting
`org-roam-encrypt-files` to `t`. When enabled, new files are created
with the .org.gpg extension and decryption are handled automatically
by EasyPG. Note that this causes Emacs to ask for password when the
cache is built (if you have an encrypted file in `org-roam-directory`)
as well as each time a new file is created. It might be a good idea to
cache the password in order to make this more managable.
## Org-roam Graph Viewer

View File

@ -5,7 +5,7 @@ A number of packages work well combined with Org-Roam:
[Deft][deft] provides a nice interface for browsing and filtering
org-roam notes.
```
```emacs-lisp
(use-package deft
:after org
:bind
@ -14,8 +14,37 @@ org-roam notes.
(deft-recursive t)
(deft-use-filter-string-for-filename t)
(deft-default-extension "org")
(deft-directory "/path/to/org-roam-files/")
(deft-use-filename-as-title t))
(deft-directory "/path/to/org-roam-files/"))
```
If the title of the Org file is not the first line, you might not get
nice titles. You may choose to patch this to use `org-roam`'s
functionality. Here I'm using [el-patch](https://github.com/raxod502/el-patch):
```emacs-lisp
(use-package el-patch
:straight (:host github
:repo "raxod502/el-patch"
:branch "develop"))
(eval-when-compile
(require 'el-patch))
(use-package deft
;; same as above...
:config/el-patch
(defun deft-parse-title (file contents)
"Parse the given FILE and CONTENTS and determine the title.
If `deft-use-filename-as-title' is nil, the title is taken to
be the first non-empty line of the FILE. Else the base name of the FILE is
used as title."
(el-patch-swap (if deft-use-filename-as-title
(deft-base-filename file)
(let ((begin (string-match "^.+$" contents)))
(if begin
(funcall deft-parse-title-function
(substring contents begin (match-end 0))))))
(org-roam--get-title-or-slug file))))
```
The Deft interface can slow down quickly when the number of files get
@ -29,7 +58,7 @@ powerful alternative to the simple function `org-roam-today`. It
provides better journaling capabilities, and a nice calendar interface
to see all dated entries.
```
```emacs-lisp
(use-package org-journal
:bind
("C-c n j" . org-journal-new-entry)

View File

@ -5,7 +5,7 @@ The gist of the setup is setting up a Bash script to trim off the
`roam://` prefix from the link, causing the desktop application to
call `emacsclient path/to/org-roam-file.org`.
## Setting Up for Linux
## Linux
Create a desktop application. I place mine in
`~/.local/share/applications/roam.desktop`:
@ -37,3 +37,42 @@ running in your shell:
```bash
xdg-mime default roam.desktop x-scheme-handler/roam
```
## Mac OS
One solution to this, recommended in [Issue
#115](https://github.com/jethrokuan/org-roam/issues/115), is to use
[Platypus](https://github.com/sveinbjornt/Platypus). Here are the
instructions for setting up with Platypus and Chrome:
1. Create an executable `launch-emacs.sh` script:
```sh
#!/usr/bin/env bash
/usr/local/bin/emacsclient --no-wait "${1#*:}"
```
2. Install and launch Platypus (with [Homebrew](https://brew.sh/)):
```sh
brew cask install playtpus
```
3. Playtpus settings:
- App Name: `OrgRoam`
- Script Type: `env` and `/usr/bin/env`
- Script Path: `/path/to/your/launch-emacs.sh`
- Tick Accept dropped items and click Settings
- Tick Accept dropped files
- Tick Register as URI scheme handler
- Add `roam` as a protocol
- Create the app
To disable the "confirm" prompt in Chrome, you can also make Chrome
show a checkbox to tick, so that the `OrgRoam` app will be used
without confirmation. To do this, run in a shell:
```sh
defaults write com.google.Chrome ExternalProtocolDialogShowAlwaysOpenCheckbox -bool true
```

View File

@ -7,18 +7,16 @@ The recommended method is using [use-package][use-package] and
(use-package org-roam
:after org
:hook
((org-mode . org-roam-mode)
(after-init . org-roam--build-cache-async) ;; optional!
)
(after-init . org-roam-mode)
:straight (:host github :repo "jethrokuan/org-roam" :branch "develop")
:custom
(org-roam-directory "/path/to/org-files/")
:bind
("C-c n l" . org-roam)
("C-c n t" . org-roam-today)
("C-c n f" . org-roam-find-file)
("C-c n i" . org-roam-insert)
("C-c n g" . org-roam-show-graph))
:bind (:map org-roam-mode-map
(("C-c n l" . org-roam)
("C-c n f" . org-roam-find-file)
("C-c n g" . org-roam-show-graph))
:map org-mode-map
(("C-c n i" . org-roam-insert))))
```
If not using package.el, you can also clone it into your Emacs
@ -33,17 +31,16 @@ git clone https://github.com/jethrokuan/org-roam/ ~/.emacs.d/elisp/org-roam
:after org
:load-path "elisp/"
:hook
((org-mode . org-roam-mode)
(after-init . org-roam--build-cache-async) ;; optional!
)
(after-init . org-roam-mode)
:straight (:host github :repo "jethrokuan/org-roam" :branch "develop")
:custom
(org-roam-directory "/path/to/org-files/")
:bind
("C-c n l" . org-roam)
("C-c n t" . org-roam-today)
("C-c n f" . org-roam-find-file)
("C-c n i" . org-roam-insert)
("C-c n g" . org-roam-show-graph))
:bind (:map org-roam-mode-map
(("C-c n l" . org-roam)
("C-c n f" . org-roam-find-file)
("C-c n g" . org-roam-show-graph))
:map org-mode-map
(("C-c n i" . org-roam-insert))))
```
Or without use-package:
@ -59,3 +56,38 @@ affect the Roam workflow. Do look through them at the
[use-package]: https://github.com/jwiegley/use-package
[straight]: https://github.com/raxod502/straight.el
## Spacemacs
If you are using Spacemacs, you can easily install org-roam by creating a simple layer that wraps org-roam. Paste the following into a new file `/.emacs.d/private/org-roam/packages.el`.
```
(defconst org-roam-packages
'((org-roam :location
(recipe :fetcher github :repo "jethrokuan/org-roam" :branch "develop"))))
(defun org-roam/init-org-roam ()
(use-package org-roam
:after org
:hook
(after-init . org-roam-mode)
:custom
(org-roam-directory "/path/to/org-files/")
:init
(progn
(spacemacs/declare-prefix "ar" "org-roam")
(spacemacs/set-leader-keys
"arl" 'org-roam
"art" 'org-roam-today
"arf" 'org-roam-find-file
"arg" 'org-roam-show-graph)
(spacemacs/declare-prefix-for-mode 'org-mode "mr" "org-roam")
(spacemacs/set-leader-keys-for-major-mode 'org-mode
"rl" 'org-roam
"rt" 'org-roam-today
"rb" 'org-roam-switch-to-buffer
"rf" 'org-roam-find-file
"ri" 'org-roam-insert
"rg" 'org-roam-show-graph))))
```
Next, append `org-roam` to the `dotspacemacs-configuration-layers` list in your `.spacemacs` configuration file. Reload (`SPC f e R`) or restart Emacs to load `org-roam`. It's functions are available under the prefix `SPC a r` and `, r` when visiting an org-mode buffer.

View File

@ -5,7 +5,7 @@
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/jethrokuan/org-roam
;; Keywords: org-mode, roam, convenience
;; Version: 0.1.0
;; Version: 0.1.2
;; Package-Requires: ((emacs "26.1"))
;; This file is NOT part of GNU Emacs.
@ -38,6 +38,24 @@
(require 'subr-x)
(require 'cl-lib)
(defun org-roam--file-name-extension (filename)
"Return file name extension for FILENAME.
Like file-name-extension, but does not strip version number."
(save-match-data
(let ((file (file-name-nondirectory filename)))
(if (and (string-match "\\.[^.]*\\'" file)
(not (eq 0 (match-beginning 0))))
(substring file (+ (match-beginning 0) 1))))))
(defun org-roam--org-file-p (path)
"Check if PATH is pointing to an org file."
(let ((ext (org-roam--file-name-extension path)))
(or (string= ext "org")
(and
(string= ext "gpg")
(string= (org-roam--file-name-extension (file-name-sans-extension path)) "org")))))
(defun org-roam--find-files (dir)
"Return all `org-roam' files in `DIR'."
(if (file-exists-p dir)
@ -53,7 +71,7 @@
(when (not (string-match dir-ignore-regexp file))
(setq result (append (org-roam--find-files file) result))))
((and (file-readable-p file)
(string= (file-name-extension file) "org"))
(org-roam--org-file-p file))
(setq result (cons (file-truename file) result)))))
result)))
@ -65,25 +83,28 @@
(path (org-element-property :path link))
(start (org-element-property :begin link)))
(when (and (string= type "file")
(string= (file-name-extension path) "org"))
(org-roam--org-file-p path))
(goto-char start)
(let* ((element (org-element-at-point))
(begin (or (org-element-property :content-begin element)
(org-element-property :begin element)))
(content (or (org-element-property :raw-value element)
(buffer-substring
(or (org-element-property :content-begin element)
(org-element-property :begin element))
begin
(or (org-element-property :content-end element)
(org-element-property :end element))))))
(list :from (or file-path
(file-truename (buffer-file-name (current-buffer))))
:to (file-truename (expand-file-name path org-roam-directory))
:content (string-trim content))))))))
(org-element-property :end element)))))
(content (string-trim content))
(file-path (or file-path
(file-truename (buffer-file-name (current-buffer))))))
(list :from file-path
:to (file-truename (expand-file-name path (file-name-directory file-path)))
:properties (list :content content :point begin))))))))
(cl-defun org-roam--insert-item (item &key forward backward)
"Insert ITEM into FORWARD and BACKWARD cache.
ITEM is of the form: (:from from-path :to to-path :content preview-content)."
(pcase-let ((`(:from ,p-from :to ,p-to :content ,content) item))
ITEM is of the form: (:from from-path :to to-path :properties (:content preview-content :point point))."
(pcase-let ((`(:from ,p-from :to ,p-to :properties ,props) item))
;; Build forward-links
(let ((links (gethash p-from forward)))
(if links
@ -96,14 +117,14 @@ ITEM is of the form: (:from from-path :to to-path :content preview-content)."
(let ((contents-hash (gethash p-to backward)))
(if contents-hash
(if-let ((contents-list (gethash p-from contents-hash)))
(let ((updated (cons content contents-list)))
(let ((updated (cons props contents-list)))
(puthash p-from updated contents-hash)
(puthash p-to contents-hash backward))
(progn
(puthash p-from (list content) contents-hash)
(puthash p-from (list props) contents-hash)
(puthash p-to contents-hash backward)))
(let ((contents-hash (make-hash-table :test #'equal)))
(puthash p-from (list content) contents-hash)
(puthash p-from (list props) contents-hash)
(puthash p-to contents-hash backward))))))
(defun org-roam--extract-title ()
@ -116,6 +137,33 @@ ITEM is of the form: (:from from-path :to to-path :content preview-content)."
(org-element-property :value kw)))
:first-match t))
(defun org-roam--build-cache (dir)
"Build the org-roam caches in DIR."
(let ((backward-links (make-hash-table :test #'equal))
(forward-links (make-hash-table :test #'equal))
(file-titles (make-hash-table :test #'equal)))
(let* ((org-roam-files (org-roam--find-files dir))
(file-items (mapcar (lambda (file)
(with-temp-buffer
(insert-file-contents file)
(org-roam--parse-content file))) org-roam-files)))
(dolist (items file-items)
(dolist (item items)
(org-roam--insert-item
item
:forward forward-links
:backward backward-links)))
(dolist (file org-roam-files)
(with-temp-buffer
(insert-file-contents file)
(when-let ((title (org-roam--extract-title)))
(puthash file title file-titles)))
org-roam-files))
(list
:forward forward-links
:backward backward-links
:titles file-titles)))
(provide 'org-roam-utils)
;;; org-roam-utils.el ends here

View File

@ -5,7 +5,7 @@
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/jethrokuan/org-roam
;; Keywords: org-mode, roam, convenience
;; Version: 0.1.0
;; Version: 0.1.2
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (async "1.9.4"))
;; This file is NOT part of GNU Emacs.
@ -55,7 +55,14 @@
"Path to Org-roam files.
All Org files, at any level of nesting, is considered part of the Org-roam."
:type 'directoy
:type 'directory
:group 'org-roam)
(defcustom org-roam-new-file-directory nil
"Path to where new Org-roam files are created.
If nil, default to the org-roam-directory (preferred)."
:type 'directory
:group 'org-roam)
(defcustom org-roam-buffer-position 'right
@ -68,18 +75,23 @@ Valid values are
(const right))
:group 'org-roam)
(defcustom org-roam-file-format "%Y%m%d%H%M%S"
"The timestamp format to use filenames."
:type 'string
:group 'org-roam)
(defcustom org-roam-file-name-function #'org-roam--file-name-timestamp-title
"The function used to generate filenames.
The function takes as parameter `TITLE', a string the user inputs."
:group 'org-roam
:type '(choice (const :tag "Default" org-roam--file-name-timestamp-title)
(function :tag "Personalized function")))
(defcustom org-roam-link-title-format "%s"
"The format string used when inserting org-roam links that use their title."
:type 'string
:group 'org-roam)
(defcustom org-roam-use-timestamp-as-filename t
"Whether to use timestamp as a file name. If not true, prompt for a file name each time."
(defcustom org-roam-filename-noconfirm t
"Whether to prompt for confirmation of fil name for new files.
If nil, always ask for filename."
:type 'boolean
:group 'org-roam)
@ -96,6 +108,11 @@ Valid values are
:type 'string
:group 'org-roam)
(defcustom org-roam-encrypt-files nil
"Whether to encrypt new files. If true, create files with .org.gpg extension."
:type 'boolean
:group 'org-roam)
(defcustom org-roam-graph-viewer (executable-find "firefox")
"Path to executable for viewing SVG."
:type 'string
@ -106,6 +123,16 @@ Valid values are
:type 'string
:group 'org-roam)
(defcustom org-roam-graph-max-title-length 100
"Maximum length of titles in graphviz graph nodes"
:type 'number
:group 'org-roam)
(defcustom org-roam-graph-node-shape "ellipse"
"Maximum length of titles in graphviz graph nodes"
:type 'string
:group 'org-roam)
;;; Polyfills
;; These are for functions I use that are only available in newer Emacs
@ -139,9 +166,12 @@ If called interactively, then PARENTS is non-nil."
(defvar org-roam-titles-cache (make-hash-table :test #'equal)
"Cache containing titles for org-roam files.")
(defvar org-roam-current-file nil
(defvar org-roam--current-buffer nil
"Currently displayed file in `org-roam' buffer.")
(defvar org-roam-last-window nil
"Last window `org-roam' was called from.")
;;; Utilities
(defun org-roam--ensure-cache-built ()
"Ensures that org-roam cache is built."
@ -149,11 +179,14 @@ If called interactively, then PARENTS is non-nil."
(org-roam--build-cache-async)
(user-error "Your Org-Roam cache isn't built yet! Please wait")))
(defun org-roam--org-roam-file-p ()
"Return t if file is part of org-roam system, false otherwise."
(and (buffer-file-name (current-buffer))
(f-descendant-of-p (file-truename (buffer-file-name (current-buffer)))
org-roam-directory)))
(defun org-roam--org-roam-file-p (&optional file)
"Return t if FILE is part of org-roam system, defaulting to the name of the current buffer. Else, return nil."
(let ((path (or file
(buffer-file-name (current-buffer)))))
(and path
(org-roam--org-file-p path)
(f-descendant-of-p (file-truename path)
(file-truename org-roam-directory)))))
(defun org-roam--get-title-from-cache (file)
"Return title of `FILE' from the cache."
@ -167,14 +200,17 @@ If called interactively, then PARENTS is non-nil."
"Return all org-roam files."
(org-roam--find-files (file-truename org-roam-directory)))
(defun org-roam--get-file-path (id &optional absolute)
"Convert identifier `ID' to file path.
(defun org-roam--make-new-file-path (id &optional absolute)
"Make new file path from identifier `ID'.
If `ABSOLUTE', return the absolute file-path. Else, return the relative file-path."
If `ABSOLUTE', return an absolute file-path. Else, return a relative file-path."
(let ((absolute-file-path (file-truename
(expand-file-name
(concat id ".org")
org-roam-directory))))
(if org-roam-encrypt-files
(concat id ".org.gpg")
(concat id ".org"))
(or org-roam-new-file-directory
org-roam-directory)))))
(if absolute
absolute-file-path
(file-relative-name absolute-file-path
@ -195,6 +231,13 @@ If `ABSOLUTE', return the absolute file-path. Else, return the relative file-pat
(s (s-join "_" s)))
s))
(defun org-roam--file-name-timestamp-title (title)
"Return a file name (without extension) for new files.
It uses TITLE and the current timestamp to form a unique title."
(let ((timestamp (format-time-string "%Y%m%d%H%M%S" (current-time)))
(slug (org-roam--title-to-slug title)))
(format "%s_%s" timestamp slug)))
;;; Creating org-roam files
(defun org-roam--populate-title (file &optional title)
@ -218,51 +261,54 @@ If not provided, derive the title from the file name."
"Create an org-roam file at FILE-PATH, optionally setting the TITLE attribute."
(if (file-exists-p file-path)
(error (format "Aborting, file already exists at %s" file-path))
(make-empty-file file-path t)
(if org-roam-autopopulate-title
(org-roam--populate-title file-path title)
(make-empty-file file-path))))
(org-roam--populate-title file-path title))
(save-excursion
(with-current-buffer (find-file-noselect file-path)
(org-roam--update-cache)))))
(defun org-roam--new-file-named (slug)
"Create a new file named `SLUG'.
`SLUG' is the short file name, without a path or a file extension."
(interactive "sNew filename (without extension): ")
(let ((file-path (org-roam--get-file-path slug t)))
(let ((file-path (org-roam--make-new-file-path slug t)))
(unless (file-exists-p file-path)
(org-roam--make-file file-path))
(find-file file-path)))
(defun org-roam--get-new-id (&optional title)
"Return a new ID, generated from the current time.
Optionally pass it the title, for a smart file name."
(if org-roam-use-timestamp-as-filename
(format-time-string org-roam-file-format (current-time))
(let* ((slug (read-string "Enter ID (without extension): "
(if title
(org-roam--title-to-slug title)
"")))
(file-path (org-roam--get-file-path slug t)))
(if (file-exists-p file-path)
(user-error "There's already a file at %s")
slug))))
(defun org-roam--get-new-id (title)
"Return a new ID, given the note TITLE."
(let* ((proposed-slug (funcall org-roam-file-name-function title))
(new-slug (if org-roam-filename-noconfirm
proposed-slug
(read-string "Enter ID (without extension): "
proposed-slug)))
(file-path (org-roam--make-new-file-path new-slug t)))
(if (file-exists-p file-path)
(user-error "There's already a file at %s")
new-slug)))
(defun org-roam-new-file ()
"Quickly create a new file, using the current timestamp."
(interactive)
(org-roam--new-file-named (org-roam--get-new-id)))
(org-roam--new-file-named (format-time-string "%Y%m%d%H%M%S" (current-time))))
;;; Inserting org-roam links
(defun org-roam-insert ()
"Find an org-roam file, and insert a relative org link to it at point."
(interactive)
(defun org-roam-insert (prefix)
"Find an org-roam file, and insert a relative org link to it at point.
If PREFIX, downcase the title before insertion."
(interactive "P")
(let* ((completions (mapcar (lambda (file)
(list (org-roam--get-title-or-slug file)
file))
(org-roam--find-all-files)))
(title (completing-read "File: " completions))
(absolute-file-path (or (cadr (assoc title completions))
(org-roam--get-file-path (org-roam--get-new-id title) t)))
(current-file-path (-> (current-buffer)
(org-roam--make-new-file-path (org-roam--get-new-id title) t)))
(current-file-path (-> (or (buffer-base-buffer)
(current-buffer))
(buffer-file-name)
(file-truename)
(file-name-directory))))
@ -271,7 +317,9 @@ Optionally pass it the title, for a smart file name."
(insert (format "[[%s][%s]]"
(concat "file:" (file-relative-name absolute-file-path
current-file-path))
(format org-roam-link-title-format title)))))
(format org-roam-link-title-format (if prefix
(downcase title)
title))))))
;;; Finding org-roam files
(defun org-roam-find-file ()
@ -282,15 +330,37 @@ Optionally pass it the title, for a smart file name."
(org-roam--find-all-files)))
(title-or-slug (completing-read "File: " completions))
(absolute-file-path (or (cadr (assoc title-or-slug completions))
(org-roam--get-file-path
(org-roam--make-new-file-path
(org-roam--get-new-id title-or-slug) t))))
(unless (file-exists-p absolute-file-path)
(org-roam--make-file absolute-file-path title-or-slug))
(find-file absolute-file-path)))
;;; Building the org-roam cache (asynchronously)
(defun org-roam--get-roam-buffers ()
"Return a list of buffers that are org-roam files."
(--filter (and (with-current-buffer it (derived-mode-p 'org-mode))
(buffer-file-name it)
(org-roam--org-roam-file-p (buffer-file-name it)))
(buffer-list)))
(defun org-roam-switch-to-buffer ()
"Switch to an existing org-roam buffer using completing-read."
(interactive)
(let* ((roam-buffers (org-roam--get-roam-buffers))
(names-and-buffers (mapcar (lambda (buffer)
(cons (or (org-roam--get-title-from-cache
(buffer-file-name buffer))
(buffer-name buffer))
buffer))
roam-buffers)))
(unless roam-buffers
(error "No roam buffers."))
(when-let ((name (completing-read "Choose a buffer: " names-and-buffers)))
(switch-to-buffer (cdr (assoc name names-and-buffers))))))
;;; Building the org-roam cache
(defun org-roam--build-cache-async ()
"Builds the cache asychronously, saving it into the org-roam caches."
"Builds the caches asychronously."
(interactive)
(async-start
`(lambda ()
@ -298,30 +368,7 @@ Optionally pass it the title, for a smart file name."
(package-initialize)
(require 'org-roam-utils)
,(async-inject-variables "org-roam-directory")
(let ((backward-links (make-hash-table :test #'equal))
(forward-links (make-hash-table :test #'equal))
(file-titles (make-hash-table :test #'equal)))
(let* ((org-roam-files (org-roam--find-files org-roam-directory))
(file-items (mapcar (lambda (file)
(with-temp-buffer
(insert-file-contents file)
(org-roam--parse-content file))) org-roam-files)))
(dolist (items file-items)
(dolist (item items)
(org-roam--insert-item
item
:forward forward-links
:backward backward-links)))
(mapcar (lambda (file)
(with-temp-buffer
(insert-file-contents file)
(when-let ((title (org-roam--extract-title)))
(puthash file title file-titles))))
org-roam-files))
(list
:forward forward-links
:backward backward-links
:titles file-titles)))
(org-roam--build-cache org-roam-directory))
(lambda (cache)
(setq org-roam-forward-links-cache (plist-get cache :forward))
(setq org-roam-backward-links-cache (plist-get cache :backward))
@ -330,10 +377,20 @@ Optionally pass it the title, for a smart file name."
(message "Org-roam cache built!"))))
(defun org-roam--clear-cache ()
"Clears all entries in the caches."
(interactive)
(setq org-roam-cache-initialized nil)
(setq org-roam-forward-links-cache (make-hash-table :test #'equal))
(setq org-roam-backward-links-cache (make-hash-table :test #'equal))
(setq org-roam-titles-cache (make-hash-table :test #'equal)))
(defun org-roam--clear-file-from-cache (&optional filepath)
"Remove any related links to the file.
This is equivalent to removing the node from the graph."
(let ((file (file-truename (buffer-file-name (current-buffer)))))
(let* ((path (or filepath
(buffer-file-name (current-buffer))))
(file (file-truename path)))
;; Step 1: Remove all existing links for file
(when-let ((forward-links (gethash file org-roam-forward-links-cache)))
;; Delete backlinks to file
@ -356,7 +413,7 @@ This is equivalent to removing the node from the graph."
(defun org-roam--update-cache ()
"Update org-roam caches for the current buffer file."
(save-excursion
(org-roam--clear-cache)
(org-roam--clear-file-from-cache)
;; Insert into title cache
(org-roam--update-cache-title)
;; Insert new items
@ -386,16 +443,48 @@ This is equivalent to removing the node from the graph."
(let ((time (org-read-date nil 'to-time nil "Date: ")))
(org-roam--new-file-named (format-time-string "%Y-%m-%d" time))))
;;; Org-roam buffer updates
;;; Org-roam buffer
(define-derived-mode org-roam-backlinks-mode org-mode "Backlinks"
"Major mode for the org-roam backlinks buffer
Bindings:
\\{org-roam-backlinks-mode-map}")
(define-key org-roam-backlinks-mode-map [mouse-1] 'org-roam-jump-to-backlink)
(define-key org-roam-backlinks-mode-map (kbd "RET") 'org-roam-jump-to-backlink)
(defun org-roam-jump-to-backlink ()
"Jumps to original file and location of the backlink content snippet at point"
(interactive)
(let ((file-from (get-text-property (point) 'file-from))
(p (get-text-property (point) 'file-from-point)))
(when (and file-from p)
(find-file file-from)
(goto-char p)
(org-show-context))))
(defun org-roam--find-file (file)
"Open FILE in the window `org-roam' was called from."
(if (and org-roam-last-window (window-valid-p org-roam-last-window))
(progn (with-selected-window org-roam-last-window
(find-file file))
(select-window org-roam-last-window))
(find-file file)))
(defun org-roam-update (file-path)
"Show the backlinks for given org file for file at `FILE-PATH'."
(org-roam--ensure-cache-built)
(let ((buffer-title (org-roam--get-title-or-slug file-path)))
(with-current-buffer org-roam-buffer
;; Locally overwrite the file opening function to re-use the
;; last window org-roam was called from
(setq-local
org-link-frame-setup
(cons '(file . org-roam--find-file) org-link-frame-setup))
(let ((inhibit-read-only t))
(erase-buffer)
(when (not (eq major-mode 'org-mode))
(org-mode))
(when (not (eq major-mode 'org-roam-backlinks-mode))
(org-roam-backlinks-mode))
(make-local-variable 'org-return-follows-link)
(setq org-return-follows-link t)
(insert
@ -408,14 +497,165 @@ This is equivalent to removing the node from the graph."
(insert (format "** [[file:%s][%s]]\n"
file-from
(org-roam--get-title-or-slug file-from)))
(dolist (content contents)
(insert (concat (propertize (s-trim (s-replace "\n" " " content))
'font-lock-face 'org-block)
"\n\n"))))
(dolist (properties contents)
(let ((content (propertize
(s-trim (s-replace "\n" " "
(plist-get properties :content)))
'font-lock-face 'org-block
'help-echo "mouse-1: visit backlinked note"
'file-from file-from
'file-from-point (plist-get properties :point))))
(insert (format "%s \n\n" content)))))
backlinks))
(insert "\n\n* No backlinks!")))
(read-only-mode 1)))
(setq org-roam-current-file file-path))
(read-only-mode 1))))
;;; Building the Graphviz graph
(defun org-roam-build-graph ()
"Build graphviz graph output."
(org-roam--ensure-cache-built)
(with-temp-buffer
(insert "digraph {\n")
(dolist (file (org-roam--find-all-files))
(let ((title (org-roam--get-title-or-slug file)))
(let ((shortened-title (s-truncate org-roam-graph-max-title-length title)))
(insert
(format " \"%s\" [label=\"%s\", shape=%s, URL=\"roam://%s\", tooltip=\"%s\"];\n"
title
shortened-title
org-roam-graph-node-shape
file
title
)))))
(maphash
(lambda (from-link to-links)
(dolist (to-link to-links)
(insert (format " \"%s\" -> \"%s\";\n"
(org-roam--get-title-or-slug from-link)
(org-roam--get-title-or-slug to-link)))))
org-roam-forward-links-cache)
(insert "}")
(buffer-string)))
(defun org-roam-show-graph ()
"Generate the org-roam graph in SVG format, and display it using `org-roam-graph-viewer'."
(interactive)
(unless org-roam-graphviz-executable
(setq org-roam-graphviz-executable (executable-find "dot")))
(unless org-roam-graphviz-executable
(user-error "Can't find graphviz executable. Please check if it is in your path"))
(declare (indent 0))
(let ((temp-dot (expand-file-name "graph.dot" temporary-file-directory))
(temp-graph (expand-file-name "graph.svg" temporary-file-directory))
(graph (org-roam-build-graph)))
(with-temp-file temp-dot
(insert graph))
(call-process org-roam-graphviz-executable nil 0 nil temp-dot "-Tsvg" "-o" temp-graph)
(if (and org-roam-graph-viewer (executable-find org-roam-graph-viewer))
(call-process org-roam-graph-viewer nil 0 nil temp-graph)
(view-file temp-graph))))
;;; Org-roam minor mode
(cl-defun org-roam--maybe-update-buffer (&key redisplay)
"Update `org-roam-buffer' with the necessary information.
This needs to be quick/infrequent, because this is run at
`post-command-hook'."
(let ((buffer (window-buffer)))
(when (and (or redisplay
(not (eq org-roam--current-buffer buffer)))
(eq 'visible (org-roam--current-visibility))
(buffer-local-value 'buffer-file-truename buffer))
(setq org-roam--current-buffer buffer)
(org-roam-update (expand-file-name
(buffer-local-value 'buffer-file-truename buffer))))))
(defun org-roam--find-file-hook-function ()
"Called by `find-file-hook' when `org-roam-mode' is on."
(when (org-roam--org-roam-file-p)
(add-hook 'post-command-hook #'org-roam--maybe-update-buffer nil t)
(add-hook 'after-save-hook #'org-roam--update-cache nil t)))
(defvar org-roam-mode-map
(make-sparse-keymap)
"Keymap for org-roam commands.")
(defun org-roam--delete-file-advice (file &optional _trash)
"Advice for maintaining cache consistency during file deletes."
(org-roam--clear-file-from-cache (file-truename file)))
(defun org-roam--rename-file-advice (file new-file &rest args)
"Rename backlinks of FILE to refer to NEW-FILE."
(when (and (not (auto-save-file-name-p file))
(not (auto-save-file-name-p new-file))
(org-roam--org-roam-file-p new-file))
(org-roam--ensure-cache-built)
(org-roam--clear-file-from-cache file)
(let* ((files (gethash file org-roam-backward-links-cache nil))
(path (file-truename file))
(new-path (file-truename new-file))
(slug (org-roam--get-title-or-slug file))
(old-title (format org-roam-link-title-format slug))
(new-slug (or (org-roam--get-title-from-cache path)
(org-roam--get-title-or-slug new-path)))
(new-title (format org-roam-link-title-format new-slug)))
(when files
(maphash (lambda (file-from props)
(let* ((file-dir (file-name-directory file-from))
(relative-path (file-relative-name new-path file-dir))
(old-relative-path (file-relative-name path file-dir))
(slug-regex (regexp-quote (format "[[file:%s][%s]]" old-relative-path old-title)))
(named-regex (concat
(regexp-quote (format "[[file:%s][" old-relative-path))
"\\(.*\\)"
(regexp-quote "]]"))))
(with-temp-file file-from
(insert-file-contents file-from)
(while (re-search-forward slug-regex nil t)
(replace-match (format "[[file:%s][%s]]" relative-path new-title)))
(goto-char (point-min))
(while (re-search-forward named-regex nil t)
(replace-match (format "[[file:%s][\\1]]" relative-path))))
(save-window-excursion
(find-file file-from)
(org-roam--update-cache))))
files))
(save-window-excursion
(find-file new-path)
(org-roam--update-cache)))))
;;;###autoload
(define-minor-mode org-roam-mode
"Minor mode for Org-roam.
When called interactively, toggle `org-roam-mode'. with prefix ARG, enable `org-roam-mode'
if ARG is posiwive, otherwise disable it.
When called from Lisp, enable `org-roam-mode' if ARG is omitted, nil, or positive.
If ARG is `toggle', toggle `org-roam-mode'. Otherwise, behave as if called interactively."
:lighter "Org-Roam "
:keymap org-roam-mode-map
:group 'org-roam
:require 'org-roam
:global t
(cond
(org-roam-mode
(unless org-roam-cache-initialized
(org-roam--build-cache-async))
(add-hook 'find-file-hook #'org-roam--find-file-hook-function)
(advice-add 'rename-file :after #'org-roam--rename-file-advice)
(advice-add 'delete-file :before #'org-roam--delete-file-advice))
(t
(remove-hook 'find-file-hook #'org-roam--find-file-hook-function)
(advice-remove 'rename-file #'org-roam--rename-file-advice)
(advice-remove 'delete-file #'org-roam--delete-file-advice)
;; Disable local hooks for all org-roam buffers
(dolist (buf (org-roam--get-roam-buffers))
(with-current-buffer buf
(remove-hook 'post-command-hook #'org-roam--maybe-update-buffer t)
(remove-hook 'after-save-hook #'org-roam--update-cache t))))))
(provide 'org-roam)
;;; Show/hide the org-roam buffer
(define-inline org-roam--current-visibility ()
@ -454,93 +694,11 @@ Valid states are 'visible, 'exists and 'none."
(defun org-roam ()
"Pops up the window `org-roam-buffer' accordingly."
(interactive)
(setq org-roam-last-window (get-buffer-window))
(pcase (org-roam--current-visibility)
('visible (delete-window (get-buffer-window org-roam-buffer)))
('exists (org-roam--setup-buffer))
('none (org-roam--setup-buffer))))
;;; The minor mode definition that updates the buffer
(defun org-roam--maybe-enable ()
"Enable org-roam updating for file, if file is an org-roam file."
(when (org-roam--org-roam-file-p)
(org-roam--enable)))
(defun org-roam--enable ()
"Enable org-roam updating for file.
1. If the cache does not yet exist, build it asynchronously.
2. Setup hooks for updating the cache, and the org-roam buffer."
(unless org-roam-cache-initialized
(org-roam--build-cache-async))
(add-hook 'post-command-hook #'org-roam--maybe-update-buffer nil t)
(add-hook 'after-save-hook #'org-roam--update-cache nil t))
(defun org-roam--disable ()
"Disable org-roam updating for file.
1. Remove hooks for updating the cache, and the org-roam buffer."
(remove-hook 'post-command-hook #'org-roam--maybe-update-buffer)
(remove-hook 'after-save-hook #'org-roam--update-cache))
(cl-defun org-roam--maybe-update-buffer (&key redisplay)
"Update `org-roam-buffer' with the necessary information.
This needs to be quick/infrequent, because this is run at
`post-command-hook'."
(with-current-buffer (window-buffer)
(when (and (get-buffer org-roam-buffer)
(buffer-file-name (current-buffer))
(file-exists-p (file-truename (buffer-file-name (current-buffer))))
(or redisplay
(not (string= org-roam-current-file
(file-truename (buffer-file-name (current-buffer)))))))
(org-roam-update (file-truename (buffer-file-name (window-buffer)))))))
(define-minor-mode org-roam-mode
"Global minor mode to automatically update the org-roam buffer."
:require 'org-roam
(if org-roam-mode
(org-roam--maybe-enable)
(org-roam--disable)))
;;; Building the Graphviz graph
(defun org-roam-build-graph ()
"Build graphviz graph output."
(org-roam--ensure-cache-built)
(with-temp-buffer
(insert "digraph {\n")
(dolist (file (org-roam--find-all-files))
(insert
(format " \"%s\" [URL=\"roam://%s\"];\n"
(org-roam--get-title-or-slug file)
file)))
(maphash
(lambda (from-link to-links)
(dolist (to-link to-links)
(insert (format " \"%s\" -> \"%s\";\n"
(org-roam--get-title-or-slug from-link)
(org-roam--get-title-or-slug to-link)))))
org-roam-forward-links-cache)
(insert "}")
(buffer-string)))
(defun org-roam-show-graph ()
"Generate the org-roam graph in SVG format, and display it using `org-roam-graph-viewer'."
(interactive)
(unless org-roam-graphviz-executable
(setq org-roam-graphviz-executable (executable-find "dot")))
(unless org-roam-graphviz-executable
(user-error "Can't find graphviz executable. Please check if it is in your path"))
(declare (indent 0))
(let ((temp-dot (expand-file-name "graph.dot" temporary-file-directory))
(temp-graph (expand-file-name "graph.svg" temporary-file-directory))
(graph (org-roam-build-graph)))
(with-temp-file temp-dot
(insert graph))
(call-process org-roam-graphviz-executable nil 0 nil temp-dot "-Tsvg" "-o" temp-graph)
(call-process org-roam-graph-viewer nil 0 nil temp-graph)))
(provide 'org-roam)
;;; org-roam.el ends here
;; Local Variables:

View File

@ -1,9 +0,0 @@
let
pkgs = import <nixpkgs> {};
in
pkgs.mkShell {
name = "docs";
buildInput = with pkgs; [
mkdocs
];
}

7
tests/roam-files/f1.org Normal file
View File

@ -0,0 +1,7 @@
#+TITLE: File 1
link to [[file:nested/f1.org][Nested File 1]]
link to [[file:f2.org][File 2]]
Arbitrary [[https://google.com][HTML]] link
Arbitrary text

3
tests/roam-files/f2.org Normal file
View File

@ -0,0 +1,3 @@
#+TITLE: File 2
This file has no links.

5
tests/roam-files/f3.org Normal file
View File

@ -0,0 +1,5 @@
#+TITLE: File 3
This file has a link to an file with no title.
[[file:no-title.org][no-title]]

View File

@ -0,0 +1,4 @@
#+TITLE: Nested File 1
Link to [[file:f2.org][Nested File 2]]
Link to [[file:../f1.org][File 1]]

View File

@ -0,0 +1,3 @@
#+TITLE: Nested File 2
Link to [[file:f1.org][Nested File 1]]

View File

@ -0,0 +1 @@
no title in this file :O

View File

@ -3,7 +3,7 @@
;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; Package-Requires: ((buttercup))
;; Package-Requires: ((buttercup) (with-simulated-input))
;; 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
@ -27,9 +27,204 @@
;;;; Requirements
(require 'buttercup)
(require 'with-simulated-input)
(require 'org-roam)
(require 'dash)
(defun abs-path (file-path)
(file-truename (expand-file-name file-path org-roam-directory)))
(defun org-roam--test-find-new-file (path)
(let ((path (abs-path path)))
(make-directory (file-name-directory path) t)
(find-file path)))
(defvar org-roam--tests-directory (file-truename (concat default-directory "tests/roam-files"))
"Directory containing org-roam test org files.")
(defun org-roam--test-init ()
(let ((original-dir org-roam--tests-directory)
(new-dir (expand-file-name (make-temp-name "org-roam") temporary-file-directory)))
(copy-directory original-dir new-dir)
(setq org-roam-directory new-dir))
(org-roam-mode +1))
(defun org-roam--test-build-cache ()
"Builds the caches synchronously."
(let ((cache (org-roam--build-cache org-roam-directory)))
(setq org-roam-forward-links-cache (plist-get cache :forward))
(setq org-roam-backward-links-cache (plist-get cache :backward))
(setq org-roam-titles-cache (plist-get cache :titles))
(setq org-roam-cache-initialized t)))
;;; Tests
(describe "Org-roam cache"
(it "Mock Test"
(expect t :to-be t)))
(describe "org-roam--build-cache-async"
(it "initializes correctly"
(org-roam--test-init)
(expect org-roam-cache-initialized :to-be nil)
(expect (hash-table-count org-roam-forward-links-cache) :to-be 0)
(expect (hash-table-count org-roam-backward-links-cache) :to-be 0)
(expect (hash-table-count org-roam-titles-cache) :to-be 0)
(org-roam--build-cache-async)
(sleep-for 3) ;; Because it's async
;; Caches should be populated
(expect org-roam-cache-initialized :to-be t)
(expect (hash-table-count org-roam-forward-links-cache) :to-be 4)
(expect (hash-table-count org-roam-backward-links-cache) :to-be 5)
(expect (hash-table-count org-roam-titles-cache) :to-be 5)
;; Forward cache
(let ((f1 (gethash (abs-path "f1.org") org-roam-forward-links-cache))
(f2 (gethash (abs-path "f2.org") org-roam-forward-links-cache))
(nested-f1 (gethash (abs-path "nested/f1.org") org-roam-forward-links-cache))
(nested-f2 (gethash (abs-path "nested/f2.org") org-roam-forward-links-cache))
(expected-f1 (list (abs-path "nested/f1.org")
(abs-path "f2.org")))
(expected-nested-f1 (list (abs-path "nested/f2.org")
(abs-path "f1.org")))
(expected-nested-f2 (list (abs-path "nested/f1.org"))))
(expect f1 :to-have-same-items-as expected-f1)
(expect f2 :to-be nil)
(expect nested-f1 :to-have-same-items-as expected-nested-f1)
(expect nested-f2 :to-have-same-items-as expected-nested-f2))
;; Backward cache
(let ((f1 (hash-table-keys (gethash (abs-path "f1.org") org-roam-backward-links-cache)))
(f2 (hash-table-keys (gethash (abs-path "f2.org") org-roam-backward-links-cache)))
(nested-f1 (hash-table-keys(gethash (abs-path "nested/f1.org") org-roam-backward-links-cache)))
(nested-f2 (hash-table-keys (gethash (abs-path "nested/f2.org") org-roam-backward-links-cache)))
(expected-f1 (list (abs-path "nested/f1.org")))
(expected-f2 (list (abs-path "f1.org")))
(expected-nested-f1 (list (abs-path "nested/f2.org")
(abs-path "f1.org")))
(expected-nested-f2 (list (abs-path "nested/f1.org"))))
(expect f1 :to-have-same-items-as expected-f1)
(expect f2 :to-have-same-items-as expected-f2)
(expect nested-f1 :to-have-same-items-as expected-nested-f1)
(expect nested-f2 :to-have-same-items-as expected-nested-f2))
;; Titles Cache
(expect (gethash (abs-path "f1.org") org-roam-titles-cache) :to-equal "File 1")
(expect (gethash (abs-path "f2.org") org-roam-titles-cache) :to-equal "File 2")
(expect (gethash (abs-path "nested/f1.org") org-roam-titles-cache) :to-equal "Nested File 1")
(expect (gethash (abs-path "nested/f2.org") org-roam-titles-cache) :to-equal "Nested File 2")
(expect (gethash (abs-path "no-title.org") org-roam-titles-cache) :to-be nil)))
(describe "org-roam-insert"
(before-each
(org-roam--test-init)
(org-roam--clear-cache)
(org-roam--test-build-cache))
(it "temp1 -> f1"
(let ((buf (org-roam--test-find-new-file "temp1.org")))
(with-current-buffer buf
(with-simulated-input
"File SPC 1 RET"
(org-roam-insert nil))))
(expect (buffer-string) :to-match (regexp-quote "file:f1.org")))
(it "temp2 -> nested/f1"
(let ((buf (org-roam--test-find-new-file "temp2.org")))
(with-current-buffer buf
(with-simulated-input
"Nested SPC File SPC 1 RET"
(org-roam-insert nil))))
(expect (buffer-string) :to-match (regexp-quote "file:nested/f1.org")))
(it "nested/temp3 -> f1"
(let ((buf (org-roam--test-find-new-file "nested/temp3.org")))
(with-current-buffer buf
(with-simulated-input
"File SPC 1 RET"
(org-roam-insert nil))))
(expect (buffer-string) :to-match (regexp-quote "file:../f1.org")))
(it "a/b/temp4 -> nested/f1"
(let ((buf (org-roam--test-find-new-file "a/b/temp4.org")))
(with-current-buffer buf
(with-simulated-input
"Nested SPC File SPC 1 RET"
(org-roam-insert nil))))
(expect (buffer-string) :to-match (regexp-quote "file:../../nested/f1.org"))))
(describe "rename file updates cache"
(before-each
(org-roam--test-init)
(org-roam--clear-cache)
(org-roam--test-build-cache))
(it "f1 -> new_f1"
(rename-file (abs-path "f1.org")
(abs-path "new_f1.org"))
;; Cache should be cleared of old file
(expect (gethash (abs-path "f1.org") org-roam-forward-links-cache) :to-be nil)
(expect (->> org-roam-backward-links-cache
(gethash (abs-path "nested/f1.org"))
(hash-table-keys)
(member (abs-path "f1.org"))) :to-be nil)
(expect (->> org-roam-forward-links-cache
(gethash (abs-path "new_f1.org"))) :not :to-be nil)
(expect (->> org-roam-forward-links-cache
(gethash (abs-path "new_f1.org"))
(member (abs-path "nested/f1.org"))) :not :to-be nil)
;; Links are updated
(expect (with-temp-buffer
(insert-file-contents (abs-path "nested/f1.org"))
(buffer-string)) :to-match (regexp-quote "[[file:../new_f1.org][File 1]]")))
(it "f1 -> f1 with spaces"
(rename-file (abs-path "f1.org")
(abs-path "f1 with spaces.org"))
;; Cache should be cleared of old file
(expect (gethash (abs-path "f1.org") org-roam-forward-links-cache) :to-be nil)
(expect (->> org-roam-backward-links-cache
(gethash (abs-path "nested/f1.org"))
(hash-table-keys)
(member (abs-path "f1.org"))) :to-be nil)
;; Links are updated
(expect (with-temp-buffer
(insert-file-contents (abs-path "nested/f1.org"))
(buffer-string)) :to-match (regexp-quote "[[file:../f1 with spaces.org][File 1]]")))
(it "no-title -> meaningful-title"
(rename-file (abs-path "no-title.org")
(abs-path "meaningful-title.org"))
;; File has no forward links
(expect (gethash (abs-path "no-title.org") org-roam-forward-links-cache) :to-be nil)
(expect (gethash (abs-path "meaningful-title.org") org-roam-forward-links-cache) :to-be nil)
(expect (->> org-roam-forward-links-cache
(gethash (abs-path "f3.org"))
(member (abs-path "no-title.org"))) :to-be nil)
(expect (->> org-roam-forward-links-cache
(gethash (abs-path "f3.org"))
(member (abs-path "meaningful-title.org"))) :not :to-be nil)
;; Links are updated with the appropriate name
(expect (with-temp-buffer
(insert-file-contents (abs-path "f3.org"))
(buffer-string)) :to-match (regexp-quote "[[file:meaningful-title.org][meaningful-title]]"))))
(describe "delete file updates cache"
(before-each
(org-roam--test-init)
(org-roam--clear-cache)
(org-roam--test-build-cache))
(it "delete f1"
(delete-file (abs-path "f1.org"))
(expect (->> org-roam-forward-links-cache
(gethash (abs-path "f1.org"))) :to-be nil)
(expect (->> org-roam-backward-links-cache
(gethash (abs-path "nested/f1.org"))
(gethash (abs-path "f1.org"))) :to-be nil)
(expect (->> org-roam-backward-links-cache
(gethash (abs-path "nested/f1.org"))
(gethash (abs-path "nested/f2.org"))) :not :to-be nil)))