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/ /.sandbox/
**/*.elc

View File

@ -1,5 +1,43 @@
# Changelog # 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) ## 0.1.1 (2020-02-15)
Mostly a documentation/cleanup release. 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-75]: https://github.com/jethrokuan/org-roam/pull/75
[gh-78]: https://github.com/jethrokuan/org-roam/pull/78 [gh-78]: https://github.com/jethrokuan/org-roam/pull/78
[gh-82]: https://github.com/jethrokuan/org-roam/pull/82 [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: # Local Variables:
# eval: (auto-fill-mode -1) # 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 (use-package org-roam
:after org :after org
:hook :hook
((org-mode . org-roam-mode) (after-init . org-roam-mode)
(after-init . org-roam--build-cache-async) ;; optional!
)
:straight (:host github :repo "jethrokuan/org-roam" :branch "develop") :straight (:host github :repo "jethrokuan/org-roam" :branch "develop")
:custom :custom
(org-roam-directory "/path/to/org-files/") (org-roam-directory "/path/to/org-files/")
:bind :bind (:map org-roam-mode-map
("C-c n l" . org-roam) (("C-c n l" . org-roam)
("C-c n t" . org-roam-today)
("C-c n f" . org-roam-find-file) ("C-c n f" . org-roam-find-file)
("C-c n i" . org-roam-insert)
("C-c n g" . org-roam-show-graph)) ("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 For more detailed installation instructions (including instructions for
installation Spacemacs users), please see [the installation
documentation](https://org-roam.readthedocs.io/en/develop/installation/). documentation](https://org-roam.readthedocs.io/en/develop/installation/).
## Knowledge Bases using Org-Roam ## 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 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 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 ## 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 `org-roam-find-file`). Hence, having any unique file name is a decent
option, and the default workflow uses the timestamp as the filename. option, and the default workflow uses the timestamp as the filename.
The format of the filename is specified by the string The format of the filename is controlled by the function
`org-roam-file-format`, which defaults to `"%Y%m%d%H%M%S"`. To see `org-roam-file-name-function`, which defaults to a format like
valid specifications, see the help (`C-h f`) for `format-time-string`. `YYYYMMDDHHMMSS_title_here.org`. You may choose to define your own
function to change this.
There are several reasons for keeping filenames meaningful. For If you wish to be prompted to change the file name on creation, set
example, one may wish to publish the Org files, and some publishing `org-roam-filename-noconfirm` to `nil`:
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`:
```emacs-lisp ```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 It is then the user's responsibility to ensure that the file names are
for a filename. It is then the user's responsibility to ensure that unique.
the file names are unique.
### Autopopulating Titles ### Autopopulating Titles
@ -94,6 +90,15 @@ typically near the top of the file. The option
attribute is automatically inserted into the files created via attribute is automatically inserted into the files created via
Org-roam commands. Setting it to `nil` will disable this behaviour. 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 ## 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 [Deft][deft] provides a nice interface for browsing and filtering
org-roam notes. org-roam notes.
``` ```emacs-lisp
(use-package deft (use-package deft
:after org :after org
:bind :bind
@ -14,8 +14,37 @@ org-roam notes.
(deft-recursive t) (deft-recursive t)
(deft-use-filter-string-for-filename t) (deft-use-filter-string-for-filename t)
(deft-default-extension "org") (deft-default-extension "org")
(deft-directory "/path/to/org-roam-files/") (deft-directory "/path/to/org-roam-files/"))
(deft-use-filename-as-title t)) ```
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 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 provides better journaling capabilities, and a nice calendar interface
to see all dated entries. to see all dated entries.
``` ```emacs-lisp
(use-package org-journal (use-package org-journal
:bind :bind
("C-c n j" . org-journal-new-entry) ("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 `roam://` prefix from the link, causing the desktop application to
call `emacsclient path/to/org-roam-file.org`. call `emacsclient path/to/org-roam-file.org`.
## Setting Up for Linux ## Linux
Create a desktop application. I place mine in Create a desktop application. I place mine in
`~/.local/share/applications/roam.desktop`: `~/.local/share/applications/roam.desktop`:
@ -37,3 +37,42 @@ running in your shell:
```bash ```bash
xdg-mime default roam.desktop x-scheme-handler/roam 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 (use-package org-roam
:after org :after org
:hook :hook
((org-mode . org-roam-mode) (after-init . org-roam-mode)
(after-init . org-roam--build-cache-async) ;; optional!
)
:straight (:host github :repo "jethrokuan/org-roam" :branch "develop") :straight (:host github :repo "jethrokuan/org-roam" :branch "develop")
:custom :custom
(org-roam-directory "/path/to/org-files/") (org-roam-directory "/path/to/org-files/")
:bind :bind (:map org-roam-mode-map
("C-c n l" . org-roam) (("C-c n l" . org-roam)
("C-c n t" . org-roam-today)
("C-c n f" . org-roam-find-file) ("C-c n f" . org-roam-find-file)
("C-c n i" . org-roam-insert)
("C-c n g" . org-roam-show-graph)) ("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 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 :after org
:load-path "elisp/" :load-path "elisp/"
:hook :hook
((org-mode . org-roam-mode) (after-init . org-roam-mode)
(after-init . org-roam--build-cache-async) ;; optional! :straight (:host github :repo "jethrokuan/org-roam" :branch "develop")
)
:custom :custom
(org-roam-directory "/path/to/org-files/") (org-roam-directory "/path/to/org-files/")
:bind :bind (:map org-roam-mode-map
("C-c n l" . org-roam) (("C-c n l" . org-roam)
("C-c n t" . org-roam-today)
("C-c n f" . org-roam-find-file) ("C-c n f" . org-roam-find-file)
("C-c n i" . org-roam-insert)
("C-c n g" . org-roam-show-graph)) ("C-c n g" . org-roam-show-graph))
:map org-mode-map
(("C-c n i" . org-roam-insert))))
``` ```
Or without use-package: 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 [use-package]: https://github.com/jwiegley/use-package
[straight]: https://github.com/raxod502/straight.el [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> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/jethrokuan/org-roam ;; URL: https://github.com/jethrokuan/org-roam
;; Keywords: org-mode, roam, convenience ;; Keywords: org-mode, roam, convenience
;; Version: 0.1.0 ;; Version: 0.1.2
;; 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.
@ -38,6 +38,24 @@
(require 'subr-x) (require 'subr-x)
(require 'cl-lib) (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) (defun org-roam--find-files (dir)
"Return all `org-roam' files in `DIR'." "Return all `org-roam' files in `DIR'."
(if (file-exists-p dir) (if (file-exists-p dir)
@ -53,7 +71,7 @@
(when (not (string-match dir-ignore-regexp file)) (when (not (string-match dir-ignore-regexp file))
(setq result (append (org-roam--find-files file) result)))) (setq result (append (org-roam--find-files file) result))))
((and (file-readable-p file) ((and (file-readable-p file)
(string= (file-name-extension file) "org")) (org-roam--org-file-p file))
(setq result (cons (file-truename file) result))))) (setq result (cons (file-truename file) result)))))
result))) result)))
@ -65,25 +83,28 @@
(path (org-element-property :path link)) (path (org-element-property :path link))
(start (org-element-property :begin link))) (start (org-element-property :begin link)))
(when (and (string= type "file") (when (and (string= type "file")
(string= (file-name-extension path) "org")) (org-roam--org-file-p path))
(goto-char start) (goto-char start)
(let* ((element (org-element-at-point)) (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) (content (or (org-element-property :raw-value element)
(buffer-substring (buffer-substring
(or (org-element-property :content-begin element) begin
(org-element-property :begin element))
(or (org-element-property :content-end element) (or (org-element-property :content-end element)
(org-element-property :end element)))))) (org-element-property :end element)))))
(list :from (or file-path (content (string-trim content))
(file-truename (buffer-file-name (current-buffer)))) (file-path (or file-path
:to (file-truename (expand-file-name path org-roam-directory)) (file-truename (buffer-file-name (current-buffer))))))
:content (string-trim content)))))))) (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) (cl-defun org-roam--insert-item (item &key forward backward)
"Insert ITEM into FORWARD and BACKWARD cache. "Insert ITEM into FORWARD and BACKWARD cache.
ITEM is of the form: (:from from-path :to to-path :content preview-content)." 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 :content ,content) item)) (pcase-let ((`(:from ,p-from :to ,p-to :properties ,props) item))
;; Build forward-links ;; Build forward-links
(let ((links (gethash p-from forward))) (let ((links (gethash p-from forward)))
(if links (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))) (let ((contents-hash (gethash p-to backward)))
(if contents-hash (if contents-hash
(if-let ((contents-list (gethash p-from 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-from updated contents-hash)
(puthash p-to contents-hash backward)) (puthash p-to contents-hash backward))
(progn (progn
(puthash p-from (list content) contents-hash) (puthash p-from (list props) contents-hash)
(puthash p-to contents-hash backward))) (puthash p-to contents-hash backward)))
(let ((contents-hash (make-hash-table :test #'equal))) (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)))))) (puthash p-to contents-hash backward))))))
(defun org-roam--extract-title () (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))) (org-element-property :value kw)))
:first-match t)) :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) (provide 'org-roam-utils)
;;; org-roam-utils.el ends here ;;; org-roam-utils.el ends here

View File

@ -5,7 +5,7 @@
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
;; URL: https://github.com/jethrokuan/org-roam ;; URL: https://github.com/jethrokuan/org-roam
;; Keywords: org-mode, roam, convenience ;; 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")) ;; 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. ;; This file is NOT part of GNU Emacs.
@ -55,7 +55,14 @@
"Path to Org-roam files. "Path to Org-roam files.
All Org files, at any level of nesting, is considered part of the Org-roam." 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) :group 'org-roam)
(defcustom org-roam-buffer-position 'right (defcustom org-roam-buffer-position 'right
@ -68,18 +75,23 @@ Valid values are
(const right)) (const right))
:group 'org-roam) :group 'org-roam)
(defcustom org-roam-file-format "%Y%m%d%H%M%S" (defcustom org-roam-file-name-function #'org-roam--file-name-timestamp-title
"The timestamp format to use filenames." "The function used to generate filenames.
:type 'string
:group 'org-roam) 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" (defcustom org-roam-link-title-format "%s"
"The format string used when inserting org-roam links that use their title." "The format string used when inserting org-roam links that use their title."
:type 'string :type 'string
:group 'org-roam) :group 'org-roam)
(defcustom org-roam-use-timestamp-as-filename t (defcustom org-roam-filename-noconfirm t
"Whether to use timestamp as a file name. If not true, prompt for a file name each time." "Whether to prompt for confirmation of fil name for new files.
If nil, always ask for filename."
:type 'boolean :type 'boolean
:group 'org-roam) :group 'org-roam)
@ -96,6 +108,11 @@ Valid values are
:type 'string :type 'string
:group 'org-roam) :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") (defcustom org-roam-graph-viewer (executable-find "firefox")
"Path to executable for viewing SVG." "Path to executable for viewing SVG."
:type 'string :type 'string
@ -106,6 +123,16 @@ Valid values are
:type 'string :type 'string
:group 'org-roam) :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 ;;; Polyfills
;; These are for functions I use that are only available in newer Emacs ;; 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) (defvar org-roam-titles-cache (make-hash-table :test #'equal)
"Cache containing titles for org-roam files.") "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.") "Currently displayed file in `org-roam' buffer.")
(defvar org-roam-last-window nil
"Last window `org-roam' was called from.")
;;; Utilities ;;; Utilities
(defun org-roam--ensure-cache-built () (defun org-roam--ensure-cache-built ()
"Ensures that org-roam cache is 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) (org-roam--build-cache-async)
(user-error "Your Org-Roam cache isn't built yet! Please wait"))) (user-error "Your Org-Roam cache isn't built yet! Please wait")))
(defun org-roam--org-roam-file-p () (defun org-roam--org-roam-file-p (&optional file)
"Return t if file is part of org-roam system, false otherwise." "Return t if FILE is part of org-roam system, defaulting to the name of the current buffer. Else, return nil."
(and (buffer-file-name (current-buffer)) (let ((path (or file
(f-descendant-of-p (file-truename (buffer-file-name (current-buffer))) (buffer-file-name (current-buffer)))))
org-roam-directory))) (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) (defun org-roam--get-title-from-cache (file)
"Return title of `FILE' from the cache." "Return title of `FILE' from the cache."
@ -167,14 +200,17 @@ If called interactively, then PARENTS is non-nil."
"Return all org-roam files." "Return all org-roam files."
(org-roam--find-files (file-truename org-roam-directory))) (org-roam--find-files (file-truename org-roam-directory)))
(defun org-roam--get-file-path (id &optional absolute) (defun org-roam--make-new-file-path (id &optional absolute)
"Convert identifier `ID' to file path. "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 (let ((absolute-file-path (file-truename
(expand-file-name (expand-file-name
(concat id ".org") (if org-roam-encrypt-files
org-roam-directory)))) (concat id ".org.gpg")
(concat id ".org"))
(or org-roam-new-file-directory
org-roam-directory)))))
(if absolute (if absolute
absolute-file-path absolute-file-path
(file-relative-name 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 (s-join "_" s)))
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 ;;; Creating org-roam files
(defun org-roam--populate-title (file &optional title) (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." "Create an org-roam file at FILE-PATH, optionally setting the TITLE attribute."
(if (file-exists-p file-path) (if (file-exists-p file-path)
(error (format "Aborting, file already exists at %s" file-path)) (error (format "Aborting, file already exists at %s" file-path))
(make-empty-file file-path t)
(if org-roam-autopopulate-title (if org-roam-autopopulate-title
(org-roam--populate-title file-path title) (org-roam--populate-title file-path title))
(make-empty-file file-path)))) (save-excursion
(with-current-buffer (find-file-noselect file-path)
(org-roam--update-cache)))))
(defun org-roam--new-file-named (slug) (defun org-roam--new-file-named (slug)
"Create a new file named `SLUG'. "Create a new file named `SLUG'.
`SLUG' is the short file name, without a path or a file extension." `SLUG' is the short file name, without a path or a file extension."
(interactive "sNew filename (without 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) (unless (file-exists-p file-path)
(org-roam--make-file file-path)) (org-roam--make-file file-path))
(find-file file-path))) (find-file file-path)))
(defun org-roam--get-new-id (&optional title) (defun org-roam--get-new-id (title)
"Return a new ID, generated from the current time. "Return a new ID, given the note TITLE."
(let* ((proposed-slug (funcall org-roam-file-name-function title))
Optionally pass it the title, for a smart file name." (new-slug (if org-roam-filename-noconfirm
(if org-roam-use-timestamp-as-filename proposed-slug
(format-time-string org-roam-file-format (current-time)) (read-string "Enter ID (without extension): "
(let* ((slug (read-string "Enter ID (without extension): " proposed-slug)))
(if title (file-path (org-roam--make-new-file-path new-slug t)))
(org-roam--title-to-slug title)
"")))
(file-path (org-roam--get-file-path slug t)))
(if (file-exists-p file-path) (if (file-exists-p file-path)
(user-error "There's already a file at %s") (user-error "There's already a file at %s")
slug)))) new-slug)))
(defun org-roam-new-file () (defun org-roam-new-file ()
"Quickly create a new file, using the current timestamp." "Quickly create a new file, using the current timestamp."
(interactive) (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 ;;; Inserting org-roam links
(defun org-roam-insert () (defun org-roam-insert (prefix)
"Find an org-roam file, and insert a relative org link to it at point." "Find an org-roam file, and insert a relative org link to it at point.
(interactive)
If PREFIX, downcase the title before insertion."
(interactive "P")
(let* ((completions (mapcar (lambda (file) (let* ((completions (mapcar (lambda (file)
(list (org-roam--get-title-or-slug file) (list (org-roam--get-title-or-slug file)
file)) file))
(org-roam--find-all-files))) (org-roam--find-all-files)))
(title (completing-read "File: " completions)) (title (completing-read "File: " completions))
(absolute-file-path (or (cadr (assoc title completions)) (absolute-file-path (or (cadr (assoc title completions))
(org-roam--get-file-path (org-roam--get-new-id title) t))) (org-roam--make-new-file-path (org-roam--get-new-id title) t)))
(current-file-path (-> (current-buffer) (current-file-path (-> (or (buffer-base-buffer)
(current-buffer))
(buffer-file-name) (buffer-file-name)
(file-truename) (file-truename)
(file-name-directory)))) (file-name-directory))))
@ -271,7 +317,9 @@ Optionally pass it the title, for a smart file name."
(insert (format "[[%s][%s]]" (insert (format "[[%s][%s]]"
(concat "file:" (file-relative-name absolute-file-path (concat "file:" (file-relative-name absolute-file-path
current-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 ;;; Finding org-roam files
(defun org-roam-find-file () (defun org-roam-find-file ()
@ -282,15 +330,37 @@ Optionally pass it the title, for a smart file name."
(org-roam--find-all-files))) (org-roam--find-all-files)))
(title-or-slug (completing-read "File: " completions)) (title-or-slug (completing-read "File: " completions))
(absolute-file-path (or (cadr (assoc title-or-slug 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)))) (org-roam--get-new-id title-or-slug) t))))
(unless (file-exists-p absolute-file-path) (unless (file-exists-p absolute-file-path)
(org-roam--make-file absolute-file-path title-or-slug)) (org-roam--make-file absolute-file-path title-or-slug))
(find-file absolute-file-path))) (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 () (defun org-roam--build-cache-async ()
"Builds the cache asychronously, saving it into the org-roam caches." "Builds the caches asychronously."
(interactive) (interactive)
(async-start (async-start
`(lambda () `(lambda ()
@ -298,30 +368,7 @@ Optionally pass it the title, for a smart file name."
(package-initialize) (package-initialize)
(require 'org-roam-utils) (require 'org-roam-utils)
,(async-inject-variables "org-roam-directory") ,(async-inject-variables "org-roam-directory")
(let ((backward-links (make-hash-table :test #'equal)) (org-roam--build-cache org-roam-directory))
(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)))
(lambda (cache) (lambda (cache)
(setq org-roam-forward-links-cache (plist-get cache :forward)) (setq org-roam-forward-links-cache (plist-get cache :forward))
(setq org-roam-backward-links-cache (plist-get cache :backward)) (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!")))) (message "Org-roam cache built!"))))
(defun org-roam--clear-cache () (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. "Remove any related links to the file.
This is equivalent to removing the node from the graph." 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 ;; Step 1: Remove all existing links for file
(when-let ((forward-links (gethash file org-roam-forward-links-cache))) (when-let ((forward-links (gethash file org-roam-forward-links-cache)))
;; Delete backlinks to file ;; Delete backlinks to file
@ -356,7 +413,7 @@ This is equivalent to removing the node from the graph."
(defun org-roam--update-cache () (defun org-roam--update-cache ()
"Update org-roam caches for the current buffer file." "Update org-roam caches for the current buffer file."
(save-excursion (save-excursion
(org-roam--clear-cache) (org-roam--clear-file-from-cache)
;; Insert into title cache ;; Insert into title cache
(org-roam--update-cache-title) (org-roam--update-cache-title)
;; Insert new items ;; 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: "))) (let ((time (org-read-date nil 'to-time nil "Date: ")))
(org-roam--new-file-named (format-time-string "%Y-%m-%d" time)))) (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) (defun org-roam-update (file-path)
"Show the backlinks for given org file for file at `FILE-PATH'." "Show the backlinks for given org file for file at `FILE-PATH'."
(org-roam--ensure-cache-built) (org-roam--ensure-cache-built)
(let ((buffer-title (org-roam--get-title-or-slug file-path))) (let ((buffer-title (org-roam--get-title-or-slug file-path)))
(with-current-buffer org-roam-buffer (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)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
(when (not (eq major-mode 'org-mode)) (when (not (eq major-mode 'org-roam-backlinks-mode))
(org-mode)) (org-roam-backlinks-mode))
(make-local-variable 'org-return-follows-link) (make-local-variable 'org-return-follows-link)
(setq org-return-follows-link t) (setq org-return-follows-link t)
(insert (insert
@ -408,14 +497,165 @@ This is equivalent to removing the node from the graph."
(insert (format "** [[file:%s][%s]]\n" (insert (format "** [[file:%s][%s]]\n"
file-from file-from
(org-roam--get-title-or-slug file-from))) (org-roam--get-title-or-slug file-from)))
(dolist (content contents) (dolist (properties contents)
(insert (concat (propertize (s-trim (s-replace "\n" " " content)) (let ((content (propertize
'font-lock-face 'org-block) (s-trim (s-replace "\n" " "
"\n\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)) backlinks))
(insert "\n\n* No backlinks!"))) (insert "\n\n* No backlinks!")))
(read-only-mode 1))) (read-only-mode 1))))
(setq org-roam-current-file file-path))
;;; 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 ;;; Show/hide the org-roam buffer
(define-inline org-roam--current-visibility () (define-inline org-roam--current-visibility ()
@ -454,93 +694,11 @@ Valid states are 'visible, 'exists and 'none."
(defun org-roam () (defun org-roam ()
"Pops up the window `org-roam-buffer' accordingly." "Pops up the window `org-roam-buffer' accordingly."
(interactive) (interactive)
(setq org-roam-last-window (get-buffer-window))
(pcase (org-roam--current-visibility) (pcase (org-roam--current-visibility)
('visible (delete-window (get-buffer-window org-roam-buffer))) ('visible (delete-window (get-buffer-window org-roam-buffer)))
('exists (org-roam--setup-buffer)) ('exists (org-roam--setup-buffer))
('none (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 ;;; org-roam.el ends here
;; Local Variables: ;; 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 ;; Copyright (C) 2020 Jethro Kuan
;; Author: Jethro Kuan <jethrokuan95@gmail.com> ;; 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 ;; 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 ;; it under the terms of the GNU General Public License as published by
@ -27,9 +27,204 @@
;;;; Requirements ;;;; Requirements
(require 'buttercup) (require 'buttercup)
(require 'with-simulated-input)
(require 'org-roam) (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 ;;; Tests
(describe "Org-roam cache" (describe "org-roam--build-cache-async"
(it "Mock Test" (it "initializes correctly"
(expect t :to-be t))) (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)))