dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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