(feat): Add a tagging system (#604)

Tags are used as meta-data for files: they facilitate interactions with notes where titles are insufficient. For example, tags allow for categorization of notes: differentiating between bibliographical and structure notes during interactive commands.

Co-authored-by: Leo Vivier <leo.vivier+dev@gmail.com>
Co-authored-by: N V <44036031+progfolio@users.noreply.github.com>
This commit is contained in:
Jethro Kuan
2020-05-15 16:10:11 +08:00
committed by GitHub
parent 59c18c0e8c
commit f390593cfb
10 changed files with 398 additions and 137 deletions

View File

@ -19,10 +19,9 @@ This manual is for Org-roam version 1.1.1.
#+BEGIN_QUOTE
Copyright (C) 2020-2020 Jethro Kuan <jethrokuan95@gmail.com>
You can redistribute this document and/or modify it under the terms
of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any
later version.
You can redistribute this document and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This document is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
@ -174,7 +173,7 @@ Org-mode. However, to support additional functionality, Org-roam adds
several Org-roam-specific keywords. These functionality are not crucial
to effective use of Org-roam.
** File Titles
** Titles
To easily find a note, a title needs to be prescribed to a note. A note can have
many titles: this allows a note to be referred to by different names, which is
@ -211,6 +210,41 @@ One can freely control which extraction methods to use by customizing
information. If all methods of title extraction return no results, the file-name
is used in place of the titles for completions.
If you wish to add your own title extraction method, you may push a symbol
='foo= into =org-roam-title-sources=, and define a
=org-roam--extract-titles-foo= which accepts no arguments. See
=org-roam--extract-titles-title= for an example.
** Tags
Tags are used as meta-data for files: they facilitate interactions with notes
where titles are insufficient. For example, tags allow for categorization of
notes: differentiating between bibliographical and structure notes during interactive commands.
Org-roam calls =org-roam--extract-tags= to extract tags from files. It uses the
variable =org-roam-tag-sources=, to control how tags are extracted. The tag
extraction methods supported are:
1. ='prop=: This extracts tags from the =#+ROAM_TAGS= property. Tags are space delimited, and can be multi-word using double quotes.
2. ='all-directories=: All sub-directories relative to =org-roam-directory= are
extracted as tags. That is, if a file is located at relative path
=foo/bar/file.org=, the file will have tags =foo= and =bar=.
3. ='last-directory=: Extracts the last directory relative to
=org-roam-directory= as the tag. That is, if a file is located at relative
path =foo/bar/file.org=, the file will have tag =bar=.
By default, only the ='prop= extraction method is enabled. To enable the other
extraction methods, you may modify =org-roam-tag-sources=:
#+BEGIN_SRC emacs-lisp
(setq org-roam-tag-sources '(prop last-directory))
#+END_SRC
If you wish to add your own tag extraction method, you may push a symbol ='foo=
into =org-roam-tag-sources=, and define a =org-roam--extract-tags-foo= which
accepts the absolute file path as its argument. See
=org-roam--extract-tags-prop= for an example.
** File Refs
Refs are unique identifiers for files. Each note can only have 1 ref.
@ -932,6 +966,7 @@ file within that directory, at least once.
* _ :ignore:
# Local Variables:
# eval: (refill-mode +1)
# before-save-hook: org-make-toc
# after-save-hook: (lambda nil (progn (require 'ox-texinfo nil t) (org-texinfo-export-to-info)))
# indent-tabs-mode: nil

View File

@ -51,10 +51,9 @@ This manual is for Org-roam version 1.1.1.
@quotation
Copyright (C) 2020-2020 Jethro Kuan <jethrokuan95@@gmail.com>
You can redistribute this document and/or modify it under the terms
of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any
later version.
You can redistribute this document and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either
version 3 of the License, or (at your option) any later version.
This document is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
@ -79,19 +78,22 @@ General Public License for more details.
* Diagnosing and Repairing Files::
* Appendix::
* FAQ::
* _: _ (2).
@detailmenu
--- The Detailed Node Listing ---
Installation
* _::
* Installing from MELPA::
* Installing from the Git Repository::
* Post-Installation Tasks::
Anatomy of an Org-roam File
* File Aliases::
* Titles::
* Tags::
* File Refs::
The Templating System
@ -117,6 +119,7 @@ Graphing
Roam Protocol
* _: _ (1).
* Installation: Installation (1).
* The @samp{roam-file} protocol::
* The @samp{roam-ref} Protocol::
@ -176,14 +179,18 @@ Emacs is also a fantastic interface for editing text, and we can inherit many of
@node Installation
@chapter Installation
Org-roam can be installed using Emacs' package manager or manually from its development repository.
@menu
* _::
* Installing from MELPA::
* Installing from the Git Repository::
* Post-Installation Tasks::
@end menu
@node _
@section _ :ignore:
Org-roam can be installed using Emacs' package manager or manually from its development repository.
@node Installing from MELPA
@section Installing from MELPA
@ -310,22 +317,100 @@ several Org-roam-specific keywords. These functionality are not crucial
to effective use of Org-roam.
@menu
* File Aliases::
* Titles::
* Tags::
* File Refs::
@end menu
@node File Aliases
@section File Aliases
@node Titles
@section Titles
Suppose you want a note to be referred to by different names (e.g.
``World War 2'', ``WWII''). You may specify such aliases using the
@samp{#+ROAM_ALIAS} attribute:
To easily find a note, a title needs to be prescribed to a note. A note can have
many titles: this allows a note to be referred to by different names, which is
especially useful for topics or concepts with acronyms. For example, for a note
like ``World War 2'', it may be desirable to also refer to it using the acronym
``WWII''.
Org-roam calls @samp{org-roam--extract-titles} to extract titles. It uses the
variable @samp{org-roam-title-sources}, to control how the titles are extracted. The
title extraction methods supported are:
@enumerate
@item
@samp{'title}: This extracts the title using the file @samp{#+TITLE} property
@item
@samp{'headline}: This extracts the title from the first headline in the Org file
@item
@samp{'alias}: This extracts a list of titles using the @samp{#ROAM_ALIAS} property.
The aliases are space-delimited, and can be multi-worded using quotes
@end enumerate
Take for example the following org file:
@example
#+TITLE: World War 2
#+ROAM_ALIAS: "WWII" "World War II"
* Headline
@end example
@multitable {aaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaa}
@headitem Method
@tab Titles
@item @samp{'title}
@tab '(``World War 2'')
@item @samp{'headline}
@tab '(``Headline'')
@item @samp{'alias}
@tab '(``WWII'' ``World War II'')
@end multitable
One can freely control which extraction methods to use by customizing
@samp{org-roam-title-sources}: see the doc-string for the variable for more
information. If all methods of title extraction return no results, the file-name
is used in place of the titles for completions.
If you wish to add your own title extraction method, you may push a symbol
@samp{'foo} into @samp{org-roam-title-sources}, and define a
@samp{org-roam--extract-titles-foo} which accepts no arguments. See
@samp{org-roam--extract-titles-title} for an example.
@node Tags
@section Tags
Tags are used as meta-data for files: they facilitate interactions with notes
where titles are insufficient. For example, tags allow for categorization of
notes: differentiating between bibliographical and structure notes during interactive commands.
Org-roam calls @samp{org-roam--extract-tags} to extract tags from files. It uses the
variable @samp{org-roam-tag-sources}, to control how tags are extracted. The tag
extraction methods supported are:
@enumerate
@item
@samp{'prop}: This extracts tags from the @samp{#+ROAM_TAGS} property. Tags are space delimited, and can be multi-word using double quotes.
@item
@samp{'all-directories}: All sub-directories relative to @samp{org-roam-directory} are
extracted as tags. That is, if a file is located at relative path
@samp{foo/bar/file.org}, the file will have tags @samp{foo} and @samp{bar}.
@item
@samp{'last-directory}: Extracts the last directory relative to
@samp{org-roam-directory} as the tag. That is, if a file is located at relative
path @samp{foo/bar/file.org}, the file will have tag @samp{bar}.
@end enumerate
By default, only the @samp{'prop} extraction method is enabled. To enable the other
extraction methods, you may modify @samp{org-roam-tag-sources}:
@lisp
(setq org-roam-tag-sources '(prop last-directory))
@end lisp
If you wish to add your own tag extraction method, you may push a symbol @samp{'foo}
into @samp{org-roam-tag-sources}, and define a @samp{org-roam--extract-tags-foo} which
accepts the absolute file path as its argument. See
@samp{org-roam--extract-tags-prop} for an example.
@node File Refs
@section File Refs
@ -754,15 +839,19 @@ Other options include @samp{'ido}, and @samp{'ivy}.
@node Roam Protocol
@chapter Roam Protocol
Org-roam extending @samp{org-protocol} with 2 protocols: the @samp{roam-file}
and @samp{roam-ref} protocol.
@menu
* _: _ (1).
* Installation: Installation (1).
* The @samp{roam-file} protocol::
* The @samp{roam-ref} Protocol::
@end menu
@node _ (1)
@section _ :ignore:
Org-roam extending @samp{org-protocol} with 2 protocols: the @samp{roam-file}
and @samp{roam-ref} protocol.
@node Installation (1)
@section Installation
@ -1191,5 +1280,8 @@ All files within that directory will be treated as their own separate
set of Org-roam files. Remember to run @samp{org-roam-db-build-cache} from a
file within that directory, at least once.
@node _ (2)
@chapter _ :ignore:
Emacs 28.0.50 (Org mode 9.4)
@bye

View File

@ -331,8 +331,11 @@ This uses the templates defined at `org-roam-capture-templates'."
(when (org-roam-capture--in-process-p)
(user-error "Nested Org-roam capture processes not supported"))
(let* ((completions (org-roam--get-title-path-completions))
(title (org-roam-completion--completing-read "File: " completions))
(file-path (cdr (assoc title completions))))
(title-with-keys (org-roam-completion--completing-read "File: "
completions))
(res (gethash title-with-keys completions))
(title (plist-get res :title))
(file-path (plist-get res :file-path)))
(let ((org-roam-capture--info (list (cons 'title title)
(cons 'slug (org-roam--title-to-slug title))
(cons 'file file-path)))

View File

@ -40,11 +40,12 @@
(defvar org-roam-verbose)
(declare-function org-roam--org-roam-file-p "org-roam")
(declare-function org-roam--extract-and-format-titles "org-roam")
(declare-function org-roam--extract-titles "org-roam")
(declare-function org-roam--extract-ref "org-roam")
(declare-function org-roam--extract-tags "org-roam")
(declare-function org-roam--extract-links "org-roam")
(declare-function org-roam--list-all-files "org-roam")
(declare-function org-roam-buffer--update-maybe "org-roam-buffer")
(declare-function org-roam-buffer--update-maybe "org-roam-buffer")
;;;; Options
(defcustom org-roam-db-location nil
@ -56,7 +57,7 @@ when used with multiple Org-roam instances."
:type 'string
:group 'org-roam)
(defconst org-roam-db--version 3)
(defconst org-roam-db--version 4)
(defconst org-roam-db--sqlite-available-p
(with-demoted-errors "Org-roam initialization: %S"
(emacsql-sqlite-ensure-binary)
@ -128,6 +129,10 @@ SQL can be either the emacsql vector representation, or a string."
(type :not-null)
(properties :not-null)])
(tags
[(file :unique :primary-key)
(tags)])
(titles
[(file :not-null)
titles])
@ -215,6 +220,13 @@ This is equivalent to removing the node from the graph."
:values $v1]
(list (vector file titles))))
(defun org-roam-db--insert-tags (file tags)
"Insert TAGS for a FILE into the Org-roam cache."
(org-roam-db-query
[:insert :into tags
:values $v1]
(list (vector file tags))))
(defun org-roam-db--insert-ref (file ref)
"Insert REF for FILE into the Org-roam cache."
(let ((key (cdr ref))
@ -297,12 +309,21 @@ connections, nil is returned."
(defun org-roam-db--update-titles ()
"Update the title of the current buffer into the cache."
(let* ((file (file-truename (buffer-file-name)))
(title (org-roam--extract-and-format-titles file)))
(title (org-roam--extract-titles)))
(org-roam-db-query [:delete :from titles
:where (= file $s1)]
file)
(org-roam-db--insert-titles file title)))
(defun org-roam-db--update-tags ()
"Update the tags of the current buffer into the cache."
(let* ((file (file-truename (buffer-file-name)))
(tags (org-roam--extract-tags)))
(org-roam-db-query [:delete :from tags
:where (= file $s1)]
file)
(org-roam-db--insert-tags file tags)))
(defun org-roam-db--update-refs ()
"Update the ref of the current buffer into the cache."
(let ((file (file-truename (buffer-file-name))))
@ -329,6 +350,7 @@ connections, nil is returned."
(current-buffer))))
(with-current-buffer buf
(save-excursion
(org-roam-db--update-tags)
(org-roam-db--update-titles)
(org-roam-db--update-refs)
(org-roam-db--update-cache-links)
@ -344,7 +366,7 @@ If FORCE, force a rebuild of the cache from scratch."
(let* ((org-roam-files (org-roam--list-all-files))
(current-files (org-roam-db--get-current-files))
(time (current-time))
all-files all-links all-titles all-refs)
all-files all-links all-titles all-refs all-tags)
(dolist (file org-roam-files)
(org-roam--with-temp-buffer
(insert-file-contents file)
@ -352,12 +374,14 @@ If FORCE, force a rebuild of the cache from scratch."
(unless (string= (gethash file current-files)
contents-hash)
(org-roam-db--clear-file file)
(setq all-files
(cons (vector file contents-hash time) all-files))
(push (vector file contents-hash time)
all-files)
(when-let (links (org-roam--extract-links file))
(setq all-links (append links all-links)))
(let ((titles (org-roam--extract-and-format-titles file)))
(setq all-titles (cons (vector file titles) all-titles)))
(push links all-links))
(when-let (tags (org-roam--extract-tags file))
(push (vector file tags) all-tags))
(let ((titles (org-roam--extract-titles)))
(push (vector file titles) all-titles))
(when-let* ((ref (org-roam--extract-ref))
(type (car ref))
(key (cdr ref)))
@ -381,6 +405,11 @@ If FORCE, force a rebuild of the cache from scratch."
[:insert :into titles
:values $v1]
all-titles))
(when all-tags
(org-roam-db-query
[:insert :into tags
:values $v1]
all-tags))
(when all-refs
(org-roam-db-query
[:insert :into refs
@ -388,12 +417,14 @@ If FORCE, force a rebuild of the cache from scratch."
all-refs))
(let ((stats (list :files (length all-files)
:links (length all-links)
:tags (length all-tags)
:titles (length all-titles)
:refs (length all-refs)
:deleted (length (hash-table-keys current-files)))))
(org-roam-message "files: %s, links: %s, titles: %s, refs: %s, deleted: %s"
(org-roam-message "files: %s, links: %s, tags: %s, titles: %s, refs: %s, deleted: %s"
(plist-get stats :files)
(plist-get stats :links)
(plist-get stats :tags)
(plist-get stats :titles)
(plist-get stats :refs)
(plist-get stats :deleted))

View File

@ -113,7 +113,7 @@ Each element in the list is either:
1. a symbol -- this symbol corresponds to a title retrieval
function, which returns the list of titles for the current buffer
2. a list of symbols -- symbols in the list are treated as
with (1). The return value of this list is the first symbol in
with (1). The return value of this list is the first symbol in
the list returning a non-nil value.
The return results of the root list are concatenated.
@ -134,6 +134,31 @@ 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)))
;;;; Dynamic variables
(defvar org-roam-last-window nil
"Last window `org-roam' was called from.")
@ -149,8 +174,8 @@ space-delimited strings.
(push (cons prop val) res)))
res))
(defun org-roam--aliases-str-to-list (str)
"Function to transform string STR into list of alias titles.
(defun org-roam--str-to-list (str)
"Function to transform string STR into list of titles.
This snippet is obtained from ox-hugo:
https://github.com/kaushalmodi/ox-hugo/blob/a80b250987bc770600c424a10b3bca6ff7282e3c/ox-hugo.el#L3131"
@ -314,69 +339,6 @@ it as FILE-PATH."
names)))))))
links))
(defcustom org-roam-title-include-subdirs nil
"When non-nil, include subdirs in title completions.
The subdirs will be relative to `org-roam-directory'."
:type 'boolean
:group 'org-roam)
(defcustom org-roam-title-subdir-format 'default
"Function to use to format the titles of entries with subdirs.
Only relevant when `org-roam-title-include-subdirs' is non-nil.
The value should be a function that takes two arguments: the
title of the note, and the subdirs as a list. If set to
'default, `org-roam--format-title-with-subdirs' is used."
:type '(choice
(const :tag "Default" 'default)
(function :tag "Custom function"))
:group 'org-roam)
(defcustom org-roam-title-subdir-separator "/"
"String to use to separate subdirs.
Only relevant when `org-roam-title-include-subdirs' is non-nil."
:type 'string
:group 'org-roam)
(defun org-roam--format-title-with-subdirs (title subdirs)
"Format TITLE with SUBDIRS as '\(SUBDIRS) TITLE'."
(let* ((separator org-roam-title-subdir-separator)
(subdirs (and subdirs
(format "(%s) " (string-join subdirs separator)))))
(concat subdirs title)))
(defun org-roam--format-title (title &optional file-path)
"Format TITLE with relative subdirs from `org-roam-directory'.
When `org-roam-title-include-subdirs' is non-nil, FILE-PATH is
used to compute which subdirs should be included in the title.
If FILE-PATH is not provided, the file associated with the
current buffer is used."
(if org-roam-title-include-subdirs
(let* ((root (expand-file-name org-roam-directory))
;; If file-path is not provided, compute it
(path (or file-path
(-> (or (buffer-base-buffer)
(current-buffer))
(buffer-file-name)
(file-truename))))
(subdirs (--> path
(file-name-directory it)
(unless (equal root it)
(--> it
(file-relative-name it root)
;; Transform path-string to list of subdirs
(split-string (substring it nil -1) "/"))))))
(pcase org-roam-title-subdir-format
((pred functionp)
(funcall org-roam-title-subdir-format title subdirs))
((or 't 'default)
(org-roam--format-title-with-subdirs title subdirs))
('nil
(error "`org-roam-title-subdir-format' should not be nil"))
(wrong-type (signal 'wrong-type-argument
`((functionp symbolp)
,wrong-type)))))
title))
(defun org-roam--extract-titles-title ()
"Return title from \"#+TITLE\" of the current buffer."
(let* ((prop (org-roam--extract-global-props '("TITLE")))
@ -389,7 +351,7 @@ current buffer is used."
Reads from the \"ROAM_ALIAS\" property."
(let* ((prop (org-roam--extract-global-props '("ROAM_ALIAS")))
(aliases (cdr (assoc "ROAM_ALIAS" prop))))
(org-roam--aliases-str-to-list aliases)))
(org-roam--str-to-list aliases)))
(defun org-roam--extract-titles-headline ()
"Return the first headline of the current buffer."
@ -418,13 +380,60 @@ If NESTED, return the first successful result from SOURCES."
(cl-return))))
coll))
(defun org-roam--extract-and-format-titles (&optional file-path)
"Extract the titles from the current buffer and format them.
If FILE-PATH is not provided, the file associated with the
current buffer is used."
(mapcar (lambda (title)
(org-roam--format-title title file-path))
(org-roam--extract-titles)))
(defun org-roam--extract-tags-all-directories (file)
"Extract tags from using the directory path FILE.
All sub-directories relative to `org-roam-directory' are used as tags."
(when-let ((dir-relative (file-name-directory
(file-relative-name file org-roam-directory))))
(f-split dir-relative)))
(defun org-roam--extract-tags-last-directory (file)
"Extract tags from using the directory path FILE.
The final directory component is used as a tag."
(when-let ((dir-relative (file-name-directory
(file-relative-name file org-roam-directory))))
(last (f-split dir-relative))))
(defun org-roam--extract-tags-prop (_file)
"Extract tags from the current buffer's \"#ROAM_TAGS\" global property."
(let* ((prop (org-roam--extract-global-props '("ROAM_TAGS"))))
(org-roam--str-to-list (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.
Tags are obtained via:
1. Directory tags: Relative to `org-roam-directory': each folder
path is considered a tag.
2. The key #+ROAM_TAGS."
(let* ((file (or file (buffer-file-name (buffer-base-buffer))))
(tags (mapcan (lambda (source)
(funcall (intern (concat "org-roam--extract-tags-"
(symbol-name source)))
file))
org-roam-tag-sources)))
(pcase org-roam-tag-sort
('nil tags)
((pred booleanp) (cl-sort tags 'string-lessp :key 'downcase))
(`(,(pred symbolp) . ,_)
(apply #'cl-sort (push tags org-roam-tag-sort)))
(wrong-type (signal 'wrong-type-argument
`((booleanp (list symbolp ))
,wrong-type))))))
(defun org-roam--ref-type-p (type)
"Return t if the ref from current buffer is TYPE."
@ -522,7 +531,7 @@ Examples:
If LOWERCASE, downcase the title before insertion.
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
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)
@ -535,10 +544,12 @@ If DESCRIPTION is provided, use this as the link label. See
(if filter-fn
(funcall filter-fn it)
it)))
(title (org-roam-completion--completing-read "File: " completions
:initial-input region-text))
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input region-text))
(res (gethash title-with-tags completions))
(title (plist-get res :title))
(target-file-path (plist-get res :path))
(description (or description region-text title))
(target-file-path (cdr (assoc title completions)))
(link-description (org-roam--format-link-title (if lowercase
(downcase description)
description))))
@ -550,8 +561,8 @@ If DESCRIPTION is provided, use this as the link label. See
(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 (list (cons 'title title)
(cons 'slug (org-roam--title-to-slug title))))
(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
@ -560,19 +571,32 @@ If DESCRIPTION is provided, use this as the link label. See
(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 a list of cons pairs for titles to absolute path of Org-roam files."
(let* ((rows (org-roam-db-query [:select [file titles] :from titles]))
res)
"Return a hash table for completion.
The key is the displayed title for completion, and the value is a
plist containing the path to the file, and the original title."
(let* ((rows (org-roam-db-query [:select [titles:file titles:titles tags:tags] :from titles
:left :join tags
:on (= titles:file tags:file)]))
(ht (make-hash-table :test 'equal)))
(dolist (row rows)
(let ((file-path (car row))
(titles (cadr row)))
(if titles
(dolist (title titles)
(push (cons title file-path) res))
(push (cons (org-roam--path-to-slug file-path)
file-path) res))))
res))
(pcase-let ((`(,file-path ,titles ,tags) row))
(let ((titles (or titles (list (org-roam--path-to-slug file-path)))))
(dolist (title titles)
(let ((k (concat
(if tags
(concat "(" (s-join org-roam-tag-separator tags) ") ")
"")
title))
(v (list :path file-path :title title)))
(puthash k v ht))))))
ht))
(defun org-roam-find-file (&optional initial-prompt filter-fn)
"Find and open an Org-roam file.
@ -585,15 +609,16 @@ which takes as its argument an alist of path-completions. See
(if filter-fn
(funcall filter-fn it)
it)))
(title (org-roam-completion--completing-read "File: " completions
:initial-input initial-prompt))
(file-path (cdr (assoc title completions))))
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input initial-prompt))
(res (gethash 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 (list (cons 'title title)
(cons 'slug (org-roam--title-to-slug title))))
(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

View File

@ -0,0 +1 @@
#+TITLE: Base

View File

@ -0,0 +1 @@
#+TITLE: Deeply Nested File

View File

@ -0,0 +1,3 @@
#+TITLE: Tagless File
This file has no tags, and should not yield any tags on extracting via =#+ROAM_TAGS=.

View File

@ -0,0 +1,4 @@
#+ROAM_TAGS: "t1" "t2 with space" t3
#+TITLE: Tags
This file is used to test functionality for =(org-roam--extract-tags)=

View File

@ -141,6 +141,72 @@
:to-equal
'("Headline" "roam" "alias" "TITLE PROP"))))))
(describe "Tag extraction"
:var (org-roam-tag-sources)
(before-all
(test-org-roam--init))
(after-all
(test-org-roam--teardown))
(cl-flet
((test (fn file)
(let* ((fname (test-org-roam--abs-path file))
(buf (find-file-noselect fname)))
(with-current-buffer buf
(funcall fn fname)))))
(it "extracts from prop"
(expect (test #'org-roam--extract-tags-prop
"tags/tag.org")
:to-equal
'("t1" "t2 with space" "t3"))
(expect (test #'org-roam--extract-tags-prop
"tags/no_tag.org")
:to-equal
nil))
(it "extracts from all directories"
(expect (test #'org-roam--extract-tags-all-directories
"base.org")
:to-equal
nil)
(expect (test #'org-roam--extract-tags-all-directories
"tags/tag.org")
:to-equal
'("tags"))
(expect (test #'org-roam--extract-tags-all-directories
"nested/deeply/deeply_nested_file.org")
:to-equal
'("nested" "deeply")))
(it "extracts from last directory"
(expect (test #'org-roam--extract-tags-last-directory
"base.org")
:to-equal
nil)
(expect (test #'org-roam--extract-tags-last-directory
"tags/tag.org")
:to-equal
'("tags"))
(expect (test #'org-roam--extract-tags-last-directory
"nested/deeply/deeply_nested_file.org")
:to-equal
'("deeply")))
(describe "uses org-roam-tag-sources correctly"
(it "'(prop)"
(expect (let ((org-roam-tag-sources '(prop)))
(test #'org-roam--extract-tags
"tags/tag.org"))
:to-equal
'("t1" "t2 with space" "t3")))
(it "'(prop all-directories)"
(expect (let ((org-roam-tag-sources '(prop all-directories)))
(test #'org-roam--extract-tags
"tags/tag.org"))
:to-equal
'("t1" "t2 with space" "t3" "tags"))))))
;;; Tests
(xdescribe "org-roam-db-build-cache"
(before-each
@ -202,7 +268,7 @@
;; Expect rebuilds to be really quick (nothing changed)
(expect (org-roam-db-build-cache)
:to-equal
(list :files 0 :links 0 :titles 0 :refs 0 :deleted 0))))
(list :files 0 :links 0 :tags 0 :titles 0 :refs 0 :deleted 0))))
(xdescribe "org-roam-insert"
(before-each
@ -216,15 +282,15 @@
(with-current-buffer buf
(with-simulated-input
"Foo RET"
(org-roam-insert nil))))
(org-roam-insert))))
(expect (buffer-string) :to-match (regexp-quote "file:foo.org")))
(it "temp2 -> nested/foo"
(let ((buf (test-org-roam--find-file "temp2.org")))
(with-current-buffer buf
(with-simulated-input
"Nested SPC Foo RET"
(org-roam-insert nil))))
"(nested) SPC Nested SPC Foo RET"
(org-roam-insert))))
(expect (buffer-string) :to-match (regexp-quote "file:nested/foo.org")))
(it "nested/temp3 -> foo"
@ -232,15 +298,15 @@
(with-current-buffer buf
(with-simulated-input
"Foo RET"
(org-roam-insert nil))))
(org-roam-insert))))
(expect (buffer-string) :to-match (regexp-quote "file:../foo.org")))
(it "a/b/temp4 -> nested/foo"
(let ((buf (test-org-roam--find-file "a/b/temp4.org")))
(with-current-buffer buf
(with-simulated-input
"Nested SPC Foo RET"
(org-roam-insert nil))))
"(nested) SPC Nested SPC Foo RET"
(org-roam-insert))))
(expect (buffer-string) :to-match (regexp-quote "file:../../nested/foo.org"))))
(xdescribe "rename file updates cache"