alphapapa / org-ql

A searching tool for Org-mode, including custom query languages, commands, saved searches and agenda-like views, etc.
GNU General Public License v3.0
1.41k stars 110 forks source link

Passing arguments to :action #33

Open jlumpe opened 5 years ago

jlumpe commented 5 years ago

It would be nice to be able to pass arguments to the function given as :action in org-ql-select. Right now I'm using a lambda with a closure but I think it would cleaner and more convenient if you could do something like ('myfunc arg1 arg2...).

alphapapa commented 5 years ago

Please see the org-ql-select docstring:

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-agenda--format-element', allowing insertion into an Org
  Agenda-like buffer.

- A sexp, which will be byte-compiled into a lambda function.

- A function symbol.
jlumpe commented 5 years ago

I'm not sure any of these work for what I want to do. My function takes an argument which is a plist that it modifies in-place. I'm not super experienced with Elisp but I don't think byte-compiling a sexp would work here?

alphapapa commented 5 years ago

If the argument to your function is static, you can either pass a sexp containing the argument, or pass a lambda which contains it. Of course, in that case, it's not so much an argument as a piece of data embedded in the function.

If the argument to your function is not static, I don't understand what you're trying to do. Where would the argument come from?

You should be able to do anything you need by either passing a lambda or mapping across the results returned by org-ql-select. I don't think it's necessary to add a way to pass arguments to the action function. But if you want to show me exactly what you're trying to do, maybe there's something I'm missing.

jlumpe commented 5 years ago

I'm trying to modify org-ql-select slightly so that it collects not just matching headlines but also their ancestors. To avoid parsing common ancestors multiple times I put all headlines in a flat list and store their indices in a hash table by buffer and position. The action callback parses the current headline if it hasn't already, adds it to the list and hash, recurses to the parent, and returns the index of the current headline. But to do that it needs access to some common state across calls.

Here's the basic version of my code with some made up function names:

(defun -select-with-parents-action (data)
  "Parse current headline and add to list+hash if not already a member. Return index in list."
    (let ((headlines-list (plist-get data :headlines-list))
          (headlines-hash (plist-get data :headlines-hash))
          (key (the-current-buffer-and-position))
          (hashed-index (gethash key headlines-hash)))
      (or hashed-index
        (let ((headline (org-element-headline-parser))
              (parent-index
                (save-excursion-and-go-up-a-headline
                  (-select-with-parents-action data)))
              (this-index (length headlines-list))
          (org-element-set-property headline :parent-index parent-index)
          (puthash key this-index headlines-hash)
          (append-to-list headlines-list headline)
          this-index))))

(defun select-with-parents (files query)
  "Get headlines matching query and their parents/ancestors."
  (let ((data (list :headlines-list nil :headlines-hash (make-hash-table))))
        (action (lambda () (-select-with-parents-action data)))
        (match-indices (org-ql-select files query :action action)))
    (cons (plist-get data :headlines-list) match-indices)))

Now that I think about it, this is probably an edge case. It's likely that most users would only use actions whose arguments are constant for the given query.

alphapapa commented 5 years ago

That's very cool! You may be interested in this WIP branch, which implements a parent selector: https://github.com/alphapapa/org-ql/tree/wip/parent-selector It hasn't been optimized, so it's not very fast, but it works.

I think it needs a bit of refactoring to support a kind of "predicate query", which would return nil or non-nil rather than a list of matching headings, acting like cl-some, that way it wouldn't continue searching after finding a match. If it also had some caching like you've implemented, that could probably make it fast.

Also, see this WIP recursive query function, which returns a parent heading and child headings which match a "sub query": https://github.com/alphapapa/org-ql/blob/master/notes.org#b-recursive-queries You might be able to implement your code in terms of these "recursive queries", although I don't know how performance would compare. I posted an example of using that code here: https://www.reddit.com/r/orgmode/comments/cwuh8k/providing_more_context_of_subtasks_for_tasks_in/eyg6jow/

There are a lot of interesting possibilities to explore here. :) Thanks for your input.

alphapapa commented 1 year ago

FWIW, I think this use case can be solved easily enough by using lexical binding and closures. That is, let-bind the list and hash table around the call to org-ql-select, with the action function being a closure around the list and table variables. For example, see the implementation in org-ql-completing-read: https://github.com/alphapapa/org-ql/blob/4c1a4b169f54d37ce541902c0ae5043759ef9d9b/org-ql-completing-read.el#L107