2130 lines
107 KiB
EmacsLisp
2130 lines
107 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.6-pre
|
||
;; Package-Requires: ((emacs "26.1") (dash "2.18.1") (f "0.17.2") (map "2.1") (org "9.0") (org-super-agenda "1.2") (ov "1.0.6") (peg "1.0") (s "1.12.0") (transient "0.1") (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 '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.")
|
||
|
||
(defconst org-ql-link-regexp
|
||
(if (bound-and-true-p org-link-bracket-re)
|
||
org-link-bracket-re
|
||
org-bracket-link-regexp)
|
||
"Regexp used to match Org bracket links.
|
||
Necessary because of changes in Org 9.something.")
|
||
|
||
(defconst org-ql-link-description-group
|
||
(if (bound-and-true-p org-link-bracket-re)
|
||
2
|
||
3)
|
||
;; I wish Org would not introduce backward-incompatible changes like this in
|
||
;; minor releases. It requires awkward workarounds to be maintained for years.
|
||
"Regexp match group used to extract description from Org bracket links.
|
||
Necessary because of backward-incompatible changes in Org
|
||
9.something: when `org-link-bracket-re' was added,
|
||
`org-bracket-link-regexp' was marked as an obsolete alias for it,
|
||
but the match groups were changed, so they are not compatible.")
|
||
|
||
;;;; 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.")
|
||
|
||
(eval-and-compile
|
||
(defvar org-ql-predicates
|
||
;; FIXME: Is this remapping still necessary? It was mapping `org-back-to-heading'
|
||
;; to itself until now, so maybe I broke it and it doesn't matter anymore.
|
||
(list (cons 'org-back-to-heading (list :name 'org-back-to-heading :fn (symbol-function 'outline-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 '(custom-manual "(org-ql)Usage")
|
||
:link '(url-link "https://github.com/alphapapa/org-ql"))
|
||
|
||
(defcustom org-ql-ask-unsafe-queries t
|
||
"Ask before running a query that could run arbitrary code.
|
||
Org QL queries in sexp form can contain arbitrary expressions.
|
||
When opening an \"org-ql-search:\" link or updating a dynamic
|
||
block that contains a query in sexp form, and this option is
|
||
non-nil, the user will be prompted for confirmation before
|
||
opening the link.
|
||
|
||
This variable may be set file-locally to disable this warning in
|
||
files that the user assumes are safe (e.g. of known provenance).
|
||
Users who are entirely unconcerned about this issue may disable
|
||
the option globally (at their own risk, however minimal it
|
||
probably is).
|
||
|
||
See Info node `(org-ql)Queries'."
|
||
:type 'boolean
|
||
:risky t)
|
||
|
||
;;;; Macros
|
||
|
||
;;;###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)
|
||
(obsolete "Please use functions `org-ql-select' or `org-ql-query' instead" "org-ql 0.5"))
|
||
`(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
|
||
;; NOTE: This etypecase is essential to opening links safely,
|
||
;; as it rejects, e.g. lambdas in the buffers-files argument.
|
||
(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--normalize-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 (let (orig-fns)
|
||
(unwind-protect
|
||
(progn
|
||
(--each org-ql-predicates
|
||
;; Set predicate functions.
|
||
(-let (((&plist :name :fn) (cdr it)))
|
||
;; Save original function.
|
||
(push (list :name name :fn (symbol-function name)) orig-fns)
|
||
;; Temporarily set new function definition.
|
||
(fset name fn)))
|
||
;; Run query on buffers.
|
||
(->> 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)))
|
||
(--each orig-fns
|
||
;; Restore original function mappings.
|
||
(-let (((&plist :name :fn) it))
|
||
(fset name fn)))))))
|
||
;; 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 sort items))
|
||
(_ (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.
|
||
(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.
|
||
;; TODO: Bind `case-fold-search' around the preamble loop.
|
||
(cond (preamble (cl-loop while (let ((case-fold-search preamble-case-fold))
|
||
(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))))))))
|
||
|
||
;;;;; Helpers
|
||
|
||
(defun org-ql--tags-at (position)
|
||
;; FIXME: This function actually assumes that point is already 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.
|
||
;; FIXME: Isn't `cached-result' a list of (INHERITED . LOCAL)? It
|
||
;; will never be just `org-ql-nil', but the CAR and CDR may be, so
|
||
;; they need to each be checked and replaced with nil if necessary.
|
||
(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))
|
||
|
||
(defun org-ql--ask-unsafe-query (query)
|
||
"Signal an error if user rejects running QUERY.
|
||
If `org-ql-view-ask-unsafe-links' is nil, does nothing and
|
||
returns nil."
|
||
(when org-ql-ask-unsafe-queries
|
||
(let ((query-string (propertize (cl-etypecase query
|
||
(list (prin1-to-string query))
|
||
(string query))
|
||
'face 'font-lock-warning-face)))
|
||
(unless (yes-or-no-p (concat "Query is in sexp form and could contain arbitrary code: "
|
||
query-string " Execute it? "))
|
||
(user-error "Query aborted by user")))))
|
||
|
||
;;;;; 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))))
|
||
|
||
(cl-defun org-ql--link-regexp (&key description-or-target description target)
|
||
"Return a regexp matching Org links according to arguments.
|
||
Each argument is treated as a regexp (so non-regexp strings
|
||
should be quoted before being passed to this function). If
|
||
DESCRIPTION-OR-TARGET, match it in either description or target.
|
||
If DESCRIPTION, match it in the description. If TARGET, match it
|
||
in the target. If both DESCRIPTION and TARGET, match both,
|
||
respectively."
|
||
(cl-labels
|
||
((no-desc
|
||
(match) (rx-to-string `(seq (or bol (1+ blank))
|
||
"[[" (0+ (not (any "]"))) (regexp ,match) (0+ (not (any "]")))
|
||
"]]")))
|
||
(match-both
|
||
(description target)
|
||
(rx-to-string `(seq (or bol (1+ blank))
|
||
"[[" (0+ (not (any "]"))) (regexp ,target) (0+ (not (any "]")))
|
||
"][" (0+ (not (any "]"))) (regexp ,description) (0+ (not (any "]")))
|
||
"]]")))
|
||
;; Note that these actually allow empty descriptions
|
||
;; or targets, depending on what they are matching.
|
||
(match-desc
|
||
(match) (rx-to-string `(seq (or bol (1+ blank))
|
||
"[[" (0+ (not (any "]")))
|
||
"][" (0+ (not (any "]"))) (regexp ,match) (0+ (not (any "]")))
|
||
"]]")))
|
||
(match-target
|
||
(match) (rx-to-string `(seq (or bol (1+ blank))
|
||
"[[" (0+ (not (any "]"))) (regexp ,match) (0+ (not (any "]")))
|
||
"][" (0+ (not (any "]")))
|
||
"]]"))))
|
||
(cond (description-or-target
|
||
(rx-to-string `(or (regexp ,(no-desc description-or-target))
|
||
(regexp ,(match-desc description-or-target))
|
||
(regexp ,(match-target description-or-target)))))
|
||
((and description target)
|
||
(match-both description target))
|
||
(description (match-desc description))
|
||
(target (rx-to-string `(or (regexp ,(no-desc target))
|
||
(regexp ,(match-target target))))))))
|
||
|
||
(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 " |