diff --git a/CHANGELOG.md b/CHANGELOG.md index 3485a10..3038d98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * [#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 +* [#124](https://github.com/jethrokuan/org-roam/pull/124) Maintain cache consistency on file rename * [#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](https://github.com/jethrokuan/org-roam/pull/99) Add keybinding so that `` or `mouse-1` in the backlinks buffer visits the source file of the backlink at point diff --git a/org-roam.el b/org-roam.el index 5990e5e..2a11ab9 100644 --- a/org-roam.el +++ b/org-roam.el @@ -162,11 +162,12 @@ If called interactively, then PARENTS is non-nil." (org-roam--build-cache-async) (user-error "Your Org-Roam cache isn't built yet! Please wait"))) -(defun org-roam--org-roam-file-p () - "Return t if file is part of org-roam system, false otherwise." - (and (buffer-file-name (current-buffer)) - (f-descendant-of-p (file-truename (buffer-file-name (current-buffer))) - (file-truename org-roam-directory)))) +(defun org-roam--org-roam-file-p (&optional file) + "Return t if FILE is part of org-roam system, defaulting to the name of the current buffer. Else, return nil." + (let ((path (or file + (buffer-file-name (current-buffer))))) + (f-descendant-of-p (file-truename path) + (file-truename org-roam-directory)))) (defun org-roam--get-title-from-cache (file) "Return title of `FILE' from the cache." @@ -340,11 +341,13 @@ If PREFIX, downcase the title before insertion." (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 () +(defun org-roam--clear-file-from-cache (&optional filepath) "Remove any related links to the file. This is equivalent to removing the node from the graph." - (let ((file (file-truename (buffer-file-name (current-buffer))))) + (let* ((path (or filepath + (buffer-file-name (current-buffer)))) + (file (file-truename path))) ;; Step 1: Remove all existing links for file (when-let ((forward-links (gethash file org-roam-forward-links-cache))) ;; Delete backlinks to file @@ -589,6 +592,50 @@ This needs to be quick/infrequent, because this is run at (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) + + (provide 'org-roam) ;;; org-roam.el ends here diff --git a/tests/roam-files/f3.org b/tests/roam-files/f3.org new file mode 100644 index 0000000..13ff7c7 --- /dev/null +++ b/tests/roam-files/f3.org @@ -0,0 +1,5 @@ +#+TITLE: File 3 + +This file has a link to an file with no title. + +[[file:no-title.org][no-title]] diff --git a/tests/test-org-roam.el b/tests/test-org-roam.el index c3c84e8..c9c0c37 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -29,6 +29,7 @@ (require 'buttercup) (require 'with-simulated-input) (require 'org-roam) +(require 'dash) (defun abs-path (file-path) (file-truename (expand-file-name file-path org-roam-directory))) @@ -69,9 +70,9 @@ ;; Caches should be populated (expect org-roam-cache-initialized :to-be t) - (expect (hash-table-count org-roam-forward-links-cache) :to-be 3) - (expect (hash-table-count org-roam-backward-links-cache) :to-be 4) - (expect (hash-table-count org-roam-titles-cache) :to-be 4) + (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)) @@ -148,3 +149,65 @@ "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]]"))))