1582 lines
79 KiB
EmacsLisp
1582 lines
79 KiB
EmacsLisp
;;; org-ql.el --- Org Query Language, search command, and agenda-like view -*- lexical-binding: t; -*-
|
||
|
||
;; Author: Adam Porter <adam@alphapapa.net>
|
||
;; Url: https://github.com/alphapapa/org-ql
|
||
;; Version: 0.5-pre
|
||
;; Package-Requires: ((emacs "26.1") (dash "2.13") (dash-functional "1.2.0") (f "0.17.2") (org "9.0") (org-super-agenda "1.2-pre") (ov "1.0.6") (peg "0.6") (s "1.12.0") (ts "0.2-pre"))
|
||
;; Keywords: hypermedia, outlines, Org, agenda
|
||
|
||
;;; Commentary:
|
||
|
||
;; `org-ql' is a lispy query language for Org files. It allows you to
|
||
;; find Org entries matching certain criteria and return a list of
|
||
;; them or perform actions on them. Commands are also provided which
|
||
;; display a buffer with matching results, similar to an Org Agenda
|
||
;; buffer.
|
||
|
||
;;; License:
|
||
|
||
;; This program is free software; you can redistribute it 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 program is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
;;; Code:
|
||
|
||
;;;; Requirements
|
||
|
||
(require 'cl-lib)
|
||
(require 'org)
|
||
(require 'org-element)
|
||
(require 'org-habit)
|
||
(require 'seq)
|
||
(require 'subr-x)
|
||
|
||
(require 'dash)
|
||
(require 'dash-functional)
|
||
(require 'map)
|
||
(require 'ts)
|
||
|
||
;;;; Constants
|
||
|
||
;; Note the use of the `rx' `blank' keyword, which matches "horizontal" whitespace.
|
||
|
||
(defconst org-ql-tsr-regexp-inactive
|
||
(concat org-ts-regexp-inactive "\\(--?-?"
|
||
org-ts-regexp-inactive "\\)?")
|
||
;; MAYBE: Propose this for org.el.
|
||
"Regular expression matching an inactive timestamp or timestamp range.")
|
||
|
||
(defconst org-ql-clock-regexp
|
||
(rx bol (0+ blank) "CLOCK:" (group (1+ not-newline)))
|
||
"Regular expression matching Org \"CLOCK:\" lines.
|
||
Like `org-clock-line-re', but matches the timestamp range in a
|
||
match group.")
|
||
|
||
(defconst org-ql-planning-regexp
|
||
(rx bol (0+ blank) (or "CLOSED" "DEADLINE" "SCHEDULED") ":" (1+ blank) (group (1+ not-newline)))
|
||
"Regular expression matching Org \"planning\" lines.
|
||
That is, \"CLOSED:\", \"DEADLINE:\", or \"SCHEDULED:\".")
|
||
|
||
(defconst org-ql-tag-line-re
|
||
"^\\*+ \\(?:.*[ \t]\\)?\\(:\\([[:alnum:]_@#%:]+\\):\\)[ \t]*$"
|
||
;; Copied from `org-tag-line-re' from org.el.
|
||
"Regexp matching tags in a headline.
|
||
Tags are stored in match group 1. Match group 2 stores the tags
|
||
without the enclosing colons.")
|
||
|
||
;;;; Variables
|
||
|
||
(defvar org-ql--today nil)
|
||
|
||
(defvar org-ql-use-preamble t
|
||
;; MAYBE: Naming things is hard. There must be a better term than "preamble."
|
||
"Use query preambles to speed up searches.
|
||
May be disabled for debugging, benchmarks, etc.")
|
||
|
||
(defvar org-ql-cache (make-hash-table :weakness 'key)
|
||
;; IIUC, setting weakness to `key' means that, when a buffer is closed,
|
||
;; its entries will be removed from this table at the next GC.
|
||
"Query cache, keyed by buffer.
|
||
Each value is a list of the buffer's modified tick and another
|
||
hash table, keyed by arguments passed to
|
||
`org-ql--select-cached'.")
|
||
|
||
(defvar org-ql-tags-cache (make-hash-table :weakness 'key)
|
||
"Per-buffer tags cache.
|
||
Keyed by buffer. Each value is a cons of the buffer's modified
|
||
tick, and another hash table keyed on buffer position, whose
|
||
values are a list of two lists, inherited tags and local tags, as
|
||
strings.")
|
||
|
||
(defvar org-ql-node-value-cache (make-hash-table :weakness 'key)
|
||
"Per-buffer node cache.
|
||
Keyed by buffer. Each value is a cons of the buffer's modified
|
||
tick, and another hash table keyed on buffer position, whose
|
||
values are alists in which the key is a function and the value is
|
||
the value returned by it at that node.")
|
||
|
||
(defvar org-ql-predicates
|
||
(list (list :name 'org-back-to-heading :fn (symbol-function 'org-back-to-heading)))
|
||
"Plist of predicates, their corresponding functions, and their docstrings.
|
||
This list should not contain any duplicates.")
|
||
|
||
;;;; Customization
|
||
|
||
(defgroup org-ql nil
|
||
"Customization for `org-ql'."
|
||
:group 'org
|
||
:link '(url-link "https://github.com/alphapapa/org-ql"))
|
||
|
||
;;;; Macros
|
||
|
||
(cl-defmacro org-ql--defpred (name args docstring &rest body)
|
||
"Define an `org-ql' selector predicate named `org-ql--predicate-NAME'.
|
||
NAME may be a symbol or a list of symbols: if a list, the first
|
||
is used as the name and the rest are aliases. ARGS is a
|
||
`cl-defun'-style argument list. DOCSTRING is the function's
|
||
docstring. BODY is the body of the predicate.
|
||
|
||
Predicates will be called with point on the beginning of an Org
|
||
heading and should return non-nil if the heading's entry is a
|
||
match."
|
||
(declare (debug ([&or symbolp listp] listp stringp def-body))
|
||
(indent defun))
|
||
(let* ((aliases (when (listp name)
|
||
(cdr name)))
|
||
(name (cl-etypecase name
|
||
(list (car name))
|
||
(atom name)))
|
||
(fn-name (intern (concat "org-ql--predicate-" (symbol-name name))))
|
||
(pred-name (intern (symbol-name name))))
|
||
`(progn
|
||
(push (list :name ',pred-name :aliases ',aliases :fn ',fn-name :docstring ,docstring :args ',args) org-ql-predicates)
|
||
(cl-defun ,fn-name ,args ,docstring ,@body))))
|
||
|
||
;;;###autoload
|
||
(cl-defmacro org-ql (buffers-or-files query &key sort narrow action)
|
||
"Expands into a call to `org-ql-select' with the same arguments.
|
||
For convenience, arguments should be unquoted."
|
||
(declare (indent defun))
|
||
`(org-ql-select ,buffers-or-files
|
||
',query
|
||
:action ',action
|
||
:narrow ,narrow
|
||
:sort ',sort))
|
||
|
||
;;;; Functions
|
||
|
||
;;;;; Query execution
|
||
|
||
(define-hash-table-test 'org-ql-hash-test #'equal (lambda (args)
|
||
(sxhash-equal (prin1-to-string args))))
|
||
|
||
;;;###autoload
|
||
(cl-defun org-ql-select (buffers-or-files query &key action narrow sort)
|
||
"Return items matching QUERY in BUFFERS-OR-FILES.
|
||
|
||
BUFFERS-OR-FILES is a file or buffer, a list of files and/or
|
||
buffers, or a function which returns such a list.
|
||
|
||
QUERY is an `org-ql' query sexp (quoted, since this is a
|
||
function).
|
||
|
||
ACTION is a function which is called on each matching entry with
|
||
point at the beginning of its heading. It may be:
|
||
|
||
- `element' or nil: Equivalent to `org-element-headline-parser'.
|
||
|
||
- `element-with-markers': Equivalent to calling
|
||
`org-element-headline-parser', with markers added using
|
||
`org-ql--add-markers'. Suitable for formatting with
|
||
`org-ql-view--format-element', allowing insertion into an Org
|
||
Agenda-like buffer.
|
||
|
||
- A sexp, which will be byte-compiled into a lambda function.
|
||
|
||
- A function symbol.
|
||
|
||
If NARROW is non-nil, buffers are not widened (the default is to
|
||
widen and search the entire buffer).
|
||
|
||
SORT is either nil, in which case items are not sorted; or one or
|
||
a list of defined `org-ql' sorting methods (`date', `deadline',
|
||
`scheduled', `todo', `priority', or `random'); or a user-defined
|
||
comparator function that accepts two items as arguments and
|
||
returns nil or non-nil."
|
||
(declare (indent defun))
|
||
(-let* ((buffers (->> (cl-typecase buffers-or-files
|
||
(null (list (current-buffer)))
|
||
(function (funcall buffers-or-files))
|
||
(list buffers-or-files)
|
||
(otherwise (list buffers-or-files)))
|
||
(--map (cl-etypecase it
|
||
(buffer it)
|
||
(string (or (find-buffer-visiting it)
|
||
(when (file-readable-p it)
|
||
;; It feels unintuitive that `find-file-noselect' returns
|
||
;; a buffer if the filename doesn't exist.
|
||
(find-file-noselect it))
|
||
(user-error "Can't open file: %s" it)))))
|
||
;; Ignore special/hidden buffers.
|
||
(--remove (string-prefix-p " " (buffer-name it)))))
|
||
(query (org-ql--pre-process-query query))
|
||
((&plist :query :preamble :preamble-case-fold) (org-ql--query-preamble query))
|
||
(predicate (org-ql--query-predicate query))
|
||
(action (pcase action
|
||
;; NOTE: These two lambdas are backquoted to prevent "unused lexical
|
||
;; variable" warnings from byte-compilation, because they don't use
|
||
;; all of the variables from their enclosing scope.
|
||
('element-with-markers (byte-compile
|
||
`(lambda (&rest _ignore)
|
||
(org-ql--add-markers
|
||
(org-element-headline-parser (line-end-position))))))
|
||
((or 'nil 'element) (byte-compile
|
||
`(lambda (&rest _ignore)
|
||
(org-element-headline-parser (line-end-position)))))
|
||
((pred functionp) action)
|
||
((and (pred listp) (guard (or (special-form-p (car action))
|
||
(macrop (car action))
|
||
(functionp (car action)))))
|
||
(byte-compile
|
||
`(lambda (&rest _ignore)
|
||
,action)))
|
||
(_ (user-error "Invalid action form: %s" action))))
|
||
(org-ql--today (ts-now))
|
||
(items (->> buffers
|
||
(--map (with-current-buffer it
|
||
(unless (derived-mode-p 'org-mode)
|
||
(user-error "Not an Org buffer: %s" (buffer-name)))
|
||
(org-ql--select-cached :query query :preamble preamble :preamble-case-fold preamble-case-fold
|
||
:predicate predicate :action action :narrow narrow)))
|
||
(-flatten-n 1))))
|
||
;; Sort items
|
||
(pcase sort
|
||
(`nil items)
|
||
((guard (cl-loop for elem in (-list sort)
|
||
always (memq elem '(date deadline scheduled todo priority random))))
|
||
;; Default sorting functions
|
||
(org-ql--sort-by items (-list sort)))
|
||
;; Sort by user-given comparator.
|
||
((pred functionp) (sort items sort))
|
||
(_ (user-error "SORT must be either nil, one or a list of the defined sorting methods (see documentation), or a comparison function of two arguments")))))
|
||
|
||
;;;###autoload
|
||
(cl-defun org-ql-query (&key (select 'element-with-markers) from where narrow order-by)
|
||
"Like `org-ql-select', but arguments are named more like a SQL query.
|
||
|
||
SELECT corresponds to the `org-ql-select' argument ACTION. It is
|
||
the function called on matching headings, the results of which
|
||
are returned by this function. It may be:
|
||
|
||
- `element' or nil: Equivalent to `org-element-headline-parser'.
|
||
|
||
- `element-with-markers': Equivalent to
|
||
`org-element-headline-parser', with markers added using
|
||
`org-ql--add-markers'. Suitable for formatting with
|
||
`org-ql-view--format-element', allowing insertion into an Org
|
||
Agenda-like buffer.
|
||
|
||
- A sexp, which will be byte-compiled into a lambda function.
|
||
|
||
- A function symbol.
|
||
|
||
FROM corresponds to the `org-ql-select' argument BUFFERS-OR-FILES.
|
||
It may be one or a list of file paths and/or buffers.
|
||
|
||
WHERE corresponds to the `org-ql-select' argument QUERY. It
|
||
should be an `org-ql' query sexp.
|
||
|
||
ORDER-BY corresponds to the `org-ql-select' argument SORT, which
|
||
see.
|
||
|
||
NARROW corresponds to the `org-ql-select' argument NARROW."
|
||
(declare (indent 0))
|
||
(org-ql-select from where
|
||
:action select
|
||
:narrow narrow
|
||
:sort order-by))
|
||
|
||
(defun org-ql--select-cached (&rest args)
|
||
"Return results for ARGS and current buffer using cache."
|
||
;; MAYBE: Timeout cached queries. Probably not necessarily since they will be removed when a
|
||
;; buffer is closed, or when a query is run after modifying a buffer.
|
||
(-let* (((&plist :query :preamble :action :narrow :preamble-case-fold) args)
|
||
(query-cache-key
|
||
;; The key must include the preamble, because some queries are replaced by
|
||
;; the preamble, leaving a nil query, which would make the key ambiguous.
|
||
(list :query query :preamble preamble :action action :preamble-case-fold preamble-case-fold
|
||
(if narrow
|
||
;; Use bounds of narrowed portion of buffer.
|
||
(cons (point-min) (point-max))
|
||
nil))))
|
||
(if-let* ((buffer-cache (gethash (current-buffer) org-ql-cache))
|
||
(query-cache (cadr buffer-cache))
|
||
(modified-tick (car buffer-cache))
|
||
(buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
|
||
(cached-result (gethash query-cache-key query-cache)))
|
||
(pcase cached-result
|
||
('org-ql-nil nil)
|
||
(_ cached-result))
|
||
(let ((new-result (apply #'org-ql--select args)))
|
||
(cond ((or (not query-cache)
|
||
(not buffer-unmodified-p))
|
||
(puthash (current-buffer)
|
||
(list (buffer-modified-tick)
|
||
(let ((table (make-hash-table :test 'org-ql-hash-test)))
|
||
(puthash query-cache-key (or new-result 'org-ql-nil) table)
|
||
table))
|
||
org-ql-cache))
|
||
(t (puthash query-cache-key (or new-result 'org-ql-nil) query-cache)))
|
||
new-result))))
|
||
|
||
(cl-defun org-ql--select (&key preamble preamble-case-fold predicate action narrow
|
||
&allow-other-keys)
|
||
"Return results of mapping function ACTION across entries in current buffer matching function PREDICATE.
|
||
If NARROW is non-nil, buffer will not be widened."
|
||
;; Since the mappings are stored in the variable `org-ql-predicates', macros like `flet'
|
||
;; can't be used, so we do it manually (this is same as the equivalent `flet' expansion).
|
||
;; Mappings are stored in the variable because it allows predicates to be defined with a
|
||
;; macro, which allows documentation to be easily generated for them.
|
||
|
||
;; MAYBE: Lift the `flet'-equivalent out of this function so it isn't done for each buffer.
|
||
(let (orig-fns)
|
||
(--each org-ql-predicates
|
||
;; Save original function mappings.
|
||
(let ((name (plist-get it :name)))
|
||
(push (list :name name :fn (symbol-function name)) orig-fns)))
|
||
(unwind-protect
|
||
(progn
|
||
(--each org-ql-predicates
|
||
;; Set predicate functions.
|
||
(fset (plist-get it :name) (plist-get it :fn)))
|
||
;; Run query.
|
||
(save-excursion
|
||
(save-restriction
|
||
(unless narrow
|
||
(widen))
|
||
(goto-char (point-min))
|
||
(when (org-before-first-heading-p)
|
||
(outline-next-heading))
|
||
(if (not (org-at-heading-p))
|
||
(progn
|
||
;; No headings in buffer: return nil.
|
||
(unless (string-prefix-p " " (buffer-name))
|
||
;; Not a special, hidden buffer: show message, because if a user accidentally
|
||
;; searches a buffer without headings, he might be confused.
|
||
(message "org-ql: No headings in buffer: %s" (current-buffer)))
|
||
nil)
|
||
;; Find matching entries.
|
||
(cond (preamble (let ((case-fold-search preamble-case-fold))
|
||
(cl-loop while (re-search-forward preamble nil t)
|
||
do (outline-back-to-heading 'invisible-ok)
|
||
when (funcall predicate)
|
||
collect (funcall action)
|
||
do (outline-next-heading))))
|
||
(t (cl-loop when (funcall predicate)
|
||
collect (funcall action)
|
||
while (outline-next-heading))))))))
|
||
(--each orig-fns
|
||
;; Restore original function mappings.
|
||
(fset (plist-get it :name) (plist-get it :fn))))))
|
||
|
||
;;;;; Helpers
|
||
|
||
(defun org-ql--tags-at (position)
|
||
"Return tags for POSITION in current buffer.
|
||
Returns cons (INHERITED-TAGS . LOCAL-TAGS)."
|
||
;; I'd like to use `-if-let*', but it doesn't leave non-nil variables
|
||
;; bound in the else clause, so destructured variables that are non-nil,
|
||
;; like found caches, are not available in the else clause.
|
||
(if-let* ((buffer-cache (gethash (current-buffer) org-ql-tags-cache))
|
||
(modified-tick (car buffer-cache))
|
||
(tags-cache (cdr buffer-cache))
|
||
(buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
|
||
(cached-result (gethash position tags-cache)))
|
||
;; Found in cache: return them.
|
||
(pcase cached-result
|
||
('org-ql-nil nil)
|
||
(_ cached-result))
|
||
;; Not found in cache: get tags and cache them.
|
||
(let* ((local-tags (or (when (looking-at org-ql-tag-line-re)
|
||
(split-string (match-string-no-properties 2) ":" t))
|
||
'org-ql-nil))
|
||
(inherited-tags (or (when org-use-tag-inheritance
|
||
(save-excursion
|
||
(if (org-up-heading-safe)
|
||
;; Return parent heading's tags.
|
||
(-let* (((inherited local) (org-ql--tags-at (point)))
|
||
(tags (when (or inherited local)
|
||
(cond ((and (listp inherited)
|
||
(listp local))
|
||
(->> (append inherited local)
|
||
-non-nil -uniq))
|
||
((listp inherited) inherited)
|
||
((listp local) local)))))
|
||
(cl-typecase org-use-tag-inheritance
|
||
(list (setf tags (-intersection tags org-use-tag-inheritance)))
|
||
(string (setf tags (--select (string-match org-use-tag-inheritance it)
|
||
tags))))
|
||
(pcase org-tags-exclude-from-inheritance
|
||
('nil tags)
|
||
(_ (-difference tags org-tags-exclude-from-inheritance))))
|
||
;; Top-level heading: use file tags.
|
||
org-file-tags)))
|
||
'org-ql-nil))
|
||
(all-tags (list inherited-tags local-tags)))
|
||
;; Check caches again, because they may have been set now.
|
||
;; TODO: Is there a clever way we could avoid doing this, or is it inherently necessary?
|
||
(setf buffer-cache (gethash (current-buffer) org-ql-tags-cache)
|
||
modified-tick (car buffer-cache)
|
||
tags-cache (cdr buffer-cache)
|
||
buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
|
||
(unless (and buffer-cache buffer-unmodified-p)
|
||
;; Buffer-local tags cache empty or invalid: make new one.
|
||
(setf tags-cache (make-hash-table))
|
||
(puthash (current-buffer)
|
||
(cons (buffer-modified-tick) tags-cache)
|
||
org-ql-tags-cache))
|
||
(puthash position all-tags tags-cache))))
|
||
|
||
(defun org-ql--outline-path ()
|
||
"Return outline path for heading at point."
|
||
(save-excursion
|
||
(let ((heading (save-match-data
|
||
(if (looking-at org-complex-heading-regexp)
|
||
(or (match-string 4) "")
|
||
""))))
|
||
(if (org-up-heading-safe)
|
||
;; MAYBE: It seems wrong to call the cache function from
|
||
;; inside this function, like a violation of separation of
|
||
;; concern. Can this be rewritten to not work that way?
|
||
(append (org-ql--value-at (point) #'org-ql--outline-path)
|
||
(list heading))
|
||
(list heading)))))
|
||
|
||
;; TODO: Use --value-at for tags cache.
|
||
|
||
(defun org-ql--value-at (position fn)
|
||
;; TODO: Either rename to `value-at-point' and remove `position' arg, or move point.
|
||
"Return FN's value at POSITION in current buffer.
|
||
Values compared with `equal'."
|
||
;; I'd like to use `-if-let*', but it doesn't leave non-nil variables
|
||
;; bound in the else clause, so destructured variables that are non-nil,
|
||
;; like found caches, are not available in the else clause.
|
||
(pcase (if-let* ((buffer-cache (gethash (current-buffer) org-ql-node-value-cache))
|
||
(modified-tick (car buffer-cache))
|
||
(position-cache (cdr buffer-cache))
|
||
(buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
|
||
(value-cache (gethash position position-cache))
|
||
(cached-value (alist-get fn value-cache nil nil #'equal)))
|
||
;; Found in cache: return it.
|
||
cached-value
|
||
;; Not found in cache: call FN, cache and return its value.
|
||
(let ((new-value (or (funcall fn) 'org-ql-nil)))
|
||
;; Check caches again, because it may have been set now, e.g. by
|
||
;; recursively going up an outline tree.
|
||
;; TODO: Is there a clever way we could avoid doing this, or is it inherently necessary?
|
||
(setf buffer-cache (gethash (current-buffer) org-ql-node-value-cache)
|
||
modified-tick (car buffer-cache)
|
||
position-cache (cdr buffer-cache)
|
||
value-cache (when position-cache
|
||
(gethash position position-cache))
|
||
buffer-unmodified-p (eq (buffer-modified-tick) modified-tick))
|
||
(unless (and buffer-cache buffer-unmodified-p)
|
||
;; Buffer-local node cache empty or invalid: make new one.
|
||
(setf position-cache (make-hash-table)
|
||
value-cache (gethash position position-cache))
|
||
(puthash (current-buffer)
|
||
(cons (buffer-modified-tick) position-cache)
|
||
org-ql-node-value-cache))
|
||
(setf (alist-get fn value-cache nil nil #'equal) new-value)
|
||
(puthash position value-cache position-cache)
|
||
new-value))
|
||
;; Return nil or the non-nil value.
|
||
('org-ql-nil nil)
|
||
(else else)))
|
||
|
||
(defun org-ql--add-markers (element)
|
||
"Return ELEMENT with Org marker text properties added.
|
||
ELEMENT should be an Org element like that returned by
|
||
`org-element-headline-parser'. This function should be called
|
||
from within ELEMENT's buffer."
|
||
;; NOTE: `org-agenda-new-marker' works, until it doesn't, because...I don't know. It sometimes
|
||
;; raises errors or returns markers that don't point into a buffer. `copy-marker' always works,
|
||
;; of course, but maybe it will leave "dangling" markers, which could affect performance over
|
||
;; time? I don't know, but for now, it seems that we have to use `copy-marker'.
|
||
(let* ((marker (copy-marker (org-element-property :begin element)))
|
||
(properties (--> (cadr element)
|
||
(plist-put it :org-marker marker)
|
||
(plist-put it :org-hd-marker marker))))
|
||
(setf (cadr element) properties)
|
||
element))
|
||
|
||
;;;;; Query processing
|
||
|
||
;; Processing, compiling, etc. for queries.
|
||
|
||
;; This error is used for when compiling a query signals an error,
|
||
;; making it easier for the UI to avoid spurious warnings, e.g. for
|
||
;; partially typed queries in the Helm commands.
|
||
(define-error 'org-ql-invalid-query "Invalid Org QL query" 'user-error)
|
||
|
||
(defun org-ql--sanity-check-form (form)
|
||
"Signal error if any forms in FORM do not have preconditions met.
|
||
Or, when possible, fix the problem."
|
||
(cl-flet ((check (symbol)
|
||
(cl-case symbol
|
||
('done (unless org-done-keywords
|
||
;; NOTE: This check needs to be done from within the Org buffer being checked.
|
||
(error "Variable `org-done-keywords' is nil. Are you running this from an Org buffer?"))))))
|
||
(cl-loop for elem in form
|
||
if (consp elem)
|
||
do (progn
|
||
(check (car elem))
|
||
(org-ql--sanity-check-form (cdr elem)))
|
||
else do (check elem))))
|
||
|
||
(defun org-ql--pre-process-query (query)
|
||
"Return QUERY having been pre-processed.
|
||
Replaces bare strings with (regexp) selectors, and appropriate
|
||
`ts'-related selectors."
|
||
;; This is unsophisticated, but it works.
|
||
;; TODO: Maybe query pre-processing should be done in one place,
|
||
;; rather than here and in --query-predicate.
|
||
;; NOTE: Don't be scared by the `pcase' patterns! They make this
|
||
;; all very easy once you grok the backquoting and unquoting.
|
||
(cl-labels ((rec (element)
|
||
(pcase element
|
||
(`(or . ,clauses) `(or ,@(mapcar #'rec clauses)))
|
||
(`(and . ,clauses) `(and ,@(mapcar #'rec clauses)))
|
||
(`(not . ,clauses) `(not ,@(mapcar #'rec clauses)))
|
||
(`(when ,condition . ,clauses) `(when ,(rec condition)
|
||
,@(mapcar #'rec clauses)))
|
||
(`(unless ,condition . ,clauses) `(unless ,(rec condition)
|
||
,@(mapcar #'rec clauses)))
|
||
;; TODO: Combine (regexp) when appropriate (i.e. inside an OR, not an AND).
|
||
((pred stringp) `(regexp ,element))
|
||
;; Quote children queries so the user doesn't have to.
|
||
(`(children ,query) `(children ',query))
|
||
(`(children) '(children (lambda () t)))
|
||
(`(descendants ,query) `(descendants ',query))
|
||
(`(descendants) '(descendants (lambda () t)))
|
||
(`(parent ,query) `(parent ,(org-ql--query-predicate (rec query))))
|
||
(`(parent) '(parent (lambda () t)))
|
||
(`(ancestors ,query) `(ancestors ,(org-ql--query-predicate (rec query))))
|
||
(`(ancestors) '(ancestors (lambda () t)))
|
||
;; Timestamp-based predicates. I think this is the way that makes the most sense:
|
||
;; set the limit to N days in the future, adjusted to 23:59:59 (since Org doesn't
|
||
;; support timestamps down to the second, anyway, there should be no need to adjust
|
||
;; it forward to 00:00:00 of the next day). That way, e.g. if it's Monday at 3 PM,
|
||
;; and N is 1, rather than showing items up to 3 PM Tuesday, it will show items any
|
||
;; time on Tuesday. If this isn't desired, the user can pass a specific timestamp.
|
||
(`(,(and pred (or 'clocked 'closed))
|
||
,(and num-days (pred numberp)))
|
||
;; (clocked) and (closed) implicitly look into the past.
|
||
(let ((from (->> (ts-now)
|
||
(ts-adjust 'day (* -1 num-days))
|
||
(ts-apply :hour 0 :minute 0 :second 0))))
|
||
`(,pred :from ,from)))
|
||
(`(deadline auto)
|
||
;; Use `org-deadline-warning-days' as the :to arg.
|
||
(let ((to (->> (ts-now)
|
||
(ts-adjust 'day org-deadline-warning-days)
|
||
(ts-apply :hour 23 :minute 59 :second 59))))
|
||
`(deadline-warning :to ,to)))
|
||
(`(,(and pred (or 'deadline 'scheduled 'planning))
|
||
,(and num-days (pred numberp)))
|
||
(let ((to (->> (ts-now)
|
||
(ts-adjust 'day num-days)
|
||
(ts-apply :hour 23 :minute 59 :second 59))))
|
||
`(,pred :to ,to)))
|
||
|
||
;; Headings.
|
||
(`(h . ,args)
|
||
;; "h" alias.
|
||
`(heading ,@args))
|
||
|
||
;; Regexps.
|
||
(`(r . ,args)
|
||
;; "r" alias.
|
||
`(regexp ,@args))
|
||
|
||
;; Outline paths.
|
||
(`(,(or 'outline-path 'olp) . ,strings)
|
||
;; Regexp quote headings.
|
||
`(outline-path ,@(mapcar #'regexp-quote strings)))
|
||
(`(,(or 'outline-path-segment 'olps) . ,strings)
|
||
;; Regexp quote headings.
|
||
`(outline-path-segment ,@(mapcar #'regexp-quote strings)))
|
||
|
||
;; Priorities
|
||
(`(priority ,(and (or '= '< '> '<= '>=) comparator) ,letter)
|
||
;; Quote comparator.
|
||
`(priority ',comparator ,letter))
|
||
|
||
;; Properties.
|
||
(`(property ,property . ,value)
|
||
;; Convert keyword property arguments to strings. Non-sexp
|
||
;; queries result in keyword property arguments (because to do
|
||
;; otherwise would require ugly special-casing in the parsing).
|
||
(when (keywordp property)
|
||
(setf property (substring (symbol-name property) 1)))
|
||
(cons 'property (cons property value)))
|
||
|
||
;; Source blocks.
|
||
(`(src . ,args)
|
||
;; Rewrite to use keyword args.
|
||
(-let (regexps lang keyword-index)
|
||
(cond ((plist-get args :lang)
|
||
;; Lang given first, or only lang given.
|
||
(setf lang (plist-get args :lang)
|
||
regexps (seq-difference args (list :lang lang))))
|
||
((setf keyword-index (-find-index #'keywordp args))
|
||
;; Regexps and lang given.
|
||
(setf lang (plist-get (cl-subseq args keyword-index) :lang)
|
||
regexps (cl-subseq args 0 keyword-index)))
|
||
(t ;; Only regexps given.
|
||
(setf regexps args)))
|
||
(when regexps
|
||
;; This feels awkward and wrong, but we have to quote lists
|
||
;; and avoid quoting nil. There must be a better way.
|
||
(setf regexps `(',regexps)))
|
||
`(src :lang ,lang :regexps ,@regexps)))
|
||
|
||
;; Tags.
|
||
(`(,(or 'tags-all 'tags&) . ,tags) `(and ,@(--map `(tags ,it) tags)))
|
||
;; MAYBE: -all versions for inherited and local.
|
||
;; Inherited and local predicate aliases.
|
||
(`(,(or 'tags-i 'itags 'inherited-tags) . ,tags) `(tags-inherited ,@tags))
|
||
(`(,(or 'tags-l 'ltags 'local-tags) . ,tags) `(tags-local ,@tags))
|
||
|
||
;; Timestamps
|
||
(`(,(or 'ts-active 'ts-a) . ,rest) `(ts :type active ,@rest))
|
||
(`(,(or 'ts-inactive 'ts-i) . ,rest) `(ts :type inactive ,@rest))
|
||
;; Any other form: passed through unchanged.
|
||
(_ element))))
|
||
(rec query)))
|
||
|
||
(defun org-ql--query-preamble (query)
|
||
"Return plist (QUERY PREAMBLE PREAMBLE-CASE-FOLD) for QUERY.
|
||
When QUERY has a clause with a corresponding preamble, and it's
|
||
appropriate to use one (i.e. the clause is not in an `or'),
|
||
replace the clause with a preamble."
|
||
(pcase org-ql-use-preamble
|
||
('nil (list :query query :preamble nil))
|
||
(_ (let ((preamble-case-fold t)
|
||
org-ql-preamble)
|
||
(cl-labels ((rec (element)
|
||
(or (when org-ql-preamble
|
||
;; Only one preamble is allowed
|
||
element)
|
||
(pcase element
|
||
(`(or _) element)
|
||
(`(clocked . ,_)
|
||
(setq org-ql-preamble org-ql-clock-regexp)
|
||
element)
|
||
(`(closed . ,_)
|
||
(setq org-ql-preamble org-closed-time-regexp)
|
||
;; Return element, because the predicate still needs testing.
|
||
element)
|
||
(`(deadline . ,_)
|
||
(setq org-ql-preamble org-deadline-time-regexp)
|
||
;; Return element, because the predicate still needs testing.
|
||
element)
|
||
(`(regexp . ,regexps)
|
||
;; Search for first regexp, then confirm with predicate.
|
||
(setq org-ql-preamble (car regexps))
|
||
element)
|
||
(`(todo . ,(and todo-keywords (guard todo-keywords)))
|
||
(setf org-ql-preamble
|
||
(rx-to-string `(seq bol (1+ "*") (1+ space) (or ,@todo-keywords) (or " " eol))
|
||
t)
|
||
preamble-case-fold nil)
|
||
;; Return nil, don't test the predicate.
|
||
nil)
|
||
(`(habit)
|
||
(setq org-ql-preamble (rx bol (0+ space) ":STYLE:" (1+ space) "habit" (0+ space) eol))
|
||
nil)
|
||
|
||
;; Heading text.
|
||
;; MAYBE: Adjust regexp to avoid matching in tag list.
|
||
(`(heading ,regexp)
|
||
;; Only one regexp: match with preamble, then let predicate confirm (because
|
||
;; the match could be in e.g. the tags rather than the heading text).
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl)
|
||
,regexp)
|
||
'no-group))
|
||
element)
|
||
(`(heading . ,regexps)
|
||
;; Multiple regexps: use preamble to match against first
|
||
;; regexp, then let the predicate match the rest.
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl)
|
||
,(car regexps))
|
||
'no-group))
|
||
element)
|
||
|
||
;; Heading levels.
|
||
(`(level ,comparator-or-num ,num)
|
||
(let ((repeat (pcase comparator-or-num
|
||
('< `(repeat 1 ,(1- num) "*"))
|
||
('<= `(repeat 1 ,num "*"))
|
||
('> `(>= ,(1+ num) "*"))
|
||
('>= `(>= ,num "*"))
|
||
((pred integerp) `(repeat ,comparator-or-num ,num "*")))))
|
||
(setq org-ql-preamble (rx-to-string `(seq bol ,repeat " ") t))
|
||
;; Return nil, because we don't need to test the predicate.
|
||
nil))
|
||
(`(level ,num)
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (repeat ,num "*") " ") t))
|
||
nil)
|
||
|
||
;; Planning lines.
|
||
(`(planning . ,_)
|
||
(setq org-ql-preamble org-ql-planning-regexp)
|
||
;; Return element, because the predicate still needs testing.
|
||
element)
|
||
|
||
;; Priorities.
|
||
;; NOTE: This only accepts A, B, or C. I haven't seen
|
||
;; other priorities in the wild, so this will do for now.
|
||
(`(priority)
|
||
;; Any priority cookie.
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) "[#" (in "ABC") "]") t))
|
||
nil)
|
||
(`(priority ,(and (or ''= ''< ''> ''<= ''>=) comparator) ,letter)
|
||
;; Comparator and priority letter.
|
||
;; NOTE: The double-quoted comparators. See below.
|
||
(let* ((priority-letters '("A" "B" "C"))
|
||
(index (-elem-index letter priority-letters))
|
||
;; NOTE: Higher priority == lower number.
|
||
;; NOTE: Because we need to support both preamble-based queries and
|
||
;; regular predicate ones, we work around an idiosyncrasy of query
|
||
;; pre-processing by accepting both quoted and double-quoted comparator
|
||
;; function symbols. Not the most elegant solution, but it works.
|
||
(priorities (s-join "" (pcase comparator
|
||
((or '= ''=) (list letter))
|
||
((or '> ''>) (cl-subseq priority-letters 0 index))
|
||
((or '>= ''>=) (cl-subseq priority-letters 0 (1+ index)))
|
||
((or '< ''<) (cl-subseq priority-letters (1+ index)))
|
||
((or '<= ''<=) (cl-subseq priority-letters index))))))
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank) (optional (1+ upper) (1+ blank))
|
||
"[#" (in ,priorities) "]") t))
|
||
nil))
|
||
(`(priority . ,letters)
|
||
;; One or more priorities.
|
||
;; MAYBE: Disable case-folding.
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ blank)
|
||
(optional (1+ upper) (1+ blank))
|
||
"[#" (or ,@letters) "]") t))
|
||
nil)
|
||
|
||
;; Properties.
|
||
;; MAYBE: Should case folding be disabled for properties? What about values?
|
||
(`(property ,property ,value)
|
||
;; We do NOT return nil, because the predicate still needs to be tested,
|
||
;; because the regexp could match a string not inside a property drawer.
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (0+ space) ":" ,property ":"
|
||
(1+ space) ,value (0+ space) eol)))
|
||
element)
|
||
(`(property ,property)
|
||
;; We do NOT return nil, because the predicate still needs to be tested,
|
||
;; because the regexp could match a string not inside a property drawer.
|
||
;; NOTE: The preamble only matches if there appears to be a value.
|
||
;; A line like ":ID: " without any other text does not match.
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (0+ space) ":" ,property ":" (1+ space)
|
||
(minimal-match (1+ not-newline)) eol)))
|
||
element)
|
||
;; MAYBE: Support (property) without args.
|
||
;; (`(property)
|
||
;; ;; We do NOT return nil, because the predicate still needs to be tested,
|
||
;; ;; because the regexp could match a string not inside a property drawer.
|
||
;; ;; NOTE: The preamble only matches if there appears to be a value.
|
||
;; ;; A line like ":ID: " without any other text does not match.
|
||
;; (setq org-ql-preamble (rx-to-string `(seq bol (0+ space) ":" (1+ (not (or space ":"))) ":"
|
||
;; (1+ space) (minimal-match (1+ not-newline)) eol)))
|
||
;; element)
|
||
|
||
;; Src blocks.
|
||
(`(src . ,args)
|
||
(setq org-ql-preamble (org-ql--format-src-block-regexp (plist-get args :lang)))
|
||
;; Always check contents with predicate.
|
||
element)
|
||
|
||
;; Scheduled.
|
||
(`(scheduled . ,_)
|
||
(setq org-ql-preamble org-scheduled-time-regexp)
|
||
;; Return element, because the predicate still needs testing.
|
||
element)
|
||
|
||
;; Tags.
|
||
(`((or 'tags-local 'local-tags 'tags-l 'ltags) . ,tags)
|
||
;; When searching for local, non-inherited tags, we can
|
||
;; search directly to headings containing one of the tags.
|
||
(setq org-ql-preamble (rx-to-string `(seq bol (1+ "*") (1+ space) (1+ not-newline)
|
||
":" (or ,@tags) ":")
|
||
t))
|
||
;; Return nil, because we don't need to test the predicate.
|
||
nil)
|
||
|
||
;; Timestamps.
|
||
(`(ts . ,rest)
|
||
(setq org-ql-preamble (pcase (plist-get rest :type)
|
||
((or 'nil 'both) org-tsr-regexp-both)
|
||
('active org-tsr-regexp)
|
||
('inactive org-ql-tsr-regexp-inactive)))
|
||
;; Predicate needs testing only when args are present.
|
||
(-let (((&keys :from :to :on) rest))
|
||
(when (or from to on)
|
||
element)))
|
||
(`(and . ,rest)
|
||
(let ((clauses (mapcar #'rec rest)))
|
||
`(and ,@(-non-nil clauses))))
|
||
(_ element)))))
|
||
(setq query (pcase (mapcar #'rec (list query))
|
||
((or `(nil)
|
||
`((nil))
|
||
`((and))
|
||
`((or)))
|
||
t)
|
||
(query (-flatten-n 1 query))))
|
||
(list :query query :preamble org-ql-preamble :preamble-case-fold preamble-case-fold))))))
|
||
|
||
(defun org-ql--format-src-block-regexp (&optional lang)
|
||
"Return regexp equivalent to `org-babel-src-block-regexp' with LANG filled in."
|
||
;; I couldn't find a way to match block contents without the regexp
|
||
;; also matching past the end of the block and into later blocks. Even
|
||
;; using `minimal-match' in several different combinations didn't work.
|
||
;; So matching contents will have to be done with the predicate.
|
||
(rx-to-string `(seq bol (group (zero-or-more (any " ")))
|
||
"#+begin_src"
|
||
(one-or-more (any " "))
|
||
,(or lang `(1+ (not (any " \n\f
|
||
"))))
|
||
(zero-or-more (any " "))
|
||
(group (or (seq (zero-or-more (not (any "\n\":")))
|
||
"\""
|
||
(zero-or-more (not (any "\n\"*")))
|
||
"\""
|
||
(zero-or-more (not (any "\n\":"))))
|
||
(zero-or-more (not (any "\n\":")))))
|
||
(group (zero-or-more (not (any "\n")))) "\n"
|
||
(63 (group (*\? (not (any " |