diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d69dc..63cc933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index b8286ba..fb50e95 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/installation.md b/doc/installation.md index f5a7896..f2904eb 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -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. diff --git a/org-roam.el b/org-roam.el index da315b6..09bb951 100644 --- a/org-roam.el +++ b/org-roam.el @@ -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: diff --git a/tests/test-org-roam.el b/tests/test-org-roam.el index 0d80b2c..6302c06 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -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."