(refactor): org-roam-graph (#490)

- Consolidate graph build/display commands into org-roam-graph command
  See #450
- Require org-roam-db
  Rather than declaring its functions.
- Move obsolete variable org-roam-graph-node-shape to org-roam-compat
- org-roam-graph--build-connected-component accepts a file argument
  Allows building a graph without having the target file as the current
  buffer
- Eliminate repeating code
- Fix checkdoc warnings
This commit is contained in:
N V
2020-04-19 03:04:24 -04:00
committed by GitHub
parent de4f5477d8
commit dc65e58405
3 changed files with 105 additions and 107 deletions

View File

@ -14,7 +14,7 @@ The recommended method of configuration is to use [use-package][use-package].
```emacs-lisp ```emacs-lisp
(use-package org-roam (use-package org-roam
:hook :hook
(after-init . org-roam-mode) (after-init . org-roam-mode)
:custom :custom
(org-roam-directory "/path/to/org-files/") (org-roam-directory "/path/to/org-files/")
@ -22,7 +22,7 @@ The recommended method of configuration is to use [use-package][use-package].
(("C-c n l" . org-roam) (("C-c n l" . org-roam)
("C-c n f" . org-roam-find-file) ("C-c n f" . org-roam-find-file)
("C-c n b" . org-roam-switch-to-buffer) ("C-c n b" . org-roam-switch-to-buffer)
("C-c n g" . org-roam-graph-show)) ("C-c n g" . org-roam-graph))
:map org-mode-map :map org-mode-map
(("C-c n i" . org-roam-insert)))) (("C-c n i" . org-roam-insert))))
``` ```
@ -34,7 +34,7 @@ Or without `use-package`:
(define-key org-roam-mode-map (kbd "C-c n l") #'org-roam) (define-key org-roam-mode-map (kbd "C-c n l") #'org-roam)
(define-key org-roam-mode-map (kbd "C-c n f") #'org-roam-find-file) (define-key org-roam-mode-map (kbd "C-c n f") #'org-roam-find-file)
(define-key org-roam-mode-map (kbd "C-c n b") #'org-roam-switch-to-buffer) (define-key org-roam-mode-map (kbd "C-c n b") #'org-roam-switch-to-buffer)
(define-key org-roam-mode-map (kbd "C-c n g") #'org-roam-graph-show) (define-key org-roam-mode-map (kbd "C-c n g") #'org-roam-graph)
(define-key org-mode-map (kbd "C-c n i") #'org-roam-insert) (define-key org-mode-map (kbd "C-c n i") #'org-roam-insert)
(org-roam-mode +1) (org-roam-mode +1)
``` ```
@ -71,7 +71,7 @@ wraps Org-roam. Paste the following into a new file
"arl" 'org-roam "arl" 'org-roam
"art" 'org-roam-dailies-today "art" 'org-roam-dailies-today
"arf" 'org-roam-find-file "arf" 'org-roam-find-file
"arg" 'org-roam-graph-show) "arg" 'org-roam-graph)
(spacemacs/declare-prefix-for-mode 'org-mode "mr" "org-roam") (spacemacs/declare-prefix-for-mode 'org-mode "mr" "org-roam")
(spacemacs/set-leader-keys-for-major-mode 'org-mode (spacemacs/set-leader-keys-for-major-mode 'org-mode
@ -80,7 +80,7 @@ wraps Org-roam. Paste the following into a new file
"rb" 'org-roam-switch-to-buffer "rb" 'org-roam-switch-to-buffer
"rf" 'org-roam-find-file "rf" 'org-roam-find-file
"ri" 'org-roam-insert "ri" 'org-roam-insert
"rg" 'org-roam-graph-show)))) "rg" 'org-roam-graph))))
``` ```
Next, append `org-roam` to the `dotspacemacs-configuration-layers` Next, append `org-roam` to the `dotspacemacs-configuration-layers`
@ -94,7 +94,7 @@ with that of the ranger layer. You might want to change it to 'aor'
## Doom Emacs ## Doom Emacs
[Doom Emacs][doom] has a `+roam` flag on its `org` module for easy [Doom Emacs][doom] has a `+roam` flag on its `org` module for easy
installation and configuration. Simply add the flag to the `org` section installation and configuration. Simply add the flag to the `org` section
of your `~/.doom.d/init.el` and run `~/.emacs.d/bin/doom sync`. of your `~/.doom.d/init.el` and run `~/.emacs.d/bin/doom sync`.
@ -108,12 +108,12 @@ of your `~/.doom.d/init.el` and run `~/.emacs.d/bin/doom sync`.
On Windows, if you follow the installation instructions above, you will likely get the error message: **"No EmacSQL SQLite binary available, aborting"**, and `org-roam` won't start properly. On Windows, if you follow the installation instructions above, you will likely get the error message: **"No EmacSQL SQLite binary available, aborting"**, and `org-roam` won't start properly.
You need to do some additional steps to get `org-roam` to work. You need to do some additional steps to get `org-roam` to work.
Essentially, you will need to have a binary file for `emacsql-sqlite` so that your Emacs can work with `sqlite` database -- `org-roam` uses it to track backlinks. The following options have been reported to work by Windows users in the community. Essentially, you will need to have a binary file for `emacsql-sqlite` so that your Emacs can work with `sqlite` database -- `org-roam` uses it to track backlinks. The following options have been reported to work by Windows users in the community.
Option 1. **Windows Subsystem for Linux (WSL)** Option 1. **Windows Subsystem for Linux (WSL)**
: This option lets you use Linux on your Windows machine. It's Linux, so you don't need to do anything specific for Windows. : This option lets you use Linux on your Windows machine. It's Linux, so you don't need to do anything specific for Windows.
Option 2. **mingw-x64** Option 2. **mingw-x64**
: Use mingw-x64. You would spend a bit of time to download it, and get familiar with how it works. You should be able to use Linux tools within your Windows [more contribution welcome]. : Use mingw-x64. You would spend a bit of time to download it, and get familiar with how it works. You should be able to use Linux tools within your Windows [more contribution welcome].
@ -175,7 +175,7 @@ You will see the process triggered with lots of text automatically scrolling dow
Once compilation is done, check that `emacsql-sqlite.exe` has been added to the directory. Once compilation is done, check that `emacsql-sqlite.exe` has been added to the directory.
**Step 6.** Relaunch Emacs, use `org-roam` **Step 6.** Relaunch Emacs, use `org-roam`
When you start `org-roam` (e.g. via `org-roam-mode`), now you should no longer see the "No EmacSQL SQLite binary available, aborting" error. You are good to go. When you start `org-roam` (e.g. via `org-roam-mode`), now you should no longer see the "No EmacSQL SQLite binary available, aborting" error. You are good to go.

View File

@ -67,17 +67,25 @@
"org-roam 1.0.0") "org-roam 1.0.0")
(define-obsolete-function-alias 'org-roam-date 'org-roam-dailies-date (define-obsolete-function-alias 'org-roam-date 'org-roam-dailies-date
"org-roam 1.0.0") "org-roam 1.0.0")
(define-obsolete-function-alias 'org-roam-graph-show 'org-roam-graph
"org-roam 1.0.0")
(define-obsolete-function-alias 'org-roam-graph-build 'org-roam-graph
"org-roam 1.0.0")
;;;; Variables ;;;; Variables
(define-obsolete-variable-alias 'org-roam-graphviz-extra-options (define-obsolete-variable-alias 'org-roam-graphviz-extra-options
'org-roam-graph-extra-config "org-roam 1.0.0") 'org-roam-graph-extra-config "org-roam 1.0.0")
(define-obsolete-variable-alias 'org-roam-grapher-extra-options (define-obsolete-variable-alias 'org-roam-grapher-extra-options
'org-roam-graph-extra-config "org-roam 1.0.0") 'org-roam-graph-extra-config "org-roam 1.0.0")
(make-obsolete-variable 'org-roam-graph-node-shape 'org-roam-graph-node-extra-config "org-roam 1.0.0")
(defcustom org-roam-graph-node-shape "ellipse"
"Shape of graph nodes."
:type 'string
:group 'org-roam)
(define-obsolete-variable-alias 'org-roam--db-connection (define-obsolete-variable-alias 'org-roam--db-connection
'org-roam-db--connection "org-roam 1.0.0") 'org-roam-db--connection "org-roam 1.0.0")
(define-obsolete-variable-alias 'org-roam--current-buffer (define-obsolete-variable-alias 'org-roam--current-buffer
'org-roam-buffer--current "org-roam 1.0.0") 'org-roam-buffer--current "org-roam 1.0.0")
(define-obsolete-variable-alias 'org-roam-date-title-format (define-obsolete-variable-alias 'org-roam-date-title-format
'org-roam-dailies-capture-templates "org-roam 1.0.0") 'org-roam-dailies-capture-templates "org-roam 1.0.0")
(define-obsolete-variable-alias 'org-roam-date-filename-format (define-obsolete-variable-alias 'org-roam-date-filename-format

View File

@ -33,12 +33,10 @@
(require 'xml) ;xml-escape-string (require 'xml) ;xml-escape-string
(require 's) ;s-truncate, s-replace (require 's) ;s-truncate, s-replace
(require 'org-roam-macs) (require 'org-roam-macs)
(require 'org-roam-db)
;;;; Declarations ;;;; Declarations
(defvar org-roam-directory) (defvar org-roam-directory)
(declare-function org-roam-db--ensure-built "org-roam-db")
(declare-function org-roam-db-query "org-roam-db")
(declare-function org-roam-db--connected-component "org-roam-db")
(declare-function org-roam--org-roam-file-p "org-roam") (declare-function org-roam--org-roam-file-p "org-roam")
(declare-function org-roam--path-to-slug "org-roam") (declare-function org-roam--path-to-slug "org-roam")
@ -100,27 +98,18 @@ are excluded."
(list :tag "Matchers")) (list :tag "Matchers"))
:group 'org-roam) :group 'org-roam)
(make-obsolete-variable 'org-roam-graph-node-shape 'org-roam-graph-node-extra-config "2020/04/01")
(defcustom org-roam-graph-node-shape "ellipse"
"Shape of graph nodes."
:type 'string
:group 'org-roam)
;;;; Functions ;;;; Functions
(defun org-roam-graph--expand-matcher (col &optional negate where) (defun org-roam-graph--expand-matcher (col &optional negate where)
"Return the exclusion regexp from `org-roam-graph-exclude-matcher'. "Return the exclusion regexp from `org-roam-graph-exclude-matcher'.
COL is the symbol to be matched against. if NEGATE, add :not to sql query. COL is the symbol to be matched against. if NEGATE, add :not to sql query.
set WHERE to true if WHERE query already exists." set WHERE to true if WHERE query already exists."
(let ((matchers (cond ((null org-roam-graph-exclude-matcher) (let ((matchers (pcase org-roam-graph-exclude-matcher
nil) ('nil nil)
((stringp org-roam-graph-exclude-matcher) ((pred stringp) `(,(concat "%" org-roam-graph-exclude-matcher "%")))
(cons (concat "%" org-roam-graph-exclude-matcher "%") nil)) ((pred listp) (mapcar (lambda (m)
((listp org-roam-graph-exclude-matcher) (concat "%" m "%"))
(mapcar (lambda (m) org-roam-graph-exclude-matcher))
(concat "%" m "%")) (_ (error "Invalid org-roam-graph-exclude-matcher"))))
org-roam-graph-exclude-matcher))
(t
(error "Invalid org-roam-graph-exclude-matcher"))))
res) res)
(dolist (match matchers) (dolist (match matchers)
(if where (if where
@ -134,8 +123,16 @@ set WHERE to true if WHERE query already exists."
(push match res)) (push match res))
(nreverse res))) (nreverse res)))
(defun org-roam-graph--build (node-query) (defun org-roam-graph--dot-option (option &optional wrap-key wrap-val)
"Build the graphviz string for NODE-QUERY. "Return dot string of form KEY=VAL for OPTION cons.
If WRAP-KEY is non-nil it wraps the KEY.
If WRAP-VAL is non-nil it wraps the VAL."
(concat wrap-key (car option) wrap-key
"="
wrap-val (cdr option) wrap-val))
(defun org-roam-graph--dot (node-query)
"Build the graphviz dot string for NODE-QUERY.
The Org-roam database titles table is read, to obtain the list of titles. The Org-roam database titles table is read, to obtain the list of titles.
The links table is then read to obtain all directed links, and formatted The links table is then read to obtain all directed links, and formatted
into a digraph." into a digraph."
@ -149,115 +146,108 @@ into a digraph."
(edges-cites-query (edges-cites-query
`[:with selected :as [:select [file] :from ,node-query] `[:with selected :as [:select [file] :from ,node-query]
:select :distinct [file from] :select :distinct [file from]
:from links :inner :join refs :on (and (= links:to refs:ref) (= links:type "cite")) :from links :inner :join refs :on (and (= links:to refs:ref)
(= links:type "cite"))
:where (and (in file selected) (in from selected))]) :where (and (in file selected) (in from selected))])
(edges (org-roam-db-query edges-query)) (edges (org-roam-db-query edges-query))
(edges-cites (org-roam-db-query edges-cites-query))) (edges-cites (org-roam-db-query edges-cites-query)))
(insert "digraph \"org-roam\" {\n") (insert "digraph \"org-roam\" {\n")
(dolist (option org-roam-graph-extra-config) (dolist (option org-roam-graph-extra-config)
(insert (concat " " (insert (org-roam-graph--dot-option option) ";\n"))
(car option) (dolist (attribute '("node" "edge"))
"=" (insert (format " %s [%s];\n" attribute
(cdr option) (mapconcat #'org-roam-graph--dot-option
";\n"))) (symbol-value
(insert (format " node [%s];\n" (intern (concat "org-roam-graph-" attribute "-extra-config")))
(->> org-roam-graph-node-extra-config ","))))
(mapcar (lambda (n)
(concat (car n) "=" (cdr n))))
(s-join ","))))
(insert (format " edge [%s];\n"
(->> org-roam-graph-edge-extra-config
(mapcar (lambda (n)
(concat (car n) "=" (cdr n))))
(s-join ","))))
(dolist (node nodes) (dolist (node nodes)
(let* ((file (xml-escape-string (car node))) (let* ((file (xml-escape-string (car node)))
(title (or (caadr node) (title (or (caadr node)
(org-roam--path-to-slug file))) (org-roam--path-to-slug file)))
(shortened-title (s-truncate org-roam-graph-max-title-length title)) (shortened-title (s-truncate org-roam-graph-max-title-length title))
(node-properties (list (cons "label" (s-replace "\"" "\\\"" shortened-title)) (node-properties
(cons "URL" (concat "org-protocol://roam-file?file=" `(("label" . ,(s-replace "\"" "\\\"" shortened-title))
(url-hexify-string file))) ("URL" . ,(concat "org-protocol://roam-file?file=" (url-hexify-string file)))
(cons "tooltip" (xml-escape-string title))))) ("tooltip" . ,(xml-escape-string title)))))
(insert (insert
(format " \"%s\" [%s];\n" (format " \"%s\" [%s];\n" file
file (mapconcat (lambda (n)
(->> node-properties (org-roam-graph--dot-option n nil "\""))
(mapcar (lambda (n) node-properties ",")))))
(concat (car n) "=" "\"" (cdr n) "\"")))
(s-join ","))))))
(dolist (edge edges) (dolist (edge edges)
(insert (format " \"%s\" -> \"%s\";\n" (insert (apply #'format `(" \"%s\" -> \"%s\";\n"
(xml-escape-string (car edge)) ,@(mapcar #'xml-escape-string edge)))))
(xml-escape-string (cadr edge)))))
(insert (format " edge [%s];\n" (insert (format " edge [%s];\n"
(->> org-roam-graph-edge-cites-extra-config (mapconcat #'org-roam-graph--dot-option
(mapcar (lambda (n) org-roam-graph-edge-cites-extra-config ",")))
(concat (car n) "=" (cdr n))))
(s-join ","))))
(dolist (edge edges-cites) (dolist (edge edges-cites)
(insert (format " \"%s\" -> \"%s\";\n" (insert (apply #'format `(" \"%s\" -> \"%s\";\n"
(xml-escape-string (car edge)) ,@(mapcar #'xml-escape-string edge)))))
(xml-escape-string (cadr edge)))))
(insert "}") (insert "}")
(buffer-string)))) (buffer-string))))
(defun org-roam-graph-build (&optional node-query) (defun org-roam-graph--build (&optional node-query)
"Generate a graph showing the relations between nodes in NODE-QUERY. "Generate a graph showing the relations between nodes in NODE-QUERY."
For building and showing the graph in a single step see `org-roam-graph-show'."
(interactive)
(unless org-roam-graph-executable (unless org-roam-graph-executable
(user-error "Can't find %s executable. Please check if it is in your path" (user-error "Can't find %s executable. Please check if it is in your path"
org-roam-graph-executable)) org-roam-graph-executable))
(let* ((node-query (or node-query (let* ((node-query (or node-query
`[:select [file titles] `[:select [file titles]
:from titles :from titles
,@(org-roam-graph--expand-matcher 'file t)])) ,@(org-roam-graph--expand-matcher 'file t)]))
(graph (org-roam-graph--build node-query)) (graph (org-roam-graph--dot node-query))
(temp-dot (make-temp-file "graph." nil ".dot" graph)) (temp-dot (make-temp-file "graph." nil ".dot" graph))
(temp-graph (make-temp-file "graph." nil ".svg"))) (temp-graph (make-temp-file "graph." nil ".svg")))
(call-process org-roam-graph-executable nil 0 nil (call-process org-roam-graph-executable nil 0 nil
temp-dot "-Tsvg" "-o" temp-graph) temp-dot "-Tsvg" "-o" temp-graph)
temp-graph)) temp-graph))
(defun org-roam-graph--build-connected-component (file &optional max-distance)
"Build a graph of nodes connected to FILE.
If MAX-DISTANCE is non-nil, limit nodes to MAX-DISTANCE steps."
(let* ((file (file-truename file))
(files (or (if (and max-distance (>= max-distance 0))
(org-roam-db--links-with-max-distance file max-distance)
(org-roam-db--connected-component file))
(list file)))
(query `[:select [file titles]
:from titles
:where (in file [,@files])]))
(org-roam-graph--build query)))
(defun org-roam-graph--open (file) (defun org-roam-graph--open (file)
"Open FILE using `org-roam-graph-viewer', with `view-file' as a fallback." "Open FILE using `org-roam-graph-viewer', with `view-file' as a fallback."
(if (and org-roam-graph-viewer (executable-find org-roam-graph-viewer)) (if (and org-roam-graph-viewer (executable-find org-roam-graph-viewer))
(call-process org-roam-graph-viewer nil 0 nil file) (call-process org-roam-graph-viewer nil 0 nil file)
(view-file file))) (view-file file)))
(defun org-roam-graph-show (&optional node-query) ;;;; Commands
"Generate and display a graph showing the relations between nodes in NODE-QUERY. ;;;###autoload
The graph is generated using `org-roam-graph-build' and subsequently displayed (defun org-roam-graph (&optional arg file node-query)
using `org-roam-graph-viewer', if it refers to a valid executable, or using "Build and possibly display a graph for FILE from NODE-QUERY.
`view-file' otherwise." If FILE is nil, default to current buffer's file name.
(interactive) ARG may be any of the following values:
(org-roam-graph--open (org-roam-graph-build node-query))) - nil show the graph.
- `\\[universal-argument]' show the graph for FILE.
(defun org-roam-graph-build-connected-component (&optional max-distance) - `\\[universal-argument]' N show the graph for FILE limiting nodes to N steps.
"Like `org-roam-graph-build', but only include nodes connected to the current entry. - `\\[universal-argument] \\[universal-argument]' build the graph.
If MAX-DISTANCE is non-nil, only nodes within the given number of steps are shown." - `\\[universal-argument]' - build the graph for FILE.
- `\\[universal-argument]' -N build the graph for FILE limiting nodes to N steps."
(interactive "P") (interactive "P")
(unless (org-roam--org-roam-file-p) (let ((file (or file (buffer-file-name))))
(user-error "Not in an Org-roam file")) (unless file
(let* ((file (file-truename (buffer-file-name))) (user-error "Cannot build graph for nil file. Is current buffer visiting a file?"))
(files (or (if (and max-distance (>= (prefix-numeric-value max-distance) 0)) (unless (org-roam--org-roam-file-p file)
(org-roam-db--links-with-max-distance file max-distance) (user-error "\"%s\" is not an org-roam file" file))
(org-roam-db--connected-component file)) (pcase arg
(list file))) ('nil (org-roam-graph--open (org-roam-graph--build node-query)))
(query `[:select [file titles] ('(4) (org-roam-graph--open (org-roam-graph--build-connected-component file)))
:from titles ((pred integerp) (let ((graph (org-roam-graph--build-connected-component (buffer-file-name) (abs arg))))
:where (in file [,@files])])) (when (>= arg 0)
(org-roam-graph-build query))) (org-roam-graph--open graph))))
('(16) (org-roam-graph--build node-query))
(defun org-roam-graph-show-connected-component (&optional max-distance) ('- (org-roam-graph--build-connected-component file))
"Like `org-roam-graph-show', but only include nodes connected to the current entry. (_ (user-error "Unrecognized ARG: %s" arg)))))
If MAX-DISTANCE is non-nil, only nodes within the given number of steps are shown."
(interactive "P")
(org-roam-graph--open (org-roam-graph-build-connected-component max-distance)))
(provide 'org-roam-graph) (provide 'org-roam-graph)