Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
Adam Porter
ed73a15c69 WIP: (org-ql-search) Rename argument 2024-01-03 18:50:00 -06:00
Adam Porter
2c2d2a1aee WIP: Docstring 2024-01-03 18:50:00 -06:00
Adam Porter
c6348e622c WIP: Docstring 2024-01-03 18:50:00 -06:00
Adam Porter
71c8079e22 WIP: (org-ql-search) Handle narrowing, and improve titles 2024-01-03 18:50:00 -06:00
Adam Porter
1fce6cc364 Comment: Add TODO 2024-01-03 18:50:00 -06:00
Adam Porter
23e44cb6f1 WIP: (org-ql-view) Update header line 2024-01-03 18:50:00 -06:00
Adam Porter
9237f0d93b WIP: (org-ql-select) Tidy, etc. 2024-01-03 18:50:00 -06:00
Adam Porter
e57d50b81c WIP: Accept entry IDs as IN argument 2024-01-03 18:50:00 -06:00
3 changed files with 150 additions and 95 deletions

View file

@ -147,19 +147,20 @@ Runs `org-occur-hook' after making the sparse tree."
num-results))) num-results)))
;;;###autoload ;;;###autoload
(cl-defun org-ql-search (buffers-files query &key narrow super-groups sort title (cl-defun org-ql-search (in query &key narrow super-groups sort title
(buffer org-ql-view-buffer)) (buffer org-ql-view-buffer))
"Search for QUERY with `org-ql'. "Search for QUERY with `org-ql'.
Interactively, prompt for these variables: Interactively, prompt for these variables:
BUFFERS-FILES: A list of buffers and/or files to search. IN: Passed to `org-ql-select', which see. Interactively, may
Interactively, may also be: also be:
- `buffer': search the current buffer - `buffer': search the current buffer
- `all': search all Org buffers - `all': search all Org buffers
- `agenda': search buffers returned by the function `org-agenda-files' - `agenda': search buffers returned by the function
`org-agenda-files'
- `directory': search Org files in `org-directory' - `directory': search Org files in `org-directory'
- A space-separated list of file or buffer names - A space-separated list of file/buffer names or Org heading IDs
QUERY: An `org-ql' query in either sexp or non-sexp form (see QUERY: An `org-ql' query in either sexp or non-sexp form (see
Info node `(org-ql)Queries'). Info node `(org-ql)Queries').
@ -168,8 +169,9 @@ SUPER-GROUPS: An `org-super-agenda' group set. See variable
`org-super-agenda-groups' and Info node `(org-super-agenda)Group `org-super-agenda-groups' and Info node `(org-super-agenda)Group
selectors'. selectors'.
NARROW: When non-nil, don't widen buffers before NARROW: Passed to `org-ql-select', but set automatically when
searching. Interactively, with prefix, leave narrowed. called interactively based on the value of BUFFERS-FILES; the
user should not need to pass this argument.
SORT: One or a list of `org-ql' sorting functions, like `date' or SORT: One or a list of `org-ql' sorting functions, like `date' or
`priority' (see Info node `(org-ql)Listing / acting-on results'). `priority' (see Info node `(org-ql)Listing / acting-on results').
@ -181,12 +183,19 @@ display the results. By default, the value of
`org-ql-view-buffer' is used, and a new buffer is created if `org-ql-view-buffer' is used, and a new buffer is created if
necessary." necessary."
(declare (indent defun)) (declare (indent defun))
(interactive (list (org-ql-view--complete-buffers-files) (interactive
(read-string "Query: " (when org-ql-view-query (let* ((in (org-ql-view--complete-buffers-files))
(format "%S" org-ql-view-query))) (query (read-string "Query: " (when org-ql-view-query
:narrow (or org-ql-view-narrow (eq current-prefix-arg '(4))) (format "%S" org-ql-view-query))))
(narrow (if (equal in (current-buffer))
(or org-ql-view-narrow
(when (buffer-narrowed-p)
(setf in (or (org-id-get (point-min))
(copy-marker (point-min))))))
org-ql-view-narrow)))
(list in query :narrow narrow
:super-groups (org-ql-view--complete-super-groups) :super-groups (org-ql-view--complete-super-groups)
:sort (org-ql-view--complete-sort))) :sort (org-ql-view--complete-sort))))
;; NOTE: Using `with-temp-buffer' is a hack to work around the fact that `make-local-variable' ;; NOTE: Using `with-temp-buffer' is a hack to work around the fact that `make-local-variable'
;; does not work reliably from inside a `let' form when the target buffer is current on entry ;; does not work reliably from inside a `let' form when the target buffer is current on entry
;; to or exit from the `let', even though `make-local-variable' is actually done in ;; to or exit from the `let', even though `make-local-variable' is actually done in
@ -200,16 +209,16 @@ necessary."
;; Parse non-sexp query into sexp query. ;; Parse non-sexp query into sexp query.
(org-ql--query-string-to-sexp query))) (org-ql--query-string-to-sexp query)))
(list query))) (list query)))
(results (org-ql-select buffers-files query (results (org-ql-select in query
:action 'element-with-markers :action 'element-with-markers
:narrow narrow :narrow narrow
:sort sort)) :sort sort))
(strings (-map #'org-ql-view--format-element results)) (strings (-map #'org-ql-view--format-element results))
(buffer (or buffer (format "%s %s*" org-ql-view-buffer-name-prefix (or title query)))) (buffer (or buffer (format "%s %s*" org-ql-view-buffer-name-prefix (or title query))))
(header (org-ql-view--header-line-format (header (org-ql-view--header-line-format
:buffers-files buffers-files :query query :title title)) :buffers-files in :query query :title title))
;; Bind variables for `org-ql-view--display' to set. ;; Bind variables for `org-ql-view--display' to set.
(org-ql-view-buffers-files buffers-files) (org-ql-view-buffers-files in)
(org-ql-view-query query) (org-ql-view-query query)
(org-ql-view-sort sort) (org-ql-view-sort sort)
(org-ql-view-narrow narrow) (org-ql-view-narrow narrow)

View file

@ -1044,38 +1044,60 @@ property."
(declare-function org-ql-search-directories-files "org-ql-search" t) (declare-function org-ql-search-directories-files "org-ql-search" t)
(defun org-ql-view--contract-buffers-files (buffers-files) (defun org-ql-view--contract-buffers-files (in)
"Return BUFFERS-FILES in its \"contracted\" form. "Return IN in its \"contracted\" form.
The contracted form is \"org-agenda-files\" if BUFFERS-FILES The contracted form is \"org-agenda-files\" if IN matches the
matches the value of `org-agenda-files' (either the function or value of `org-agenda-files' (either the function or the
the variable), \"org-directory\" if it matches the value of variable), \"org-directory\" if it matches the value of
`org-ql-search-directories-files', or \"buffer\" if it is the `org-ql-search-directories-files', or \"buffer\" if it is the
current buffer. Otherwise BUFFERS-FILES is returned unchanged." current buffer. Otherwise IN is returned unchanged."
;; Used in `org-ql-view--complete-buffers-files' and ;; Used in `org-ql-view--complete-buffers-files' and
;; `org-ql-view--header-line-format'. ;; `org-ql-view--header-line-format'.
(cl-labels ((expand-files (list) (cl-labels ((expand-elt (it)
(--map (cl-typecase it (cl-typecase it
(string (expand-file-name it)) (string (or (expanded it)
(otherwise it)) (location-of it)
list))) (error "Unknown expansion for %S" it)))
(marker (or (location-of it)
(error "Unknown location for %S" it)))
(otherwise it)))
(expanded (filename)
(when-let ((expanded (expand-file-name filename))
((file-readable-p expanded)))
expanded))
(location-of (place)
(when-let ((marker (cl-etypecase place
(marker place)
(string (org-id-find place 'marker))))
(heading (org-link-display-format (org-entry-get marker "ITEM"))))
(setf heading (if (string-empty-p heading)
place
(or (org-id-get marker)
(string-join (org-with-point-at marker
(org-get-outline-path t))
" ")))
;; FIXME: The help-echo gets overridden elsewhere.
heading (propertize heading 'help-echo (format "%s" place)))
(format "\"%s\" in %S"
heading (abbreviate-file-name (buffer-file-name (marker-buffer marker)))))))
;; TODO: Test this more exhaustively. ;; TODO: Test this more exhaustively.
(pcase buffers-files (pcase in
((pred listp) ((pred listp)
(pcase (expand-files buffers-files) (pcase (mapcar #'expand-elt in)
((pred (seq-set-equal-p (mapcar #'expand-file-name (org-agenda-files)))) ((pred (seq-set-equal-p (mapcar #'expand-file-name (org-agenda-files))))
"org-agenda-files") "org-agenda-files")
((and (guard (file-exists-p org-directory)) ((and (guard (file-exists-p org-directory))
(pred (seq-set-equal-p (org-ql-search-directories-files (pred (seq-set-equal-p (org-ql-search-directories-files
:directories (list org-directory))))) :directories (list org-directory)))))
"org-directory") "org-directory")
(_ buffers-files))) (_ in)))
((pred (equal (current-buffer))) ((pred (equal (current-buffer)))
"buffer") (buffer-name (current-buffer)))
((or 'org-agenda-files '(function org-agenda-files)) ((or 'org-agenda-files '(function org-agenda-files))
"org-agenda-files") "org-agenda-files")
((and (pred bufferp) (guard (buffer-file-name buffers-files))) ((and (pred bufferp) (guard (buffer-file-name in)))
(buffer-file-name buffers-files)) (buffer-file-name in))
(_ buffers-files)))) (_ (expand-elt in)))))
(defun org-ql-view--complete-buffers-files () (defun org-ql-view--complete-buffers-files ()
"Return value for `org-ql-view-buffers-files' using completion." "Return value for `org-ql-view-buffers-files' using completion."

View file

@ -333,11 +333,12 @@ See Info node `(org-ql)Queries'."
(sxhash-equal (prin1-to-string args)))) (sxhash-equal (prin1-to-string args))))
;;;###autoload ;;;###autoload
(cl-defun org-ql-select (buffers-or-files query &key action narrow sort) (cl-defun org-ql-select (in query &key action narrow sort)
"Return items matching QUERY in BUFFERS-OR-FILES. "Return items matching QUERY in IN.
BUFFERS-OR-FILES is a file or buffer, a list of files and/or IN is a buffer, file, or Org entry ID string (i.e. findable with
buffers, or a function which returns such a list. `org-id-goto'), or a list of one or more of such items, or a
function which returns such a list.
QUERY is an `org-ql' query sexp (quoted, since this is a QUERY is an `org-ql' query sexp (quoted, since this is a
function). function).
@ -374,23 +375,27 @@ would appear first. In contrast, `(date reverse priority)' would
also present items with the highest priority first, but within also present items with the highest priority first, but within
each priority the newest items would appear first." each priority the newest items would appear first."
(declare (indent defun)) (declare (indent defun))
(-let* ((buffers (->> (cl-typecase buffers-or-files (-let* ((sources (->> (cl-typecase in
(null (list (current-buffer))) (null (list (current-buffer)))
(function (funcall buffers-or-files)) (list in)
(list buffers-or-files) (otherwise (list in)))
(otherwise (list buffers-or-files))) (--map (pcase-exhaustive it
(--map (cl-etypecase it ;; NOTE: This exhaustive pcase is essential to opening links
;; NOTE: This etypecase is essential to opening links safely, ;; safely, as it rejects, e.g. lambdas in the IN argument.
;; as it rejects, e.g. lambdas in the buffers-files argument. ((cl-type buffer) it)
(buffer it) ((cl-type marker) it)
(string (or (find-buffer-visiting it) ((and (cl-type string)
(pred file-readable-p))
(or (find-buffer-visiting it)
(when (file-readable-p 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)) (find-file-noselect it))
(display-warning 'org-ql-select (format "Can't open file: %s" it) :error))))) (display-warning 'org-ql-select (format "Can't open file: %s" it) :error)))
((cl-type string)
(org-id-find it 'marker))
((cl-type function)
(funcall it))))
;; Ignore special/hidden buffers. ;; Ignore special/hidden buffers.
(--remove (string-prefix-p " " (buffer-name it))))) (--remove (and (bufferp it) (string-prefix-p " " (buffer-name it))))))
(query (org-ql--normalize-query query)) (query (org-ql--normalize-query query))
((&plist :query :preamble :preamble-case-fold) (org-ql--query-preamble query)) ((&plist :query :preamble :preamble-case-fold) (org-ql--query-preamble query))
(predicate (org-ql--query-predicate query)) (predicate (org-ql--query-predicate query))
@ -414,6 +419,18 @@ each priority the newest items would appear first."
,action))) ,action)))
(_ (user-error "Invalid action form: %s" action)))) (_ (user-error "Invalid action form: %s" action))))
(org-ql--today (ts-now)) (org-ql--today (ts-now))
(select-in (lambda (it)
(let* ((marker)
(buffer (cl-etypecase it
(buffer it)
(marker (marker-buffer (setf marker it))))))
(with-current-buffer buffer
(unless (derived-mode-p 'org-mode)
(display-warning 'org-ql-select (format "Not an Org buffer: %s" (buffer-name)) :error))
(org-ql--select-cached :query query :preamble preamble
:preamble-case-fold preamble-case-fold
:predicate predicate :action action
:narrow (or marker narrow))))))
(items (let (orig-fns) (items (let (orig-fns)
(unwind-protect (unwind-protect
(progn (progn
@ -424,14 +441,8 @@ each priority the newest items would appear first."
(push (list :name name :fn (symbol-function name)) orig-fns) (push (list :name name :fn (symbol-function name)) orig-fns)
;; Temporarily set new function definition. ;; Temporarily set new function definition.
(fset name fn))) (fset name fn)))
;; Run query on buffers. ;; Collect results.
(->> buffers (-flatten-n 1 (mapcar select-in sources)))
(--map (with-current-buffer it
(unless (derived-mode-p 'org-mode)
(display-warning 'org-ql-select (format "Not an Org buffer: %s" (buffer-name)) :error))
(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 (--each orig-fns
;; Restore original function mappings. ;; Restore original function mappings.
(-let (((&plist :name :fn) it)) (-let (((&plist :name :fn) it))
@ -466,8 +477,7 @@ are returned by this function. It may be:
- A function symbol. - A function symbol.
FROM corresponds to the `org-ql-select' argument BUFFERS-OR-FILES. FROM corresponds to the `org-ql-select' argument IN, which see.
It may be one or a list of file paths and/or buffers.
WHERE corresponds to the `org-ql-select' argument QUERY. It WHERE corresponds to the `org-ql-select' argument QUERY. It
should be an `org-ql' query sexp. should be an `org-ql' query sexp.
@ -491,10 +501,12 @@ NARROW corresponds to the `org-ql-select' argument NARROW."
;; The key must include the preamble, because some queries are replaced by ;; The key must include the preamble, because some queries are replaced by
;; the preamble, leaving a nil query, which would make the key ambiguous. ;; 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 (list :query query :preamble preamble :action action :preamble-case-fold preamble-case-fold
(if narrow :narrow (pcase-exhaustive narrow
;; Use bounds of narrowed portion of buffer. ((cl-type string) narrow)
(cons (point-min) (point-max)) ((cl-type marker) narrow)
nil)))) (`t ;; Use bounds of narrowed portion of buffer.
(cons (point-min) (point-max)))
(`nil nil)))))
(if-let* ((buffer-cache (gethash (current-buffer) org-ql-cache)) (if-let* ((buffer-cache (gethash (current-buffer) org-ql-cache))
(query-cache (cadr buffer-cache)) (query-cache (cadr buffer-cache))
(modified-tick (car buffer-cache)) (modified-tick (car buffer-cache))
@ -529,10 +541,18 @@ PREAMBLE-CASE-FOLD."
;; can't be used, so we do it manually (this is same as the equivalent `flet' expansion). ;; 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 ;; 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. ;; macro, which allows documentation to be easily generated for them.
(let (old-restriction)
(save-excursion (save-excursion
(save-restriction (save-restriction
(unless narrow (pcase narrow
(widen)) ((cl-type marker)
(switch-to-buffer (marker-buffer narrow)) ;; Can change buffer!
(setf old-restriction (if (buffer-narrowed-p)
(cons (point-min) (point-max))
t))
(goto-char narrow)
(org-narrow-to-subtree))
(`nil (widen)))
(goto-char (point-min)) (goto-char (point-min))
(when (org-before-first-heading-p) (when (org-before-first-heading-p)
(outline-next-heading)) (outline-next-heading))
@ -546,6 +566,7 @@ PREAMBLE-CASE-FOLD."
nil) nil)
;; Find matching entries. ;; Find matching entries.
;; TODO: Bind `case-fold-search' around the preamble loop. ;; TODO: Bind `case-fold-search' around the preamble loop.
(unwind-protect
(cond (preamble (cl-loop while (let ((case-fold-search preamble-case-fold)) (cond (preamble (cl-loop while (let ((case-fold-search preamble-case-fold))
(re-search-forward preamble nil t)) (re-search-forward preamble nil t))
do (outline-back-to-heading 'invisible-ok) do (outline-back-to-heading 'invisible-ok)
@ -554,7 +575,10 @@ PREAMBLE-CASE-FOLD."
do (outline-next-heading))) do (outline-next-heading)))
(t (cl-loop when (funcall predicate) (t (cl-loop when (funcall predicate)
collect (funcall action) collect (funcall action)
while (outline-next-heading)))))))) while (outline-next-heading))))
(pcase old-restriction
(`t (widen))
(`(,start . ,end) (narrow-to-region start end)))))))))
;;;;; Helpers ;;;;; Helpers