(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
This commit is contained in:
Jethro Kuan
2020-02-20 17:33:30 +08:00
committed by GitHub
parent dd4b1a97a1
commit 8523fb43b4
5 changed files with 188 additions and 181 deletions

View File

@ -7,6 +7,7 @@
* [#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
### 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
@ -82,6 +83,7 @@ Mostly a documentation/cleanup release.
[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-142]: https://github.com/jethrokuan/org-roam/pull/143
# Local Variables:
# eval: (auto-fill-mode -1)

View File

@ -43,19 +43,16 @@ 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 b" . org-roam-switch-to-buffer)
("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 (including instructions for

View File

@ -7,19 +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 b" . org-roam-switch-to-buffer)
("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
@ -34,18 +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 b" . org-roam-switch-to-buffer)
("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:
@ -73,9 +68,7 @@ If you are using Spacemacs, you can easily install org-roam by creating a simple
(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)
:custom
(org-roam-directory "/path/to/org-files/")
:init
@ -91,9 +84,10 @@ If you are using Spacemacs, you can easily install org-roam by creating a simple
(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)
)))
"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

@ -174,6 +174,7 @@ If called interactively, then PARENTS is non-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)))))
@ -325,15 +326,17 @@ If PREFIX, downcase the title before insertion."
(org-roam--make-file absolute-file-path title-or-slug))
(find-file absolute-file-path)))
(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* ((all-buffers (buffer-list))
(roam-buffers
(--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)))
all-buffers))
(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))
@ -430,16 +433,7 @@ 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))))
(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))))
;;; Org-roam buffer
(define-derived-mode org-roam-backlinks-mode org-mode "Backlinks"
"Major mode for the org-roam backlinks buffer
@ -449,7 +443,15 @@ Bindings:
(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)
;;; Org-roam buffer updates
(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."
@ -498,6 +500,145 @@ Bindings:
(insert "\n\n* No backlinks!")))
(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))
(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)))
;;; 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 ()
"Return whether the current visibility state of the org-roam buffer.
@ -540,134 +681,6 @@ Valid states are 'visible, 'exists and 'none."
('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 t)
(remove-hook 'after-save-hook #'org-roam--update-cache t))
(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))))))
;;;###autoload
(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)))
(defun org-roam--rename-file-links (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)))))
(advice-add 'rename-file :after 'org-roam--rename-file-links)
(advice-add 'delete-file :before 'org-roam--clear-file-from-cache)
(provide 'org-roam)
;;; org-roam.el ends here
;; Local Variables:

View File

@ -46,7 +46,8 @@
(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)))
(setq org-roam-directory new-dir))
(org-roam-mode +1))
(defun org-roam--test-build-cache ()
"Builds the caches synchronously."