emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/taxy ade9a02 42/42: Merge branch 'wip/magit-section-col


From: ELPA Syncer
Subject: [elpa] externals/taxy ade9a02 42/42: Merge branch 'wip/magit-section-column-formatting'
Date: Wed, 15 Sep 2021 12:57:34 -0400 (EDT)

branch: externals/taxy
commit ade9a02b25dd100c41c6d96332a829e498f86da1
Merge: 468e9b9 45cdf29
Author: Adam Porter <adam@alphapapa.net>
Commit: Adam Porter <adam@alphapapa.net>

    Merge branch 'wip/magit-section-column-formatting'
---
 README.org            | 352 ++++++++++++++++++++++++------------
 examples/deffy.el     | 300 +++++++++++++++++++++++++++++++
 taxy-magit-section.el | 258 ++++++++++++++++++++++++---
 taxy.el               |  86 +++++++++
 taxy.info             | 480 ++++++++++++++++++++++++++++++++------------------
 5 files changed, 1168 insertions(+), 308 deletions(-)

diff --git a/README.org b/README.org
index c5ed5f3..53d9683 100644
--- a/README.org
+++ b/README.org
@@ -424,10 +424,10 @@ Some example applications may be found in the 
[[file:examples/README.org][exampl
 :TOC:      :include descendants :depth 1 :ignore (descendants)
 :END:
 :CONTENTS:
-- [[#dynamic-taxys][Dynamic taxys]]
 - [[#reusable-taxys][Reusable taxys]]
 - [[#threading-macros][Threading macros]]
 - [[#modifying-filled-taxys][Modifying filled taxys]]
+- [[#dynamic-taxys][Dynamic taxys]]
 - [[#magit-section][Magit section]]
 :END:
 
@@ -449,6 +449,124 @@ After defining a taxy, call ~taxy-fill~ with it and a 
list of objects to fill th
 
 To return a taxy in a more human-readable format (with only relevant fields 
included), use ~taxy-plain~.  You may also use ~taxy-mapcar~ to replace items 
in a taxy with, e.g. a more useful representation.
 
+** Reusable taxys
+
+Since taxys are structs, they may be stored in variables and used in other 
structs (being sure to copy the root taxy with ~taxy-emptied~ before filling).  
For example, this shows using =taxy= to classify Matrix rooms in 
[[https://github.com/alphapapa/ement.el][Ement.el]]:
+
+#+BEGIN_SRC elisp
+  (defun ement-roomy-buffer (room)
+    (alist-get 'buffer (ement-room-local room)))
+
+  (defvar ement-roomy-unread
+    (make-taxy :name "Unread"
+               :predicate (lambda (room)
+                            (buffer-modified-p (ement-roomy-buffer room)))))
+
+  (defvar ement-roomy-opened
+    (make-taxy :name "Opened"
+               :description "Rooms with buffers"
+               :predicate #'ement-roomy-buffer
+               :taxys (list ement-roomy-unread
+                            (make-taxy))))
+
+  (defvar ement-roomy-closed
+    (make-taxy :name "Closed"
+               :description "Rooms without buffers"
+               :predicate (lambda (room)
+                            (not (ement-roomy-buffer room)))))
+
+  (defvar ement-roomy
+    (make-taxy
+     :name "Ement Rooms"
+     :taxys (list (make-taxy
+                   :name "Direct"
+                   :description "Direct messaging rooms"
+                   :predicate (lambda (room)
+                                (ement-room--direct-p room ement-session))
+                   :taxys (list ement-roomy-opened
+                                ement-roomy-closed))
+                  (make-taxy
+                   :name "Non-direct"
+                   :description "Group chat rooms"
+                   :taxys (list ement-roomy-opened
+                                ement-roomy-closed)))))
+#+END_SRC
+
+Note how the taxys defined in the first three variables are used in subsequent 
taxys.  As well, the ~ement-roomy-opened~ taxy has an "anonymous" taxy, which 
collects any rooms that aren't collected by its sibling taxy (otherwise those 
objects would be collected into the parent, "Opened" taxy, which may not always 
be the most useful way to present the objects).
+
+Using those defined taxys, we then fill the ~ement-roomy~ taxy with all of the 
rooms in the user's session, and then use ~taxy-mapcar~ to replace the room 
structs with useful representations for display:
+
+#+BEGIN_SRC elisp
+  (taxy-plain
+   (taxy-mapcar (lambda (room)
+                  (list (ement-room--room-display-name room)
+                        (ement-room-id room)))
+     (taxy-fill (ement-session-rooms ement-session)
+                (taxy-emptied ement-roomy))))
+#+END_SRC
+
+This produces:
+
+#+BEGIN_SRC elisp
+  ("Ement Rooms"
+   (("Direct" "Direct messaging rooms"
+     (("Opened" "Rooms with buffers"
+       (("Unread"
+         (("Lars Ingebrigtsen" "!nope:gnus.org")))))
+      ("Closed" "Rooms without buffers"
+       (("John Wiegley" "!not-really:newartisans.com")
+        ("Eli Zaretskii" "!im-afraid-not:gnu.org")))))
+    ("Non-direct" "Group chat rooms"
+     (("Opened" "Rooms with buffers"
+       (("Unread"
+         (("Emacs" "!WfZsmtnxbxTdoYPkaT:greyface.org")
+          ("#emacs" "!KuaCUVGoCiunYyKEpm:libera.chat")))
+        ;; The non-unread buffers in the "anonymous" taxy.
+        ((("magit/magit" "!HZYimOcmEAsAxOcgpE:gitter.im")
+          ("Ement.el" "!NicAJNwJawmHrEhqZs:matrix.org")
+          ("#emacsconf" "!UjTTDnYmSAslLTtMCF:libera.chat")
+          ("Emacs Matrix Client" "!ZrZoyXEyFrzcBZKNis:matrix.org")
+          ("org-mode" "!rUhEinythPhVTdddsb:matrix.org")
+          ("This Week in Matrix (TWIM)" "!xYvNcQPhnkrdUmYczI:matrix.org")))))
+      ("Closed" "Rooms without buffers"
+       (("#matrix-spec" "!NasysSDfxKxZBzJJoE:matrix.org")
+        ("#commonlisp" "!IiGsrmKRHzpupHRaKS:libera.chat")
+        ("Matrix HQ" "!OGEhHVWSdvArJzumhm:matrix.org")
+        ("#lisp" "!czLxhhEegTEGNKUBgo:libera.chat")
+        ("Emacs" "!gLamGIXTWBaDFfhEeO:matrix.org")
+        ("#matrix-dev:matrix.org" "!jxlRxnrZCsjpjDubDX:matrix.org")))))))
+#+END_SRC
+
+** Threading macros
+
+If you happen to like macros, ~taxy~ works well with threading (i.e. 
~thread-last~ or ~->>~):
+
+#+BEGIN_SRC elisp
+  (thread-last ement-roomy
+    taxy-emptied
+    (taxy-fill (ement-session-rooms ement-session))
+    (taxy-mapcar (lambda (room)
+                   (list (ement-room--room-display-name room)
+                         (ement-room-id room))))
+    taxy-plain)
+#+END_SRC
+
+** Modifying filled taxys
+
+Sometimes it's necessary to modify a taxy after filling it with objects, e.g. 
to sort the items and/or the sub-taxys.  For this, use the function 
~taxy-mapc-taxys~ (a.k.a. ~taxy-mapc*~).  For example, in the sample 
application [[file:examples/musicy.el][musicy.el]], the taxys and their items 
are sorted after filling, like so:
+
+#+BEGIN_SRC elisp
+  (defun musicy-files (files)
+    (thread-last musicy-taxy
+      taxy-emptied
+      (taxy-fill files)
+      ;; Sort sub-taxys by their name.
+      (taxy-sort* #'string< #'taxy-name)
+      ;; Sort sub-taxys' items by name.
+      (taxy-sort #'string< #'identity)
+      taxy-magit-section-pp))
+#+END_SRC
+
 ** Dynamic taxys
 :PROPERTIES:
 :TOC:      :include descendants
@@ -456,6 +574,7 @@ To return a taxy in a more human-readable format (with only 
relevant fields incl
 :CONTENTS:
 - [[#multi-level-dynamic-taxys][Multi-level dynamic taxys]]
 - [[#chains-of-independent-multi-level-dynamic-taxys]["Chains" of independent, 
multi-level dynamic taxys]]
+- [[#defining-a-classification-domain-specific-language][Defining a 
classification domain-specific language]]
 :END:
 
 You may not always know in advance what taxonomy a set of objects fits into, 
so =taxy= lets you add taxys dynamically by using the ~:take~ function to add a 
taxy when an object is "taken into" a parent taxy's items.  For example, you 
could dynamically classify buffers by their major mode like so:
@@ -623,123 +742,117 @@ Now let's fill the taxy with the sports and format it:
        ("Soccer" "Tennis" "Football" "Baseball"))))))
 #+END_SRC
 
-** Reusable taxys
-
-Since taxys are structs, they may be stored in variables and used in other 
structs (being sure to copy the root taxy with ~taxy-emptied~ before filling).  
For example, this shows using =taxy= to classify Matrix rooms in 
[[https://github.com/alphapapa/ement.el][Ement.el]]:
-
-#+BEGIN_SRC elisp
-  (defun ement-roomy-buffer (room)
-    (alist-get 'buffer (ement-room-local room)))
-
-  (defvar ement-roomy-unread
-    (make-taxy :name "Unread"
-               :predicate (lambda (room)
-                            (buffer-modified-p (ement-roomy-buffer room)))))
-
-  (defvar ement-roomy-opened
-    (make-taxy :name "Opened"
-               :description "Rooms with buffers"
-               :predicate #'ement-roomy-buffer
-               :taxys (list ement-roomy-unread
-                            (make-taxy))))
-
-  (defvar ement-roomy-closed
-    (make-taxy :name "Closed"
-               :description "Rooms without buffers"
-               :predicate (lambda (room)
-                            (not (ement-roomy-buffer room)))))
-
-  (defvar ement-roomy
-    (make-taxy
-     :name "Ement Rooms"
-     :taxys (list (make-taxy
-                   :name "Direct"
-                   :description "Direct messaging rooms"
-                   :predicate (lambda (room)
-                                (ement-room--direct-p room ement-session))
-                   :taxys (list ement-roomy-opened
-                                ement-roomy-closed))
-                  (make-taxy
-                   :name "Non-direct"
-                   :description "Group chat rooms"
-                   :taxys (list ement-roomy-opened
-                                ement-roomy-closed)))))
-#+END_SRC
-
-Note how the taxys defined in the first three variables are used in subsequent 
taxys.  As well, the ~ement-roomy-opened~ taxy has an "anonymous" taxy, which 
collects any rooms that aren't collected by its sibling taxy (otherwise those 
objects would be collected into the parent, "Opened" taxy, which may not always 
be the most useful way to present the objects).
-
-Using those defined taxys, we then fill the ~ement-roomy~ taxy with all of the 
rooms in the user's session, and then use ~taxy-mapcar~ to replace the room 
structs with useful representations for display:
-
-#+BEGIN_SRC elisp
-  (taxy-plain
-   (taxy-mapcar (lambda (room)
-                  (list (ement-room--room-display-name room)
-                        (ement-room-id room)))
-     (taxy-fill (ement-session-rooms ement-session)
-                (taxy-emptied ement-roomy))))
-#+END_SRC
-
-This produces:
-
-#+BEGIN_SRC elisp
-  ("Ement Rooms"
-   (("Direct" "Direct messaging rooms"
-     (("Opened" "Rooms with buffers"
-       (("Unread"
-         (("Lars Ingebrigtsen" "!nope:gnus.org")))))
-      ("Closed" "Rooms without buffers"
-       (("John Wiegley" "!not-really:newartisans.com")
-        ("Eli Zaretskii" "!im-afraid-not:gnu.org")))))
-    ("Non-direct" "Group chat rooms"
-     (("Opened" "Rooms with buffers"
-       (("Unread"
-         (("Emacs" "!WfZsmtnxbxTdoYPkaT:greyface.org")
-          ("#emacs" "!KuaCUVGoCiunYyKEpm:libera.chat")))
-        ;; The non-unread buffers in the "anonymous" taxy.
-        ((("magit/magit" "!HZYimOcmEAsAxOcgpE:gitter.im")
-          ("Ement.el" "!NicAJNwJawmHrEhqZs:matrix.org")
-          ("#emacsconf" "!UjTTDnYmSAslLTtMCF:libera.chat")
-          ("Emacs Matrix Client" "!ZrZoyXEyFrzcBZKNis:matrix.org")
-          ("org-mode" "!rUhEinythPhVTdddsb:matrix.org")
-          ("This Week in Matrix (TWIM)" "!xYvNcQPhnkrdUmYczI:matrix.org")))))
-      ("Closed" "Rooms without buffers"
-       (("#matrix-spec" "!NasysSDfxKxZBzJJoE:matrix.org")
-        ("#commonlisp" "!IiGsrmKRHzpupHRaKS:libera.chat")
-        ("Matrix HQ" "!OGEhHVWSdvArJzumhm:matrix.org")
-        ("#lisp" "!czLxhhEegTEGNKUBgo:libera.chat")
-        ("Emacs" "!gLamGIXTWBaDFfhEeO:matrix.org")
-        ("#matrix-dev:matrix.org" "!jxlRxnrZCsjpjDubDX:matrix.org")))))))
-#+END_SRC
-
-** Threading macros
-
-If you happen to like macros, ~taxy~ works well with threading (i.e. 
~thread-last~ or ~->>~):
-
-#+BEGIN_SRC elisp
-  (thread-last ement-roomy
-    taxy-emptied
-    (taxy-fill (ement-session-rooms ement-session))
-    (taxy-mapcar (lambda (room)
-                   (list (ement-room--room-display-name room)
-                         (ement-room-id room))))
-    taxy-plain)
-#+END_SRC
-
-** Modifying filled taxys
+*** Defining a classification domain-specific language
+
+When writing a larger Taxy-based application, it may be necessary to define a 
number of key functions that would be unwieldy to manage in a ~cl-labels~ form. 
 For this case, Taxy provides the macro ~taxy-define-key-definer~ to easily 
define Taxy key functions in an application library.  Those functions are then 
passed to the function ~taxy-make-take-function~ at runtime, along with a list 
of keys being used to classify items.  Using these allows key functions to be 
defined in top-level f [...]
+
+Extending the previous ~sporty~ example, let's redefine its key functions 
using ~taxy-define-key-definer~:
+
+#+begin_src elisp :exports code :results silent :lexical t
+  (taxy-define-key-definer sporty-define-key
+    sporty-keys "sporty"
+    "Define a `sporty' key function.")
+
+  (sporty-define-key disc-based ()
+    (if (member 'disc (sport-uses item))
+        "Disc-based"
+      "Non-disc-based"))
+
+  (sporty-define-key uses (&optional thing)
+    (pcase thing
+      (`nil (sport-uses item))
+      (_ (when (cl-typecase (sport-uses item)
+                 (symbol (equal thing (sport-uses item)))
+                 (list (member thing (sport-uses item))))
+           thing))))
+
+  (sporty-define-key venue (&optional place)
+    (pcase place
+      (`nil (sport-venue item))
+      (_ (when (equal place (sport-venue item))
+           (sport-venue item)))))
+#+end_src
+
+Now we'll define the default keys to use when classifying items.  This list is 
equivalent to the one passed to ~taxy-take-keyed~ in the previous, "Chains" 
example.
+
+#+begin_src elisp :exports code :results silent :lexical t
+  (defvar sporty-default-keys
+    '(
+      ((venue 'outdoor)
+       disc-based)
+
+      ((venue 'indoor)
+       (uses 'ball)
+       (uses 'disc)
+       (uses 'glove)
+       (uses 'racket))))
+#+end_src
+
+Finally, rather than using a pre-made taxy struct, we make one at runtime, 
making the ~:take~ function with ~taxy-make-take-function~.
+
+#+begin_src elisp :exports both :results code :lexical t
+  (let ((taxy (make-taxy
+               :name "Sporty (DSL)"
+               :take (taxy-make-take-function sporty-default-keys
+                                              sporty-keys))))
+    (thread-last taxy
+      (taxy-fill sports)
+      (taxy-mapcar #'sport-name)
+      taxy-plain))
+#+end_src
 
-Sometimes it's necessary to modify a taxy after filling it with objects, e.g. 
to sort the items and/or the sub-taxys.  For this, use the function 
~taxy-mapc-taxys~ (a.k.a. ~taxy-mapc*~).  For example, in the sample 
application [[file:examples/musicy.el][musicy.el]], the taxys and their items 
are sorted after filling, like so:
+Which gives us:
 
-#+BEGIN_SRC elisp
-  (defun musicy-files (files)
-    (thread-last musicy-taxy
-      taxy-emptied
-      (taxy-fill files)
-      ;; Sort sub-taxys by their name.
-      (taxy-sort* #'string< #'taxy-name)
-      ;; Sort sub-taxys' items by name.
-      (taxy-sort #'string< #'identity)
-      taxy-magit-section-pp))
-#+END_SRC
+#+RESULTS:
+#+begin_src elisp
+  ("Sporty (DSL)"
+   ((indoor
+     ((ball
+       ("Volleyball" "Basketball")
+       ((glove
+         ("Handball"))
+        (racket
+         ("Racquetball"))))))
+    (outdoor
+     (("Disc-based"
+       ("Ultimate" "Disc golf"))
+      ("Non-disc-based"
+       ("Soccer" "Tennis" "Football" "Baseball"))))))
+#+end_src
+
+As you can see, the result is the same as that in the previous example, but 
we've defined a kind of DSL for grouping sports in a modular, extendable way.
+
+This also allows the grouping keys to be easily changed at runtime, producing 
a different result.  For example, we could group sports by, first, whether they 
use a ball, and then by venue.  Let's do this in a function so that users can 
pass their own list of keys:
+
+#+begin_src elisp :exports both :results code :lexical t
+  (cl-defun sporty-classify (sports &key (keys sporty-default-keys))
+    (declare (indent defun))
+    (let* ((taxy (make-taxy
+                  :name "Sporty (DSL)"
+                  :take (taxy-make-take-function keys
+                                                 sporty-keys))))
+      (thread-last taxy
+        (taxy-fill sports)
+        (taxy-mapcar #'sport-name)
+        taxy-plain)))
+
+  (sporty-classify sports
+    :keys '((uses 'ball) venue))
+#+end_src
+
+And this produces:
+
+#+RESULTS:
+#+begin_src elisp
+  ("Sporty (DSL)"
+   ((outdoor
+     ("Ultimate" "Disc golf"))
+    (ball
+     ((indoor
+       ("Volleyball" "Handball" "Racquetball" "Basketball"))
+      (outdoor
+       ("Soccer" "Tennis" "Football" "Baseball"))))))
+#+end_src
 
 ** Magit section
 
@@ -773,6 +886,13 @@ Note that while =taxy-magit-section.el= is installed with 
the =taxy= package, th
 + Sorting functions:
   + ~taxy-sort-items~ (alias: ~taxy-sort~) sorts the items in a taxy and its 
sub-taxys.
   + ~taxy-sort-taxys~ (alias: ~taxy-sort*~) sorts a taxy's sub-taxys.
++ Defining classification domain-specific languages:
+  + Macro ~taxy-define-key-definer~ defines a key-function-defining macro.
+  + Function ~taxy-make-take-function~ makes a ~:take~ function using a list 
of key functions and a set of classification keys.
++ Table-like, column-based formatting system for ~taxy-magit-section~:
+  + Function ~taxy-magit-section-format-items~, which formats items by columns.
+  + Variable ~taxy-magit-section-insert-indent-items~, which controls whether 
~taxy-magit-section-insert~ applies indentation to each item.  (Used to disable 
that behavior when items are pre-indented strings, e.g. as formatted by 
~taxy-magit-section-format-items~.)
++ Example application =deffy=, which shows an overview of top-level 
definitions and forms in an Elisp project or file.  (Likely to be published as 
a separate package later.)
 
 ** 0.5
 
diff --git a/examples/deffy.el b/examples/deffy.el
new file mode 100644
index 0000000..c47105f
--- /dev/null
+++ b/examples/deffy.el
@@ -0,0 +1,300 @@
+;;; deffy.el --- Show definitions in an Elisp project/buffer  -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2021  Free Software Foundation, Inc.
+
+;; Author: Adam Porter <adam@alphapapa.net>
+;; Keywords: convenience, lisp
+
+;; 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/>.
+
+;;; Commentary:
+
+;; This library provides commands that show top-level forms and
+;; definitions found in an Emacs Lisp project or buffer, organized by
+;; file and type.
+
+;;; Code:
+
+(require 'taxy-magit-section)
+
+(cl-defstruct deffy-def
+  ;; Okay, the name of this struct is silly, but at least it's concise.
+  file pos form)
+
+(defgroup deffy nil
+  "Show an overview of definitions in an Emacs Lisp project or buffer."
+  :group 'emacs-lisp-mode)
+
+;;;; Keys
+
+(taxy-define-key-definer deffy-define-key deffy-keys "deffy"
+  "FIXME: Docstring.")
+
+(deffy-define-key file ()
+  (file-relative-name (deffy-def-file item) deffy-directory))
+
+(deffy-define-key type ()
+  (pcase-let* (((cl-struct deffy-def form) item)
+              (type (pcase form
+                      (`(,(or 'defun 'cl-defun) . ,_)
+                       (if (cl-find-if (lambda (form)
+                                         (pcase form
+                                           (`(interactive . ,_) t)))
+                                       form)
+                           'command
+                         'function))
+                      (`(,(or 'defmacro 'cl-defmacro) . ,_)
+                       'macro)
+                      (`(,car . ,_) car))))
+    (when type
+      (format "%s" type))))
+
+(defvar deffy-taxy-default-keys
+  '(type file))
+
+;;;; Columns
+
+(taxy-magit-section-define-column-definer "deffy")
+
+(deffy-define-column "Definition" (:max-width 45 :face 
font-lock-function-name-face)
+  (let ((form-defines (pcase-exhaustive (cadr (deffy-def-form item))
+                       ((and (pred atom) it) it)
+                       (`(quote ,it) it)
+                       (`(,it . ,_) it))))
+    (format "%s" form-defines)))
+
+(deffy-define-column "Type" (:max-width 25 :face font-lock-type-face)
+  (format "%s" (car (deffy-def-form item))))
+
+(deffy-define-column "Docstring" (:max-width nil :face font-lock-doc-face)
+  (when-let ((docstring
+             (pcase (deffy-def-form item)
+               (`(,(or 'defun 'cl-defun 'defmacro 'cl-defmacro) ,_name ,_args
+                  ,(and (pred stringp) docstring) . ,_)
+                docstring)
+               (`(,(or 'defvar 'defvar-local 'defcustom) ,_name ,_value
+                  ,(and (pred stringp) docstring) . ,_)
+                docstring)
+               (_ ;; Use the first string found, if any.
+                (cl-find-if #'stringp (deffy-def-form item))))))
+    (replace-regexp-in-string "\n" "  " docstring)))
+
+(unless deffy-columns
+  ;; TODO: Automate this or document it
+  (setq-default deffy-columns
+               (get 'deffy-columns 'standard-value)))
+
+;;;; Variables
+
+(defvar deffy-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "RET") #'deffy-RET)
+    (define-key map [mouse-1] #'deffy-mouse-1)
+    map))
+
+(defvar-local deffy-directory nil
+  "Directory relative to which filenames should be expanded.")
+
+(defvar-local deffy-files nil
+  "Files shown in the current Deffy buffer.")
+
+(defvar-local deffy-display-buffer-action nil
+  "Last-used display-buffer-action in the current Deffy buffer.")
+
+;;;; Options
+
+(defcustom deffy-side-window-action
+  '(display-buffer-in-side-window
+    (side . right)
+    (window-parameters
+     (window-side . right)
+     (no-delete-other-windows . t)))
+  "`display-buffer' action used when displaying Deffy buffer in a side window.
+See Info node `(elisp)Displaying Buffers in Side Windows'."
+  :type 'sexp)
+
+;;;; Commands
+
+;;;###autoload
+(cl-defun deffy (&key (project (or (project-current)
+                                  (cons 'transient default-directory)))
+                     (keys deffy-taxy-default-keys)
+                     (files deffy-files)
+                     (buffer-name (format "*Deffy: %s*"
+                                          (if files
+                                              (string-join (mapcar 
#'file-relative-name files) ", ")
+                                            (file-name-nondirectory
+                                             (directory-file-name 
(project-root project))))))
+                     visibility-fn display-buffer-action)
+  "Show definitions defined in PROJECT or FILES.
+Interactively, with PREFIX, show only definitions in current
+buffer."
+  (interactive (list :files (when current-prefix-arg
+                             (list (buffer-file-name)))
+                    :keys (if current-prefix-arg
+                              (remove 'file deffy-taxy-default-keys)
+                            deffy-taxy-default-keys)))
+  (let (format-table column-sizes)
+    (cl-labels (;; (heading-face
+               ;;  (depth) (list :inherit (list 'bufler-group 
(bufler-level-face depth))))
+               (elisp-file-p (file) (string-match-p (rx ".el" (optional ".gz") 
eos) file))
+               (file-visible-p
+                (file) (not (string-match-p (rx bos ".") 
(file-name-nondirectory file))))
+               (format-item (item) (gethash item format-table))
+               (make-fn (&rest args)
+                        (apply #'make-taxy-magit-section
+                               :make #'make-fn
+                               :format-fn #'format-item
+                               :heading-indent deffy-level-indent
+                               :visibility-fn visibility-fn
+                               ;; :heading-face-fn #'heading-face
+                               args))
+               (def-name (def) (format "%s" (cl-second (deffy-def-form def)))))
+      (when (get-buffer buffer-name)
+       (kill-buffer buffer-name))
+      (with-current-buffer (get-buffer-create buffer-name)
+       (deffy-mode)
+       (setq-local deffy-taxy-default-keys keys
+                   deffy-directory (project-root project)
+                   deffy-files files
+                   deffy-display-buffer-action display-buffer-action
+                   default-directory deffy-directory)
+       (setf files (cl-reduce #'cl-remove-if-not (list #'elisp-file-p 
#'file-visible-p)
+                              :initial-value (or files (project-files project))
+                              :from-end t))
+       (cl-assert files nil "No files to show")
+       (let* ((forms (apply #'append (mapcar #'deffy--file-forms files)))
+              (taxy (thread-last
+                        (make-fn
+                         :name "Deffy"
+                         :description (format "Definitions in %s:"
+                                              (if files
+                                                  (string-join (mapcar 
#'file-relative-name files) ", ")
+                                                (file-name-nondirectory
+                                                 (directory-file-name 
(project-root project)))))
+                         :take (taxy-make-take-function keys deffy-keys))
+                      (taxy-fill forms)
+                      (taxy-sort* #'string< #'taxy-name)
+                      (taxy-sort #'string< #'def-name)))
+              (taxy-magit-section-insert-indent-items nil)
+              (inhibit-read-only t)
+              (format-cons (taxy-magit-section-format-items
+                            deffy-columns deffy-column-formatters taxy)))
+         (setf format-table (car format-cons)
+               column-sizes (cdr format-cons)
+               header-line-format (taxy-magit-section-format-header
+                                   column-sizes deffy-column-formatters))
+         (save-excursion
+           (taxy-magit-section-insert taxy :items 'last
+             ;; :blank-between-depth bufler-taxy-blank-between-depth
+             :initial-depth 0))))
+      (pop-to-buffer buffer-name display-buffer-action))))
+
+;;;###autoload
+(cl-defun deffy-buffer
+    (&optional (buffer (current-buffer))
+              &key display-buffer-action)
+  "Show an Deffy view for BUFFER.
+Interactively, with prefix, display in dedicated side window."
+  (interactive
+   (list (current-buffer)
+        :display-buffer-action (when current-prefix-arg
+                                 deffy-side-window-action)))
+  (unless (buffer-file-name buffer)
+    (user-error "Buffer is not file-backed: %S.  See command `deffy-project'"
+               buffer))
+  (deffy :files (list (buffer-file-name buffer))
+    :keys (remove 'file deffy-taxy-default-keys)
+    :display-buffer-action display-buffer-action))
+
+(cl-defun deffy-project (&optional project &key display-buffer-action)
+  "Show an Deffy view for PROJECT.
+Interactively, with prefix, display in dedicated side window."
+  (interactive
+   (list nil :display-buffer-action (when current-prefix-arg
+                                     deffy-side-window-action)))
+  (deffy :project (or project
+                     (project-current)
+                     (cons 'transient default-directory))
+    :display-buffer-action display-buffer-action))
+
+(defun deffy-revert (_ignore-auto _noconfirm)
+  "Revert current Deffy buffer."
+  (interactive)
+  (deffy :display-buffer-action (or deffy-display-buffer-action
+                                   '((display-buffer-same-window)))))
+
+(defun deffy-goto-form ()
+  "Go to form at point."
+  (interactive)
+  (pcase-let (((cl-struct deffy-def file pos)
+              (oref (magit-current-section) value)))
+    (pop-to-buffer
+     (or (find-buffer-visiting file)
+        (find-file-noselect file))
+     `(display-buffer-in-previous-window
+       (previous-window . ,(get-mru-window))))
+    (goto-char pos)
+    (backward-sexp 1)))
+
+(defun deffy-mouse-1 (event)
+  (interactive "e")
+  (mouse-set-point event)
+  (call-interactively #'deffy-RET))
+
+(defun deffy-RET ()
+  (interactive)
+  (cl-etypecase (oref (magit-current-section) value)
+    (deffy-def (call-interactively #'deffy-goto-form))
+    (taxy-magit-section (call-interactively #'magit-section-cycle))
+    (null nil)))
+
+(define-derived-mode deffy-mode magit-section-mode "Deffy"
+  :global nil
+  (setq-local bookmark-make-record-function #'deffy--bookmark-make-record
+             revert-buffer-function #'deffy-revert))
+
+;;;; Functions
+
+(cl-defun deffy--file-forms (file)
+  "Return forms defined in FILE."
+  (with-temp-buffer
+    (save-excursion
+      (insert-file-contents file))
+    (cl-loop for form = (ignore-errors
+                         (read (current-buffer)))
+            while form
+            when (listp form)
+            collect (make-deffy-def :file file :pos (point) :form form))))
+
+;;;;; Bookmark support
+
+(defvar bookmark-make-record-function)
+
+(defun deffy--bookmark-make-record ()
+  "Return a bookmark record for current Deffy buffer."
+  (list (concat "Deffy: %s" deffy-directory)
+       (cons 'directory deffy-directory)
+       (cons 'files deffy-files)
+       (cons 'handler #'deffy--bookmark-handler)))
+
+(defun deffy--bookmark-handler (record)
+  "Show Deffy buffer for bookmark RECORD."
+  (pcase-let* ((`(,_ . ,(map directory files)) record))
+    (deffy :files files :project (project-current nil directory))
+    (current-buffer)))
+
+(provide 'deffy)
+
+;;; deffy.el ends here
diff --git a/taxy-magit-section.el b/taxy-magit-section.el
index 49a573a..00dd217 100644
--- a/taxy-magit-section.el
+++ b/taxy-magit-section.el
@@ -32,8 +32,22 @@
 
 ;;;; Variables
 
-(defvar taxy-magit-section-indent 2
-  "Default indentation per level.")
+(defvar taxy-magit-section-heading-indent 2
+  "Default heading indentation per level.")
+
+(defvar taxy-magit-section-item-indent 2
+  "Default item indentation per level.")
+
+(defvar taxy-magit-section-depth nil
+  "Bound to current depth around calls to a taxy's format-fn.")
+
+(defvar taxy-magit-section-insert-indent-items t
+  ;; NOTE: I hate to use a variable to control this, but it seems like
+  ;; the cleanest way for now.
+  "Whether to indent items in `taxy-magit-section-insert'.
+May be disabled when `taxy-magit-section-insert' should not
+indent items itself, e.g. if items are pre-indented.  Note that
+this does not disable indentation of section headings.")
 
 ;;;; Customization
 
@@ -58,9 +72,11 @@
   ;; inheritance easier (and/or use EIEIO, but that would reduce
   ;; performance, since slot accessors can't be optimized).
   (visibility-fn #'taxy-magit-section-visibility)
-  (heading-face (lambda (_depth) 'magit-section-heading))
-  (indent 2)
-  format-fn)
+  (heading-face-fn (lambda (_depth) 'magit-section-heading))
+  ;; TODO: Rename heading-indent slot to level-indent.
+  (heading-indent 2)
+  (item-indent 2)
+  (format-fn #'prin1-to-string))
 
 ;;;; Commands
 
@@ -78,7 +94,7 @@ which blank lines are inserted between sections at that 
level."
   (let* ((magit-section-set-visibility-hook
           (cons #'taxy-magit-section-visibility 
magit-section-set-visibility-hook)))
     (cl-labels ((insert-item
-                 (item format-fn depth)
+                 (item taxy depth)
                  (magit-insert-section (magit-section item)
                    (magit-insert-section-body
                     ;; This is a tedious way to give the indent
@@ -88,10 +104,12 @@ which blank lines are inserted between sections at that 
level."
                     ;; something was wrong about the properties, and
                     ;; `magit-section' didn't navigate the sections
                     ;; properly anymore.
-                    (let* ((formatted (funcall format-fn item))
-                           (indent-size (pcase depth
-                                           ((pred (> 0)) 0)
-                                           (_ (* depth 
taxy-magit-section-indent))))
+                    (let* ((formatted (funcall (taxy-magit-section-format-fn 
taxy) item))
+                           (indent-size (if (or (not 
taxy-magit-section-insert-indent-items)
+                                                (< depth 0))
+                                            0
+                                          (+ (* depth 
(taxy-magit-section-heading-indent taxy))
+                                             (taxy-magit-section-item-indent 
taxy))))
                             (indent-string (make-string indent-size ? )))
                       (add-text-properties 0 (length indent-string)
                                            (text-properties-at 0 formatted)
@@ -99,23 +117,19 @@ which blank lines are inserted between sections at that 
level."
                       (insert indent-string formatted "\n")))))
                 (insert-taxy
                  (taxy depth) (let ((magit-section-set-visibility-hook 
magit-section-set-visibility-hook)
-                                    (format-fn (cl-typecase taxy
-                                                 (taxy-magit-section
-                                                  
(taxy-magit-section-format-fn taxy))
-                                                 (t (lambda (o) (format "%s" 
o)))))
-                                    (taxy-magit-section-indent 
(taxy-magit-section-indent taxy)))
+                                    (taxy-magit-section-heading-indent 
(taxy-magit-section-heading-indent taxy))
+                                    (taxy-magit-section-item-indent 
(taxy-magit-section-item-indent taxy)))
                                 (cl-typecase taxy
                                   (taxy-magit-section
                                    (when (taxy-magit-section-visibility-fn 
taxy)
                                      (push (taxy-magit-section-visibility-fn 
taxy) magit-section-set-visibility-hook))))
                                 (magit-insert-section (magit-section taxy)
                                   (magit-insert-heading
-                                    (make-string (* (pcase depth
-                                                      ((pred (> 0)) 0)
-                                                      (_ depth))
-                                                    (taxy-magit-section-indent 
taxy)) ? )
+                                    (make-string (* (if (< depth 0) 0 depth)
+                                                    
(taxy-magit-section-heading-indent taxy))
+                                                ? )
                                     (propertize (taxy-name taxy)
-                                                'face (funcall 
(taxy-magit-section-heading-face taxy) depth))
+                                                'face (funcall 
(taxy-magit-section-heading-face-fn taxy) depth))
                                     (format " (%s%s)"
                                             (if (taxy-description taxy)
                                                 (concat (taxy-description 
taxy) " ")
@@ -124,12 +138,12 @@ which blank lines are inserted between sections at that 
level."
                                   (magit-insert-section-body
                                     (when (eq 'first items)
                                       (dolist (item (taxy-items taxy))
-                                        (insert-item item format-fn depth)))
+                                        (insert-item item taxy depth)))
                                     (dolist (taxy (taxy-taxys taxy))
                                       (insert-taxy taxy (1+ depth)))
                                     (when (eq 'last items)
                                       (dolist (item (taxy-items taxy))
-                                        (insert-item item format-fn depth))))
+                                        (insert-item item taxy depth))))
                                   (when (<= depth blank-between-depth)
                                     (insert "\n"))))))
       (magit-insert-section (magit-section)
@@ -155,6 +169,206 @@ Default visibility function for
        (_ 'show)))
     (_ nil)))
 
+;;;; Column-based formatting
+
+;; Column-based, or "table"?
+
+;; MAYBE: Move this to a separate library, since it's not directly
+;; related to using taxy or magit-section.  Maybe it could be called
+;; something like `flextab' (or, keeping with the theme, `tabley').
+;; But see also <https://github.com/kiwanami/emacs-ctable>.
+
+;;;;; Macros
+
+(cl-defmacro taxy-magit-section-define-column-definer (prefix &key 
columns-variable-docstring)
+  "Define a column-defining macro.
+The macro is named \"PREFIX-define-column\".
+
+These customization options are defined, which are to be used in
+a `taxy-magit-section' in its `:heading-indent' and
+`:item-indent' slots, respectively:
+
+  - PREFIX-level-indent
+  - PREFIX-item-indent
+
+As well as these variables, which are to be passed to
+`taxy-magit-section-format-items':
+
+  - PREFIX-columns
+  - PREFIX-column-formatters"
+  ;; TODO: Document this.
+  (let* ((definer-name (intern (format "%s-define-column" prefix)))
+        (definer-docstring (format "Define a column formatting function with 
NAME.
+NAME should be a string.  BODY should return a string or nil.  In
+the BODY, `item' is bound to the item being formatted, and `depth' is
+bound to the item's depth in the hierarchy.
+
+PLIST may be a plist setting the following options:
+
+  `:align' may be `left' or `right' to align the column
+  accordingly.
+
+  `:face' is a face applied to the string.
+
+  `:max-width' defines a customization option for the column's
+  maximum width with the specified value as its default: an
+  integer limits the width, while nil does not."))
+        (level-indent-variable-name (intern (format "%s-level-indent" prefix)))
+        (level-indent-docstring (format "Indentation applied to each level of 
depth for `%s' columns."
+                                        prefix))
+        (item-indent-variable-name (intern (format "%s-item-indent" prefix)))
+        (item-indent-docstring (format "Indentation applied to each item for 
`%s' columns."
+                                       prefix))
+        (columns-variable-name (intern (format "%s-columns" prefix)))
+        (columns-variable-docstring (or columns-variable-docstring
+                                        (format "Columns defined by `%s'."
+                                                definer-name)))
+        (column-formatters-variable-name (intern (format 
"%s-column-formatters" prefix)))
+        (column-formatters-variable-docstring (format "Column formatters 
defined by `%s'."
+                                                      definer-name)))
+    `(let ((columns-variable ',columns-variable-name)
+          (column-formatters-variable ',column-formatters-variable-name))
+       (defcustom ,level-indent-variable-name 2
+        ,level-indent-docstring
+        :type 'integer)
+       (defcustom ,item-indent-variable-name 2
+        ,item-indent-docstring
+        :type 'integer)
+       (defvar ,columns-variable-name nil
+        ,columns-variable-docstring)
+       (defvar ,column-formatters-variable-name nil
+        ,column-formatters-variable-docstring)
+       (defmacro ,definer-name (name plist &rest body)
+        ,definer-docstring
+        (declare (indent defun))
+        (cl-check-type name string)
+        (pcase-let* ((fn-name (intern (concat ,prefix "-column-format-" 
(downcase name))))
+                     (columns-variable-name ',columns-variable-name)
+                     (level-indent-variable-name ',level-indent-variable-name)
+                     (item-indent-variable-name ',item-indent-variable-name)
+                     ((map (:face face) (:max-width max-width)) plist)
+                     (max-width-variable (intern (concat ,prefix "-column-" 
name "-max-width")))
+                     (max-width-docstring (format "Maximum width of the %s 
column." name)))
+          `(progn
+             ,(when (plist-member plist :max-width)
+                `(defcustom ,max-width-variable
+                   ,max-width
+                   ,max-width-docstring
+                   :type '(choice (integer :tag "Maximum width")
+                                  (const :tag "Unlimited width" nil))))
+             (defun ,fn-name (item depth)
+               (if-let ((string (progn ,@body)))
+                   (progn
+                     ,(when max-width
+                        `(when ,max-width-variable
+                           (setf string (truncate-string-to-width string 
,max-width-variable nil nil "…"))))
+                     ,(when face
+                        ;; Faces are not defined until load time, while this 
checks type at expansion
+                        ;; time, so we can only test that the argument is a 
symbol, not a face.
+                        (cl-check-type face symbol ":face must be a face 
symbol")
+                        `(setf string (propertize string 'face ',face)))
+                     (when (equal ,name (car ,columns-variable-name))
+                       ;; First column: apply indentation.
+                       (let ((indentation (make-string (+ (* depth 
,level-indent-variable-name)
+                                                          
,item-indent-variable-name)
+                                                       ? )))
+                         (setf string (concat indentation string))))
+                     string)
+                 ""))
+             (setf (alist-get 'formatter (alist-get ,name 
,column-formatters-variable nil nil #'equal))
+                   #',fn-name)
+             (setf (alist-get 'align (alist-get ,name 
,column-formatters-variable nil nil #'equal))
+                   ,(plist-get plist :align))
+             ;; Add column to the columns-variable's standard value.
+             (unless (member ,name (get ',columns-variable 'standard-value))
+               (setf (get ',columns-variable 'standard-value)
+                     (append (get ',columns-variable 'standard-value)
+                             (list ,name))))
+             ;; Add column to the columns-variable's custom type.
+             (cl-pushnew ,name (get ',columns-variable 'custom-type)
+                         :test #'equal)))))))
+
+;;;;; Functions
+
+;; MAYBE: Consider using spaces with `:align-to', rather than formatting 
strings with indentation, as used by `epkg'
+;; (see 
<https://github.com/emacscollective/epkg/blob/edf8c009066360af61caedf67a2482eaa19481b0/epkg-desc.el#L363>).
+;; I'm not sure which would perform better; I guess that with many lines, 
redisplay might take longer to use the
+;; display properties for alignment than just having pre-aligned lines of text.
+
+(defun taxy-magit-section-format-items (columns formatters taxy)
+  ;; TODO: Document this.
+  "Return a cons (table . column-sizes) for COLUMNS, FORMATTERS, and TAXY.
+COLUMNS is a list of column names, each of which should have an
+associated formatting function in FORMATTERS.
+
+Table is a hash table keyed by item whose values are display
+strings.  Column-sizes is an alist whose keys are column names
+and values are the column width.  Each string is formatted
+according to `columns' and takes into account the width of all
+the items' values for each column."
+  (let ((table (make-hash-table))
+       column-aligns column-sizes)
+    (cl-labels ((format-column
+                (item depth column-name)
+                (let* ((column-alist (alist-get column-name formatters nil nil 
#'equal))
+                       (fn (alist-get 'formatter column-alist))
+                       (value (funcall fn item depth))
+                       (current-column-size (or (map-elt column-sizes 
column-name) 0)))
+                  (setf (map-elt column-sizes column-name)
+                        (max current-column-size (string-width value)))
+                  (setf (map-elt column-aligns column-name)
+                        (or (alist-get 'align column-alist)
+                            'left))
+                  value))
+               (format-item
+                (depth item) (puthash item
+                                      (cl-loop for column in columns
+                                               collect (format-column item 
depth column))
+                                      table))
+               (format-taxy (depth taxy)
+                            (dolist (item (taxy-items taxy))
+                              (format-item depth item))
+                            (dolist (taxy (taxy-taxys taxy))
+                              (format-taxy (1+ depth) taxy))))
+      (format-taxy 0 taxy)
+      ;; Now format each item's string using the column sizes.
+      (let* ((column-sizes (nreverse column-sizes))
+            (format-string
+             (string-join
+              (cl-loop for (name . size) in column-sizes
+                       for align = (pcase-exhaustive (alist-get name 
column-aligns nil nil #'equal)
+                                     ((or `nil 'left) "-")
+                                     ('right ""))
+                       collect (format "%%%s%ss" align size))
+              " ")))
+       (maphash (lambda (item column-values)
+                  (puthash item (apply #'format format-string column-values)
+                           table))
+                table)
+       (cons table column-sizes)))))
+
+(defun taxy-magit-section-format-header (column-sizes formatters)
+  ;; TODO: Document this.
+  "Return header string for COLUMN-SIZES and FORMATTERS.
+COLUMN-SIZES should be the CDR of the cell returned by
+`taxy-magit-section-format-items'.  FORMATTERS should be the
+variable passed to that function, which see."
+  (let* ((first-column-name (caar column-sizes))
+        (first-column-alist (alist-get first-column-name formatters nil nil 
#'equal))
+        (first-column-align (pcase-exhaustive (alist-get 'align 
first-column-alist)
+                              ((or `nil 'left) "-")
+                              ('right ""))))
+    (concat (format (format " %%%s%ss"
+                           first-column-align (cdar column-sizes))
+                   (caar column-sizes))
+           (cl-loop for (name . size) in (cdr column-sizes)
+                    for column-alist = (alist-get name formatters nil nil 
#'equal)
+                    for align = (pcase-exhaustive (alist-get 'align 
column-alist)
+                                  ((or `nil 'left) "-")
+                                  ('right ""))
+                    for spec = (format " %%%s%ss" align size)
+                    concat (format spec name)))))
+
 ;;;; Footer
 
 (provide 'taxy-magit-section)
diff --git a/taxy.el b/taxy.el
index b161c3b..972c4e4 100644
--- a/taxy.el
+++ b/taxy.el
@@ -263,6 +263,92 @@ KEY is passed to `cl-sort', which see."
 
 (defalias 'taxy-sort* #'taxy-sort-taxys)
 
+;;;; Key functions
+
+;; Utilities to define key and take functions in a standard way.
+
+(defmacro taxy-define-key-definer (name variable prefix docstring)
+  "Define a macro NAME that defines a key-function-defining macro.
+The defined macro, having string DOCSTRING, associates the
+defined key functions with their aliases in an alist stored in
+symbol VARIABLE.  The defined key functions are named having
+string PREFIX, which will have a hyphen appended to it.  The key
+functions take one or more arguments, the first of which is the
+item being tested, bound within the function to `item'."
+  ;; Example docstring:
+
+  ;;   "Define a `taxy-org-ql-view' key function by NAME having BODY taking 
ARGS.
+  ;; Within BODY, `element' is bound to the `org-element' element
+  ;; being tested.
+
+  ;; Defines a function named `taxy-org-ql--predicate-NAME', and adds
+  ;; an entry to `taxy-org-ql-view-keys' mapping NAME to the new
+  ;; function symbol."
+  (declare (indent defun))
+  ;; I'm not sure why it's necessary to bind the variable in the first
+  ;; level of the expansion here, but double-unquoting the variable in
+  ;; the defined macro's form leaves the second comma in place, which
+  ;; breaks the second expansion, and this works around that.
+  `(let ((variable ',variable))
+     (defvar ,variable nil
+       ,(format "Alist mapping key aliases to key functions defined with `%s'."
+               name))
+     (defmacro ,name (name args &rest body)
+       ,docstring
+       (declare (indent defun)
+               (debug (&define symbolp listp &rest def-form)))
+       (let* ((fn-symbol (intern (format "%s-%s" ,prefix name)))
+             (fn `(cl-function
+                   (lambda (item ,@args)
+                     ,@body))))
+        `(progn
+           (fset ',fn-symbol ,fn)
+           (setf (map-elt ,variable ',name) ',fn-symbol))))))
+
+(defun taxy-make-take-function (keys aliases)
+  "Return a `taxy' \"take\" function for KEYS.
+Each of KEYS should be a function alias defined in ALIASES, or a
+list of such KEY-FNS (recursively, ad infinitum, approximately).
+ALIASES should be an alist mapping aliases to functions (such as
+defined with a definer defined by `taxy-define-key-definer')."
+  (let ((macrolets (cl-loop for (name . fn) in aliases
+                            collect `(,name ',fn))))
+    (cl-labels ((expand-form
+                 ;; Is using (cadr (macroexpand-all ...)) really better than 
`eval'?
+                 (form) (cadr (macroexpand-all
+                               `(cl-symbol-macrolet (,@macrolets)
+                                  ,form))))
+                (quote-fn
+                 (fn) (pcase fn
+                        ((pred symbolp) (expand-form fn))
+                        (`(,(and (or 'and 'or 'not) boolean) . ,(and args (map 
:name :keys)))
+                         ;; Well, that pcase expression isn't confusing at 
all...  ;)
+                         ;;  (cl-assert name t "Boolean key functions require 
a NAME")
+                         ;;  (cl-assert keys t "Boolean key functions require 
KEYS")
+                         `(lambda (buffer)
+                            (when (cl-loop for fn in ',(mapcar #'quote-fn (or 
keys args))
+                                           ,(pcase boolean
+                                              ('and 'always)
+                                              ('or 'thereis)
+                                              ('not 'never))
+                                           (funcall fn buffer))
+                              (or ,name ""))))
+                        (`(,(and (pred symbolp) fn)
+                           . ,(and args (guard (pcase (car args)
+                                                 ((or (pred keywordp)
+                                                      (and (pred atom)
+                                                           (pred (not 
symbolp)))
+                                                     `(quote ,_))
+                                                  t)))))
+                         ;; Key with args: replace with a lambda that
+                         ;; calls that key's function with given args.
+                         `(lambda (element)
+                            (,(expand-form fn) element ,@args)))
+                        ((pred listp) (mapcar #'quote-fn fn)))))
+      (setf keys (mapcar #'quote-fn keys))
+      `(lambda (item taxy)
+         (taxy-take-keyed ',keys item taxy)))))
+
 ;;;; Footer
 
 (provide 'taxy)
diff --git a/taxy.info b/taxy.info
index bc57bf4..3f810b9 100644
--- a/taxy.info
+++ b/taxy.info
@@ -50,16 +50,17 @@ Examples
 
 Usage
 
-* Dynamic taxys::
 * Reusable taxys::
 * Threading macros::
 * Modifying filled taxys::
+* Dynamic taxys::
 * Magit section::
 
 Dynamic taxys
 
 * Multi-level dynamic taxys::
 * "Chains" of independent, multi-level dynamic taxys: "Chains" of independent 
multi-level dynamic taxys.
+* Defining a classification domain-specific language::
 
 Changelog
 
@@ -534,19 +535,154 @@ replace items in a taxy with, e.g.  a more useful 
representation.
 
 * Menu:
 
-* Dynamic taxys::
 * Reusable taxys::
 * Threading macros::
 * Modifying filled taxys::
+* Dynamic taxys::
 * Magit section::
 
 
-File: README.info,  Node: Dynamic taxys,  Next: Reusable taxys,  Up: Usage
+File: README.info,  Node: Reusable taxys,  Next: Threading macros,  Up: Usage
+
+3.1 Reusable taxys
+==================
+
+Since taxys are structs, they may be stored in variables and used in
+other structs (being sure to copy the root taxy with ‘taxy-emptied’
+before filling).  For example, this shows using ‘taxy’ to classify
+Matrix rooms in Ement.el (https://github.com/alphapapa/ement.el):
+
+     (defun ement-roomy-buffer (room)
+       (alist-get 'buffer (ement-room-local room)))
+
+     (defvar ement-roomy-unread
+       (make-taxy :name "Unread"
+                  :predicate (lambda (room)
+                               (buffer-modified-p (ement-roomy-buffer room)))))
+
+     (defvar ement-roomy-opened
+       (make-taxy :name "Opened"
+                  :description "Rooms with buffers"
+                  :predicate #'ement-roomy-buffer
+                  :taxys (list ement-roomy-unread
+                               (make-taxy))))
+
+     (defvar ement-roomy-closed
+       (make-taxy :name "Closed"
+                  :description "Rooms without buffers"
+                  :predicate (lambda (room)
+                               (not (ement-roomy-buffer room)))))
+
+     (defvar ement-roomy
+       (make-taxy
+        :name "Ement Rooms"
+        :taxys (list (make-taxy
+                      :name "Direct"
+                      :description "Direct messaging rooms"
+                      :predicate (lambda (room)
+                                   (ement-room--direct-p room ement-session))
+                      :taxys (list ement-roomy-opened
+                                   ement-roomy-closed))
+                     (make-taxy
+                      :name "Non-direct"
+                      :description "Group chat rooms"
+                      :taxys (list ement-roomy-opened
+                                   ement-roomy-closed)))))
+
+   Note how the taxys defined in the first three variables are used in
+subsequent taxys.  As well, the ‘ement-roomy-opened’ taxy has an
+"anonymous" taxy, which collects any rooms that aren’t collected by its
+sibling taxy (otherwise those objects would be collected into the
+parent, "Opened" taxy, which may not always be the most useful way to
+present the objects).
+
+   Using those defined taxys, we then fill the ‘ement-roomy’ taxy with
+all of the rooms in the user’s session, and then use ‘taxy-mapcar’ to
+replace the room structs with useful representations for display:
+
+     (taxy-plain
+      (taxy-mapcar (lambda (room)
+                     (list (ement-room--room-display-name room)
+                           (ement-room-id room)))
+        (taxy-fill (ement-session-rooms ement-session)
+                   (taxy-emptied ement-roomy))))
+
+   This produces:
 
-3.1 Dynamic taxys
+     ("Ement Rooms"
+      (("Direct" "Direct messaging rooms"
+        (("Opened" "Rooms with buffers"
+          (("Unread"
+            (("Lars Ingebrigtsen" "!nope:gnus.org")))))
+         ("Closed" "Rooms without buffers"
+          (("John Wiegley" "!not-really:newartisans.com")
+           ("Eli Zaretskii" "!im-afraid-not:gnu.org")))))
+       ("Non-direct" "Group chat rooms"
+        (("Opened" "Rooms with buffers"
+          (("Unread"
+            (("Emacs" "!WfZsmtnxbxTdoYPkaT:greyface.org")
+             ("#emacs" "!KuaCUVGoCiunYyKEpm:libera.chat")))
+           ;; The non-unread buffers in the "anonymous" taxy.
+           ((("magit/magit" "!HZYimOcmEAsAxOcgpE:gitter.im")
+             ("Ement.el" "!NicAJNwJawmHrEhqZs:matrix.org")
+             ("#emacsconf" "!UjTTDnYmSAslLTtMCF:libera.chat")
+             ("Emacs Matrix Client" "!ZrZoyXEyFrzcBZKNis:matrix.org")
+             ("org-mode" "!rUhEinythPhVTdddsb:matrix.org")
+             ("This Week in Matrix (TWIM)" 
"!xYvNcQPhnkrdUmYczI:matrix.org")))))
+         ("Closed" "Rooms without buffers"
+          (("#matrix-spec" "!NasysSDfxKxZBzJJoE:matrix.org")
+           ("#commonlisp" "!IiGsrmKRHzpupHRaKS:libera.chat")
+           ("Matrix HQ" "!OGEhHVWSdvArJzumhm:matrix.org")
+           ("#lisp" "!czLxhhEegTEGNKUBgo:libera.chat")
+           ("Emacs" "!gLamGIXTWBaDFfhEeO:matrix.org")
+           ("#matrix-dev:matrix.org" "!jxlRxnrZCsjpjDubDX:matrix.org")))))))
+
+
+File: README.info,  Node: Threading macros,  Next: Modifying filled taxys,  
Prev: Reusable taxys,  Up: Usage
+
+3.2 Threading macros
+====================
+
+If you happen to like macros, ‘taxy’ works well with threading (i.e.
+‘thread-last’ or ‘->>’):
+
+     (thread-last ement-roomy
+       taxy-emptied
+       (taxy-fill (ement-session-rooms ement-session))
+       (taxy-mapcar (lambda (room)
+                      (list (ement-room--room-display-name room)
+                            (ement-room-id room))))
+       taxy-plain)
+
+
+File: README.info,  Node: Modifying filled taxys,  Next: Dynamic taxys,  Prev: 
Threading macros,  Up: Usage
+
+3.3 Modifying filled taxys
+==========================
+
+Sometimes it’s necessary to modify a taxy after filling it with objects,
+e.g.  to sort the items and/or the sub-taxys.  For this, use the
+function ‘taxy-mapc-taxys’ (a.k.a.  ‘taxy-mapc*’).  For example, in the
+sample application musicy.el (examples/musicy.el), the taxys and their
+items are sorted after filling, like so:
+
+     (defun musicy-files (files)
+       (thread-last musicy-taxy
+         taxy-emptied
+         (taxy-fill files)
+         ;; Sort sub-taxys by their name.
+         (taxy-sort* #'string< #'taxy-name)
+         ;; Sort sub-taxys' items by name.
+         (taxy-sort #'string< #'identity)
+         taxy-magit-section-pp))
+
+
+File: README.info,  Node: Dynamic taxys,  Next: Magit section,  Prev: 
Modifying filled taxys,  Up: Usage
+
+3.4 Dynamic taxys
 =================
 
-   • • 
+   • • • 
    You may not always know in advance what taxonomy a set of objects
 fits into, so ‘taxy’ lets you add taxys dynamically by using the ‘:take’
 function to add a taxy when an object is "taken into" a parent taxy’s
@@ -605,11 +741,12 @@ and it produces this taxonomy of buffers:
 
 * Multi-level dynamic taxys::
 * "Chains" of independent, multi-level dynamic taxys: "Chains" of independent 
multi-level dynamic taxys.
+* Defining a classification domain-specific language::
 
 
 File: README.info,  Node: Multi-level dynamic taxys,  Next: "Chains" of 
independent multi-level dynamic taxys,  Up: Dynamic taxys
 
-3.1.1 Multi-level dynamic taxys
+3.4.1 Multi-level dynamic taxys
 -------------------------------
 
 Of course, the point of taxonomies is that they aren’t restricted to a
@@ -662,9 +799,9 @@ directory.
             (#<buffer taxy-magit-section.el> #<buffer taxy.el<taxy.el> 
#<buffer scratch.el>))))))))
 
 
-File: README.info,  Node: "Chains" of independent multi-level dynamic taxys,  
Prev: Multi-level dynamic taxys,  Up: Dynamic taxys
+File: README.info,  Node: "Chains" of independent multi-level dynamic taxys,  
Next: Defining a classification domain-specific language,  Prev: Multi-level 
dynamic taxys,  Up: Dynamic taxys
 
-3.1.2 "Chains" of independent, multi-level dynamic taxys
+3.4.2 "Chains" of independent, multi-level dynamic taxys
 --------------------------------------------------------
 
 _Naming things is hard._
@@ -734,142 +871,126 @@ use:
           ("Soccer" "Tennis" "Football" "Baseball"))))))
 
 
-File: README.info,  Node: Reusable taxys,  Next: Threading macros,  Prev: 
Dynamic taxys,  Up: Usage
-
-3.2 Reusable taxys
-==================
-
-Since taxys are structs, they may be stored in variables and used in
-other structs (being sure to copy the root taxy with ‘taxy-emptied’
-before filling).  For example, this shows using ‘taxy’ to classify
-Matrix rooms in Ement.el (https://github.com/alphapapa/ement.el):
-
-     (defun ement-roomy-buffer (room)
-       (alist-get 'buffer (ement-room-local room)))
-
-     (defvar ement-roomy-unread
-       (make-taxy :name "Unread"
-                  :predicate (lambda (room)
-                               (buffer-modified-p (ement-roomy-buffer room)))))
-
-     (defvar ement-roomy-opened
-       (make-taxy :name "Opened"
-                  :description "Rooms with buffers"
-                  :predicate #'ement-roomy-buffer
-                  :taxys (list ement-roomy-unread
-                               (make-taxy))))
-
-     (defvar ement-roomy-closed
-       (make-taxy :name "Closed"
-                  :description "Rooms without buffers"
-                  :predicate (lambda (room)
-                               (not (ement-roomy-buffer room)))))
-
-     (defvar ement-roomy
-       (make-taxy
-        :name "Ement Rooms"
-        :taxys (list (make-taxy
-                      :name "Direct"
-                      :description "Direct messaging rooms"
-                      :predicate (lambda (room)
-                                   (ement-room--direct-p room ement-session))
-                      :taxys (list ement-roomy-opened
-                                   ement-roomy-closed))
-                     (make-taxy
-                      :name "Non-direct"
-                      :description "Group chat rooms"
-                      :taxys (list ement-roomy-opened
-                                   ement-roomy-closed)))))
-
-   Note how the taxys defined in the first three variables are used in
-subsequent taxys.  As well, the ‘ement-roomy-opened’ taxy has an
-"anonymous" taxy, which collects any rooms that aren’t collected by its
-sibling taxy (otherwise those objects would be collected into the
-parent, "Opened" taxy, which may not always be the most useful way to
-present the objects).
-
-   Using those defined taxys, we then fill the ‘ement-roomy’ taxy with
-all of the rooms in the user’s session, and then use ‘taxy-mapcar’ to
-replace the room structs with useful representations for display:
-
-     (taxy-plain
-      (taxy-mapcar (lambda (room)
-                     (list (ement-room--room-display-name room)
-                           (ement-room-id room)))
-        (taxy-fill (ement-session-rooms ement-session)
-                   (taxy-emptied ement-roomy))))
-
-   This produces:
-
-     ("Ement Rooms"
-      (("Direct" "Direct messaging rooms"
-        (("Opened" "Rooms with buffers"
-          (("Unread"
-            (("Lars Ingebrigtsen" "!nope:gnus.org")))))
-         ("Closed" "Rooms without buffers"
-          (("John Wiegley" "!not-really:newartisans.com")
-           ("Eli Zaretskii" "!im-afraid-not:gnu.org")))))
-       ("Non-direct" "Group chat rooms"
-        (("Opened" "Rooms with buffers"
-          (("Unread"
-            (("Emacs" "!WfZsmtnxbxTdoYPkaT:greyface.org")
-             ("#emacs" "!KuaCUVGoCiunYyKEpm:libera.chat")))
-           ;; The non-unread buffers in the "anonymous" taxy.
-           ((("magit/magit" "!HZYimOcmEAsAxOcgpE:gitter.im")
-             ("Ement.el" "!NicAJNwJawmHrEhqZs:matrix.org")
-             ("#emacsconf" "!UjTTDnYmSAslLTtMCF:libera.chat")
-             ("Emacs Matrix Client" "!ZrZoyXEyFrzcBZKNis:matrix.org")
-             ("org-mode" "!rUhEinythPhVTdddsb:matrix.org")
-             ("This Week in Matrix (TWIM)" 
"!xYvNcQPhnkrdUmYczI:matrix.org")))))
-         ("Closed" "Rooms without buffers"
-          (("#matrix-spec" "!NasysSDfxKxZBzJJoE:matrix.org")
-           ("#commonlisp" "!IiGsrmKRHzpupHRaKS:libera.chat")
-           ("Matrix HQ" "!OGEhHVWSdvArJzumhm:matrix.org")
-           ("#lisp" "!czLxhhEegTEGNKUBgo:libera.chat")
-           ("Emacs" "!gLamGIXTWBaDFfhEeO:matrix.org")
-           ("#matrix-dev:matrix.org" "!jxlRxnrZCsjpjDubDX:matrix.org")))))))
-
-
-File: README.info,  Node: Threading macros,  Next: Modifying filled taxys,  
Prev: Reusable taxys,  Up: Usage
+File: README.info,  Node: Defining a classification domain-specific language,  
Prev: "Chains" of independent multi-level dynamic taxys,  Up: Dynamic taxys
 
-3.3 Threading macros
-====================
-
-If you happen to like macros, ‘taxy’ works well with threading (i.e.
-‘thread-last’ or ‘->>’):
-
-     (thread-last ement-roomy
-       taxy-emptied
-       (taxy-fill (ement-session-rooms ement-session))
-       (taxy-mapcar (lambda (room)
-                      (list (ement-room--room-display-name room)
-                            (ement-room-id room))))
-       taxy-plain)
+3.4.3 Defining a classification domain-specific language
+--------------------------------------------------------
 
-
-File: README.info,  Node: Modifying filled taxys,  Next: Magit section,  Prev: 
Threading macros,  Up: Usage
+When writing a larger Taxy-based application, it may be necessary to
+define a number of key functions that would be unwieldy to manage in a
+‘cl-labels’ form.  For this case, Taxy provides the macro
+‘taxy-define-key-definer’ to easily define Taxy key functions in an
+application library.  Those functions are then passed to the function
+‘taxy-make-take-function’ at runtime, along with a list of keys being
+used to classify items.  Using these allows key functions to be defined
+in top-level forms, and it allows an application to be extended by users
+by defining additional key functions in their configurations.
+
+   Extending the previous ‘sporty’ example, let’s redefine its key
+functions using ‘taxy-define-key-definer’:
+
+     (taxy-define-key-definer sporty-define-key
+       sporty-keys "sporty"
+       "Define a `sporty' key function.")
+
+     (sporty-define-key disc-based ()
+       (if (member 'disc (sport-uses item))
+           "Disc-based"
+         "Non-disc-based"))
+
+     (sporty-define-key uses (&optional thing)
+       (pcase thing
+         (`nil (sport-uses item))
+         (_ (when (cl-typecase (sport-uses item)
+                    (symbol (equal thing (sport-uses item)))
+                    (list (member thing (sport-uses item))))
+              thing))))
+
+     (sporty-define-key venue (&optional place)
+       (pcase place
+         (`nil (sport-venue item))
+         (_ (when (equal place (sport-venue item))
+              (sport-venue item)))))
+
+   Now we’ll define the default keys to use when classifying items.
+This list is equivalent to the one passed to ‘taxy-take-keyed’ in the
+previous, "Chains" example.
+
+     (defvar sporty-default-keys
+       '(
+         ((venue 'outdoor)
+          disc-based)
+
+         ((venue 'indoor)
+          (uses 'ball)
+          (uses 'disc)
+          (uses 'glove)
+          (uses 'racket))))
+
+   Finally, rather than using a pre-made taxy struct, we make one at
+runtime, making the ‘:take’ function with ‘taxy-make-take-function’.
+
+     (let ((taxy (make-taxy
+                  :name "Sporty (DSL)"
+                  :take (taxy-make-take-function sporty-default-keys
+                                                 sporty-keys))))
+       (thread-last taxy
+         (taxy-fill sports)
+         (taxy-mapcar #'sport-name)
+         taxy-plain))
 
-3.4 Modifying filled taxys
-==========================
+   Which gives us:
 
-Sometimes it’s necessary to modify a taxy after filling it with objects,
-e.g.  to sort the items and/or the sub-taxys.  For this, use the
-function ‘taxy-mapc-taxys’ (a.k.a.  ‘taxy-mapc*’).  For example, in the
-sample application musicy.el (examples/musicy.el), the taxys and their
-items are sorted after filling, like so:
+     ("Sporty (DSL)"
+      ((indoor
+        ((ball
+          ("Volleyball" "Basketball")
+          ((glove
+            ("Handball"))
+           (racket
+            ("Racquetball"))))))
+       (outdoor
+        (("Disc-based"
+          ("Ultimate" "Disc golf"))
+         ("Non-disc-based"
+          ("Soccer" "Tennis" "Football" "Baseball"))))))
 
-     (defun musicy-files (files)
-       (thread-last musicy-taxy
-         taxy-emptied
-         (taxy-fill files)
-         ;; Sort sub-taxys by their name.
-         (taxy-sort* #'string< #'taxy-name)
-         ;; Sort sub-taxys' items by name.
-         (taxy-sort #'string< #'identity)
-         taxy-magit-section-pp))
+   As you can see, the result is the same as that in the previous
+example, but we’ve defined a kind of DSL for grouping sports in a
+modular, extendable way.
+
+   This also allows the grouping keys to be easily changed at runtime,
+producing a different result.  For example, we could group sports by,
+first, whether they use a ball, and then by venue.  Let’s do this in a
+function so that users can pass their own list of keys:
+
+     (cl-defun sporty-classify (sports &key (keys sporty-default-keys))
+       (declare (indent defun))
+       (let* ((taxy (make-taxy
+                     :name "Sporty (DSL)"
+                     :take (taxy-make-take-function keys
+                                                    sporty-keys))))
+         (thread-last taxy
+           (taxy-fill sports)
+           (taxy-mapcar #'sport-name)
+           taxy-plain)))
+
+     (sporty-classify sports
+       :keys '((uses 'ball) venue))
+
+   And this produces:
+
+     ("Sporty (DSL)"
+      ((outdoor
+        ("Ultimate" "Disc golf"))
+       (ball
+        ((indoor
+          ("Volleyball" "Handball" "Racquetball" "Basketball"))
+         (outdoor
+          ("Soccer" "Tennis" "Football" "Baseball"))))))
 
 
-File: README.info,  Node: Magit section,  Prev: Modifying filled taxys,  Up: 
Usage
+File: README.info,  Node: Magit section,  Prev: Dynamic taxys,  Up: Usage
 
 3.5 Magit section
 =================
@@ -926,6 +1047,24 @@ File: README.info,  Node: Additions,  Up: 06-pre
           taxy and its sub-taxys.
         • ‘taxy-sort-taxys’ (alias: ‘taxy-sort*’) sorts a taxy’s
           sub-taxys.
+   • Defining classification domain-specific languages:
+        • Macro ‘taxy-define-key-definer’ defines a
+          key-function-defining macro.
+        • Function ‘taxy-make-take-function’ makes a ‘:take’ function
+          using a list of key functions and a set of classification
+          keys.
+   • Table-like, column-based formatting system for
+     ‘taxy-magit-section’:
+        • Function ‘taxy-magit-section-format-items’, which formats
+          items by columns.
+        • Variable ‘taxy-magit-section-insert-indent-items’, which
+          controls whether ‘taxy-magit-section-insert’ applies
+          indentation to each item.  (Used to disable that behavior when
+          items are pre-indented strings, e.g.  as formatted by
+          ‘taxy-magit-section-format-items’.)
+   • Example application ‘deffy’, which shows an overview of top-level
+     definitions and forms in an Elisp project or file.  (Likely to be
+     published as a separate package later.)
 
 
 File: README.info,  Node: 05,  Next: 04,  Prev: 06-pre,  Up: Changelog
@@ -1116,39 +1255,40 @@ GPLv3
 
 Tag Table:
 Node: Top218
-Node: Examples1761
-Node: Numbery (starting basically)2080
-Node: Lettery (filling incrementally)7841
-Node: Sporty (understanding completely)10355
-Node: Applications16342
-Node: Installation16742
-Node: Usage17055
-Node: Dynamic taxys19192
-Node: Multi-level dynamic taxys21752
-Node: "Chains" of independent multi-level dynamic taxys23945
-Node: Reusable taxys26817
-Node: Threading macros30992
-Node: Modifying filled taxys31531
-Node: Magit section32349
-Node: Changelog33037
-Node: 06-pre33229
-Node: Additions33341
-Node: 0533665
-Node: Additions (1)33804
-Node: Fixes34910
-Node: 0435064
-Node: 0335286
-Node: Changes35415
-Node: Fixes (1)35778
-Node: 0236213
-Node: Changes (1)36382
-Node: Additions (2)36674
-Node: Fixes (2)37533
-Node: 0137787
-Node: Development37886
-Node: Copyright assignment38092
-Node: Credits38680
-Node: License38870
+Node: Examples1816
+Node: Numbery (starting basically)2135
+Node: Lettery (filling incrementally)7896
+Node: Sporty (understanding completely)10410
+Node: Applications16397
+Node: Installation16797
+Node: Usage17110
+Node: Reusable taxys19247
+Node: Threading macros23400
+Node: Modifying filled taxys23939
+Node: Dynamic taxys24757
+Node: Multi-level dynamic taxys27406
+Node: "Chains" of independent multi-level dynamic taxys29599
+Node: Defining a classification domain-specific language32530
+Node: Magit section36693
+Node: Changelog37372
+Node: 06-pre37564
+Node: Additions37676
+Node: 0539026
+Node: Additions (1)39165
+Node: Fixes40271
+Node: 0440425
+Node: 0340647
+Node: Changes40776
+Node: Fixes (1)41139
+Node: 0241574
+Node: Changes (1)41743
+Node: Additions (2)42035
+Node: Fixes (2)42894
+Node: 0143148
+Node: Development43247
+Node: Copyright assignment43453
+Node: Credits44041
+Node: License44231
 
 End Tag Table
 



reply via email to

[Prev in Thread] Current Thread [Next in Thread]