diff --git a/org-roam.el b/org-roam.el index a10d8ca..8042b98 100644 --- a/org-roam.el +++ b/org-roam.el @@ -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