(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,8 +40,9 @@
(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")
@ -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

@ -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."
@ -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
(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
(pcase-let ((`(,file-path ,titles ,tags) row))
(let ((titles (or titles (list (org-roam--path-to-slug file-path)))))
(dolist (title titles)
(push (cons title file-path) res))
(push (cons (org-roam--path-to-slug file-path)
file-path) res))))
res))
(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
(title-with-tags (org-roam-completion--completing-read "File: " completions
:initial-input initial-prompt))
(file-path (cdr (assoc title completions))))
(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"