org-roam.el (13930B)
1 ;;; org-roam.el --- A database abstraction layer for Org-mode -*- coding: utf-8; lexical-binding: t; -*- 2 3 ;; Copyright © 2020-2022 Jethro Kuan <jethrokuan95@gmail.com> 4 5 ;; Author: Jethro Kuan <jethrokuan95@gmail.com> 6 ;; URL: https://github.com/org-roam/org-roam 7 ;; Keywords: org-mode, roam, convenience 8 ;; Version: 2.2.2 9 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org "9.4") (emacsql "3.0.0") (emacsql-sqlite "1.0.0") (magit-section "3.0.0")) 10 11 ;; This file is NOT part of GNU Emacs. 12 13 ;; This program is free software; you can redistribute it and/or modify 14 ;; it under the terms of the GNU General Public License as published by 15 ;; the Free Software Foundation; either version 3, or (at your option) 16 ;; any later version. 17 ;; 18 ;; This program is distributed in the hope that it will be useful, 19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 ;; GNU General Public License for more details. 22 ;; 23 ;; You should have received a copy of the GNU General Public License 24 ;; along with GNU Emacs; see the file COPYING. If not, write to the 25 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 26 ;; Boston, MA 02110-1301, USA. 27 28 ;;; Commentary: 29 ;; 30 ;; Org-roam is a Roam Research inspired Emacs package and is an addition to 31 ;; Org-mode to have a way to quickly process complex SQL-like queries over a 32 ;; large set of plain text Org-mode files. To achieve this Org-roam provides a 33 ;; database abstraction layer, the capabilities of which include, but are not 34 ;; limited to: 35 ;; 36 ;; - Link graph traversal and visualization. 37 ;; - Instantaneous SQL-like queries on headlines 38 ;; - What are my TODOs, scheduled for X, or due by Y? 39 ;; - Accessing the properties of a node, such as its tags, refs, TODO state or 40 ;; priority. 41 ;; 42 ;; All of these functionality is powered by this layer. Hence, at its core 43 ;; Org-roam's primary goal is to provide a resilient dual representation of 44 ;; what's already available in plain text, while cached in a binary database, 45 ;; that is cheap to maintain, easy to understand, and is as up-to-date as it 46 ;; possibly can. For users who would like to perform arbitrary programmatic 47 ;; queries on their Org files Org-roam also exposes an API to this database 48 ;; abstraction layer. 49 ;; 50 ;; ----------------------------------------------------------------------------- 51 ;; 52 ;; In order for the package to correctly work through your interactive session 53 ;; it's mandatory to add somewhere to your configuration the next form: 54 ;; 55 ;; (org-roam-db-autosync-mode) 56 ;; 57 ;; The form can be called both, before or after loading the package, which is up 58 ;; to your preferences. If you call this before the package is loaded, then it 59 ;; will automatically load the package. 60 ;; 61 ;; ----------------------------------------------------------------------------- 62 ;; 63 ;; This package also comes with a set of officially supported extensions that 64 ;; provide extra features. You can find them in the "extensions/" subdirectory. 65 ;; These extensions are not automatically loaded with `org-roam`, but they still 66 ;; will be lazy-loaded through their own `autoload's. 67 ;; 68 ;; Org-roam also has other extensions that don't come together with this package. 69 ;; Such extensions are distributed as their own packages, while also 70 ;; authored and maintained by different people on distinct repositories. The 71 ;; majority of them can be found at https://github.com/org-roam and MELPA. 72 ;; 73 ;;; Code: 74 (require 'dash) 75 76 (require 'rx) 77 (require 'seq) 78 (require 'cl-lib) 79 80 (require 'magit-section) 81 82 (require 'emacsql) 83 (require 'emacsql-sqlite) 84 85 (require 'org) 86 (require 'org-attach) ; To set `org-attach-id-dir' 87 (require 'org-id) 88 (require 'ol) 89 (require 'org-element) 90 (require 'org-capture) 91 92 (require 'ansi-color) ; to strip ANSI color codes in `org-roam--list-files' 93 94 (eval-when-compile 95 (require 'subr-x)) 96 97 ;;; Options 98 (defgroup org-roam nil 99 "A database abstraction layer for Org-mode." 100 :group 'org 101 :prefix "org-roam-" 102 :link '(url-link :tag "Github" "https://github.com/org-roam/org-roam") 103 :link '(url-link :tag "Online Manual" "https://www.orgroam.com/manual.html")) 104 105 (defgroup org-roam-faces nil 106 "Faces used by Org-roam." 107 :group 'org-roam 108 :group 'faces) 109 110 (defcustom org-roam-verbose t 111 "Echo messages that are not errors." 112 :type 'boolean 113 :group 'org-roam) 114 115 (defcustom org-roam-directory (expand-file-name "~/org-roam/") 116 "Default path to Org-roam files. 117 All Org files, at any level of nesting, are considered part of the Org-roam." 118 :type 'directory 119 :group 'org-roam) 120 121 (defcustom org-roam-find-file-hook nil 122 "Hook run when an Org-roam file is visited." 123 :group 'org-roam 124 :type 'hook) 125 126 (defcustom org-roam-post-node-insert-hook nil 127 "Hook run when an Org-roam node is inserted as an Org link. 128 Each function takes two arguments: the id of the node, and the link description." 129 :group 'org-roam 130 :type 'hook) 131 132 (defcustom org-roam-file-extensions '("org") 133 "List of file extensions to be included by Org-Roam. 134 While a file extension different from \".org\" may be used, the 135 file still needs to be an `org-mode' file, and it is the user's 136 responsibility to ensure that." 137 :type '(repeat string) 138 :group 'org-roam) 139 140 (defcustom org-roam-file-exclude-regexp (list org-attach-id-dir) 141 "Files matching this regular expression are excluded from the Org-roam." 142 :type '(choice 143 (repeat 144 (string :tag "Regular expression matching files to ignore")) 145 (string :tag "Regular expression matching files to ignore") 146 (const :tag "Include everything" nil)) 147 :group 'org-roam) 148 149 (defcustom org-roam-list-files-commands 150 (if (member system-type '(windows-nt ms-dos cygwin)) 151 nil 152 '(find fd fdfind rg)) 153 "Commands that will be used to find Org-roam files. 154 155 It should be a list of symbols or cons cells representing any of the following 156 supported file search methods. 157 158 The commands will be tried in order until an executable for a command is found. 159 The Elisp implementation is used if no command in the list is found. 160 161 `find' 162 Use find as the file search method. 163 Example command: 164 find /path/to/dir -type f \( -name \"*.org\" -o -name \"*.org.gpg\" -name \"*.org.age\" \) 165 166 `fd' 167 Use fd as the file search method. 168 Example command: fd /path/to/dir/ --type file -e \".org\" -e \".org.gpg\" -e \".org.age\" 169 170 `fdfind' 171 Same as `fd'. It's an alias that used in some OSes (e.g. Debian, Ubuntu) 172 173 `rg' 174 Use ripgrep as the file search method. 175 Example command: rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\" -g \"*.org.age\" 176 177 By default, `executable-find' will be used to look up the path to the 178 executable. If a custom path is required, it can be specified together with the 179 method symbol as a cons cell. For example: '(find (rg . \"/path/to/rg\"))." 180 :type '(set (const :tag "find" find) 181 (const :tag "fd" fd) 182 (const :tag "fdfind" fdfind) 183 (const :tag "rg" rg) 184 (const :tag "elisp" nil))) 185 186 ;;; Library 187 (defun org-roam-file-p (&optional file) 188 "Return t if FILE is an Org-roam file, nil otherwise. 189 If FILE is not specified, use the current buffer's file-path. 190 191 FILE is an Org-roam file if: 192 - It's located somewhere under `org-roam-directory' 193 - It has a matching file extension (`org-roam-file-extensions') 194 - It doesn't match excluded regexp (`org-roam-file-exclude-regexp')" 195 (when (or file (buffer-file-name (buffer-base-buffer))) 196 (let* ((path (or file (buffer-file-name (buffer-base-buffer)))) 197 (relative-path (file-relative-name path org-roam-directory)) 198 (ext (org-roam--file-name-extension path)) 199 (ext (if (or (string= ext "gpg") 200 (string= ext "age")) 201 (org-roam--file-name-extension (file-name-sans-extension path)) 202 ext)) 203 (org-roam-dir-p (org-roam-descendant-of-p path org-roam-directory)) 204 (valid-file-ext-p (member ext org-roam-file-extensions)) 205 (match-exclude-regexp-p 206 (cond 207 ((not org-roam-file-exclude-regexp) nil) 208 ((stringp org-roam-file-exclude-regexp) 209 (string-match-p org-roam-file-exclude-regexp relative-path)) 210 ((listp org-roam-file-exclude-regexp) 211 (let (is-match) 212 (dolist (exclude-re org-roam-file-exclude-regexp) 213 (setq is-match (or is-match (string-match-p exclude-re relative-path)))) 214 is-match))))) 215 (save-match-data 216 (and 217 path 218 org-roam-dir-p 219 valid-file-ext-p 220 (not match-exclude-regexp-p)))))) 221 222 ;;;###autoload 223 (defun org-roam-list-files () 224 "Return a list of all Org-roam files under `org-roam-directory'. 225 See `org-roam-file-p' for how each file is determined to be as 226 part of Org-Roam." 227 (org-roam--list-files (expand-file-name org-roam-directory))) 228 229 (defun org-roam-buffer-p (&optional buffer) 230 "Return t if BUFFER is for an Org-roam file. 231 If BUFFER is not specified, use the current buffer." 232 (let ((buffer (or buffer (current-buffer))) 233 path) 234 (with-current-buffer buffer 235 (and (derived-mode-p 'org-mode) 236 (setq path (buffer-file-name (buffer-base-buffer))) 237 (org-roam-file-p path))))) 238 239 (defun org-roam-buffer-list () 240 "Return a list of buffers that are Org-roam files." 241 (--filter (org-roam-buffer-p it) 242 (buffer-list))) 243 244 (defun org-roam--file-name-extension (filename) 245 "Return file name extension for FILENAME. 246 Like `file-name-extension', but does not strip version number." 247 (save-match-data 248 (let ((file (file-name-nondirectory filename))) 249 (if (and (string-match "\\.[^.]*\\'" file) 250 (not (eq 0 (match-beginning 0)))) 251 (substring file (+ (match-beginning 0) 1)))))) 252 253 (defun org-roam--list-files (dir) 254 "Return all Org-roam files located recursively within DIR. 255 Use external shell commands if defined in `org-roam-list-files-commands'." 256 (let (path exe) 257 (cl-dolist (cmd org-roam-list-files-commands) 258 (pcase cmd 259 (`(,e . ,path) 260 (setq path (executable-find path) 261 exe (symbol-name e))) 262 ((pred symbolp) 263 (setq path (executable-find (symbol-name cmd)) 264 exe (symbol-name cmd))) 265 (wrong-type 266 (signal 'wrong-type-argument 267 `((consp symbolp) 268 ,wrong-type)))) 269 (when path (cl-return))) 270 (if-let* ((files (when path 271 (let ((fn (intern (concat "org-roam--list-files-" exe)))) 272 (unless (fboundp fn) (user-error "%s is not an implemented search method" fn)) 273 (funcall fn path (format "\"%s\"" dir))))) 274 (files (seq-filter #'org-roam-file-p files)) 275 (files (mapcar #'expand-file-name files))) ; canonicalize names 276 files 277 (org-roam--list-files-elisp dir)))) 278 279 (defun org-roam--shell-command-files (cmd) 280 "Run CMD in the shell and return a list of files. 281 If no files are found, an empty list is returned." 282 (--> cmd 283 (shell-command-to-string it) 284 (ansi-color-filter-apply it) 285 (split-string it "\n") 286 (seq-filter (lambda (s) 287 (not (or (null s) (string= "" s)))) it))) 288 289 (defun org-roam--list-files-search-globs (exts) 290 "Given EXTS, return a list of search globs. 291 E.g. (\".org\") => (\"*.org\" \"*.org.gpg\")" 292 (cl-loop for e in exts 293 append (list (format "\"*.%s\"" e) 294 (format "\"*.%s.gpg\"" e) 295 (format "\"*.%s.age\"" e)))) 296 297 (defun org-roam--list-files-find (executable dir) 298 "Return all Org-roam files under DIR, using \"find\", provided as EXECUTABLE." 299 (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) 300 (names (string-join (mapcar (lambda (glob) (concat "-name " glob)) globs) " -o ")) 301 (command (string-join `(,executable "-L" ,dir "-type f \\(" ,names "\\)") " "))) 302 (org-roam--shell-command-files command))) 303 304 (defun org-roam--list-files-fd (executable dir) 305 "Return all Org-roam files under DIR, using \"fd\", provided as EXECUTABLE." 306 (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) 307 (extensions (string-join (mapcar (lambda (glob) (concat "-e " (substring glob 2 -1))) globs) " ")) 308 (command (string-join `(,executable "-L" "--type file" ,extensions "." ,dir) " "))) 309 (org-roam--shell-command-files command))) 310 311 (defalias 'org-roam--list-files-fdfind #'org-roam--list-files-fd) 312 313 (defun org-roam--list-files-rg (executable dir) 314 "Return all Org-roam files under DIR, using \"rg\", provided as EXECUTABLE." 315 (let* ((globs (org-roam--list-files-search-globs org-roam-file-extensions)) 316 (command (string-join `(,executable "-L" ,dir "--files" 317 ,@(mapcar (lambda (glob) (concat "-g " glob)) globs)) " "))) 318 (org-roam--shell-command-files command))) 319 320 (declare-function org-roam--directory-files-recursively "org-roam-compat") 321 322 (defun org-roam--list-files-elisp (dir) 323 "Return all Org-roam files under DIR, using Elisp based implementation." 324 (let ((regex (concat "\\.\\(?:"(mapconcat 325 #'regexp-quote org-roam-file-extensions 326 "\\|" )"\\)\\(?:\\.gpg\\|\\.age\\)?\\'")) 327 result) 328 (dolist (file (org-roam--directory-files-recursively dir regex nil nil t) result) 329 (when (and (file-readable-p file) 330 (org-roam-file-p file)) 331 (push file result))))) 332 333 ;;; Package bootstrap 334 (provide 'org-roam) 335 336 (cl-eval-when (load eval) 337 (require 'org-roam-compat) 338 (require 'org-roam-utils) 339 (require 'org-roam-db) 340 (require 'org-roam-node) 341 (require 'org-roam-id) 342 (require 'org-roam-capture) 343 (require 'org-roam-mode) 344 (require 'org-roam-log) 345 (require 'org-roam-migrate)) 346 347 ;;; org-roam.el ends here