diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ee277..fe8a012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Org-roam also now does not resolve symlinks. This significantly speeds up cache ### Features +- [#1163](https://github.com/org-roam/org-roam/pull/1163) Support file-level IDs introduced in Org 9.4 - [#1093](https://github.com/org-roam/org-roam/pull/1093) Add `vanilla` org-roam-tag-source to extract buffer Org tags - [#1079](https://github.com/org-roam/org-roam/pull/1079) Add `org-roam-tag-face` to customize appearance of tags in interactive commandsg - [#1073](https://github.com/org-roam/org-roam/pull/1073) Rename file on title change, when `org-roam-rename-file-on-title-change` is non-nil. diff --git a/org-roam-buffer.el b/org-roam-buffer.el index 8b2272c..0287857 100644 --- a/org-roam-buffer.el +++ b/org-roam-buffer.el @@ -53,7 +53,7 @@ (declare-function org-roam-backlinks-mode "org-roam") (declare-function org-roam-mode "org-roam") (declare-function org-roam--find-file "org-roam") -(declare-function org-roam--format-link "org-roam") +(declare-function org-roam-format-link "org-roam") (declare-function org-roam-link-make-string "org-roam-compat") (declare-function org-roam-link-get-path "org-roam-link") @@ -161,7 +161,7 @@ ORIG-PATH is the path where the CONTENT originated." (let ((file-from (car group)) (bls (cdr group))) (insert (format "** %s\n" - (org-roam--format-link file-from + (org-roam-format-link file-from (org-roam--get-title-or-slug file-from) "file"))) (dolist (backlink bls) @@ -189,7 +189,7 @@ ORIG-PATH is the path where the CONTENT originated." (bls (mapcar (lambda (row) (nth 2 row)) (cdr group)))) (insert (format "** %s\n" - (org-roam--format-link file-from + (org-roam-format-link file-from (org-roam--get-title-or-slug file-from) "file"))) ;; Sort backlinks according to time of occurrence in buffer diff --git a/org-roam-capture.el b/org-roam-capture.el index 4e23360..1edc4d8 100644 --- a/org-roam-capture.el +++ b/org-roam-capture.el @@ -45,7 +45,7 @@ (declare-function org-roam--get-ref-path-completions "org-roam") (declare-function org-roam--file-path-from-id "org-roam") (declare-function org-roam--find-file "org-roam") -(declare-function org-roam--format-link "org-roam") +(declare-function org-roam-format-link "org-roam") (declare-function org-roam-mode "org-roam") (declare-function org-roam-completion--completing-read "org-roam-completion") @@ -337,9 +337,9 @@ the capture)." (type (org-roam-capture--get :link-type)) (desc (org-roam-capture--get :link-description))) (if (eq (point) (marker-position mkr)) - (insert (org-roam--format-link path desc type)) + (insert (org-roam-format-link path desc type)) (org-with-point-at mkr - (insert (org-roam--format-link path desc type)))))))))) + (insert (org-roam-format-link path desc type)))))))))) (when region (set-marker beg nil) (set-marker end nil)) diff --git a/org-roam-db.el b/org-roam-db.el index a428e02..c67ddf6 100644 --- a/org-roam-db.el +++ b/org-roam-db.el @@ -47,7 +47,7 @@ (declare-function org-roam--extract-titles "org-roam") (declare-function org-roam--extract-ref "org-roam") (declare-function org-roam--extract-tags "org-roam") -(declare-function org-roam--extract-headlines "org-roam") +(declare-function org-roam--extract-ids "org-roam") (declare-function org-roam--extract-links "org-roam") (declare-function org-roam--list-all-files "org-roam") (declare-function org-roam--path-to-slug "org-roam") @@ -79,7 +79,7 @@ value like `most-positive-fixnum'." :type 'int :group 'org-roam) -(defconst org-roam-db--version 8) +(defconst org-roam-db--version 9) (defvar org-roam-db--connection (make-hash-table :test #'equal) "Database connection to Org-roam database.") @@ -140,9 +140,10 @@ SQL can be either the emacsql vector representation, or a string." (hash :not-null) (meta :not-null)]) - (headlines + (ids [(id :unique :primary-key) - (file :not-null)]) + (file :not-null) + (level :not-null)]) (links [(from :not-null) @@ -249,25 +250,25 @@ This is equivalent to removing the node from the graph." (mapcar (lambda (title) (vector file title)) titles))) -(defun org-roam-db--insert-headlines (headlines) - "Insert HEADLINES into the Org-roam cache. +(defun org-roam-db--insert-ids (ids) + "Insert IDS into the Org-roam cache. Returns t if the insertion was successful, nil otherwise. Insertions can fail when there is an ID conflict." (condition-case nil (progn (org-roam-db-query - [:insert :into headlines + [:insert :into ids :values $v1] - headlines) + ids) t) (error - (unless (listp headlines) - (setq headlines (list headlines))) + (unless (listp ids) + (setq ids (list ids))) (lwarn '(org-roam) :error (format "Duplicate IDs in %s, one of:\n\n%s\n\nskipping..." - (aref (car headlines) 1) + (aref (car ids) 1) (string-join (mapcar (lambda (hl) - (aref hl 0)) headlines) "\n"))) + (aref hl 0)) ids) "\n"))) nil))) (defun org-roam-db--insert-tags (file tags) @@ -441,14 +442,14 @@ connections, nil is returned." (when-let ((links (org-roam--extract-links))) (org-roam-db--insert-links links)))) -(defun org-roam-db--update-headlines () - "Update the file headlines of the current buffer into the cache." +(defun org-roam-db--update-ids () + "Update the ids of the current buffer into the cache." (let* ((file (or org-roam-file-name (buffer-file-name)))) - (org-roam-db-query [:delete :from headlines + (org-roam-db-query [:delete :from ids :where (= file $s1)] file) - (when-let ((headlines (org-roam--extract-headlines file))) - (org-roam-db--insert-headlines headlines)))) + (when-let ((ids (org-roam--extract-ids file))) + (org-roam-db--insert-ids ids)))) (defun org-roam-db--update-file (&optional file-path) "Update Org-roam cache for FILE-PATH. @@ -469,7 +470,7 @@ If the file exists, update the cache with information." (org-roam-db--update-titles) (org-roam-db--update-refs) (when org-roam-enable-headline-linking - (org-roam-db--update-headlines)) + (org-roam-db--update-ids)) (org-roam-db--update-links))))) (defun org-roam-db-build-cache (&optional force) @@ -484,16 +485,18 @@ If FORCE, force a rebuild of the cache from scratch." (org-roam-files (org-roam--list-all-files)) (current-files (org-roam-db--get-current-files)) (file-count 0) - (headline-count 0) + (id-count 0) (link-count 0) (tag-count 0) (title-count 0) (ref-count 0) - (deleted-count 0)) + (deleted-count 0) + (processed-count 0)) (emacsql-with-transaction (org-roam-db) ;; Two-step building - ;; First step: Rebuild files and headlines + ;; First step: Rebuild files and ids (dolist (file org-roam-files) + (org-roam-message "Processed %s/%s files..." processed-count (length org-roam-files)) (let* ((attr (file-attributes file)) (atime (file-attribute-access-time attr)) (mtime (file-attribute-modification-time attr))) @@ -509,47 +512,42 @@ If FORCE, force a rebuild of the cache from scratch." (vector file contents-hash (list :atime atime :mtime mtime))) (setq file-count (1+ file-count)) (when org-roam-enable-headline-linking - (when-let ((headlines (org-roam--extract-headlines file))) - (when (org-roam-db--insert-headlines headlines) - (setq headline-count (1+ headline-count)))))) + (when-let ((ids (org-roam--extract-ids file))) + (when (org-roam-db--insert-ids ids) + (setq id-count (+ id-count (length ids)))))) + (when-let (links (org-roam--extract-links file)) + (org-roam-db-query + [:insert :into links + :values $v1] + links) + (setq link-count (1+ link-count))) + (when-let (tags (org-roam--extract-tags file)) + (org-roam-db-query + [:insert :into tags + :values $v1] + (vector file tags)) + (setq tag-count (1+ tag-count))) + (let ((titles (or (org-roam--extract-titles) + (list (org-roam--path-to-slug file))))) + (org-roam-db--insert-titles file titles) + (setq title-count (+ title-count (length titles)))) + (when-let* ((ref (org-roam--extract-ref))) + (when (org-roam-db--insert-ref file ref) + (setq ref-count (1+ ref-count))))) (file-error (setq org-roam-files (remove file org-roam-files)) (org-roam-db--clear-file file) (lwarn '(org-roam) :warning - "Skipping unreadable file while building cache: %s" file))))))) - ;; Second step: Rebuild the rest - (dolist (file org-roam-files) - (let ((contents-hash (org-roam-db--file-hash file))) - (unless (string= (gethash file current-files) - contents-hash) - (org-roam--with-temp-buffer file - (when-let (links (org-roam--extract-links file)) - (org-roam-db-query - [:insert :into links - :values $v1] - links) - (setq link-count (1+ link-count))) - (when-let (tags (org-roam--extract-tags file)) - (org-roam-db-query - [:insert :into tags - :values $v1] - (vector file tags)) - (setq tag-count (1+ tag-count))) - (let ((titles (or (org-roam--extract-titles) - (list (org-roam--path-to-slug file))))) - (org-roam-db--insert-titles file titles) - (setq title-count (+ title-count (length titles)))) - (when-let* ((ref (org-roam--extract-ref))) - (when (org-roam-db--insert-ref file ref) - (setq ref-count (1+ ref-count)))))) - (remhash file current-files))) + "Skipping unreadable file while building cache: %s" file)))) + (remhash file current-files) + (setq processed-count (+ processed-count 1))))) (dolist (file (hash-table-keys current-files)) ;; These files are no longer around, remove from cache... (org-roam-db--clear-file file) (setq deleted-count (1+ deleted-count)))) - (org-roam-message "files: Δ%s, headlines: Δ%s, links: Δ%s, tags: Δ%s, titles: Δ%s, refs: Δ%s, deleted: Δ%s" + (org-roam-message "files: Δ%s, ids: Δ%s, links: Δ%s, tags: Δ%s, titles: Δ%s, refs: Δ%s, deleted: Δ%s" file-count - headline-count + id-count link-count tag-count title-count diff --git a/org-roam-link.el b/org-roam-link.el index 924a75e..72d8e76 100644 --- a/org-roam-link.el +++ b/org-roam-link.el @@ -41,7 +41,7 @@ (defvar org-roam-directory) (declare-function org-roam--find-file "org-roam") (declare-function org-roam-find-file "org-roam") - +(declare-function org-roam-format-link "org-roam") (defcustom org-roam-link-auto-replace t "When non-nil, replace Org-roam's roam links with file or id links whenever possible." @@ -236,9 +236,7 @@ DESC is the link description." (unless (org-in-regexp org-link-bracket-re 1) (user-error "No link at point")) (replace-match "") - (when (string-equal link-type "file") - (setq loc (org-roam-link-get-path loc))) - (insert (org-roam-link-make-string (concat link-type ":" loc) desc))))) + (insert (org-roam-format-link loc desc link-type))))) (defun org-roam-link-replace-all () "Replace all roam links in the current buffer." diff --git a/org-roam.el b/org-roam.el index 507c8f4..c79c2cf 100644 --- a/org-roam.el +++ b/org-roam.el @@ -380,15 +380,6 @@ If FILE is not specified, use the current buffer's file-path." (string-match-p org-roam-file-exclude-regexp path))) (f-descendant-of-p path (expand-file-name org-roam-directory)))))) -(defun org-roam--org-roam-headline-p (&optional id) - "Return t if ID is part of Org-roam system, nil otherwise. -If ID is not specified, use the ID of the entry at point." - (if-let ((id (or id - (org-id-get)))) - (org-roam-db-query [:select [file] :from headlines - :where (= id $s1)] - id))) - (defun org-roam--shell-command-files (cmd) "Run CMD in the shell and return a list of files. If no files are found, an empty list is returned." (--> cmd @@ -625,20 +616,21 @@ it as FILE-PATH." (push (vector file-path name type properties) links)))))) links))) -(defun org-roam--extract-headlines (&optional file-path) - "Extract all headlines with IDs within the current buffer. +(defun org-roam--extract-ids (&optional file-path) + "Extract all IDs within the current buffer. If FILE-PATH is nil, use the current file." - (let ((file-path (or file-path - (buffer-file-name)))) - ;; Use `org-map-region' instead of `org-map-entries' as the latter - ;; would require another step to remove all nil values. - (let ((result nil)) + (setq file-path (or file-path org-roam-file-name (buffer-file-name))) + (let (result) + ;; We need to handle the special case of the file property drawer (at outline level 0) + (org-with-point-at (point-min) + (when-let ((id (org-entry-get nil "ID"))) + (push (vector id file-path (org-outline-level)) result))) (org-map-region (lambda () (when-let ((id (org-entry-get nil "ID"))) - (push (vector id file-path) result))) + (push (vector id file-path (org-outline-level)) result))) (point-min) (point-max)) - result))) + result)) (defun org-roam--extract-titles-title () "Return title from \"#+title\" of the current buffer." @@ -817,10 +809,18 @@ If `org-roam-link-title-format title' is defined, use it with TYPE." (funcall org-roam-link-title-format title type) (format org-roam-link-title-format title))) -(defun org-roam--format-link (target &optional description type) +(defun org-roam-format-link (target &optional description type) "Formats an org link for a given file TARGET, link DESCRIPTION and link TYPE. -TYPE defaults to \"file\"." +TYPE defaults to \"file\". +Here, we also check if there is an ID for the file." (setq type (or type "file")) + (when-let ((id (and (string-equal type "file") + (caar (org-roam-db-query [:select [id] :from ids + :where (= file $s1) + :and (= level 0) + :limit 1] + target))))) + (setq type "id" target id)) (when (string-equal type "file") (setq target (org-roam-link-get-path target))) (org-roam-link-make-string (concat type ":" target) description)) @@ -1035,14 +1035,14 @@ citation key, for Org-ref cite links." "Return the file if ID exists in the Org-roam database. Return nil otherwise." (caar (org-roam-db-query [:select [file] - :from headlines + :from ids :where (= id $s1) :limit 1] id))) (defun org-roam-id-find (id &optional markerp strict keep-buffer-p) "Return the location of the entry with the id ID. -When MARKERP is non-nil, return a marker pointing to theheadline. +When MARKERP is non-nil, return a marker pointing to the headline. Otherwise, return a cons formatted as \(file . pos). When STRICT is non-nil, only consider Org-roam’s database. When KEEP-BUFFER-P is non-nil, keep the buffers navigated by Org-roam open." @@ -1296,10 +1296,7 @@ update with NEW-DESC." (new-label (if (string-equal label old-desc) new-desc label))) - (replace-match (org-roam-link-make-string - (concat type ":" - (file-relative-name new-path (file-name-directory (buffer-file-name)))) - new-label))))))))) + (replace-match (org-roam-format-link new-path new-label type))))))))) (defun org-roam--fix-relative-links (old-path) "Fix file-relative links in current buffer. @@ -1629,7 +1626,7 @@ If DESCRIPTION is provided, use this as the link label. See (delete-region beg end) (set-marker beg nil) (set-marker end nil)) - (insert (org-roam--format-link target-file-path link-description link-type))) + (insert (org-roam-format-link target-file-path link-description link-type))) (t (let ((org-roam-capture--info `((title . ,title-with-tags) (slug . ,(funcall org-roam-title-to-slug-function title-with-tags)))) diff --git a/tests/test-org-roam.el b/tests/test-org-roam.el index 69bd67f..94b11b5 100644 --- a/tests/test-org-roam.el +++ b/tests/test-org-roam.el @@ -239,7 +239,7 @@ :to-equal '("t1" "t2 with space" "t3" "tags")))))) -(describe "Headline extraction" +(describe "ID extraction" (before-all (test-org-roam--init)) @@ -252,12 +252,12 @@ (buf (find-file-noselect fname))) (with-current-buffer buf (funcall fn fname))))) - (it "extracts headlines" - (expect (test #'org-roam--extract-headlines + (it "extracts ids" + (expect (test #'org-roam--extract-ids "headlines/headline.org") :to-have-same-items-as - `(["e84d0630-efad-4017-9059-5ef917908823" ,(test-org-roam--abs-path "headlines/headline.org")] - ["801b58eb-97e2-435f-a33e-ff59a2f0c213" ,(test-org-roam--abs-path "headlines/headline.org")]))))) + `(["e84d0630-efad-4017-9059-5ef917908823" ,(test-org-roam--abs-path "headlines/headline.org") 1] + ["801b58eb-97e2-435f-a33e-ff59a2f0c213" ,(test-org-roam--abs-path "headlines/headline.org") 1]))))) (describe "Test roam links" (it ""