(internal): org-roam.el formatting (#687)

Organize org-roam.el:
- Move functions so they are defined before they are referenced
- Move defcustoms to appropriate section, sort alphabetically
- Move interactive commands to appropriate section, autoload, sort.

Should not change any behavior, but make review easier.
This commit is contained in:
N V
2020-05-22 21:34:50 -04:00
committed by GitHub
parent 4d0b5734c8
commit 4f5a82e291

View File

@ -33,34 +33,38 @@
;;
;;
;;; Code:
;;;; Library Requires
;;;; Dependencies
(require 'org)
(require 'org-element)
(require 'ob-core) ;for org-babel-parse-header-arguments
(eval-when-compile (require 'subr-x))
(require 'dash)
(require 's)
(require 'f)
(require 'ob-core) ;for org-babel-parse-header-arguments
(require 'org-ref nil t) ; To detect cite: links
(require 'cl-lib)
(require 'dash)
(require 'f)
(require 's)
(require 'seq)
(eval-when-compile (require 'subr-x))
;;;; org-roam features
;;;; Features
(require 'org-roam-compat)
(require 'org-roam-macs)
(require 'org-roam-db)
;; These features should be able to be loaded order independently.
;; @TODO: implement something akin to `org-modules' that allows
;; selectively loading different sets of features.
;; ~NV [2020-05-22 Fri]
(require 'org-roam-buffer)
(require 'org-roam-capture)
(require 'org-roam-graph)
(require 'org-roam-completion)
(require 'org-roam-capture)
(require 'org-roam-dailies)
(require 'org-roam-db)
(require 'org-roam-doctor)
(require 'org-roam-graph)
;; To detect cite: links
(require 'org-ref nil t)
;;;; Declarations
(defvar org-ref-cite-types) ;; from org-ref-core.el
(declare-function org-ref-split-and-strip-string "ext:org-ref-utils" (string))
;;;; Customizable Variables
;;;; Customizable variables
(defgroup org-roam nil
"Roam Research replica in Org-mode."
:group 'org
@ -75,29 +79,15 @@
(defcustom org-roam-directory (expand-file-name "~/org-roam/")
"Default path to Org-roam files.
All Org files, at any level of nesting, are considered part of the Org-roam."
:type 'directory
:group 'org-roam)
(defcustom org-roam-link-title-format "%s"
"The formatter used when inserting Org-roam links that use their title.
Formatter may be a function that takes title as its only argument."
:type '(choice
(string :tag "String Format" "%s")
(function :tag "Custom function"))
:group 'org-roam)
(defcustom org-roam-encrypt-files nil
"Whether to encrypt new files. If true, create files with .gpg extension."
:type 'boolean
:group 'org-roam)
(defcustom org-roam-verbose t
"Echo messages that are not errors."
:type 'boolean
:group 'org-roam)
(defcustom org-roam-file-extensions '("org")
"Detected file extensions to include in the Org-roam ecosystem.
The first item in the list is used as the default file extension.
@ -107,6 +97,101 @@ ensure that."
:type '(repeat string)
:group 'org-roam)
(defcustom org-roam-include-type-in-ref-path-completions nil
"When t, include the type in ref-path completions.
Note that this only affects interactive calls.
See `org-roam--get-ref-path-completions' for details."
:type 'boolean
:group 'org-roam)
(defcustom org-roam-index-file "index.org"
"Path to the Org-roam index file.
The path can be a string or a function. If it is a string, it
should be the path (absolute or relative to `org-roam-directory')
to the index file. If it is is a function, the function should
return the path to the index file. Otherwise, the index is
assumed to be a note in `org-roam-directory' whose title is
'Index'."
:type '(choice
(string :tag "Path to index" "%s")
(function :tag "Function to generate the path"))
:group 'org-roam)
(defcustom org-roam-link-title-format "%s"
"The formatter used when inserting Org-roam links that use their title.
Formatter may be a function that takes title as its only argument."
:type '(choice
(string :tag "String Format" "%s")
(function :tag "Custom function"))
:group 'org-roam)
(defcustom org-roam-list-files-commands '(find rg)
"Commands that will be used to find Org-roam files.
It should be a list of symbols or cons cells representing any of the following
supported file search methods.
The commands will be tried in order until an executable for a command is found.
The Elisp implementation is used if no command in the list is found.
`rg'
Use ripgrep as the file search method.
Example command: rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\"
`find'
Use find as the file search method.
Example command:
find /path/to/dir -type f \( -name \"*.org\" -o -name \"*.org.gpg\" \)
By default, `executable-find' will be used to look up the path to the
executable. If a custom path is required, it can be specified together with the
method symbol as a cons cell. For example: '(find (rg . \"/path/to/rg\"))."
:type '(set (const :tag "find" find)
(const :tag "rg" rg)))
(defcustom org-roam-tag-separator ","
"String to use to separate tags when `org-roam-tag-sources' is non-nil."
:type 'string
:group 'org-roam)
(defcustom org-roam-tag-sort nil
"When non-nil, sort the tags in the completions.
When t, sort the tags alphabetically, regardless of case.
`org-roam-tag-sort' can also be a list of arguments to be applied
to `cl-sort'. For example, these are the arguments used when
`org-roam-tag-sort' is set to t:
\('string-lessp :key 'downcase)
Only relevant when `org-roam-tag-sources' is non-nil."
:type '(choice
(boolean)
(list :tag "Arguments to cl-loop"))
:group 'org-roam)
(defcustom org-roam-tag-sources '(prop)
"Sources to obtain tags from.
It should be a list of symbols representing any of the following
extraction methods:
`prop'
Extract tags from the #+ROAM_TAGS property.
Tags are space delimited.
Tags may contain spaces if they are double-quoted.
e.g. #+ROAM_TAGS: tag \"tag with spaces\"
`all-directories'
Extract sub-directories relative to `org-roam-directory'.
That is, if a file is located at relative path foo/bar/file.org,
the file will have tags \"foo\" and \"bar\".
`last-directory'
Extract the last directory relative to `org-roam-directory'.
That is, if a file is located at relative path foo/bar/file.org,
the file will have tag \"bar\"."
:type '(set (const :tag "#+ROAM_TAGS" prop)
(const :tag "sub-directories" all-directories)
(const :tag "parent directory" last-directory)))
(defcustom org-roam-title-sources '((title headline) alias)
"The list of sources from which to retrieve a note title.
Each element in the list is either:
@ -135,61 +220,21 @@ space-delimited strings.
(symbol)))
:group 'org-roam)
(defcustom org-roam-tag-sources '(prop)
"Sources to obtain tags from.
It should be a list of symbols representing any of the following
extraction methods:
`prop'
Extract tags from the #+ROAM_TAGS property.
Tags are space delimited.
Tags may contain spaces if they are double-quoted.
e.g. #+ROAM_TAGS: tag \"tag with spaces\"
`all-directories'
Extract sub-directories relative to `org-roam-directory'.
That is, if a file is located at relative path foo/bar/file.org,
the file will have tags \"foo\" and \"bar\".
`last-directory'
Extract the last directory relative to `org-roam-directory'.
That is, if a file is located at relative path foo/bar/file.org,
the file will have tag \"bar\"."
:type '(set (const :tag "#+ROAM_TAGS" prop)
(const :tag "sub-directories" all-directories)
(const :tag "parent directory" last-directory)))
(defcustom org-roam-list-files-commands '(find rg)
"Commands that will be used to find Org-roam files.
It should be a list of symbols or cons cells representing any of the following
supported file search methods.
The commands will be tried in order until an executable for a command is found.
The Elisp implementation is used if no command in the list is found.
`rg'
Use ripgrep as the file search method.
Example command: rg /path/to/dir/ --files -g \"*.org\" -g \"*.org.gpg\"
`find'
Use find as the file search method.
Example command:
find /path/to/dir -type f \( -name \"*.org\" -o -name \"*.org.gpg\" \)
By default, `executable-find' will be used to look up the path to the
executable. If a custom path is required, it can be specified together with the
method symbol as a cons cell. For example: '(find (rg . \"/path/to/rg\"))."
:type '(set (const :tag "find" find)
(const :tag "rg" rg)))
(defcustom org-roam-verbose t
"Echo messages that are not errors."
:type 'boolean
:group 'org-roam)
;;;; Dynamic variables
(defvar org-roam-last-window nil
"Last window `org-roam' was called from.")
;;; Utilities
;;;; General Utilities
(defvar org-roam--org-link-file-bracket-re
"\\[\\[file:\\(\\(?:[^][\\]\\|\\\\\\(?:\\\\\\\\\\)*[][]\\|\\\\+[^][]\\)+\\)]\\(?:\\[\\(\\(?:.\\|
\\)+?\\)]\\)?]"
"Matches a 'file:' link in double brackets.")
;;;; Utilities
(defun org-roam--plist-to-alist (plist)
"Return an alist of the property-value pairs in PLIST."
(let (res)
@ -248,7 +293,7 @@ Like `file-name-extension', but does not strip version number."
"Return t if FILE is part of Org-roam system, nil otherwise.
If FILE is not specified, use the current buffer's file-path."
(if-let ((path (or file
(buffer-file-name))))
(buffer-file-name))))
(save-match-data
(and
(org-roam--org-file-p path)
@ -302,8 +347,8 @@ Use external shell commands if defined in `org-roam-list-files-commands'."
exe (symbol-name cmd)))
(wrong-type
(signal 'wrong-type-argument
`((consp symbolp)
,wrong-type))))
`((consp symbolp)
,wrong-type))))
(when path (cl-return)))
(if path
(let ((fn (intern (concat "org-roam--list-files-" exe))))
@ -330,11 +375,6 @@ The search terminates when the first property is encountered."
(push (cons prop p) res)))
res))
(defvar org-roam--org-link-file-bracket-re
"\\[\\[file:\\(\\(?:[^][\\]\\|\\\\\\(?:\\\\\\\\\\)*[][]\\|\\\\+[^][]\\)+\\)]\\(?:\\[\\(\\(?:.\\|
\\)+?\\)]\\)?]"
"Matches a 'file:' link in double brackets.")
(defun org-roam--expand-links (content path)
"Crawl CONTENT for relative links and expand them.
PATH should be the root from which to compute the relativity."
@ -475,19 +515,6 @@ The final directory component is used as a tag."
(let* ((prop (org-roam--extract-global-props '("ROAM_TAGS"))))
(org-roam--parse-tags (cdr (assoc "ROAM_TAGS" prop)))))
(defcustom org-roam-tag-sort nil
"When non-nil, sort the tags in the completions.
When t, sort the tags alphabetically, regardless of case.
`org-roam-tag-sort' can also be a list of arguments to be applied
to `cl-sort'. For example, these are the arguments used when
`org-roam-tag-sort' is set to t:
\('string-lessp :key 'downcase)
Only relevant when `org-roam-tag-sources' is non-nil."
:type '(choice
(boolean)
(list :tag "Arguments to cl-loop"))
:group 'org-roam)
(defun org-roam--extract-tags (&optional file)
"Extract tags from the current buffer.
If file-path FILE, use it to determine the directory tags.
@ -511,10 +538,28 @@ Tags are obtained via:
`((booleanp (list symbolp ))
,wrong-type))))))
(defun org-roam--ref-type-p (type)
"Return t if the ref from current buffer is TYPE."
(let ((current (car (org-roam--extract-ref))))
(eq current type)))
(defun org-roam--cite-prefix (ref)
"Return the citation prefix of REF, or nil otherwise.
The prefixes are defined in `org-ref-cite-types`.
Examples:
(org-roam--cite-prefix \"cite:foo\") -> \"cite:\"
(org-roam--cite-prefix \"https://google.com\") -> nil"
(when (require 'org-ref nil t)
(seq-find
(lambda (prefix) (s-prefix? prefix ref))
(-map (lambda (type) (concat type ":"))
org-ref-cite-types))))
(defun org-roam--ref-type (ref)
"Determine the type of the REF from the prefix."
(let* ((cite-prefix (org-roam--cite-prefix ref))
(is-website (seq-some
(lambda (prefix) (s-prefix? prefix ref))
'("http" "https")))
(type (cond (cite-prefix "cite")
(is-website "website")
(t "roam"))))
type))
(defun org-roam--extract-ref ()
"Extract the ref from current buffer and return the type and the key of the ref."
@ -530,28 +575,10 @@ Tags are obtained via:
(t ref))))
(cons type key)))))
(defun org-roam--ref-type (ref)
"Determine the type of the REF from the prefix."
(let* ((cite-prefix (org-roam--cite-prefix ref))
(is-website (seq-some
(lambda (prefix) (s-prefix? prefix ref))
'("http" "https")))
(type (cond (cite-prefix "cite")
(is-website "website")
(t "roam"))))
type))
(defun org-roam--cite-prefix (ref)
"Return the citation prefix of REF, or nil otherwise.
The prefixes are defined in `org-ref-cite-types`.
Examples:
(org-roam--cite-prefix \"cite:foo\") -> \"cite:\"
(org-roam--cite-prefix \"https://google.com\") -> nil"
(when (require 'org-ref nil t)
(seq-find
(lambda (prefix) (s-prefix? prefix ref))
(-map (lambda (type) (concat type ":"))
org-ref-cite-types))))
(defun org-roam--ref-type-p (type)
"Return t if the ref from current buffer is TYPE."
(let ((current (car (org-roam--extract-ref))))
(eq current type)))
;;;; Title/Path/Slug conversion
(defun org-roam--path-to-slug (path)
@ -581,7 +608,6 @@ Examples:
(slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs)))
(downcase slug))))
;;; Interactive Commands
(defun org-roam--format-link-title (title)
"Return the link title, given the file TITLE."
(if (functionp org-roam-link-title-format)
@ -602,61 +628,6 @@ Examples:
target))
description)))
(defun org-roam-insert (&optional lowercase completions filter-fn description)
"Find an Org-roam file, and insert a relative org link to it at point.
If LOWERCASE, downcase the title before insertion.
COMPLETIONS is a list of completions to be used instead of
`org-roam--get-title-path-completions`.
FILTER-FN is the name of a function to apply on the candidates
which takes as its argument an alist of path-completions.
If DESCRIPTION is provided, use this as the link label. See
`org-roam--get-title-path-completions' for details."
(interactive "P")
(let* ((region (and (region-active-p)
;; following may lose active region, so save it
(cons (region-beginning) (region-end))))
(region-text (when region
(buffer-substring-no-properties
(car region) (cdr region))))
(completions (--> (or completions
(org-roam--get-title-path-completions))
(if filter-fn
(funcall filter-fn it)
it)))
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input region-text))
(res (cdr (assoc title-with-tags completions)))
(title (or (plist-get res :title)
title-with-tags))
(target-file-path (plist-get res :path))
(description (or description region-text title))
(link-description (org-roam--format-link-title (if lowercase
(downcase description)
description))))
(if (and target-file-path
(file-exists-p target-file-path))
(progn
(when region ;; Remove previously selected text.
(delete-region (car region) (cdr region)))
(insert (org-roam--format-link target-file-path link-description)))
(when (org-roam-capture--in-process-p)
(user-error "Nested Org-roam capture processes not supported"))
(let ((org-roam-capture--info `((title . ,title-with-tags)
(slug . ,(org-roam--title-to-slug title-with-tags))))
(org-roam-capture--context 'title))
(add-hook 'org-capture-after-finalize-hook #'org-roam-capture--insert-link-h)
(setq org-roam-capture-additional-template-props (list :region region
:link-description link-description
:capture-fn 'org-roam-insert))
(org-roam--with-template-error 'org-roam-capture-templates
(org-roam-capture--capture))))))
(defcustom org-roam-tag-separator ","
"String to use to separate tags.
Only relevant when `org-roam-tag-sources' is non-nil."
:type 'string
:group 'org-roam)
(defun org-roam--get-title-path-completions ()
"Return an alist for completion.
The car is the displayed title for completion, and the cdr is the
@ -682,53 +653,6 @@ to the file."
(v (list :path file-path :title title)))
(push (cons k v) completions))))))))
(defun org-roam-find-file (&optional initial-prompt completions filter-fn)
"Find and open an Org-roam file.
INITIAL-PROMPT is the initial title prompt.
COMPLETIONS is a list of completions to be used instead of
`org-roam--get-title-path-completions`.
FILTER-FN is the name of a function to apply on the candidates
which takes as its argument an alist of path-completions. See
`org-roam--get-title-path-completions' for details."
(interactive)
(let* ((completions (--> (or completions
(org-roam--get-title-path-completions))
(if filter-fn
(funcall filter-fn it)
it)))
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input initial-prompt))
(res (cdr (assoc title-with-tags completions)))
(file-path (plist-get res :path)))
(if file-path
(find-file file-path)
(if (org-roam-capture--in-process-p)
(user-error "Org-roam capture in process")
(let ((org-roam-capture--info `((title . ,title-with-tags)
(slug . ,(org-roam--title-to-slug title-with-tags))))
(org-roam-capture--context 'title))
(add-hook 'org-capture-after-finalize-hook #'org-roam-capture--find-file-h)
(org-roam--with-template-error 'org-roam-capture-templates
(org-roam-capture--capture)))))))
(defun org-roam-find-directory ()
"Find and open `org-roam-directory'."
(interactive)
(find-file org-roam-directory))
(defcustom org-roam-index-file "index.org"
"Path to the Org-roam index file.
The path can be a string or a function. If it is a string, it
should be the path (absolute or relative to `org-roam-directory')
to the index file. If it is is a function, the function should
return the path to the index file. Otherwise, the index is
assumed to be a note in `org-roam-directory' whose title is
'Index'."
:type '(choice
(string :tag "Path to index" "%s")
(function :tag "Function to generate the path"))
:group 'org-roam)
(defun org-roam--get-index-path ()
"Return the path to the index in `org-roam-directory'.
The path to the index can be defined in `org-roam-index-file'.
@ -746,28 +670,7 @@ whose title is 'Index'."
(concat (file-truename org-roam-directory) path)
index)))
(defun org-roam-jump-to-index ()
"Find the index file in `org-roam-directory'.
The path to the index can be defined in `org-roam-index-file'.
Otherwise, the function will look in your `org-roam-directory'
for a note whose title is 'Index'. If it does not exist, the
command will offer you to create one."
(interactive)
(let ((index (org-roam--get-index-path)))
(if (and index
(file-exists-p index))
(find-file index)
(when (y-or-n-p "Index file does not exist. Would you like to create it? ")
(org-roam-find-file "Index")))))
;;;; org-roam-find-ref
(defcustom org-roam-include-type-in-ref-path-completions nil
"When t, include the type in ref-path completions.
Note that this only affects interactive calls.
See `org-roam--get-ref-path-completions' for details."
:type 'boolean
:group 'org-roam)
(defun org-roam--get-ref-path-completions (&optional interactive filter)
"Return a alist of refs to absolute path of Org-roam files.
When `org-roam-include-type-in-ref-path-completions' and
@ -815,26 +718,6 @@ Return nil if the file does not exist."
(file (plist-get (cdr (assoc ref completions)) :path)))
(find-file file)))
(defun org-roam-find-ref (arg &optional filter)
"Find and open an Org-roam file from a ref.
ARG is used to forward interactive calls to
`org-roam--get-ref-path-completions'
FILTER can either be a string or a function:
- If it is a string, it should be the type of refs to include as
candidates (e.g. \"cite\" ,\"website\" ,etc.)
- If it is a function, it should be the name of a function that
takes three arguments: the type, the ref, and the file of the
current candidate. It should return t if that candidate is to be
included as a candidate."
(interactive "p")
(let* ((completions (org-roam--get-ref-path-completions arg filter))
(ref (org-roam-completion--completing-read "Ref: "
completions
:require-match t))
(file (-> (cdr (assoc ref completions))
(plist-get :path))))
(find-file file)))
(defun org-roam--get-roam-buffers ()
"Return a list of buffers that are Org-roam files."
(--filter (and (with-current-buffer it (derived-mode-p 'org-mode))
@ -842,27 +725,11 @@ included as a candidate."
(org-roam--org-roam-file-p (buffer-file-name it)))
(buffer-list)))
(defun org-roam-switch-to-buffer ()
"Switch to an existing Org-roam buffer."
(interactive)
(let* ((roam-buffers (org-roam--get-roam-buffers))
(names-and-buffers (mapcar (lambda (buffer)
(cons (or (org-roam--get-title-or-slug
(buffer-file-name buffer))
(buffer-name buffer))
buffer))
roam-buffers)))
(unless roam-buffers
(user-error "No roam buffers"))
(when-let ((name (org-roam-completion--completing-read "Buffer: " names-and-buffers
:require-match t)))
(switch-to-buffer (cdr (assoc name names-and-buffers))))))
(defun org-roam--file-path-from-id (id)
"The file path for an Org-roam file, with identifier ID."
(file-truename
(let* ((ext (or (car org-roam-file-extensions)
"org"))
"org"))
(file (concat id "." ext)))
(expand-file-name
(if org-roam-encrypt-files
@ -991,51 +858,7 @@ for Org-ref cite links."
:link (format "file:%s" (abbreviate-file-name buffer-file-name))
:description title))))
;;;###autoload
(defalias 'org-roam 'org-roam-buffer-toggle-display)
;;; The global minor org-roam-mode
;;;###autoload
(define-minor-mode org-roam-mode
"Minor mode for Org-roam.
This mode sets up several hooks, to ensure that the cache is updated on file
changes, renames and deletes. It is also in charge of graceful termination of
the database connection.
When called interactively, toggle `org-roam-mode'. with prefix
ARG, enable `org-roam-mode' 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."
:lighter " Org-roam"
:keymap (let ((map (make-sparse-keymap))) map)
:group 'org-roam
:require 'org-roam
:global t
(cond
(org-roam-mode
(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)
(org-roam-db-build-cache))
(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
(org-link-set-parameters "file" :face 'org-link)
(remove-hook 'post-command-hook #'org-roam-buffer--update-maybe t)
(remove-hook 'after-save-hook #'org-roam-db--update-file t))))))
(defun org-roam--find-file-hook-function ()
"Called by `find-file-hook' when mode symbol `org-roam-mode' is on."
(when (org-roam--org-roam-file-p)
@ -1061,27 +884,27 @@ update with NEW-DESC."
(find-file-noselect file))
(save-excursion
(let ((link-markers (org-element-map (org-element-parse-buffer) 'link
(lambda (l)
(let ((type (org-element-property :type l))
(path (org-element-property :path l)))
(when (and (equal "file" type)
(string-equal (file-truename path)
old-path))
(set-marker (make-marker) (org-element-property :begin l))))))))
(dolist (m link-markers)
(goto-char m)
(save-match-data
(unless (org-in-regexp org-link-bracket-re 1)
(user-error "No link at point"))
(let* ((label (if (match-end 2)
(match-string-no-properties 2)
(org-link-unescape (match-string-no-properties 1))))
(new-label (if (string-equal label old-desc)
new-desc
label)))
(replace-match (org-link-make-string
(concat "file:" (file-relative-name new-path (file-name-directory (buffer-file-name))))
new-label)))))))
(lambda (l)
(let ((type (org-element-property :type l))
(path (org-element-property :path l)))
(when (and (equal "file" type)
(string-equal (file-truename path)
old-path))
(set-marker (make-marker) (org-element-property :begin l))))))))
(dolist (m link-markers)
(goto-char m)
(save-match-data
(unless (org-in-regexp org-link-bracket-re 1)
(user-error "No link at point"))
(let* ((label (if (match-end 2)
(match-string-no-properties 2)
(org-link-unescape (match-string-no-properties 1))))
(new-label (if (string-equal label old-desc)
new-desc
label)))
(replace-match (org-link-make-string
(concat "file:" (file-relative-name new-path (file-name-directory (buffer-file-name))))
new-label)))))))
(save-buffer)))
(defun org-roam--fix-relative-links (old-path)
@ -1153,23 +976,49 @@ replaced links are made relative to the current buffer."
(save-buffer)))
(org-roam-db--update-file new-path)))))
;;;; Diagnostics
;;;###autoload
(defun org-roam-version (&optional message)
"Return `org-roam' version.
Interactively, or when MESSAGE is non-nil, show in the echo area."
(interactive)
(let* ((version
(with-temp-buffer
(insert-file-contents-literally (locate-library "org-roam.el"))
(goto-char (point-min))
(save-match-data
(if (re-search-forward "\\(?:;; Version: \\([^z-a]*?$\\)\\)" nil nil)
(substring-no-properties (match-string 1))
"N/A")))))
(if (or message (called-interactively-p 'interactive))
(message "%s" version)
version)))
(define-minor-mode org-roam-mode
"Minor mode for Org-roam.
This mode sets up several hooks, to ensure that the cache is updated on file
changes, renames and deletes. It is also in charge of graceful termination of
the database connection.
When called interactively, toggle `org-roam-mode'. with prefix
ARG, enable `org-roam-mode' 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."
:lighter " Org-roam"
:keymap (let ((map (make-sparse-keymap))) map)
:group 'org-roam
:require 'org-roam
:global t
(cond
(org-roam-mode
(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)
(org-roam-db-build-cache))
(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
(org-link-set-parameters "file" :face 'org-link)
(remove-hook 'post-command-hook #'org-roam-buffer--update-maybe t)
(remove-hook 'after-save-hook #'org-roam-db--update-file t))))))
;;; Interactive Commands
;;;###autoload
(defalias 'org-roam 'org-roam-buffer-toggle-display)
;;;###autoload
(defun org-roam-diagnostics ()
@ -1187,5 +1036,160 @@ Interactively, or when MESSAGE is non-nil, show in the echo area."
(insert (format "- Org: %s\n" (org-version nil 'full)))
(insert (format "- Org-roam: %s" (org-roam-version)))))
;;;###autoload
(defun org-roam-find-file (&optional initial-prompt completions filter-fn)
"Find and open an Org-roam file.
INITIAL-PROMPT is the initial title prompt.
COMPLETIONS is a list of completions to be used instead of
`org-roam--get-title-path-completions`.
FILTER-FN is the name of a function to apply on the candidates
which takes as its argument an alist of path-completions. See
`org-roam--get-title-path-completions' for details."
(interactive)
(let* ((completions (--> (or completions
(org-roam--get-title-path-completions))
(if filter-fn
(funcall filter-fn it)
it)))
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input initial-prompt))
(res (cdr (assoc title-with-tags completions)))
(file-path (plist-get res :path)))
(if file-path
(find-file file-path)
(if (org-roam-capture--in-process-p)
(user-error "Org-roam capture in process")
(let ((org-roam-capture--info `((title . ,title-with-tags)
(slug . ,(org-roam--title-to-slug title-with-tags))))
(org-roam-capture--context 'title))
(add-hook 'org-capture-after-finalize-hook #'org-roam-capture--find-file-h)
(org-roam--with-template-error 'org-roam-capture-templates
(org-roam-capture--capture)))))))
;;;###autoload
(defun org-roam-find-directory ()
"Find and open `org-roam-directory'."
(interactive)
(find-file org-roam-directory))
;;;###autoload
(defun org-roam-find-ref (arg &optional filter)
"Find and open an Org-roam file from a ref.
ARG is used to forward interactive calls to
`org-roam--get-ref-path-completions'
FILTER can either be a string or a function:
- If it is a string, it should be the type of refs to include as
candidates (e.g. \"cite\" ,\"website\" ,etc.)
- If it is a function, it should be the name of a function that
takes three arguments: the type, the ref, and the file of the
current candidate. It should return t if that candidate is to be
included as a candidate."
(interactive "p")
(let* ((completions (org-roam--get-ref-path-completions arg filter))
(ref (org-roam-completion--completing-read "Ref: "
completions
:require-match t))
(file (-> (cdr (assoc ref completions))
(plist-get :path))))
(find-file file)))
;;;###autoload
(defun org-roam-insert (&optional lowercase completions filter-fn description)
"Find an Org-roam file, and insert a relative org link to it at point.
If LOWERCASE, downcase the title before insertion.
COMPLETIONS is a list of completions to be used instead of
`org-roam--get-title-path-completions`.
FILTER-FN is the name of a function to apply on the candidates
which takes as its argument an alist of path-completions.
If DESCRIPTION is provided, use this as the link label. See
`org-roam--get-title-path-completions' for details."
(interactive "P")
(let* ((region (and (region-active-p)
;; following may lose active region, so save it
(cons (region-beginning) (region-end))))
(region-text (when region
(buffer-substring-no-properties (car region) (cdr region))))
(completions (--> (or completions
(org-roam--get-title-path-completions))
(if filter-fn
(funcall filter-fn it)
it)))
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input region-text))
(res (cdr (assoc title-with-tags completions)))
(title (or (plist-get res :title)
title-with-tags))
(target-file-path (plist-get res :path))
(description (or description region-text title))
(link-description (org-roam--format-link-title (if lowercase
(downcase description)
description))))
(if (and target-file-path
(file-exists-p target-file-path))
(progn
(when region ;; Remove previously selected text.
(delete-region (car region) (cdr region)))
(insert (org-roam--format-link target-file-path link-description)))
(when (org-roam-capture--in-process-p)
(user-error "Nested Org-roam capture processes not supported"))
(let ((org-roam-capture--info `((title . ,title-with-tags)
(slug . ,(org-roam--title-to-slug title-with-tags))))
(org-roam-capture--context 'title))
(add-hook 'org-capture-after-finalize-hook #'org-roam-capture--insert-link-h)
(setq org-roam-capture-additional-template-props (list :region region
:link-description link-description
:capture-fn 'org-roam-insert))
(org-roam--with-template-error 'org-roam-capture-templates
(org-roam-capture--capture))))))
;;;###autoload
(defun org-roam-jump-to-index ()
"Find the index file in `org-roam-directory'.
The path to the index can be defined in `org-roam-index-file'.
Otherwise, the function will look in your `org-roam-directory'
for a note whose title is 'Index'. If it does not exist, the
command will offer you to create one."
(interactive)
(let ((index (org-roam--get-index-path)))
(if (and index
(file-exists-p index))
(find-file index)
(when (y-or-n-p "Index file does not exist. Would you like to create it? ")
(org-roam-find-file "Index")))))
;;;###autoload
(defun org-roam-switch-to-buffer ()
"Switch to an existing Org-roam buffer."
(interactive)
(let* ((roam-buffers (org-roam--get-roam-buffers))
(names-and-buffers (mapcar (lambda (buffer)
(cons (or (org-roam--get-title-or-slug
(buffer-file-name buffer))
(buffer-name buffer))
buffer))
roam-buffers)))
(unless roam-buffers
(user-error "No roam buffers"))
(when-let ((name (org-roam-completion--completing-read "Buffer: " names-and-buffers
:require-match t)))
(switch-to-buffer (cdr (assoc name names-and-buffers))))))
;;;###autoload
(defun org-roam-version (&optional message)
"Return `org-roam' version.
Interactively, or when MESSAGE is non-nil, show in the echo area."
(interactive)
(let* ((version
(with-temp-buffer
(insert-file-contents-literally (locate-library "org-roam.el"))
(goto-char (point-min))
(save-match-data
(if (re-search-forward "\\(?:;; Version: \\([^z-a]*?$\\)\\)" nil nil)
(substring-no-properties (match-string 1))
"N/A")))))
(if (or message (called-interactively-p 'interactive))
(message "%s" version)
version)))
(provide 'org-roam)
;;; org-roam.el ends here