mirror of
https://github.com/org-roam/org-roam
synced 2025-08-01 12:17:21 -05:00
(feature): use sqlite as backing database (#200)
All org-roam related information will now be stored in the database. Henceforth, the cache needs to be built synchronously once (via `M-x org-roam-build-cache`), which is then incrementally updated.
This commit is contained in:
@ -2,6 +2,12 @@
|
||||
|
||||
## 0.1.3 (TBD)
|
||||
|
||||
The biggest change, by far, the shift of database storage into SQLite.
|
||||
This means that the org-roam cache needs to be built manually at least
|
||||
once via `M-x org-roam-build-cache`.
|
||||
### Breaking Changes
|
||||
* [#200][gh-200] Move Org-roam cache into a SQLite database.
|
||||
|
||||
### New Features
|
||||
* [#182][gh-182] Support file name aliases via `#+ROAM_ALIAS`.
|
||||
* [#188][gh-188] Add `org-roam-protocol`, shifting `roam://` link handling into Emacs-lisp.
|
||||
@ -99,6 +105,7 @@ Mostly a documentation/cleanup release.
|
||||
[gh-165]: https://github.com/jethrokuan/org-roam/pull/165
|
||||
[gh-182]: https://github.com/jethrokuan/org-roam/pull/182
|
||||
[gh-188]: https://github.com/jethrokuan/org-roam/pull/188
|
||||
[gh-200]: https://github.com/jethrokuan/org-roam/pull/200
|
||||
|
||||
# Local Variables:
|
||||
# eval: (auto-fill-mode -1)
|
||||
|
@ -28,10 +28,9 @@ As of February 2020, it is in a very early stage of development.
|
||||
|
||||
Here's a screenshot of `org-roam`. The `org-roam` buffer shows
|
||||
backlinks for the active org buffer in the left window, as well as the
|
||||
surrounding content in the backlink file. The backlink database is
|
||||
built asynchronously in the background, and is not noticeable to the
|
||||
end user. The graph is generated from the link structure, and can be
|
||||
used to navigate to the respective files.
|
||||
surrounding content in the backlink file. The database is built once,
|
||||
and updated incrementally. The graph is generated from the link
|
||||
structure, and can be used to navigate to the respective files.
|
||||
|
||||

|
||||
|
||||
|
@ -35,9 +35,12 @@ Here is an example `.dir-locals.el` file that would be placed in a
|
||||
second Org-roam directory.
|
||||
|
||||
```emacs-lisp
|
||||
((nil . ((eval . (setq-local org-roam-directory (locate-dominating-file default-directory ".dir-locals.el"))))))
|
||||
((nil . ((org-roam-directory . "/path/to/here/"))))
|
||||
```
|
||||
|
||||
Remember to run `org-roam-build-cache` from a file within that
|
||||
directory, at least once.
|
||||
|
||||
## Org-roam Buffer
|
||||
|
||||
The Org-roam buffer defaults to popping up from the right. You may
|
||||
|
12
doc/tour.md
12
doc/tour.md
@ -13,13 +13,11 @@ Without further ado, let's begin!
|
||||
|
||||
## Building the Cache
|
||||
|
||||
Assuming you've set `org-roam-directory` appropriately, running `M-x
|
||||
org-roam--build-cache-async` should build up the caches that will
|
||||
allow you to begin using Org-roam. I do this on startup:
|
||||
|
||||
```emacs-lisp
|
||||
(add-hook 'after-init-hook 'org-roam--build-cache-async)
|
||||
```
|
||||
The cache is a sqlite database named `org-roam.db`, which resides at
|
||||
the root of your `org-roam-directory`. To begin, we need to do a first
|
||||
build of this cache. To do so, run `M-x org-roam-build-cache`. This
|
||||
may take a while the first time, but is generally instantaneous in
|
||||
subsequent runs.
|
||||
|
||||
## Finding a Note
|
||||
|
||||
|
146
org-roam-db.el
Normal file
146
org-roam-db.el
Normal file
@ -0,0 +1,146 @@
|
||||
;;; org-roam-db.el --- Roam Research replica with Org-mode -*- coding: utf-8; lexical-binding: t -*-
|
||||
|
||||
;; Copyright © 2020 Jethro Kuan <jethrokuan95@gmail.com>
|
||||
|
||||
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; 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
|
||||
;; the Free Software Foundation; either version 3, or (at your option)
|
||||
;; any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
;; Boston, MA 02110-1301, USA.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; This code is heavily referenced from https://github.com/magit/forge.
|
||||
;;
|
||||
;;; Code:
|
||||
|
||||
(require 'emacsql)
|
||||
(require 'emacsql-sqlite)
|
||||
|
||||
;;; Options
|
||||
|
||||
(defcustom org-roam-directory (expand-file-name "~/org-roam/")
|
||||
"Default path to Org-roam files.
|
||||
|
||||
All Org files, at any level of nesting, is considered part of the Org-roam."
|
||||
:type 'directory
|
||||
:group 'org-roam)
|
||||
|
||||
(defconst org-roam-db-filename "org-roam.db"
|
||||
"Name of the Org-roam database file.")
|
||||
|
||||
(defconst org-roam--db-version 1)
|
||||
(defconst org-roam--sqlite-available-p
|
||||
(with-demoted-errors "Org-roam initialization: %S"
|
||||
(emacsql-sqlite-ensure-binary)
|
||||
t))
|
||||
|
||||
(defvar org-roam--db-connection (make-hash-table :test #'equal)
|
||||
"Database connection to Org-roam database.")
|
||||
|
||||
;;; Core
|
||||
|
||||
(defun org-roam--get-db ()
|
||||
"Return the sqlite db file."
|
||||
(interactive "P")
|
||||
(expand-file-name org-roam-db-filename org-roam-directory))
|
||||
|
||||
(defun org-roam--get-db-connection ()
|
||||
"Return the database connection, if any."
|
||||
(gethash (file-truename org-roam-directory)
|
||||
org-roam--db-connection))
|
||||
|
||||
(defun org-roam-db ()
|
||||
(unless (and (org-roam--get-db-connection)
|
||||
(emacsql-live-p (org-roam--get-db-connection)))
|
||||
(let* ((db-file (org-roam--get-db))
|
||||
(init-db (not (file-exists-p db-file))))
|
||||
(make-directory (file-name-directory db-file) t)
|
||||
(let ((conn (emacsql-sqlite db-file)))
|
||||
(puthash (file-truename org-roam-directory)
|
||||
conn
|
||||
org-roam--db-connection)
|
||||
(when init-db
|
||||
(org-roam--db-init conn))
|
||||
(let* ((version (caar (emacsql conn "PRAGMA user_version")))
|
||||
(version (org-roam--db-maybe-update conn version)))
|
||||
(cond
|
||||
((> version org-roam--db-version)
|
||||
(emacsql-close conn)
|
||||
(user-error
|
||||
"The Org-roam database was created with a newer Org-roam version. %s"
|
||||
"You need to update the Org-roam package.")
|
||||
((< version org-roam--db-version)
|
||||
(emacsql-close conn)
|
||||
(error "BUG: The Org-roam database scheme changed %s"
|
||||
"and there is no upgrade path"))))))))
|
||||
(org-roam--get-db-connection))
|
||||
|
||||
;;; Api
|
||||
|
||||
(defun org-roam-sql (sql &rest args)
|
||||
(if (stringp sql)
|
||||
(emacsql (org-roam-db) (apply #'format sql args))
|
||||
(apply #'emacsql (org-roam-db) sql args)))
|
||||
|
||||
;;; Schemata
|
||||
|
||||
(defconst org-roam--db-table-schemata
|
||||
'((files
|
||||
[(file :unique :primary-key)
|
||||
(hash :not-null)
|
||||
(last-modified :not-null)
|
||||
])
|
||||
|
||||
(file-links
|
||||
[(file-from :not-null)
|
||||
(file-to :not-null)
|
||||
(properties :not-null)])
|
||||
|
||||
(titles
|
||||
[
|
||||
(file :not-null)
|
||||
titles])
|
||||
|
||||
(refs
|
||||
[(ref :unique :not-null)
|
||||
(file :not-null)])))
|
||||
|
||||
(defun org-roam--db-init (db)
|
||||
(emacsql-with-transaction db
|
||||
(pcase-dolist (`(,table . ,schema) org-roam--db-table-schemata)
|
||||
(emacsql db [:create-table $i1 $S2] table schema))
|
||||
(emacsql db (format "PRAGMA user_version = %s" org-roam--db-version))))
|
||||
|
||||
(defun org-roam--db-maybe-update (db version)
|
||||
(emacsql-with-transaction db
|
||||
'ignore
|
||||
;; Do nothing now
|
||||
version))
|
||||
|
||||
(defun org-roam--db-close (&optional db)
|
||||
(unless db
|
||||
(setq db (org-roam--get-db-connection)))
|
||||
(when (and db (emacsql-live-p db))
|
||||
(emacsql-close db)))
|
||||
|
||||
(defun org-roam--db-close-all ()
|
||||
(dolist (conn (hash-table-values org-roam--db-connection))
|
||||
(org-roam--db-close conn)))
|
||||
|
||||
(provide 'org-roam-db)
|
||||
|
||||
;;; org-roam-db.el ends here
|
@ -2,10 +2,6 @@
|
||||
|
||||
;; Copyright © 2020 Jethro Kuan <jethrokuan95@gmail.com>
|
||||
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
|
||||
;; URL: https://github.com/jethrokuan/org-roam
|
||||
;; Keywords: org-mode, roam, convenience
|
||||
;; Version: 0.1.2
|
||||
;; Package-Requires: ((emacs "26.1") (org "9.0"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
@ -71,8 +67,7 @@ If the function returns nil, the filename is removed from the
|
||||
list of filenames passed from emacsclient to the server. If the
|
||||
function returns a non-nil value, that value is passed to the
|
||||
server as filename."
|
||||
(let ((the-protocol (concat (regexp-quote org-roam-protocol-the-protocol)
|
||||
":")))
|
||||
(let ((the-protocol (concat (regexp-quote org-roam-protocol-the-protocol) ":")))
|
||||
(when (string-match the-protocol fname)
|
||||
(cadr (split-string fname the-protocol)))))
|
||||
|
||||
|
@ -1,222 +0,0 @@
|
||||
;;; org-roam-utils.el --- Roam Research replica with Org-mode -*- coding: utf-8; lexical-binding: t -*-
|
||||
|
||||
;; Copyright © 2020 Jethro Kuan <jethrokuan95@gmail.com>
|
||||
|
||||
;; Author: Jethro Kuan <jethrokuan95@gmail.com>
|
||||
;; URL: https://github.com/jethrokuan/org-roam
|
||||
;; Keywords: org-mode, roam, convenience
|
||||
;; Version: 0.1.2
|
||||
;; Package-Requires: ((emacs "26.1") (org "9.0"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; 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
|
||||
;; the Free Software Foundation; either version 3, or (at your option)
|
||||
;; any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
;; Boston, MA 02110-1301, USA.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; This library is an attempt at injecting Roam functionality into Org-mode.
|
||||
;; This is achieved primarily through building caches for forward links,
|
||||
;; backward links, and file titles.
|
||||
;;
|
||||
;;; Code:
|
||||
|
||||
(require 'org)
|
||||
(require 'org-element)
|
||||
(require 'ob-core) ;for org-babel-parse-header-arguments
|
||||
(require 'subr-x)
|
||||
(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)
|
||||
"Return all `org-roam' files in `DIR'."
|
||||
(if (file-exists-p dir)
|
||||
(let ((files (directory-files dir t "." t))
|
||||
(dir-ignore-regexp (concat "\\(?:"
|
||||
"\\."
|
||||
"\\|\\.\\."
|
||||
"\\)$"))
|
||||
result)
|
||||
(dolist (file files)
|
||||
(cond
|
||||
((file-directory-p file)
|
||||
(when (not (string-match dir-ignore-regexp file))
|
||||
(setq result (append (org-roam--find-files file) result))))
|
||||
((and (file-readable-p file)
|
||||
(org-roam--org-file-p file))
|
||||
(setq result (cons (file-truename file) result)))))
|
||||
result)))
|
||||
|
||||
(defun org-roam--parse-content (&optional file-path)
|
||||
"Parse the current buffer, and return a list of items for processing."
|
||||
(org-element-map (org-element-parse-buffer) 'link
|
||||
(lambda (link)
|
||||
(let ((type (org-element-property :type link))
|
||||
(path (org-element-property :path link))
|
||||
(start (org-element-property :begin link)))
|
||||
(when (and (string= type "file")
|
||||
(org-roam--org-file-p path))
|
||||
(goto-char start)
|
||||
(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)
|
||||
(buffer-substring
|
||||
begin
|
||||
(or (org-element-property :content-end element)
|
||||
(org-element-property :end element)))))
|
||||
(content (string-trim content))
|
||||
(file-path (or file-path
|
||||
(file-truename (buffer-file-name (current-buffer))))))
|
||||
(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)
|
||||
"Insert ITEM into FORWARD and BACKWARD cache.
|
||||
|
||||
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 :properties ,props) item))
|
||||
;; Build forward-links
|
||||
(let ((links (gethash p-from forward)))
|
||||
(if links
|
||||
(puthash p-from
|
||||
(if (member p-to links)
|
||||
links
|
||||
(cons p-to links)) forward)
|
||||
(puthash p-from (list p-to) forward)))
|
||||
;; Build backward-links
|
||||
(let ((contents-hash (gethash p-to backward)))
|
||||
(if contents-hash
|
||||
(if-let ((contents-list (gethash p-from contents-hash)))
|
||||
(let ((updated (cons props contents-list)))
|
||||
(puthash p-from updated contents-hash)
|
||||
(puthash p-to contents-hash backward))
|
||||
(progn
|
||||
(puthash p-from (list props) contents-hash)
|
||||
(puthash p-to contents-hash backward)))
|
||||
(let ((contents-hash (make-hash-table :test #'equal)))
|
||||
(puthash p-from (list props) contents-hash)
|
||||
(puthash p-to contents-hash backward))))))
|
||||
|
||||
(defun org-roam--extract-global-props (props)
|
||||
"Extract PROPS from the current buffer."
|
||||
(let ((buf (org-element-parse-buffer))
|
||||
(res '()))
|
||||
(dolist (prop props)
|
||||
(let ((p (org-element-map
|
||||
buf
|
||||
'keyword
|
||||
(lambda (kw)
|
||||
(when (string= (org-element-property :key kw) prop)
|
||||
(org-element-property :value kw)))
|
||||
:first-match t)))
|
||||
(setq res (cons (cons prop p) res))))
|
||||
res))
|
||||
|
||||
(defun org-roam--aliases-str-to-list (str)
|
||||
"Function to transform string STR into list of alias titles.
|
||||
|
||||
This snippet is obtained from ox-hugo:
|
||||
https://github.com/kaushalmodi/ox-hugo/blob/a80b250987bc770600c424a10b3bca6ff7282e3c/ox-hugo.el#L3131"
|
||||
(when (stringp str)
|
||||
(let* ((str (org-trim str))
|
||||
(str-list (split-string str "\n"))
|
||||
ret)
|
||||
(dolist (str-elem str-list)
|
||||
(let* ((format-str ":dummy '(%s)") ;The :dummy key is discarded in the `lst' var below.
|
||||
(alist (org-babel-parse-header-arguments (format format-str str-elem)))
|
||||
(lst (cdr (car alist)))
|
||||
(str-list2 (mapcar (lambda (elem)
|
||||
(cond
|
||||
((symbolp elem)
|
||||
(symbol-name elem))
|
||||
(t
|
||||
elem)))
|
||||
lst)))
|
||||
(setq ret (append ret str-list2))))
|
||||
ret)))
|
||||
|
||||
(defun org-roam--extract-titles ()
|
||||
"Extract the titles from current buffer.
|
||||
|
||||
Titles are obtained via the #+TITLE property, or aliases
|
||||
specified via the #+ROAM_ALIAS property."
|
||||
(let* ((props (org-roam--extract-global-props '("TITLE" "ROAM_ALIAS")))
|
||||
(aliases (cdr (assoc "ROAM_ALIAS" props)))
|
||||
(title (cdr (assoc "TITLE" props)))
|
||||
(alias-list (org-roam--aliases-str-to-list aliases)))
|
||||
(if title
|
||||
(cons title alias-list)
|
||||
alias-list)))
|
||||
|
||||
(defun org-roam--extract-ref ()
|
||||
"Extract the ref from current buffer."
|
||||
(cdr (assoc "ROAM_KEY" (org-roam--extract-global-props '("ROAM_KEY")))))
|
||||
|
||||
(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))
|
||||
(refs (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 ((titles (org-roam--extract-titles)))
|
||||
(puthash file titles file-titles))
|
||||
(when-let ((ref (org-roam--extract-ref)))
|
||||
;; FIXME: this overrides previous refs, should probably have a
|
||||
;; warning when ref is not unique
|
||||
(puthash ref file refs)))
|
||||
org-roam-files))
|
||||
(list
|
||||
:directory dir
|
||||
:forward forward-links
|
||||
:backward backward-links
|
||||
:titles file-titles
|
||||
:refs refs)))
|
||||
|
||||
(provide 'org-roam-utils)
|
||||
|
||||
;;; org-roam-utils.el ends here
|
677
org-roam.el
677
org-roam.el
@ -6,7 +6,7 @@
|
||||
;; URL: https://github.com/jethrokuan/org-roam
|
||||
;; Keywords: org-mode, roam, convenience
|
||||
;; Version: 0.1.2
|
||||
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (async "1.9.4") (org "9.0"))
|
||||
;; Package-Requires: ((emacs "26.1") (dash "2.13") (f "0.17.2") (s "1.12.0") (org "9.0") (emacsql "3.0.0") (emacsql-sqlite "1.0.0"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
@ -35,14 +35,15 @@
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile (require 'cl-lib))
|
||||
(require 'dash)
|
||||
(require 'org)
|
||||
(require 'org-element)
|
||||
(require 'async)
|
||||
(require 'ob-core) ;for org-babel-parse-header-arguments
|
||||
(require 'subr-x)
|
||||
(require 'dash)
|
||||
(require 's)
|
||||
(require 'f)
|
||||
(require 'org-roam-utils)
|
||||
(require 'eieio)
|
||||
(require 'cl-lib)
|
||||
(require 'org-roam-db)
|
||||
|
||||
;;; Customizations
|
||||
(defgroup org-roam nil
|
||||
@ -52,12 +53,10 @@
|
||||
:link '(url-link :tag "Github" "https://github.com/jethrokuan/org-roam")
|
||||
:link '(url-link :tag "Online Manual" "https://org-roam.readthedocs.io/"))
|
||||
|
||||
(defcustom org-roam-directory (expand-file-name "~/org-roam/")
|
||||
"Path to Org-roam files.
|
||||
|
||||
All Org files, at any level of nesting, is considered part of the Org-roam."
|
||||
:type 'directory
|
||||
:group 'org-roam)
|
||||
(defgroup org-roam-faces nil
|
||||
"Faces used by Org-Roam."
|
||||
:group 'org-roam
|
||||
:group 'faces)
|
||||
|
||||
(defcustom org-roam-new-file-directory nil
|
||||
"Path to where new Org-roam files are created.
|
||||
@ -66,11 +65,6 @@ If nil, default to the org-roam-directory (preferred)."
|
||||
:type 'directory
|
||||
:group 'org-roam)
|
||||
|
||||
(defcustom org-roam-mute-cache-build nil
|
||||
"Whether to mute the cache build message."
|
||||
:type 'boolean
|
||||
:group 'org-roam)
|
||||
|
||||
(defcustom org-roam-buffer-position 'right
|
||||
"Position of `org-roam' buffer.
|
||||
|
||||
@ -81,14 +75,6 @@ Valid values are
|
||||
(const right))
|
||||
:group 'org-roam)
|
||||
|
||||
(defcustom org-roam-file-name-function #'org-roam--file-name-timestamp-title
|
||||
"The function used to generate filenames.
|
||||
|
||||
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"
|
||||
"The format string used when inserting org-roam links that use their title."
|
||||
:type 'string
|
||||
@ -126,55 +112,15 @@ If nil, always ask for filename."
|
||||
:group 'org-roam)
|
||||
|
||||
(defcustom org-roam-graph-max-title-length 100
|
||||
"Maximum length of titles in graphviz graph nodes"
|
||||
"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"
|
||||
"Shape of Graphviz nodes."
|
||||
:type 'string
|
||||
:group 'org-roam)
|
||||
|
||||
(defgroup org-roam-faces nil
|
||||
"Faces used by Org-Roam."
|
||||
:group 'org-roam
|
||||
:group 'faces)
|
||||
|
||||
;;; Polyfills
|
||||
;; These are for functions I use that are only available in newer Emacs
|
||||
|
||||
;; Introduced in Emacs 27.1
|
||||
(unless (fboundp 'make-empty-file)
|
||||
(defun make-empty-file (filename &optional parents)
|
||||
"Create an empty file FILENAME.
|
||||
Optional arg PARENTS, if non-nil then creates parent dirs as needed.
|
||||
|
||||
If called interactively, then PARENTS is non-nil."
|
||||
(interactive
|
||||
(let ((filename (read-file-name "Create empty file: ")))
|
||||
(list filename t)))
|
||||
(when (and (file-exists-p filename) (null parents))
|
||||
(signal 'file-already-exists `("File exists" ,filename)))
|
||||
(let ((paren-dir (file-name-directory filename)))
|
||||
(when (and paren-dir (not (file-exists-p paren-dir)))
|
||||
(make-directory paren-dir parents)))
|
||||
(write-region "" nil filename nil 0)))
|
||||
|
||||
;;; Classes
|
||||
|
||||
(defclass org-roam-cache ()
|
||||
((initialized :initarg :initialized
|
||||
:documentation "Is cache valid?")
|
||||
(forward-links :initarg :forward-links
|
||||
:documentation "Cache containing forward links.")
|
||||
(backward-links :initarg :backward-links
|
||||
:documentation "Cache containing backward links.")
|
||||
(titles :initarg :titles
|
||||
:documentation "Cache containing titles for org-roam files.")
|
||||
(refs :initarg :refs
|
||||
:documentation "Cache with ref as key, and file path as value."))
|
||||
"All cache for an org-roam directory.")
|
||||
|
||||
;;; Dynamic variables
|
||||
(defvar org-roam--current-buffer nil
|
||||
"Currently displayed file in `org-roam' buffer.")
|
||||
@ -182,70 +128,196 @@ If called interactively, then PARENTS is non-nil."
|
||||
(defvar org-roam-last-window nil
|
||||
"Last window `org-roam' was called from.")
|
||||
|
||||
(defvar org-roam--cache (make-hash-table :test 'equal)
|
||||
"The list of cache separated by directory.")
|
||||
|
||||
(defvar-local org-roam--local-cache-ref nil
|
||||
"Local reference of the buffer's cache object.")
|
||||
|
||||
(defvar-local org-roam--local-cache-id nil
|
||||
"Local reference of the buffer's cache object id, which is
|
||||
comparable by \"eq\".")
|
||||
|
||||
;;; Utilities
|
||||
(defun org-roam-directory-normalized ()
|
||||
"Get the org-roam-directory normalized so that it can be used
|
||||
as a unique key."
|
||||
(directory-file-name (file-truename org-roam-directory)))
|
||||
(defun org-roam--touch-file (path)
|
||||
"Touches an empty file at PATH."
|
||||
(make-directory (file-name-directory path) t)
|
||||
(f-touch path))
|
||||
|
||||
(defmacro org-roam--get-local (name)
|
||||
"Get a variable that is local to the current org-roam-directory."
|
||||
`(gethash (org-roam-directory-normalized) ,name nil))
|
||||
(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))))))
|
||||
|
||||
(defmacro org-roam--set-local (name value)
|
||||
"Set a variable that is local to the current org-roam-directory."
|
||||
`(puthash (org-roam-directory-normalized) ,value ,name))
|
||||
(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--get-directory-cache ()
|
||||
"Get the cache object for the current org-roam-directory."
|
||||
(unless (eq org-roam--local-cache-id org-roam-directory)
|
||||
;; Prevent needless repeated calls to org-roam-directory-normalized by
|
||||
;; having a reference to the cache object that matches the local buffer.
|
||||
(setq org-roam--local-cache-ref
|
||||
(let* ((cache (org-roam--get-local org-roam--cache)))
|
||||
(if cache
|
||||
cache
|
||||
(let ((new-cache (org-roam--default-cache)))
|
||||
(org-roam--set-local org-roam--cache new-cache)
|
||||
new-cache))))
|
||||
(setq org-roam--local-cache-id org-roam-directory))
|
||||
org-roam--local-cache-ref)
|
||||
(defun org-roam--find-files (dir)
|
||||
"Return all `org-roam' files in `DIR'."
|
||||
(if (file-exists-p dir)
|
||||
(let ((files (directory-files dir t "." t))
|
||||
(dir-ignore-regexp (concat "\\(?:"
|
||||
"\\."
|
||||
"\\|\\.\\."
|
||||
"\\)$"))
|
||||
result)
|
||||
(dolist (file files)
|
||||
(cond
|
||||
((file-directory-p file)
|
||||
(when (not (string-match dir-ignore-regexp file))
|
||||
(setq result (append (org-roam--find-files file) result))))
|
||||
((and (file-readable-p file)
|
||||
(org-roam--org-file-p file))
|
||||
(setq result (cons (file-truename file) result)))))
|
||||
result)))
|
||||
|
||||
(defun org-roam--get-links (&optional file-path)
|
||||
"Get the links in the buffer.
|
||||
If FILE-PATH is passed, use that as the source file."
|
||||
(let ((file-path (or file-path
|
||||
(file-truename (buffer-file-name (current-buffer))))))
|
||||
(org-element-map (org-element-parse-buffer) 'link
|
||||
(lambda (link)
|
||||
(let ((type (org-element-property :type link))
|
||||
(path (org-element-property :path link))
|
||||
(start (org-element-property :begin link)))
|
||||
(when (and (string= type "file")
|
||||
(org-roam--org-file-p path))
|
||||
(goto-char start)
|
||||
(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)
|
||||
(buffer-substring
|
||||
begin
|
||||
(or (org-element-property :content-end element)
|
||||
(org-element-property :end element)))))
|
||||
(content (string-trim content)))
|
||||
(vector file-path
|
||||
(file-truename (expand-file-name path (file-name-directory file-path)))
|
||||
(list :content content :point begin)))))))))
|
||||
|
||||
(defun org-roam--extract-global-props (props)
|
||||
"Extract PROPS from the current buffer."
|
||||
(let ((buf (org-element-parse-buffer))
|
||||
(res '()))
|
||||
(dolist (prop props)
|
||||
(let ((p (org-element-map
|
||||
buf
|
||||
'keyword
|
||||
(lambda (kw)
|
||||
(when (string= (org-element-property :key kw) prop)
|
||||
(org-element-property :value kw)))
|
||||
:first-match t)))
|
||||
(setq res (cons (cons prop p) res))))
|
||||
res))
|
||||
|
||||
(defun org-roam--aliases-str-to-list (str)
|
||||
"Function to transform string STR into list of alias titles.
|
||||
|
||||
This snippet is obtained from ox-hugo:
|
||||
https://github.com/kaushalmodi/ox-hugo/blob/a80b250987bc770600c424a10b3bca6ff7282e3c/ox-hugo.el#L3131"
|
||||
(when (stringp str)
|
||||
(let* ((str (org-trim str))
|
||||
(str-list (split-string str "\n"))
|
||||
ret)
|
||||
(dolist (str-elem str-list)
|
||||
(let* ((format-str ":dummy '(%s)") ;The :dummy key is discarded in the `lst' var below.
|
||||
(alist (org-babel-parse-header-arguments (format format-str str-elem)))
|
||||
(lst (cdr (car alist)))
|
||||
(str-list2 (mapcar (lambda (elem)
|
||||
(cond
|
||||
((symbolp elem)
|
||||
(symbol-name elem))
|
||||
(t
|
||||
elem)))
|
||||
lst)))
|
||||
(setq ret (append ret str-list2))))
|
||||
ret)))
|
||||
|
||||
(defun org-roam--extract-titles ()
|
||||
"Extract the titles from current buffer.
|
||||
Titles are obtained via the #+TITLE property, or aliases
|
||||
specified via the #+ROAM_ALIAS property."
|
||||
(let* ((props (org-roam--extract-global-props '("TITLE" "ROAM_ALIAS")))
|
||||
(aliases (cdr (assoc "ROAM_ALIAS" props)))
|
||||
(title (cdr (assoc "TITLE" props)))
|
||||
(alias-list (org-roam--aliases-str-to-list aliases)))
|
||||
(if title
|
||||
(cons title alias-list)
|
||||
alias-list)))
|
||||
|
||||
(defun org-roam--extract-ref ()
|
||||
"Extract the ref from current buffer."
|
||||
(cdr (assoc "ROAM_KEY" (org-roam--extract-global-props '("ROAM_KEY")))))
|
||||
|
||||
(defun org-roam--insert-links (links)
|
||||
"Insert LINK into the org-roam cache."
|
||||
(org-roam-sql
|
||||
[:insert :into file-links
|
||||
:values $v1]
|
||||
links))
|
||||
|
||||
(defun org-roam--insert-titles (file titles)
|
||||
"Insert TITLES into the org-roam-cache."
|
||||
(org-roam-sql
|
||||
[:insert :into titles
|
||||
:values $v1]
|
||||
(list (vector file titles))))
|
||||
|
||||
(defun org-roam--insert-ref (file ref)
|
||||
"Insert REF into the Org-roam cache."
|
||||
(org-roam-sql
|
||||
[:insert :into refs
|
||||
:values $v1]
|
||||
(list (vector ref file))))
|
||||
|
||||
(defun org-roam--clear-cache ()
|
||||
"Clears all entries in the caches."
|
||||
(interactive)
|
||||
(when (file-exists-p (org-roam--get-db))
|
||||
(org-roam-sql [:delete :from files])
|
||||
(org-roam-sql [:delete :from titles])
|
||||
(org-roam-sql [:delete :from file-links])
|
||||
(org-roam-sql [:delete :from files])
|
||||
(org-roam-sql [:delete :from refs])))
|
||||
|
||||
(defun org-roam--clear-file-from-cache (&optional filepath)
|
||||
"Remove any related links to the file at FILEPATH.
|
||||
This is equivalent to removing the node from the graph."
|
||||
(let* ((path (or filepath
|
||||
(buffer-file-name (current-buffer))))
|
||||
(file (file-truename path)))
|
||||
(org-roam-sql [:delete :from files
|
||||
:where (= file $s1)]
|
||||
file)
|
||||
(org-roam-sql [:delete :from file-links
|
||||
:where (= file-from $s1)]
|
||||
file)
|
||||
(org-roam-sql [:delete :from titles
|
||||
:where (= file $s1)]
|
||||
file)
|
||||
(org-roam-sql [:delete :from refs
|
||||
:where (= file $s1)]
|
||||
file)))
|
||||
|
||||
(defun org-roam--get-current-files ()
|
||||
"Return a hash of file to buffer string hash."
|
||||
(let* ((current-files (org-roam-sql [:select * :from files]))
|
||||
(ht (make-hash-table :test #'equal)))
|
||||
(dolist (row current-files)
|
||||
(puthash (car row) (cadr row) ht))
|
||||
ht))
|
||||
|
||||
(defun org-roam--cache-initialized-p ()
|
||||
"Is cache valid?"
|
||||
(oref (org-roam--get-directory-cache) initialized))
|
||||
|
||||
(defun org-roam--forward-links-cache ()
|
||||
"Cache containing forward links."
|
||||
(oref (org-roam--get-directory-cache) forward-links))
|
||||
|
||||
(defun org-roam--backward-links-cache ()
|
||||
"Cache containing backward links."
|
||||
(oref (org-roam--get-directory-cache) backward-links))
|
||||
|
||||
(defun org-roam--titles-cache ()
|
||||
"Cache containing titles for org-roam files."
|
||||
(oref (org-roam--get-directory-cache) titles))
|
||||
|
||||
(defun org-roam--refs-cache ()
|
||||
"Cache containing refs for org-roam files."
|
||||
(oref (org-roam--get-directory-cache) refs))
|
||||
"Whether the cache has been initialized."
|
||||
(and (file-exists-p (org-roam--get-db))
|
||||
(> (caar (org-roam-sql [:select (funcall count) :from titles]))
|
||||
0)))
|
||||
|
||||
(defun org-roam--ensure-cache-built ()
|
||||
"Ensures that org-roam cache is built."
|
||||
(unless (org-roam--cache-initialized-p)
|
||||
(org-roam--build-cache-async)
|
||||
(user-error "Your Org-Roam cache isn't built yet! Please wait")))
|
||||
(error "[Org-roam] your cache isn't built yet! Please wait.")))
|
||||
|
||||
(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."
|
||||
@ -258,11 +330,10 @@ as a unique key."
|
||||
|
||||
(defun org-roam--get-titles-from-cache (file)
|
||||
"Return titles and aliases of `FILE' from the cache."
|
||||
(or (gethash file (org-roam--titles-cache))
|
||||
(progn
|
||||
(unless (org-roam--cache-initialized-p)
|
||||
(user-error "The Org-Roam caches aren't built! Please run org-roam--build-cache-async"))
|
||||
nil)))
|
||||
(caar (org-roam-sql [:select [titles] :from titles
|
||||
:where (= file $s1)]
|
||||
file
|
||||
:limit 1)))
|
||||
|
||||
(defun org-roam--get-title-from-cache (file)
|
||||
"Return the title of `FILE' from the cache."
|
||||
@ -341,7 +412,7 @@ It uses TITLE and the current timestamp to form a unique title."
|
||||
(setq file-path (org-roam--new-file-path (file-name-fn title) t))
|
||||
(if (file-exists-p file-path)
|
||||
file-path
|
||||
(make-empty-file file-path t)
|
||||
(org-roam--touch-file file-path)
|
||||
(write-region
|
||||
(s-format (plist-get template :content)
|
||||
'aget
|
||||
@ -350,18 +421,6 @@ It uses TITLE and the current timestamp to form a unique title."
|
||||
nil file-path nil)
|
||||
file-path))))
|
||||
|
||||
(defun org-roam--get-new-id (title)
|
||||
"Return a new ID, given the note TITLE."
|
||||
(let* ((proposed-slug (funcall org-roam-file-name-function title))
|
||||
(new-slug (if org-roam-filename-noconfirm
|
||||
proposed-slug
|
||||
(read-string "Enter ID (without extension): "
|
||||
proposed-slug)))
|
||||
(file-path (org-roam--new-file-path new-slug t)))
|
||||
(if (file-exists-p file-path)
|
||||
(user-error "There's already a file at %s")
|
||||
new-slug)))
|
||||
|
||||
;;; Inserting org-roam links
|
||||
(defun org-roam-insert (prefix)
|
||||
"Find an org-roam file, and insert a relative org link to it at point.
|
||||
@ -397,13 +456,16 @@ If PREFIX, downcase the title before insertion."
|
||||
;;; Finding org-roam files
|
||||
(defun org-roam--get-title-path-completions ()
|
||||
"Return a list of cons pairs for titles to absolute path of Org-roam files."
|
||||
(let ((files (org-roam--find-all-files))
|
||||
(res '()))
|
||||
(dolist (file files)
|
||||
(if-let (titles (org-roam--get-titles-from-cache file))
|
||||
(dolist (title titles)
|
||||
(setq res (cons (cons title file) res)))
|
||||
(setq res (cons (cons (org-roam--path-to-slug file) file) res))))
|
||||
(let* ((rows (org-roam-sql [:select [file titles] :from titles]))
|
||||
res)
|
||||
(dolist (row rows)
|
||||
(let ((file-path (car row))
|
||||
(titles (cadr row)))
|
||||
(if titles
|
||||
(dolist (title titles)
|
||||
(setq res (cons (cons title file-path) res)))
|
||||
(setq res (cons (cons (org-roam--path-to-slug file-path)
|
||||
file-path) res)))))
|
||||
res))
|
||||
|
||||
(defun org-roam-find-file ()
|
||||
@ -437,115 +499,102 @@ If PREFIX, downcase the title before insertion."
|
||||
(when-let ((name (completing-read "Choose a buffer: " names-and-buffers)))
|
||||
(switch-to-buffer (cdr (assoc name names-and-buffers))))))
|
||||
|
||||
(defvar org-roam--ongoing-async-build (make-hash-table :test 'equal)
|
||||
"Prevent multiple async cache builds. This can happen when
|
||||
restoring a session or loading multiple org-roam files before a
|
||||
build has completed.")
|
||||
|
||||
;;; Building the org-roam cache
|
||||
(defun org-roam--build-cache-async (&optional on-success)
|
||||
"Builds the caches asychronously."
|
||||
(defun org-roam-build-cache ()
|
||||
"Build the cache for `org-roam-directory'."
|
||||
(interactive)
|
||||
(let ((existing (org-roam--get-local org-roam--ongoing-async-build)))
|
||||
(unless (and (processp existing)
|
||||
(not (async-ready existing)))
|
||||
(org-roam--set-local
|
||||
org-roam--ongoing-async-build
|
||||
(async-start
|
||||
`(lambda ()
|
||||
(setq load-path ',load-path)
|
||||
(package-initialize)
|
||||
(require 'org-roam-utils)
|
||||
,(async-inject-variables "org-roam-directory")
|
||||
(org-roam--build-cache org-roam-directory))
|
||||
(lambda (cache)
|
||||
(let ((org-roam-directory (plist-get cache :directory)))
|
||||
(let ((obj (org-roam--get-directory-cache)))
|
||||
(oset obj initialized t)
|
||||
(oset obj forward-links (plist-get cache :forward))
|
||||
(oset obj backward-links (plist-get cache :backward))
|
||||
(oset obj titles (plist-get cache :titles))
|
||||
(oset obj refs (plist-get cache :refs)))
|
||||
(unless org-roam-mute-cache-build
|
||||
(message "Org-roam cache built!"))
|
||||
(when on-success
|
||||
(funcall on-success)))))))))
|
||||
|
||||
(defun org-roam--clear-cache ()
|
||||
"Clears all entries in the caches."
|
||||
(interactive)
|
||||
(let ((cache (org-roam--get-directory-cache)))
|
||||
(oset cache initialized nil)
|
||||
(oset cache forward-links (make-hash-table :test #'equal))
|
||||
(oset cache backward-links (make-hash-table :test #'equal))
|
||||
(oset cache titles (make-hash-table :test #'equal))
|
||||
(oset cache refs (make-hash-table :test #'equal))))
|
||||
|
||||
(defun org-roam--default-cache ()
|
||||
"A default, uninitialized cache object."
|
||||
(org-roam-cache :initialized nil
|
||||
:forward-links (make-hash-table :test #'equal)
|
||||
:backward-links (make-hash-table :test #'equal)
|
||||
:titles (make-hash-table :test #'equal)
|
||||
:refs (make-hash-table :test #'equal)))
|
||||
|
||||
(defun org-roam--clear-file-from-cache (&optional filepath)
|
||||
"Remove any related links to the file at FILEPATH.
|
||||
This is equivalent to removing the node from the graph."
|
||||
(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
|
||||
(dolist (link forward-links)
|
||||
(when-let ((backward-links (gethash link (org-roam--backward-links-cache))))
|
||||
(remhash file backward-links)
|
||||
(puthash link backward-links (org-roam--backward-links-cache))))
|
||||
;; Clean out forward links
|
||||
(remhash file (org-roam--forward-links-cache)))
|
||||
;; Step 2: Remove from the title cache
|
||||
(remhash file (org-roam--titles-cache))
|
||||
;; Step 3: Remove from the refs cache
|
||||
(maphash (lambda (k v)
|
||||
(when (string= v file)
|
||||
(remhash k (org-roam--refs-cache))))
|
||||
(org-roam--refs-cache))))
|
||||
(org-roam-db) ;; To initialize the database, no-op if already initialized
|
||||
(let* ((org-roam-files (org-roam--find-files org-roam-directory))
|
||||
(current-files (org-roam--get-current-files))
|
||||
(time (current-time))
|
||||
all-files all-links all-titles all-refs)
|
||||
(dolist (file org-roam-files)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(let ((contents-hash (secure-hash 'sha1 (current-buffer))))
|
||||
(unless (string= (gethash file current-files)
|
||||
contents-hash)
|
||||
(org-roam--clear-file-from-cache file)
|
||||
(setq all-files
|
||||
(cons (vector file contents-hash time) all-files))
|
||||
(when-let (links (org-roam--get-links file))
|
||||
(setq all-links (append links all-links)))
|
||||
(let ((titles (org-roam--extract-titles)))
|
||||
(setq all-titles (cons (vector file titles) all-titles)))
|
||||
(when-let ((ref (org-roam--extract-ref)))
|
||||
(setq all-refs (cons (vector ref file) all-refs))))
|
||||
(remhash file current-files))))
|
||||
(dolist (file (hash-table-keys current-files))
|
||||
;; These files are no longer around, remove from cache...
|
||||
(org-roam--clear-file-from-cache file))
|
||||
(when all-files
|
||||
(org-roam-sql
|
||||
[:insert :into files
|
||||
:values $v1]
|
||||
all-files))
|
||||
(when all-links
|
||||
(org-roam-sql
|
||||
[:insert :into file-links
|
||||
:values $v1]
|
||||
all-links))
|
||||
(when all-titles
|
||||
(org-roam-sql
|
||||
[:insert :into titles
|
||||
:values $v1]
|
||||
all-titles))
|
||||
(when all-refs
|
||||
(org-roam-sql
|
||||
[:insert :into refs
|
||||
:values $v1]
|
||||
all-refs))
|
||||
(let ((stats (list :files (length all-files)
|
||||
:links (length all-links)
|
||||
:titles (length all-titles)
|
||||
:refs (length all-refs)
|
||||
:deleted (length (hash-table-keys current-files)))))
|
||||
(message (format "files: %s, links: %s, titles: %s, refs: %s, deleted: %s"
|
||||
(plist-get stats :files)
|
||||
(plist-get stats :links)
|
||||
(plist-get stats :titles)
|
||||
(plist-get stats :refs)
|
||||
(plist-get stats :deleted)))
|
||||
stats)))
|
||||
|
||||
(defun org-roam--update-cache-titles ()
|
||||
"Insert the title of the current buffer into the cache."
|
||||
(when-let ((titles (org-roam--extract-titles)))
|
||||
(puthash (file-truename (buffer-file-name (current-buffer)))
|
||||
titles
|
||||
(org-roam--titles-cache))))
|
||||
"Update the title of the current buffer into the cache."
|
||||
(let ((file (file-truename (buffer-file-name (current-buffer)))))
|
||||
(org-roam-sql [:delete :from titles
|
||||
:where (= file $s1)]
|
||||
file)
|
||||
(org-roam--insert-titles file (org-roam--extract-titles))))
|
||||
|
||||
(defun org-roam--update-cache-refs ()
|
||||
"Insert the ref of the current buffer into the cache."
|
||||
(when-let ((ref (org-roam--extract-ref)))
|
||||
(puthash ref
|
||||
(file-truename (buffer-file-name (current-buffer)))
|
||||
(org-roam--refs-cache))))
|
||||
"Update the ref of the current buffer into the cache."
|
||||
(let ((file (file-truename (buffer-file-name (current-buffer)))))
|
||||
(org-roam-sql [:delete :from refs
|
||||
:where (= file $s1)]
|
||||
file)
|
||||
(when-let ((ref (org-roam--extract-ref)))
|
||||
(org-roam--insert-ref file ref))))
|
||||
|
||||
(defun org-roam--update-cache-links ()
|
||||
"Update the file links of the current buffer in the cache."
|
||||
(let ((file (file-truename (buffer-file-name (current-buffer)))))
|
||||
(org-roam-sql [:delete :from file-links
|
||||
:where (= file-from $s1)]
|
||||
file)
|
||||
(when-let ((links (org-roam--get-links)))
|
||||
(org-roam--insert-links links))))
|
||||
|
||||
(defun org-roam--update-cache ()
|
||||
"Update org-roam caches for the current buffer file."
|
||||
(save-excursion
|
||||
(org-roam--clear-file-from-cache)
|
||||
;; Insert into title cache
|
||||
(org-roam--update-cache-titles)
|
||||
;; Insert into ref cache
|
||||
(org-roam--update-cache-refs)
|
||||
;; Insert new items
|
||||
(let ((items (org-roam--parse-content)))
|
||||
(dolist (item items)
|
||||
(org-roam--insert-item
|
||||
item
|
||||
:forward (org-roam--forward-links-cache)
|
||||
:backward (org-roam--backward-links-cache))))
|
||||
;; Rerender buffer
|
||||
(org-roam--update-cache-links)
|
||||
(org-roam--maybe-update-buffer :redisplay t)))
|
||||
|
||||
;;; Org-roam daily notes
|
||||
|
||||
(defun org-roam--file-for-time (time)
|
||||
"Create and find file for TIME."
|
||||
(let* ((org-roam-templates (list (list "daily" (list :file (lambda (title) title)
|
||||
@ -618,10 +667,15 @@ If item at point is not org-roam specific, default to Org behaviour."
|
||||
(select-window org-roam-last-window))
|
||||
(find-file file)))
|
||||
|
||||
(defun org-roam--get-backlinks (file)
|
||||
(org-roam-sql [:select [file-from, file-to, properties] :from file-links
|
||||
:where (= file-to $s1)]
|
||||
file))
|
||||
|
||||
(defun org-roam-update (file-path)
|
||||
"Show the backlinks for given org file for file at `FILE-PATH'."
|
||||
(org-roam--ensure-cache-built)
|
||||
(let* ((source-org-roam-directory org-roam-directory))
|
||||
(org-roam--ensure-cache-built)
|
||||
(let ((buffer-title (org-roam--get-title-or-slug file-path)))
|
||||
(with-current-buffer org-roam-buffer
|
||||
;; When dir-locals.el is used to override org-roam-directory,
|
||||
@ -640,24 +694,27 @@ If item at point is not org-roam specific, default to Org behaviour."
|
||||
(setq org-return-follows-link t)
|
||||
(insert
|
||||
(propertize buffer-title 'font-lock-face 'org-document-title))
|
||||
(if-let ((backlinks (gethash file-path (org-roam--backward-links-cache))))
|
||||
(if-let* ((backlinks (org-roam--get-backlinks file-path))
|
||||
(grouped-backlinks (--group-by (nth 0 it) backlinks)))
|
||||
(progn
|
||||
(insert (format "\n\n* %d Backlinks\n"
|
||||
(hash-table-count backlinks)))
|
||||
(maphash (lambda (file-from contents)
|
||||
(insert (format "** [[file:%s][%s]]\n"
|
||||
file-from
|
||||
(org-roam--get-title-or-slug file-from)))
|
||||
(dolist (properties contents)
|
||||
(let ((content (propertize
|
||||
(s-trim (s-replace "\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))
|
||||
(length backlinks)))
|
||||
(dolist (group grouped-backlinks)
|
||||
(let ((file-from (car group))
|
||||
(bls (cdr group)))
|
||||
(insert (format "** [[file:%s][%s]]\n"
|
||||
file-from
|
||||
(org-roam--get-title-or-slug file-from)))
|
||||
(dolist (backlink bls)
|
||||
(pcase-let ((`(,file-from ,file-to ,props) backlink))
|
||||
(insert (propertize
|
||||
(s-trim (s-replace "\n" " "
|
||||
(plist-get props :content)))
|
||||
'font-lock-face 'org-block
|
||||
'help-echo "mouse-1: visit backlinked note"
|
||||
'file-from file-from
|
||||
'file-from-point (plist-get props :point)))
|
||||
(insert "\n\n"))))))
|
||||
(insert "\n\n* No backlinks!")))
|
||||
(read-only-mode 1))))))
|
||||
|
||||
@ -667,24 +724,24 @@ If item at point is not org-roam specific, default to Org behaviour."
|
||||
(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
|
||||
(let ((rows (org-roam-sql [:select [file titles] :from titles])))
|
||||
(dolist (row rows)
|
||||
(let* ((file (car row))
|
||||
(title (or (caadr row)
|
||||
(org-roam--path-to-slug file)))
|
||||
(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
|
||||
file
|
||||
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))
|
||||
title)))))
|
||||
(let ((link-rows (org-roam-sql [:select :distinct [file-to file-from] :from file-links])))
|
||||
(dolist (row link-rows)
|
||||
(insert (format " \"%s\" -> \"%s\";\n"
|
||||
(car row)
|
||||
(cadr row)))))
|
||||
(insert "}")
|
||||
(buffer-string)))
|
||||
|
||||
@ -715,7 +772,6 @@ This needs to be quick/infrequent, because this is run at
|
||||
(when (and (or redisplay
|
||||
(not (eq org-roam--current-buffer buffer)))
|
||||
(eq 'visible (org-roam--current-visibility))
|
||||
(org-roam--cache-initialized-p)
|
||||
(buffer-local-value 'buffer-file-truename buffer))
|
||||
(setq org-roam--current-buffer buffer)
|
||||
(org-roam-update (expand-file-name
|
||||
@ -740,13 +796,8 @@ Applies `org-roam-link-face' if PATH correponds to a Roam file."
|
||||
(setq org-roam-last-window (get-buffer-window))
|
||||
(add-hook 'post-command-hook #'org-roam--maybe-update-buffer nil t)
|
||||
(add-hook 'after-save-hook #'org-roam--update-cache nil t)
|
||||
(if (org-roam--cache-initialized-p)
|
||||
(org-roam--setup-found-file)
|
||||
(org-roam--build-cache-async
|
||||
(let ((buf (buffer-name)))
|
||||
#'(lambda ()
|
||||
(with-current-buffer buf
|
||||
(org-roam--setup-found-file))))))))
|
||||
(org-roam--setup-file-links)
|
||||
(org-roam--maybe-update-buffer :redisplay nil)))
|
||||
|
||||
(defun org-roam--setup-file-links ()
|
||||
"Set up `file:' Org links with org-roam-link-face."
|
||||
@ -759,11 +810,6 @@ This sets `file:' Org links to have the org-link face."
|
||||
(unless (version< org-version "9.2")
|
||||
(org-link-set-parameters "file" :face 'org-link)))
|
||||
|
||||
(defun org-roam--setup-found-file ()
|
||||
"Setup a buffer recognized via the \"find-file-hook\"."
|
||||
(org-roam--setup-file-links)
|
||||
(org-roam--maybe-update-buffer :redisplay nil))
|
||||
|
||||
(defvar org-roam-mode-map
|
||||
(make-sparse-keymap)
|
||||
"Keymap for org-roam commands.")
|
||||
@ -778,9 +824,10 @@ This sets `file:' Org links to have the org-link face."
|
||||
(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))
|
||||
(let* ((files-to-rename (org-roam-sql [:select :distinct [file-from]
|
||||
:from file-links
|
||||
:where (= file-to $s1)]
|
||||
file))
|
||||
(path (file-truename file))
|
||||
(new-path (file-truename new-file))
|
||||
(slug (org-roam--get-title-or-slug file))
|
||||
@ -788,27 +835,31 @@ This sets `file:' Org links to have the org-link face."
|
||||
(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))
|
||||
(org-roam--clear-file-from-cache file)
|
||||
(dolist (file-from files-to-rename)
|
||||
(let* ((file-from (car file-from))
|
||||
(file-from (if (string-equal (file-truename file-from)
|
||||
path)
|
||||
new-path
|
||||
file-from))
|
||||
(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))))
|
||||
(save-window-excursion
|
||||
(find-file new-path)
|
||||
(org-roam--update-cache)))))
|
||||
@ -818,7 +869,7 @@ This sets `file:' Org links to have the org-link face."
|
||||
"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.
|
||||
if ARG is positive, 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."
|
||||
@ -829,15 +880,16 @@ If ARG is `toggle', toggle `org-roam-mode'. Otherwise, behave as if called inter
|
||||
:global t
|
||||
(cond
|
||||
(org-roam-mode
|
||||
(unless (org-roam--cache-initialized-p)
|
||||
(org-roam--build-cache-async))
|
||||
(add-hook 'find-file-hook #'org-roam--find-file-hook-function)
|
||||
(add-hook 'kill-emacs-hook #'org-roam--db-close-all)
|
||||
(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)
|
||||
(remove-hook 'kill-emacs-hook #'org-roam--db-close-all)
|
||||
(advice-remove 'rename-file #'org-roam--rename-file-advice)
|
||||
(advice-remove 'delete-file #'org-roam--delete-file-advice)
|
||||
(org-roam--db-close-all)
|
||||
;; Disable local hooks for all org-roam buffers
|
||||
(dolist (buf (org-roam--get-roam-buffers))
|
||||
(with-current-buffer buf
|
||||
@ -845,8 +897,6 @@ If ARG is `toggle', toggle `org-roam-mode'. Otherwise, behave as if called inter
|
||||
(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.
|
||||
@ -889,6 +939,9 @@ 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))))
|
||||
|
||||
;;; -
|
||||
(provide 'org-roam)
|
||||
;;; org-roam.el ends here
|
||||
|
||||
;; Local Variables:
|
||||
|
@ -1,7 +0,0 @@
|
||||
#+TITLE: Multi-File 1
|
||||
|
||||
link to [[file:nested/mf1.org][Nested Multi-File 1]]
|
||||
link to [[file:mf2.org][Multi-File 2]]
|
||||
|
||||
Arbitrary [[https://google.com][HTML]] link
|
||||
Arbitrary text
|
@ -1,3 +0,0 @@
|
||||
#+TITLE: Multi-File 2
|
||||
|
||||
This file has no links.
|
@ -1,5 +0,0 @@
|
||||
#+TITLE: Multi-File 3
|
||||
|
||||
This file has a link to an file with no title.
|
||||
|
||||
[[file:multi-no-title.org][multi-no-title]]
|
@ -1 +0,0 @@
|
||||
no title in this file :O
|
@ -1,4 +0,0 @@
|
||||
#+TITLE: Nested Multi-File 1
|
||||
|
||||
Link to [[file:mf2.org][Nested Multi-File 2]]
|
||||
Link to [[file:../mf1.org][Mulit-File 1]]
|
@ -1,3 +0,0 @@
|
||||
#+TITLE: Nested Multi-File 2
|
||||
|
||||
Link to [[file:mf1.org][Nested Multi-File 1]]
|
3
tests/roam-files/bar.org
Normal file
3
tests/roam-files/bar.org
Normal file
@ -0,0 +1,3 @@
|
||||
#+TITLE: Bar
|
||||
|
||||
This is file bar. Bar links to [[file:nested/bar.org][Nested Bar]].
|
@ -1,7 +0,0 @@
|
||||
#+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
|
@ -1,5 +0,0 @@
|
||||
#+TITLE: File 3
|
||||
|
||||
This file has a link to an file with no title.
|
||||
|
||||
[[file:no-title.org][no-title]]
|
8
tests/roam-files/foo.org
Normal file
8
tests/roam-files/foo.org
Normal file
@ -0,0 +1,8 @@
|
||||
#+TITLE: Foo
|
||||
|
||||
This is the foo file. It contains a link to [[file:bar.org][Bar]].
|
||||
|
||||
To make the tests more robust, here are some arbitrary links:
|
||||
|
||||
- [[https:google.com][Google]]
|
||||
- [[mailto:foo@john.com][mail to foo]]
|
3
tests/roam-files/nested/bar.org
Normal file
3
tests/roam-files/nested/bar.org
Normal file
@ -0,0 +1,3 @@
|
||||
#+TITLE: Nested Bar
|
||||
|
||||
This file is nested, 1 level deeper. It links to both [[file:../foo.org][Foo]] and [[file:foo.org][Nested Foo]].
|
@ -1,4 +0,0 @@
|
||||
#+TITLE: Nested File 1
|
||||
|
||||
Link to [[file:f2.org][Nested File 2]]
|
||||
Link to [[file:../f1.org][File 1]]
|
@ -1,3 +0,0 @@
|
||||
#+TITLE: Nested File 2
|
||||
|
||||
Link to [[file:f1.org][Nested File 1]]
|
@ -1,3 +1,3 @@
|
||||
#+TITLE: File 2
|
||||
#+TITLE: Nested Foo
|
||||
|
||||
This file has no links.
|
@ -1 +1,3 @@
|
||||
no title in this file :O
|
||||
|
||||
links to itself, with no title: [[file:no-title.org][no-title]]
|
||||
|
3
tests/roam-files/unlinked.org
Normal file
3
tests/roam-files/unlinked.org
Normal file
@ -0,0 +1,3 @@
|
||||
#+TITLE: Unlinked
|
||||
|
||||
Nothing links here :(
|
@ -29,6 +29,7 @@
|
||||
(require 'buttercup)
|
||||
(require 'with-simulated-input)
|
||||
(require 'org-roam)
|
||||
(require 'org-roam-db)
|
||||
(require 'dash)
|
||||
|
||||
(defun abs-path (file-path)
|
||||
@ -42,320 +43,262 @@
|
||||
(defvar org-roam--tests-directory (file-truename (concat default-directory "tests/roam-files"))
|
||||
"Directory containing org-roam test org files.")
|
||||
|
||||
(defvar org-roam--tests-multi (file-truename (concat default-directory "tests/roam-files-multi"))
|
||||
"Directory containing org-roam test org files.")
|
||||
|
||||
(defun org-roam--test-init ()
|
||||
(org-roam--db-close)
|
||||
(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-mute-cache-build t))
|
||||
(org-roam-mode +1))
|
||||
|
||||
(defun org-roam--test-multi-init ()
|
||||
(let ((original-dir-1 org-roam--tests-directory)
|
||||
(original-dir-2 org-roam--tests-multi)
|
||||
(new-dir-1 (expand-file-name (make-temp-name "org-roam") temporary-file-directory))
|
||||
(new-dir-2 (expand-file-name (make-temp-name "org-roam") temporary-file-directory)))
|
||||
(copy-directory original-dir-1 new-dir-1)
|
||||
(copy-directory original-dir-2 new-dir-2)
|
||||
(setq org-roam-directory new-dir-1)
|
||||
(setq org-roam-directory2 new-dir-2)
|
||||
(setq org-roam-mute-cache-build t))
|
||||
(org-roam-mode +1))
|
||||
|
||||
(defun org-roam--test-build-cache ()
|
||||
"Builds the caches synchronously."
|
||||
(let ((cache (org-roam--build-cache org-roam-directory)))
|
||||
(let ((obj (org-roam--get-directory-cache)))
|
||||
(oset obj initialized t)
|
||||
(oset obj forward-links (plist-get cache :forward))
|
||||
(oset obj backward-links (plist-get cache :backward))
|
||||
(oset obj titles (plist-get cache :titles))
|
||||
(oset obj refs (plist-get cache :refs)))))
|
||||
(org-roam-mode +1)))
|
||||
|
||||
;;; Tests
|
||||
(describe "org-roam--build-cache-async"
|
||||
(describe "org-roam-build-cache"
|
||||
(it "initializes correctly"
|
||||
(org-roam--clear-cache)
|
||||
(org-roam--test-multi-init)
|
||||
(expect (org-roam--cache-initialized-p) :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--test-init)
|
||||
(org-roam-build-cache)
|
||||
|
||||
(org-roam--build-cache-async)
|
||||
(sleep-for 3) ;; Because it's async
|
||||
;; Cache
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from files])) :to-be 8)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from file-links])) :to-be 5)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from titles])) :to-be 8)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from titles
|
||||
:where titles :is-null])) :to-be 2)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from refs])) :to-be 1)
|
||||
|
||||
;; Caches should be populated
|
||||
(expect (org-roam--cache-initialized-p) :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 6)
|
||||
(expect (hash-table-count (org-roam--refs-cache)) :to-be 1)
|
||||
;; TODO Test files
|
||||
|
||||
;; 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"))))
|
||||
;; Links -- File-from
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "foo.org"))) :to-be 1)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "nested/bar.org"))) :to-be 2)
|
||||
|
||||
(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))
|
||||
;; Links -- File-to
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from file-links
|
||||
:where (= file-to $s1)]
|
||||
(abs-path "nested/foo.org"))) :to-be 1)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from file-links
|
||||
:where (= file-to $s1)]
|
||||
(abs-path "nested/bar.org"))) :to-be 1)
|
||||
(expect (caar (org-roam-sql [:select (funcall count) :from file-links
|
||||
:where (= file-to $s1)]
|
||||
(abs-path "unlinked.org"))) :to-be 0)
|
||||
;; TODO Test titles
|
||||
(expect (org-roam-sql [:select * :from titles])
|
||||
:to-have-same-items-as
|
||||
(list (list (abs-path "alias.org")
|
||||
(list "t1" "a1" "a 2"))
|
||||
(list (abs-path "bar.org")
|
||||
(list "Bar"))
|
||||
(list (abs-path "foo.org")
|
||||
(list "Foo"))
|
||||
(list (abs-path "nested/bar.org")
|
||||
(list "Nested Bar"))
|
||||
(list (abs-path "nested/foo.org")
|
||||
(list "Nested Foo"))
|
||||
(list (abs-path "no-title.org") nil)
|
||||
(list (abs-path "web_ref.org") nil)
|
||||
(list (abs-path "unlinked.org")
|
||||
(list "Unlinked"))))
|
||||
|
||||
;; 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))
|
||||
(expect (org-roam-sql [:select * :from refs])
|
||||
:to-have-same-items-as
|
||||
(list (list "https://google.com/" (abs-path "web_ref.org"))))
|
||||
|
||||
;; Titles Cache
|
||||
(expect (gethash (abs-path "f1.org")
|
||||
(org-roam--titles-cache)) :to-equal (list "File 1"))
|
||||
(expect (gethash (abs-path "f2.org")
|
||||
(org-roam--titles-cache)) :to-equal (list "File 2"))
|
||||
(expect (gethash (abs-path "nested/f1.org")
|
||||
(org-roam--titles-cache)) :to-equal (list "Nested File 1"))
|
||||
(expect (gethash (abs-path "nested/f2.org")
|
||||
(org-roam--titles-cache)) :to-equal (list "Nested File 2"))
|
||||
(expect (gethash (abs-path "alias.org")
|
||||
(org-roam--titles-cache)) :to-equal (list "t1" "a1" "a 2"))
|
||||
(expect (gethash (abs-path "no-title.org")
|
||||
(org-roam--titles-cache)) :to-be nil)
|
||||
|
||||
;; Refs Cache
|
||||
(expect (gethash "https://google.com/"
|
||||
(org-roam--refs-cache)) :to-equal (abs-path "web_ref.org"))
|
||||
|
||||
;; Multi
|
||||
(let ((org-roam-directory org-roam-directory2))
|
||||
(org-roam--build-cache-async)
|
||||
(sleep-for 3) ;; Because it's async
|
||||
|
||||
;; Caches should be populated
|
||||
(expect (org-roam--cache-initialized-p) :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 ((mf1 (gethash (abs-path "mf1.org")
|
||||
(org-roam--forward-links-cache)))
|
||||
(mf2 (gethash (abs-path "mf2.org")
|
||||
(org-roam--forward-links-cache)))
|
||||
(nested-mf1 (gethash (abs-path "nested/mf1.org")
|
||||
(org-roam--forward-links-cache)))
|
||||
(nested-mf2 (gethash (abs-path "nested/mf2.org")
|
||||
(org-roam--forward-links-cache)))
|
||||
(expected-mf1 (list (abs-path "nested/mf1.org")
|
||||
(abs-path "mf2.org")))
|
||||
(expected-nested-mf1 (list (abs-path "nested/mf2.org")
|
||||
(abs-path "mf1.org")))
|
||||
(expected-nested-mf2 (list (abs-path "nested/mf1.org"))))
|
||||
|
||||
(expect mf1 :to-have-same-items-as expected-mf1)
|
||||
(expect mf2 :to-be nil)
|
||||
(expect nested-mf1 :to-have-same-items-as expected-nested-mf1)
|
||||
(expect nested-mf2 :to-have-same-items-as expected-nested-mf2))
|
||||
|
||||
;; Backward cache
|
||||
(let ((mf1 (hash-table-keys
|
||||
(gethash (abs-path "mf1.org")
|
||||
(org-roam--backward-links-cache))))
|
||||
(mf2 (hash-table-keys
|
||||
(gethash (abs-path "mf2.org")
|
||||
(org-roam--backward-links-cache))))
|
||||
(nested-mf1 (hash-table-keys
|
||||
(gethash (abs-path "nested/mf1.org")
|
||||
(org-roam--backward-links-cache))))
|
||||
(nested-mf2 (hash-table-keys
|
||||
(gethash (abs-path "nested/mf2.org")
|
||||
(org-roam--backward-links-cache))))
|
||||
(expected-mf1 (list (abs-path "nested/mf1.org")))
|
||||
(expected-mf2 (list (abs-path "mf1.org")))
|
||||
(expected-nested-mf1 (list (abs-path "nested/mf2.org")
|
||||
(abs-path "mf1.org")))
|
||||
(expected-nested-mf2 (list (abs-path "nested/mf1.org"))))
|
||||
(expect mf1 :to-have-same-items-as expected-mf1)
|
||||
(expect mf2 :to-have-same-items-as expected-mf2)
|
||||
(expect nested-mf1 :to-have-same-items-as expected-nested-mf1)
|
||||
(expect nested-mf2 :to-have-same-items-as expected-nested-mf2))
|
||||
|
||||
;; Titles Cache
|
||||
(expect (gethash (abs-path "mf1.org")
|
||||
(org-roam--titles-cache))
|
||||
:to-equal (list "Multi-File 1"))
|
||||
(expect (gethash (abs-path "mf2.org")
|
||||
(org-roam--titles-cache))
|
||||
:to-equal (list "Multi-File 2"))
|
||||
(expect (gethash (abs-path "nested/mf1.org")
|
||||
(org-roam--titles-cache))
|
||||
:to-equal (list "Nested Multi-File 1"))
|
||||
(expect (gethash (abs-path "nested/mf2.org")
|
||||
(org-roam--titles-cache))
|
||||
:to-equal (list "Nested Multi-File 2"))
|
||||
(expect (gethash (abs-path "no-title.org")
|
||||
(org-roam--titles-cache))
|
||||
:to-be nil))))
|
||||
;; Expect rebuilds to be really quick (nothing changed)
|
||||
(expect (org-roam-build-cache)
|
||||
:to-equal
|
||||
(list :files 0 :links 0 :titles 0 :refs 0 :deleted 0))))
|
||||
|
||||
(describe "org-roam-insert"
|
||||
(before-each
|
||||
(org-roam--test-init)
|
||||
(org-roam--clear-cache)
|
||||
(org-roam--test-build-cache))
|
||||
(org-roam-build-cache))
|
||||
|
||||
(it "temp1 -> f1"
|
||||
(it "temp1 -> foo"
|
||||
(let ((buf (org-roam--test-find-new-file "temp1.org")))
|
||||
(with-current-buffer buf
|
||||
(with-simulated-input
|
||||
"File SPC 1 RET"
|
||||
"Foo RET"
|
||||
(org-roam-insert nil))))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:f1.org")))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:foo.org")))
|
||||
|
||||
(it "temp2 -> nested/f1"
|
||||
(it "temp2 -> nested/foo"
|
||||
(let ((buf (org-roam--test-find-new-file "temp2.org")))
|
||||
(with-current-buffer buf
|
||||
(with-simulated-input
|
||||
"Nested SPC File SPC 1 RET"
|
||||
"Nested SPC Foo RET"
|
||||
(org-roam-insert nil))))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:nested/f1.org")))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:nested/foo.org")))
|
||||
|
||||
(it "nested/temp3 -> f1"
|
||||
(it "nested/temp3 -> foo"
|
||||
(let ((buf (org-roam--test-find-new-file "nested/temp3.org")))
|
||||
(with-current-buffer buf
|
||||
(with-simulated-input
|
||||
"File SPC 1 RET"
|
||||
"Foo RET"
|
||||
(org-roam-insert nil))))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:../f1.org")))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:../foo.org")))
|
||||
|
||||
(it "a/b/temp4 -> nested/f1"
|
||||
(it "a/b/temp4 -> nested/foo"
|
||||
(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"
|
||||
"Nested SPC Foo RET"
|
||||
(org-roam-insert nil))))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:../../nested/f1.org"))))
|
||||
(expect (buffer-string) :to-match (regexp-quote "file:../../nested/foo.org"))))
|
||||
|
||||
(describe "rename file updates cache"
|
||||
(before-each
|
||||
(org-roam--test-init)
|
||||
(org-roam--clear-cache)
|
||||
(org-roam--test-build-cache))
|
||||
(org-roam-build-cache))
|
||||
|
||||
(it "f1 -> new_f1"
|
||||
(rename-file (abs-path "f1.org")
|
||||
(abs-path "new_f1.org"))
|
||||
(it "foo -> new_foo"
|
||||
(rename-file (abs-path "foo.org")
|
||||
(abs-path "new_foo.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 (caar (org-roam-sql [:select (funcall count)
|
||||
:from titles
|
||||
:where (= file $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from refs
|
||||
:where (= file $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
|
||||
(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)
|
||||
;; Cache should be updated
|
||||
(expect (org-roam-sql [:select [file-to]
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "new_foo.org"))
|
||||
:to-have-same-items-as
|
||||
(list (list (abs-path "bar.org"))))
|
||||
(expect (org-roam-sql [:select [file-from]
|
||||
:from file-links
|
||||
:where (= file-to $s1)]
|
||||
(abs-path "new_foo.org"))
|
||||
:to-have-same-items-as
|
||||
(list (list (abs-path "nested/bar.org"))))
|
||||
|
||||
;; 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]]")))
|
||||
(insert-file-contents (abs-path "nested/bar.org"))
|
||||
(buffer-string))
|
||||
:to-match
|
||||
(regexp-quote "[[file:../new_foo.org][Foo]]")))
|
||||
|
||||
(it "f1 -> f1 with spaces"
|
||||
(rename-file (abs-path "f1.org")
|
||||
(abs-path "f1 with spaces.org"))
|
||||
(it "foo -> foo with spaces"
|
||||
(rename-file (abs-path "foo.org")
|
||||
(abs-path "foo 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)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from titles
|
||||
:where (= file $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from refs
|
||||
:where (= file $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
|
||||
;; Cache should be updated
|
||||
(expect (org-roam-sql [:select [file-to]
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "foo with spaces.org"))
|
||||
:to-have-same-items-as
|
||||
(list (list (abs-path "bar.org"))))
|
||||
(expect (org-roam-sql [:select [file-from]
|
||||
:from file-links
|
||||
:where (= file-to $s1)]
|
||||
(abs-path "foo with spaces.org"))
|
||||
:to-have-same-items-as
|
||||
(list (list (abs-path "nested/bar.org"))))
|
||||
|
||||
;; 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]]")))
|
||||
(insert-file-contents (abs-path "nested/bar.org"))
|
||||
(buffer-string))
|
||||
:to-match
|
||||
(regexp-quote "[[file:../foo with spaces.org][Foo]]")))
|
||||
|
||||
(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)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "no-title.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "meaningful-title.org"))) :to-be 1)
|
||||
|
||||
;; 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]]")))
|
||||
(insert-file-contents (abs-path "meaningful-title.org"))
|
||||
(buffer-string))
|
||||
:to-match
|
||||
(regexp-quote "[[file:meaningful-title.org][meaningful-title]]")))
|
||||
|
||||
(it "web_ref -> hello"
|
||||
(expect (->> (org-roam--refs-cache)
|
||||
(gethash "https://google.com/"))
|
||||
:to-equal (abs-path "web_ref.org"))
|
||||
(expect (org-roam-sql
|
||||
[:select [file] :from refs
|
||||
:where (= ref $s1)]
|
||||
"https://google.com/")
|
||||
:to-equal
|
||||
(list (list (abs-path "web_ref.org"))))
|
||||
(rename-file (abs-path "web_ref.org")
|
||||
(abs-path "hello.org"))
|
||||
(expect (->> (org-roam--refs-cache)
|
||||
(gethash "https://google.com/"))
|
||||
:to-equal (abs-path "hello.org"))))
|
||||
(expect (org-roam-sql
|
||||
[:select [file] :from refs
|
||||
:where (= ref $s1)]
|
||||
"https://google.com/")
|
||||
:to-equal (list (list (abs-path "hello.org"))))
|
||||
(expect (caar (org-roam-sql
|
||||
[:select [ref] :from refs
|
||||
:where (= file $s1)]
|
||||
(abs-path "web_ref.org")))
|
||||
:to-equal nil)))
|
||||
|
||||
(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))
|
||||
(org-roam-build-cache)
|
||||
(sleep-for 1))
|
||||
|
||||
(it "delete foo"
|
||||
(delete-file (abs-path "foo.org"))
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from titles
|
||||
:where (= file $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from refs
|
||||
:where (= file $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0)
|
||||
(expect (caar (org-roam-sql [:select (funcall count)
|
||||
:from file-links
|
||||
:where (= file-from $s1)]
|
||||
(abs-path "foo.org"))) :to-be 0))
|
||||
|
||||
(it "delete web_ref"
|
||||
(expect (->> (org-roam--refs-cache)
|
||||
(gethash "https://google.com/"))
|
||||
:to-equal (abs-path "web_ref.org"))
|
||||
(expect (org-roam-sql [:select * :from refs])
|
||||
:to-have-same-items-as
|
||||
(list (list "https://google.com/" (abs-path "web_ref.org"))))
|
||||
(delete-file (abs-path "web_ref.org"))
|
||||
(expect (->> (org-roam--refs-cache)
|
||||
(gethash "https://google.com/"))
|
||||
:to-be nil)))
|
||||
(expect (org-roam-sql [:select * :from refs])
|
||||
:to-have-same-items-as
|
||||
(list))))
|
||||
|
Reference in New Issue
Block a user