diff options
| author | Damien Cassou | 2020-08-09 14:48:22 +0200 |
|---|---|---|
| committer | Lars Ingebrigtsen | 2020-08-09 14:48:22 +0200 |
| commit | 8e82baf5a730ff542118ddba5b76afdc1db643f6 (patch) | |
| tree | 6870659b38a168709c2f98f571c35a6451ecb64f | |
| parent | d586bae501a3d6ec8e6a8088d05b0abfa541dece (diff) | |
| download | emacs-8e82baf5a730ff542118ddba5b76afdc1db643f6.tar.gz emacs-8e82baf5a730ff542118ddba5b76afdc1db643f6.zip | |
Add the new library hierarchy.el
* lisp/emacs-lisp/hierarchy.el: New file.
| -rw-r--r-- | etc/NEWS | 4 | ||||
| -rw-r--r-- | lisp/emacs-lisp/hierarchy.el | 579 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/hierarchy-tests.el | 556 |
3 files changed, 1139 insertions, 0 deletions
| @@ -737,6 +737,10 @@ The recentf files are no longer backed up. | |||
| 737 | 737 | ||
| 738 | ** Miscellaneous | 738 | ** Miscellaneous |
| 739 | 739 | ||
| 740 | *** The new library hierarchy.el has been added. | ||
| 741 | It's a library to create, query, navigate and display hierarchy | ||
| 742 | structures. | ||
| 743 | |||
| 740 | --- | 744 | --- |
| 741 | *** The width of the buffer-name column in 'list-buffers' is now dynamic. | 745 | *** The width of the buffer-name column in 'list-buffers' is now dynamic. |
| 742 | The width now depends of the width of the window, but will never be | 746 | The width now depends of the width of the window, but will never be |
diff --git a/lisp/emacs-lisp/hierarchy.el b/lisp/emacs-lisp/hierarchy.el new file mode 100644 index 00000000000..8cef029c4cf --- /dev/null +++ b/lisp/emacs-lisp/hierarchy.el | |||
| @@ -0,0 +1,579 @@ | |||
| 1 | ;;; hierarchy.el --- Library to create and display hierarchy structures -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2020 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: Damien Cassou <damien@cassou.me> | ||
| 6 | ;; Maintainer: emacs-devel@gnu.org | ||
| 7 | |||
| 8 | ;; This file is part of GNU Emacs. | ||
| 9 | |||
| 10 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 11 | ;; it under the terms of the GNU General Public License as published by | ||
| 12 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 13 | ;; (at your option) any later version. | ||
| 14 | |||
| 15 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 18 | ;; GNU General Public License for more details. | ||
| 19 | |||
| 20 | ;; You should have received a copy of the GNU General Public License | ||
| 21 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 22 | |||
| 23 | ;;; Commentary: | ||
| 24 | |||
| 25 | ;; Library to create, query, navigate and display hierarchy structures. | ||
| 26 | |||
| 27 | ;; Creation: After having created a hierarchy with `hierarchy-new', | ||
| 28 | ;; populate it by calling `hierarchy-add-tree' or | ||
| 29 | ;; `hierarchy-add-trees'. You can then optionally sort its element | ||
| 30 | ;; with `hierarchy-sort'. | ||
| 31 | |||
| 32 | ;; Querying: You can learn more about your hierarchy by using | ||
| 33 | ;; functions such as `hierarchy-roots', `hierarchy-has-item', | ||
| 34 | ;; `hierarchy-length', `hierarchy-parent', `hierarchy-descendant-p'. | ||
| 35 | |||
| 36 | ;; Navigation: When your hierarchy is ready, you can use | ||
| 37 | ;; `hierarchy-map-item', `hierarchy-map', and `map-tree' to apply | ||
| 38 | ;; functions to elements of the hierarchy. | ||
| 39 | |||
| 40 | ;; Display: You can display a hierarchy as a tabulated list using | ||
| 41 | ;; `hierarchy-tabulated-display' and as an expandable/foldable tree | ||
| 42 | ;; using `hierarchy-convert-to-tree-widget'. The | ||
| 43 | ;; `hierarchy-labelfn-*' functions will help you display each item of | ||
| 44 | ;; the hierarchy the way you want it. | ||
| 45 | |||
| 46 | ;;; Limitation: | ||
| 47 | |||
| 48 | ;; - Current implementation uses #'equal to find and distinguish | ||
| 49 | ;; elements. Support for user-provided equality definition is | ||
| 50 | ;; desired but not yet implemented; | ||
| 51 | ;; | ||
| 52 | ;; - nil can't be added to a hierarchy; | ||
| 53 | ;; | ||
| 54 | ;; - the hierarchy is computed eagerly. | ||
| 55 | |||
| 56 | ;;; Code: | ||
| 57 | |||
| 58 | (require 'seq) | ||
| 59 | (require 'map) | ||
| 60 | (require 'subr-x) | ||
| 61 | (require 'cl-lib) | ||
| 62 | |||
| 63 | |||
| 64 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 65 | ;; Helpers | ||
| 66 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 67 | |||
| 68 | (cl-defstruct (hierarchy | ||
| 69 | (:constructor hierarchy--make) | ||
| 70 | (:conc-name hierarchy--)) | ||
| 71 | (roots (list)) ; list of the hierarchy roots (no parent) | ||
| 72 | (parents (make-hash-table :test 'equal)) ; map an item to its parent | ||
| 73 | (children (make-hash-table :test 'equal)) ; map an item to its childre | ||
| 74 | ;; cache containing the set of all items in the hierarchy | ||
| 75 | (seen-items (make-hash-table :test 'equal))) ; map an item to t | ||
| 76 | |||
| 77 | (defun hierarchy--seen-items-add (hierarchy item) | ||
| 78 | "In HIERARCHY, add ITEM to seen items." | ||
| 79 | (map-put! (hierarchy--seen-items hierarchy) item t)) | ||
| 80 | |||
| 81 | (defun hierarchy--compute-roots (hierarchy) | ||
| 82 | "Search roots of HIERARCHY and return them." | ||
| 83 | (cl-set-difference | ||
| 84 | (map-keys (hierarchy--seen-items hierarchy)) | ||
| 85 | (map-keys (hierarchy--parents hierarchy)) | ||
| 86 | :test #'equal)) | ||
| 87 | |||
| 88 | (defun hierarchy--sort-roots (hierarchy sortfn) | ||
| 89 | "Compute, sort and store the roots of HIERARCHY. | ||
| 90 | |||
| 91 | SORTFN is a function taking two items of the hierarchy as parameter and | ||
| 92 | returning non-nil if the first parameter is lower than the second." | ||
| 93 | (setf (hierarchy--roots hierarchy) | ||
| 94 | (sort (hierarchy--compute-roots hierarchy) | ||
| 95 | sortfn))) | ||
| 96 | |||
| 97 | (defun hierarchy--add-relation (hierarchy item parent acceptfn) | ||
| 98 | "In HIERARCHY, add ITEM as child of PARENT. | ||
| 99 | |||
| 100 | ACCEPTFN is a function returning non-nil if its parameter (any object) | ||
| 101 | should be an item of the hierarchy." | ||
| 102 | (let* ((existing-parent (hierarchy-parent hierarchy item)) | ||
| 103 | (has-parent-p (funcall acceptfn existing-parent))) | ||
| 104 | (cond | ||
| 105 | ((and has-parent-p (not (equal existing-parent parent))) | ||
| 106 | (error "An item (%s) can only have one parent: '%s' vs '%s'" | ||
| 107 | item existing-parent parent)) | ||
| 108 | ((not has-parent-p) | ||
| 109 | (let ((existing-children (map-elt (hierarchy--children hierarchy) | ||
| 110 | parent (list)))) | ||
| 111 | (map-put! (hierarchy--children hierarchy) | ||
| 112 | parent (append existing-children (list item)))) | ||
| 113 | (map-put! (hierarchy--parents hierarchy) item parent))))) | ||
| 114 | |||
| 115 | (defun hierarchy--set-equal (list1 list2 &rest cl-keys) | ||
| 116 | "Return non-nil if LIST1 and LIST2 have same elements. | ||
| 117 | |||
| 118 | I.e., if every element of LIST1 also appears in LIST2 and if | ||
| 119 | every element of LIST2 also appears in LIST1. | ||
| 120 | |||
| 121 | CL-KEYS are key-value pairs just like in `cl-subsetp'. Supported | ||
| 122 | keys are :key and :test." | ||
| 123 | (and (apply 'cl-subsetp list1 list2 cl-keys) | ||
| 124 | (apply 'cl-subsetp list2 list1 cl-keys))) | ||
| 125 | |||
| 126 | |||
| 127 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 128 | ;; Creation | ||
| 129 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 130 | |||
| 131 | (defun hierarchy-new () | ||
| 132 | "Create a hierarchy and return it." | ||
| 133 | (hierarchy--make)) | ||
| 134 | |||
| 135 | (defun hierarchy-add-tree (hierarchy item parentfn &optional childrenfn acceptfn) | ||
| 136 | "In HIERARCHY, add ITEM. | ||
| 137 | |||
| 138 | PARENTFN is either nil or a function defining the child-to-parent | ||
| 139 | relationship: this function takes an item as parameter and should return | ||
| 140 | the parent of this item in the hierarchy. If the item has no parent in the | ||
| 141 | hierarchy (i.e., it should be a root), the function should return an object | ||
| 142 | not accepted by acceptfn (i.e., nil for the default value of acceptfn). | ||
| 143 | |||
| 144 | CHILDRENFN is either nil or a function defining the parent-to-children | ||
| 145 | relationship: this function takes an item as parameter and should return a | ||
| 146 | list of children of this item in the hierarchy. | ||
| 147 | |||
| 148 | If both PARENTFN and CHILDRENFN are non-nil, the results of PARENTFN and | ||
| 149 | CHILDRENFN are expected to be coherent with each other. | ||
| 150 | |||
| 151 | ACCEPTFN is a function returning non-nil if its parameter (any object) | ||
| 152 | should be an item of the hierarchy. By default, ACCEPTFN returns non-nil | ||
| 153 | if its parameter is non-nil." | ||
| 154 | (unless (hierarchy-has-item hierarchy item) | ||
| 155 | (let ((acceptfn (or acceptfn #'identity))) | ||
| 156 | (hierarchy--seen-items-add hierarchy item) | ||
| 157 | (let ((parent (and parentfn (funcall parentfn item)))) | ||
| 158 | (when (funcall acceptfn parent) | ||
| 159 | (hierarchy--add-relation hierarchy item parent acceptfn) | ||
| 160 | (hierarchy-add-tree hierarchy parent parentfn childrenfn))) | ||
| 161 | (let ((children (and childrenfn (funcall childrenfn item)))) | ||
| 162 | (mapc (lambda (child) | ||
| 163 | (when (funcall acceptfn child) | ||
| 164 | (hierarchy--add-relation hierarchy child item acceptfn) | ||
| 165 | (hierarchy-add-tree hierarchy child parentfn childrenfn))) | ||
| 166 | children))))) | ||
| 167 | |||
| 168 | (defun hierarchy-add-trees (hierarchy items parentfn &optional childrenfn acceptfn) | ||
| 169 | "Call `hierarchy-add-tree' on HIERARCHY and each element of ITEMS. | ||
| 170 | |||
| 171 | PARENTFN, CHILDRENFN and ACCEPTFN have the same meaning as in `hierarchy-add'." | ||
| 172 | (seq-map (lambda (item) | ||
| 173 | (hierarchy-add-tree hierarchy item parentfn childrenfn acceptfn)) | ||
| 174 | items)) | ||
| 175 | |||
| 176 | (defun hierarchy-add-list (hierarchy list &optional wrap childrenfn) | ||
| 177 | "Add to HIERARCHY the sub-lists in LIST. | ||
| 178 | |||
| 179 | If WRAP is non-nil, allow duplicate items in LIST by wraping each | ||
| 180 | item in a cons (id . item). The root's id is 1. | ||
| 181 | |||
| 182 | CHILDRENFN is a function (defaults to `cdr') taking LIST as a | ||
| 183 | parameter which should return LIST's children (a list). Each | ||
| 184 | child is (recursively) passed as a parameter to CHILDRENFN to get | ||
| 185 | its own children. Because of this parameter, LIST can be | ||
| 186 | anything, not necessarily a list." | ||
| 187 | (let* ((childrenfn (or childrenfn #'cdr)) | ||
| 188 | (id 0) | ||
| 189 | (wrapfn (lambda (item) | ||
| 190 | (if wrap | ||
| 191 | (cons (setq id (1+ id)) item) | ||
| 192 | item))) | ||
| 193 | (unwrapfn (if wrap #'cdr #'identity))) | ||
| 194 | (hierarchy-add-tree | ||
| 195 | hierarchy (funcall wrapfn list) nil | ||
| 196 | (lambda (item) | ||
| 197 | (mapcar wrapfn (funcall childrenfn | ||
| 198 | (funcall unwrapfn item))))) | ||
| 199 | hierarchy)) | ||
| 200 | |||
| 201 | (defun hierarchy-from-list (list &optional wrap childrenfn) | ||
| 202 | "Create and return a hierarchy built from LIST. | ||
| 203 | |||
| 204 | This function passes LIST, WRAP and CHILDRENFN unchanged to | ||
| 205 | `hierarchy-add-list'." | ||
| 206 | (hierarchy-add-list (hierarchy-new) list wrap childrenfn)) | ||
| 207 | |||
| 208 | (defun hierarchy-sort (hierarchy &optional sortfn) | ||
| 209 | "Modify HIERARCHY so that its roots and item's children are sorted. | ||
| 210 | |||
| 211 | SORTFN is a function taking two items of the hierarchy as parameter and | ||
| 212 | returning non-nil if the first parameter is lower than the second. By | ||
| 213 | default, SORTFN is `string-lessp'." | ||
| 214 | (let ((sortfn (or sortfn #'string-lessp))) | ||
| 215 | (hierarchy--sort-roots hierarchy sortfn) | ||
| 216 | (mapc (lambda (parent) | ||
| 217 | (setf | ||
| 218 | (map-elt (hierarchy--children hierarchy) parent) | ||
| 219 | (sort (map-elt (hierarchy--children hierarchy) parent) sortfn))) | ||
| 220 | (map-keys (hierarchy--children hierarchy))))) | ||
| 221 | |||
| 222 | (defun hierarchy-extract-tree (hierarchy item) | ||
| 223 | "Return a copy of HIERARCHY with ITEM's descendants and parents." | ||
| 224 | (if (not (hierarchy-has-item hierarchy item)) | ||
| 225 | nil | ||
| 226 | (let ((tree (hierarchy-new))) | ||
| 227 | (hierarchy-add-tree tree item | ||
| 228 | (lambda (each) (hierarchy-parent hierarchy each)) | ||
| 229 | (lambda (each) | ||
| 230 | (when (or (equal each item) | ||
| 231 | (hierarchy-descendant-p hierarchy each item)) | ||
| 232 | (hierarchy-children hierarchy each)))) | ||
| 233 | tree))) | ||
| 234 | |||
| 235 | (defun hierarchy-copy (hierarchy) | ||
| 236 | "Return a copy of HIERARCHY. | ||
| 237 | |||
| 238 | Items in HIERARCHY are shared, but structure is not." | ||
| 239 | (hierarchy-map-hierarchy (lambda (item _) (identity item)) hierarchy)) | ||
| 240 | |||
| 241 | |||
| 242 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 243 | ;; Querying | ||
| 244 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 245 | |||
| 246 | (defun hierarchy-items (hierarchy) | ||
| 247 | "Return a list of all items of HIERARCHY." | ||
| 248 | (map-keys (hierarchy--seen-items hierarchy))) | ||
| 249 | |||
| 250 | (defun hierarchy-has-item (hierarchy item) | ||
| 251 | "Return t if HIERARCHY includes ITEM." | ||
| 252 | (map-contains-key (hierarchy--seen-items hierarchy) item)) | ||
| 253 | |||
| 254 | (defun hierarchy-empty-p (hierarchy) | ||
| 255 | "Return t if HIERARCHY is empty." | ||
| 256 | (= 0 (hierarchy-length hierarchy))) | ||
| 257 | |||
| 258 | (defun hierarchy-length (hierarchy) | ||
| 259 | "Return the number of items in HIERARCHY." | ||
| 260 | (hash-table-count (hierarchy--seen-items hierarchy))) | ||
| 261 | |||
| 262 | (defun hierarchy-has-root (hierarchy item) | ||
| 263 | "Return t if one of HIERARCHY's roots is ITEM. | ||
| 264 | |||
| 265 | A root is an item with no parent." | ||
| 266 | (seq-contains-p (hierarchy-roots hierarchy) item)) | ||
| 267 | |||
| 268 | (defun hierarchy-roots (hierarchy) | ||
| 269 | "Return all roots of HIERARCHY. | ||
| 270 | |||
| 271 | A root is an item with no parent." | ||
| 272 | (let ((roots (hierarchy--roots hierarchy))) | ||
| 273 | (or roots | ||
| 274 | (hierarchy--compute-roots hierarchy)))) | ||
| 275 | |||
| 276 | (defun hierarchy-leafs (hierarchy &optional node) | ||
| 277 | "Return all leafs of HIERARCHY. | ||
| 278 | |||
| 279 | A leaf is an item with no child. | ||
| 280 | |||
| 281 | If NODE is an item of HIERARCHY, only return leafs under NODE." | ||
| 282 | (let ((leafs (cl-set-difference | ||
| 283 | (map-keys (hierarchy--seen-items hierarchy)) | ||
| 284 | (map-keys (hierarchy--children hierarchy))))) | ||
| 285 | (if (hierarchy-has-item hierarchy node) | ||
| 286 | (seq-filter (lambda (item) | ||
| 287 | (hierarchy-descendant-p hierarchy item node)) | ||
| 288 | leafs) | ||
| 289 | leafs))) | ||
| 290 | |||
| 291 | (defun hierarchy-parent (hierarchy item) | ||
| 292 | "In HIERARCHY, return parent of ITEM." | ||
| 293 | (map-elt (hierarchy--parents hierarchy) item)) | ||
| 294 | |||
| 295 | (defun hierarchy-children (hierarchy parent) | ||
| 296 | "In HIERARCHY, return children of PARENT." | ||
| 297 | (map-elt (hierarchy--children hierarchy) parent (list))) | ||
| 298 | |||
| 299 | (defun hierarchy-child-p (hierarchy item1 item2) | ||
| 300 | "In HIERARCHY, return non-nil if and only if ITEM1 is a child of ITEM2." | ||
| 301 | (equal (hierarchy-parent hierarchy item1) item2)) | ||
| 302 | |||
| 303 | (defun hierarchy-descendant-p (hierarchy item1 item2) | ||
| 304 | "In HIERARCHY, return non-nil if and only if ITEM1 is a descendant of ITEM2. | ||
| 305 | |||
| 306 | ITEM1 is a descendant of ITEM2 if and only if both are items of HIERARCHY | ||
| 307 | and either: | ||
| 308 | |||
| 309 | - ITEM1 is child of ITEM2, or | ||
| 310 | - ITEM1's parent is a descendant of ITEM2." | ||
| 311 | (and | ||
| 312 | (hierarchy-has-item hierarchy item1) | ||
| 313 | (hierarchy-has-item hierarchy item2) | ||
| 314 | (or | ||
| 315 | (hierarchy-child-p hierarchy item1 item2) | ||
| 316 | (hierarchy-descendant-p | ||
| 317 | hierarchy (hierarchy-parent hierarchy item1) item2)))) | ||
| 318 | |||
| 319 | (defun hierarchy-equal (hierarchy1 hierarchy2) | ||
| 320 | "Return t if HIERARCHY1 and HIERARCHY2 are equal. | ||
| 321 | |||
| 322 | Two equal hierarchies share the same items and the same | ||
| 323 | relationships among them." | ||
| 324 | (and (hierarchy-p hierarchy1) | ||
| 325 | (hierarchy-p hierarchy2) | ||
| 326 | (= (hierarchy-length hierarchy1) (hierarchy-length hierarchy2)) | ||
| 327 | ;; parents are the same | ||
| 328 | (seq-every-p (lambda (child) | ||
| 329 | (equal (hierarchy-parent hierarchy1 child) | ||
| 330 | (hierarchy-parent hierarchy2 child))) | ||
| 331 | (map-keys (hierarchy--parents hierarchy1))) | ||
| 332 | ;; children are the same | ||
| 333 | (seq-every-p (lambda (parent) | ||
| 334 | (hierarchy--set-equal | ||
| 335 | (hierarchy-children hierarchy1 parent) | ||
| 336 | (hierarchy-children hierarchy2 parent) | ||
| 337 | :test #'equal)) | ||
| 338 | (map-keys (hierarchy--children hierarchy1))))) | ||
| 339 | |||
| 340 | |||
| 341 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 342 | ;; Navigation | ||
| 343 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 344 | |||
| 345 | (defun hierarchy-map-item (func item hierarchy &optional indent) | ||
| 346 | "Return the result of applying FUNC to ITEM and its descendants in HIERARCHY. | ||
| 347 | |||
| 348 | This function navigates the tree top-down: FUNCTION is first called on item | ||
| 349 | and then on each of its children. Results are concatenated in a list. | ||
| 350 | |||
| 351 | INDENT is a number (default 0) representing the indentation of ITEM in | ||
| 352 | HIERARCHY. FUNC should take 2 argument: the item and its indentation | ||
| 353 | level." | ||
| 354 | (let ((indent (or indent 0))) | ||
| 355 | (cons | ||
| 356 | (funcall func item indent) | ||
| 357 | (seq-mapcat (lambda (child) (hierarchy-map-item func child | ||
| 358 | hierarchy (1+ indent))) | ||
| 359 | (hierarchy-children hierarchy item))))) | ||
| 360 | |||
| 361 | (defun hierarchy-map (func hierarchy &optional indent) | ||
| 362 | "Return the result of applying FUNC to each element of HIERARCHY. | ||
| 363 | |||
| 364 | This function navigates the tree top-down: FUNCTION is first called on each | ||
| 365 | root. To do so, it calls `hierarchy-map-item' on each root | ||
| 366 | sequentially. Results are concatenated in a list. | ||
| 367 | |||
| 368 | FUNC should take 2 arguments: the item and its indentation level. | ||
| 369 | |||
| 370 | INDENT is a number (default 0) representing the indentation of HIERARCHY's | ||
| 371 | roots." | ||
| 372 | (let ((indent (or indent 0))) | ||
| 373 | (seq-mapcat (lambda (root) (hierarchy-map-item func root hierarchy indent)) | ||
| 374 | (hierarchy-roots hierarchy)))) | ||
| 375 | |||
| 376 | (defun hierarchy-map-tree (function hierarchy &optional item indent) | ||
| 377 | "Apply FUNCTION on each item of HIERARCHY under ITEM. | ||
| 378 | |||
| 379 | This function navigates the tree bottom-up: FUNCTION is first called on | ||
| 380 | leafs and the result is passed as parameter when calling FUNCTION on | ||
| 381 | parents. | ||
| 382 | |||
| 383 | FUNCTION should take 3 parameters: the current item, its indentation | ||
| 384 | level (a number), and a list representing the result of applying | ||
| 385 | `hierarchy-map-tree' to each child of the item. | ||
| 386 | |||
| 387 | INDENT is 0 by default and is passed as second parameter to FUNCTION. | ||
| 388 | INDENT is incremented by 1 at each level of the tree. | ||
| 389 | |||
| 390 | This function returns the result of applying FUNCTION to ITEM (the first | ||
| 391 | root if nil)." | ||
| 392 | (let ((item (or item (car (hierarchy-roots hierarchy)))) | ||
| 393 | (indent (or indent 0))) | ||
| 394 | (funcall function item indent | ||
| 395 | (mapcar (lambda (child) | ||
| 396 | (hierarchy-map-tree function hierarchy | ||
| 397 | child (1+ indent))) | ||
| 398 | (hierarchy-children hierarchy item))))) | ||
| 399 | |||
| 400 | (defun hierarchy-map-hierarchy (function hierarchy) | ||
| 401 | "Apply FUNCTION to each item of HIERARCHY in a new hierarchy. | ||
| 402 | |||
| 403 | FUNCTION should take 2 parameters, the current item and its | ||
| 404 | indentation level (a number), and should return an item to be | ||
| 405 | added to the new hierarchy." | ||
| 406 | (let* ((items (make-hash-table :test #'equal)) | ||
| 407 | (transform (lambda (item) (map-elt items item)))) | ||
| 408 | ;; Make 'items', a table mapping original items to their | ||
| 409 | ;; transformation | ||
| 410 | (hierarchy-map (lambda (item indent) | ||
| 411 | (map-put! items item (funcall function item indent))) | ||
| 412 | hierarchy) | ||
| 413 | (hierarchy--make | ||
| 414 | :roots (mapcar transform (hierarchy-roots hierarchy)) | ||
| 415 | :parents (let ((result (make-hash-table :test #'equal))) | ||
| 416 | (map-apply (lambda (child parent) | ||
| 417 | (map-put! result | ||
| 418 | (funcall transform child) | ||
| 419 | (funcall transform parent))) | ||
| 420 | (hierarchy--parents hierarchy)) | ||
| 421 | result) | ||
| 422 | :children (let ((result (make-hash-table :test #'equal))) | ||
| 423 | (map-apply (lambda (parent children) | ||
| 424 | (map-put! result | ||
| 425 | (funcall transform parent) | ||
| 426 | (seq-map transform children))) | ||
| 427 | (hierarchy--children hierarchy)) | ||
| 428 | result) | ||
| 429 | :seen-items (let ((result (make-hash-table :test #'equal))) | ||
| 430 | (map-apply (lambda (item v) | ||
| 431 | (map-put! result | ||
| 432 | (funcall transform item) | ||
| 433 | v)) | ||
| 434 | (hierarchy--seen-items hierarchy)) | ||
| 435 | result)))) | ||
| 436 | |||
| 437 | |||
| 438 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 439 | ;; Display | ||
| 440 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| 441 | |||
| 442 | (defun hierarchy-labelfn-indent (labelfn &optional indent-string) | ||
| 443 | "Return a function rendering LABELFN indented with INDENT-STRING. | ||
| 444 | |||
| 445 | INDENT-STRING defaults to a 2-space string. Indentation is | ||
| 446 | multiplied by the depth of the displayed item." | ||
| 447 | (let ((indent-string (or indent-string " "))) | ||
| 448 | (lambda (item indent) | ||
| 449 | (dotimes (_ indent) (insert indent-string)) | ||
| 450 | (funcall labelfn item indent)))) | ||
| 451 | |||
| 452 | (defun hierarchy-labelfn-button (labelfn actionfn) | ||
| 453 | "Return a function rendering LABELFN in a button. | ||
| 454 | |||
| 455 | Clicking the button triggers ACTIONFN. ACTIONFN is a function | ||
| 456 | taking an item of HIERARCHY and an indentation value (a number) | ||
| 457 | as input. This function is called when an item is clicked. The | ||
| 458 | return value of ACTIONFN is ignored." | ||
| 459 | (lambda (item indent) | ||
| 460 | (let ((start (point))) | ||
| 461 | (funcall labelfn item indent) | ||
| 462 | (make-text-button start (point) | ||
| 463 | 'action (lambda (_) (funcall actionfn item indent)))))) | ||
| 464 | |||
| 465 | (defun hierarchy-labelfn-button-if (labelfn buttonp actionfn) | ||
| 466 | "Return a function rendering LABELFN as a button if BUTTONP. | ||
| 467 | |||
| 468 | Pass LABELFN and ACTIONFN to `hierarchy-labelfn-button' if | ||
| 469 | BUTTONP is non-nil. Otherwise, render LABELFN without making it | ||
| 470 | a button. | ||
| 471 | |||
| 472 | BUTTONP is a function taking an item of HIERARCHY and an | ||
| 473 | indentation value (a number) as input." | ||
| 474 | (lambda (item indent) | ||
| 475 | (if (funcall buttonp item indent) | ||
| 476 | (funcall (hierarchy-labelfn-button labelfn actionfn) item indent) | ||
| 477 | (funcall labelfn item indent)))) | ||
| 478 | |||
| 479 | (defun hierarchy-labelfn-to-string (labelfn item indent) | ||
| 480 | "Execute LABELFN on ITEM and INDENT. Return result as a string." | ||
| 481 | (with-temp-buffer | ||
| 482 | (funcall labelfn item indent) | ||
| 483 | (buffer-substring (point-min) (point-max)))) | ||
| 484 | |||
| 485 | (defun hierarchy-print (hierarchy &optional to-string) | ||
| 486 | "Insert HIERARCHY in current buffer as plain text. | ||
| 487 | |||
| 488 | Use TO-STRING to convert each element to a string. TO-STRING is | ||
| 489 | a function taking an item of HIERARCHY as input and returning a | ||
| 490 | string. If nil, TO-STRING defaults to a call to `format' with \"%s\"." | ||
| 491 | (let ((to-string (or to-string (lambda (item) (format "%s" item))))) | ||
| 492 | (hierarchy-map | ||
| 493 | (hierarchy-labelfn-indent (lambda (item _) | ||
| 494 | (insert (funcall to-string item) "\n"))) | ||
| 495 | hierarchy))) | ||
| 496 | |||
| 497 | (defun hierarchy-to-string (hierarchy &optional to-string) | ||
| 498 | "Return a string representing HIERARCHY. | ||
| 499 | |||
| 500 | TO-STRING is passed unchanged to `hierarchy-print'." | ||
| 501 | (with-temp-buffer | ||
| 502 | (hierarchy-print hierarchy to-string) | ||
| 503 | (buffer-substring (point-min) (point-max)))) | ||
| 504 | |||
| 505 | (defun hierarchy-tabulated-imenu-action (_item-name position) | ||
| 506 | "Move to ITEM-NAME at POSITION in current buffer." | ||
| 507 | (goto-char position) | ||
| 508 | (back-to-indentation)) | ||
| 509 | |||
| 510 | (define-derived-mode hierarchy-tabulated-mode tabulated-list-mode "Hierarchy tabulated" | ||
| 511 | "Major mode to display a hierarchy as a tabulated list." | ||
| 512 | (setq-local imenu-generic-expression | ||
| 513 | ;; debbugs: 26457 - Cannot pass a function to | ||
| 514 | ;; imenu-generic-expression. Add | ||
| 515 | ;; `hierarchy-tabulated-imenu-action' to the end of the | ||
| 516 | ;; list when bug is fixed | ||
| 517 | '(("Item" "^[[:space:]]+\\(?1:.+\\)$" 1)))) | ||
| 518 | |||
| 519 | (defun hierarchy-tabulated-display (hierarchy labelfn &optional buffer) | ||
| 520 | "Display HIERARCHY as a tabulated list in `hierarchy-tabulated-mode'. | ||
| 521 | |||
| 522 | LABELFN is a function taking an item of HIERARCHY and an indentation | ||
| 523 | level (a number) as input and inserting a string to be displayed in the | ||
| 524 | table. | ||
| 525 | |||
| 526 | The tabulated list is displayed in BUFFER, or a newly created buffer if | ||
| 527 | nil. The buffer is returned." | ||
| 528 | (let ((buffer (or buffer (generate-new-buffer "hierarchy-tabulated")))) | ||
| 529 | (with-current-buffer buffer | ||
| 530 | (hierarchy-tabulated-mode) | ||
| 531 | (setq tabulated-list-format | ||
| 532 | (vector '("Item name" 0 nil))) | ||
| 533 | (setq tabulated-list-entries | ||
| 534 | (hierarchy-map (lambda (item indent) | ||
| 535 | (list item (vector (hierarchy-labelfn-to-string | ||
| 536 | labelfn item indent)))) | ||
| 537 | hierarchy)) | ||
| 538 | (tabulated-list-init-header) | ||
| 539 | (tabulated-list-print)) | ||
| 540 | buffer)) | ||
| 541 | |||
| 542 | (declare-function widget-convert "wid-edit") | ||
| 543 | (defun hierarchy-convert-to-tree-widget (hierarchy labelfn) | ||
| 544 | "Return a tree-widget for HIERARCHY. | ||
| 545 | |||
| 546 | LABELFN is a function taking an item of HIERARCHY and an indentation | ||
| 547 | value (a number) as parameter and inserting a string to be displayed as a | ||
| 548 | node label." | ||
| 549 | (require 'wid-edit) | ||
| 550 | (require 'tree-widget) | ||
| 551 | (hierarchy-map-tree (lambda (item indent children) | ||
| 552 | (widget-convert | ||
| 553 | 'tree-widget | ||
| 554 | :tag (hierarchy-labelfn-to-string labelfn item indent) | ||
| 555 | :args children)) | ||
| 556 | hierarchy)) | ||
| 557 | |||
| 558 | (defun hierarchy-tree-display (hierarchy labelfn &optional buffer) | ||
| 559 | "Display HIERARCHY as a tree widget in a new buffer. | ||
| 560 | |||
| 561 | HIERARCHY and LABELFN are passed unchanged to | ||
| 562 | `hierarchy-convert-to-tree-widget'. | ||
| 563 | |||
| 564 | The tree widget is displayed in BUFFER, or a newly created buffer if | ||
| 565 | nil. The buffer is returned." | ||
| 566 | (let ((buffer (or buffer (generate-new-buffer "*hierarchy-tree*"))) | ||
| 567 | (tree-widget (hierarchy-convert-to-tree-widget hierarchy labelfn))) | ||
| 568 | (with-current-buffer buffer | ||
| 569 | (setq-local buffer-read-only t) | ||
| 570 | (let ((inhibit-read-only t)) | ||
| 571 | (erase-buffer) | ||
| 572 | (widget-create tree-widget) | ||
| 573 | (goto-char (point-min)) | ||
| 574 | (special-mode))) | ||
| 575 | buffer)) | ||
| 576 | |||
| 577 | (provide 'hierarchy) | ||
| 578 | |||
| 579 | ;;; hierarchy.el ends here | ||
diff --git a/test/lisp/emacs-lisp/hierarchy-tests.el b/test/lisp/emacs-lisp/hierarchy-tests.el new file mode 100644 index 00000000000..23cfc79d848 --- /dev/null +++ b/test/lisp/emacs-lisp/hierarchy-tests.el | |||
| @@ -0,0 +1,556 @@ | |||
| 1 | ;;; hierarchy-tests.el --- Tests for hierarchy.el | ||
| 2 | |||
| 3 | ;; Copyright (C) 2017-2019 Damien Cassou | ||
| 4 | |||
| 5 | ;; Author: Damien Cassou <damien@cassou.me> | ||
| 6 | ;; Maintainer: emacs-devel@gnu.org | ||
| 7 | |||
| 8 | ;; This file is part of GNU Emacs. | ||
| 9 | |||
| 10 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 11 | ;; it under the terms of the GNU General Public License as published by | ||
| 12 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 13 | ;; (at your option) any later version. | ||
| 14 | |||
| 15 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 18 | ;; GNU General Public License for more details. | ||
| 19 | |||
| 20 | ;; You should have received a copy of the GNU General Public License | ||
| 21 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 22 | |||
| 23 | ;;; Commentary: | ||
| 24 | |||
| 25 | ;; Tests for hierarchy.el | ||
| 26 | |||
| 27 | ;;; Code: | ||
| 28 | |||
| 29 | (require 'ert) | ||
| 30 | (require 'hierarchy) | ||
| 31 | |||
| 32 | (defun hierarchy-animals () | ||
| 33 | "Create a sorted animal hierarchy." | ||
| 34 | (let ((parentfn (lambda (item) (cl-case item | ||
| 35 | (dove 'bird) | ||
| 36 | (pigeon 'bird) | ||
| 37 | (bird 'animal) | ||
| 38 | (dolphin 'animal) | ||
| 39 | (cow 'animal)))) | ||
| 40 | (hierarchy (hierarchy-new))) | ||
| 41 | (hierarchy-add-tree hierarchy 'dove parentfn) | ||
| 42 | (hierarchy-add-tree hierarchy 'pigeon parentfn) | ||
| 43 | (hierarchy-add-tree hierarchy 'dolphin parentfn) | ||
| 44 | (hierarchy-add-tree hierarchy 'cow parentfn) | ||
| 45 | (hierarchy-sort hierarchy) | ||
| 46 | hierarchy)) | ||
| 47 | |||
| 48 | (ert-deftest hierarchy-add-one-root () | ||
| 49 | (let ((parentfn (lambda (_) nil)) | ||
| 50 | (hierarchy (hierarchy-new))) | ||
| 51 | (hierarchy-add-tree hierarchy 'animal parentfn) | ||
| 52 | (should (equal (hierarchy-roots hierarchy) '(animal))))) | ||
| 53 | |||
| 54 | (ert-deftest hierarchy-add-one-item-with-parent () | ||
| 55 | (let ((parentfn (lambda (item) | ||
| 56 | (cl-case item | ||
| 57 | (bird 'animal)))) | ||
| 58 | (hierarchy (hierarchy-new))) | ||
| 59 | (hierarchy-add-tree hierarchy 'bird parentfn) | ||
| 60 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 61 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))))) | ||
| 62 | |||
| 63 | (ert-deftest hierarchy-add-one-item-with-parent-and-grand-parent () | ||
| 64 | (let ((parentfn (lambda (item) | ||
| 65 | (cl-case item | ||
| 66 | (dove 'bird) | ||
| 67 | (bird 'animal)))) | ||
| 68 | (hierarchy (hierarchy-new))) | ||
| 69 | (hierarchy-add-tree hierarchy 'dove parentfn) | ||
| 70 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 71 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))) | ||
| 72 | (should (equal (hierarchy-children hierarchy 'bird) '(dove))))) | ||
| 73 | |||
| 74 | (ert-deftest hierarchy-add-same-root-twice () | ||
| 75 | (let ((parentfn (lambda (_) nil)) | ||
| 76 | (hierarchy (hierarchy-new))) | ||
| 77 | (hierarchy-add-tree hierarchy 'animal parentfn) | ||
| 78 | (hierarchy-add-tree hierarchy 'animal parentfn) | ||
| 79 | (should (equal (hierarchy-roots hierarchy) '(animal))))) | ||
| 80 | |||
| 81 | (ert-deftest hierarchy-add-same-child-twice () | ||
| 82 | (let ((parentfn (lambda (item) | ||
| 83 | (cl-case item | ||
| 84 | (bird 'animal)))) | ||
| 85 | (hierarchy (hierarchy-new))) | ||
| 86 | (hierarchy-add-tree hierarchy 'bird parentfn) | ||
| 87 | (hierarchy-add-tree hierarchy 'bird parentfn) | ||
| 88 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 89 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))))) | ||
| 90 | |||
| 91 | (ert-deftest hierarchy-add-item-and-its-parent () | ||
| 92 | (let ((parentfn (lambda (item) | ||
| 93 | (cl-case item | ||
| 94 | (bird 'animal)))) | ||
| 95 | (hierarchy (hierarchy-new))) | ||
| 96 | (hierarchy-add-tree hierarchy 'bird parentfn) | ||
| 97 | (hierarchy-add-tree hierarchy 'animal parentfn) | ||
| 98 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 99 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))))) | ||
| 100 | |||
| 101 | (ert-deftest hierarchy-add-item-and-its-child () | ||
| 102 | (let ((parentfn (lambda (item) | ||
| 103 | (cl-case item | ||
| 104 | (bird 'animal)))) | ||
| 105 | (hierarchy (hierarchy-new))) | ||
| 106 | (hierarchy-add-tree hierarchy 'animal parentfn) | ||
| 107 | (hierarchy-add-tree hierarchy 'bird parentfn) | ||
| 108 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 109 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))))) | ||
| 110 | |||
| 111 | (ert-deftest hierarchy-add-two-items-sharing-parent () | ||
| 112 | (let ((parentfn (lambda (item) | ||
| 113 | (cl-case item | ||
| 114 | (dove 'bird) | ||
| 115 | (pigeon 'bird)))) | ||
| 116 | (hierarchy (hierarchy-new))) | ||
| 117 | (hierarchy-add-tree hierarchy 'dove parentfn) | ||
| 118 | (hierarchy-add-tree hierarchy 'pigeon parentfn) | ||
| 119 | (should (equal (hierarchy-roots hierarchy) '(bird))) | ||
| 120 | (should (equal (hierarchy-children hierarchy 'bird) '(dove pigeon))))) | ||
| 121 | |||
| 122 | (ert-deftest hierarchy-add-two-hierarchies () | ||
| 123 | (let ((parentfn (lambda (item) | ||
| 124 | (cl-case item | ||
| 125 | (dove 'bird) | ||
| 126 | (circle 'shape)))) | ||
| 127 | (hierarchy (hierarchy-new))) | ||
| 128 | (hierarchy-add-tree hierarchy 'dove parentfn) | ||
| 129 | (hierarchy-add-tree hierarchy 'circle parentfn) | ||
| 130 | (should (equal (hierarchy-roots hierarchy) '(bird shape))) | ||
| 131 | (should (equal (hierarchy-children hierarchy 'bird) '(dove))) | ||
| 132 | (should (equal (hierarchy-children hierarchy 'shape) '(circle))))) | ||
| 133 | |||
| 134 | (ert-deftest hierarchy-add-with-childrenfn () | ||
| 135 | (let ((childrenfn (lambda (item) | ||
| 136 | (cl-case item | ||
| 137 | (animal '(bird)) | ||
| 138 | (bird '(dove pigeon))))) | ||
| 139 | (hierarchy (hierarchy-new))) | ||
| 140 | (hierarchy-add-tree hierarchy 'animal nil childrenfn) | ||
| 141 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 142 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))) | ||
| 143 | (should (equal (hierarchy-children hierarchy 'bird) '(dove pigeon))))) | ||
| 144 | |||
| 145 | (ert-deftest hierarchy-add-with-parentfn-and-childrenfn () | ||
| 146 | (let ((parentfn (lambda (item) | ||
| 147 | (cl-case item | ||
| 148 | (bird 'animal) | ||
| 149 | (animal 'life-form)))) | ||
| 150 | (childrenfn (lambda (item) | ||
| 151 | (cl-case item | ||
| 152 | (bird '(dove pigeon)) | ||
| 153 | (pigeon '(ashy-wood-pigeon))))) | ||
| 154 | (hierarchy (hierarchy-new))) | ||
| 155 | (hierarchy-add-tree hierarchy 'bird parentfn childrenfn) | ||
| 156 | (should (equal (hierarchy-roots hierarchy) '(life-form))) | ||
| 157 | (should (equal (hierarchy-children hierarchy 'life-form) '(animal))) | ||
| 158 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))) | ||
| 159 | (should (equal (hierarchy-children hierarchy 'bird) '(dove pigeon))) | ||
| 160 | (should (equal (hierarchy-children hierarchy 'pigeon) '(ashy-wood-pigeon))))) | ||
| 161 | |||
| 162 | (ert-deftest hierarchy-add-twice-with-parentfn-and-childrenfn () | ||
| 163 | (let* ((parentfn (lambda (item) | ||
| 164 | (cl-case item | ||
| 165 | (dove 'bird) | ||
| 166 | (bird 'animal)))) | ||
| 167 | (childrenfn (lambda (item) | ||
| 168 | (cl-case item | ||
| 169 | (animal '(bird)) | ||
| 170 | (bird '(dove))))) | ||
| 171 | (hierarchy (hierarchy-new))) | ||
| 172 | (hierarchy-add-tree hierarchy 'bird parentfn childrenfn) | ||
| 173 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))) | ||
| 174 | (should (equal (hierarchy-children hierarchy 'bird) '(dove))))) | ||
| 175 | |||
| 176 | (ert-deftest hierarchy-add-trees () | ||
| 177 | (let ((parentfn (lambda (item) | ||
| 178 | (cl-case item | ||
| 179 | (dove 'bird) | ||
| 180 | (pigeon 'bird) | ||
| 181 | (bird 'animal)))) | ||
| 182 | (hierarchy (hierarchy-new))) | ||
| 183 | (hierarchy-add-trees hierarchy '(dove pigeon) parentfn) | ||
| 184 | (should (equal (hierarchy-roots hierarchy) '(animal))) | ||
| 185 | (should (equal (hierarchy-children hierarchy 'animal) '(bird))) | ||
| 186 | (should (equal (hierarchy-children hierarchy 'bird) '(dove pigeon))))) | ||
| 187 | |||
| 188 | (ert-deftest hierarchy-from-list () | ||
| 189 | (let ((hierarchy (hierarchy-from-list | ||
| 190 | '(animal (bird (dove) | ||
| 191 | (pigeon)) | ||
| 192 | (cow) | ||
| 193 | (dolphin))))) | ||
| 194 | (hierarchy-sort hierarchy (lambda (item1 item2) | ||
| 195 | (string< (car item1) | ||
| 196 | (car item2)))) | ||
| 197 | (should (equal (hierarchy-to-string hierarchy (lambda (item) (symbol-name (car item)))) | ||
| 198 | "animal\n bird\n dove\n pigeon\n cow\n dolphin\n")))) | ||
| 199 | |||
| 200 | (ert-deftest hierarchy-from-list-with-duplicates () | ||
| 201 | (let ((hierarchy (hierarchy-from-list | ||
| 202 | '(a (b) (b)) | ||
| 203 | t))) | ||
| 204 | (hierarchy-sort hierarchy (lambda (item1 item2) | ||
| 205 | ;; sort by ID | ||
| 206 | (< (car item1) (car item2)))) | ||
| 207 | (should (equal (hierarchy-length hierarchy) 3)) | ||
| 208 | (should (equal (hierarchy-to-string | ||
| 209 | hierarchy | ||
| 210 | (lambda (item) | ||
| 211 | (format "%s(%s)" | ||
| 212 | (cadr item) | ||
| 213 | (car item)))) | ||
| 214 | "a(1)\n b(2)\n b(3)\n")))) | ||
| 215 | |||
| 216 | (ert-deftest hierarchy-from-list-with-childrenfn () | ||
| 217 | (let ((hierarchy (hierarchy-from-list | ||
| 218 | "abc" | ||
| 219 | nil | ||
| 220 | (lambda (item) | ||
| 221 | (when (string= item "abc") | ||
| 222 | (split-string item "" t)))))) | ||
| 223 | (hierarchy-sort hierarchy (lambda (item1 item2) (string< item1 item2))) | ||
| 224 | (should (equal (hierarchy-length hierarchy) 4)) | ||
| 225 | (should (equal (hierarchy-to-string hierarchy) | ||
| 226 | "abc\n a\n b\n c\n")))) | ||
| 227 | |||
| 228 | (ert-deftest hierarchy-add-relation-check-error-when-different-parent () | ||
| 229 | (let ((parentfn (lambda (item) | ||
| 230 | (cl-case item | ||
| 231 | (bird 'animal)))) | ||
| 232 | (hierarchy (hierarchy-new))) | ||
| 233 | (hierarchy-add-tree hierarchy 'bird parentfn) | ||
| 234 | (should-error | ||
| 235 | (hierarchy--add-relation hierarchy 'bird 'cow #'identity)))) | ||
| 236 | |||
| 237 | (ert-deftest hierarchy-empty-p-return-non-nil-for-empty () | ||
| 238 | (should (hierarchy-empty-p (hierarchy-new)))) | ||
| 239 | |||
| 240 | (ert-deftest hierarchy-empty-p-return-nil-for-non-empty () | ||
| 241 | (should-not (hierarchy-empty-p (hierarchy-animals)))) | ||
| 242 | |||
| 243 | (ert-deftest hierarchy-length-of-empty-is-0 () | ||
| 244 | (should (equal (hierarchy-length (hierarchy-new)) 0))) | ||
| 245 | |||
| 246 | (ert-deftest hierarchy-length-of-non-empty-counts-items () | ||
| 247 | (let ((parentfn (lambda (item) | ||
| 248 | (cl-case item | ||
| 249 | (bird 'animal) | ||
| 250 | (dove 'bird) | ||
| 251 | (pigeon 'bird)))) | ||
| 252 | (hierarchy (hierarchy-new))) | ||
| 253 | (hierarchy-add-tree hierarchy 'dove parentfn) | ||
| 254 | (hierarchy-add-tree hierarchy 'pigeon parentfn) | ||
| 255 | (should (equal (hierarchy-length hierarchy) 4)))) | ||
| 256 | |||
| 257 | (ert-deftest hierarchy-has-root () | ||
| 258 | (let ((parentfn (lambda (item) | ||
| 259 | (cl-case item | ||
| 260 | (bird 'animal) | ||
| 261 | (dove 'bird) | ||
| 262 | (pigeon 'bird)))) | ||
| 263 | (hierarchy (hierarchy-new))) | ||
| 264 | (should-not (hierarchy-has-root hierarchy 'animal)) | ||
| 265 | (should-not (hierarchy-has-root hierarchy 'bird)) | ||
| 266 | (hierarchy-add-tree hierarchy 'dove parentfn) | ||
| 267 | (hierarchy-add-tree hierarchy 'pigeon parentfn) | ||
| 268 | (should (hierarchy-has-root hierarchy 'animal)) | ||
| 269 | (should-not (hierarchy-has-root hierarchy 'bird)))) | ||
| 270 | |||
| 271 | (ert-deftest hierarchy-leafs () | ||
| 272 | (let ((animals (hierarchy-animals))) | ||
| 273 | (should (equal (hierarchy-leafs animals) | ||
| 274 | '(dove pigeon dolphin cow))))) | ||
| 275 | |||
| 276 | (ert-deftest hierarchy-leafs-includes-lonely-roots () | ||
| 277 | (let ((parentfn (lambda (item) nil)) | ||
| 278 | (hierarchy (hierarchy-new))) | ||
| 279 | (hierarchy-add-tree hierarchy 'foo parentfn) | ||
| 280 | (should (equal (hierarchy-leafs hierarchy) | ||
| 281 | '(foo))))) | ||
| 282 | |||
| 283 | (ert-deftest hierarchy-leafs-of-node () | ||
| 284 | (let ((animals (hierarchy-animals))) | ||
| 285 | (should (equal (hierarchy-leafs animals 'cow) '())) | ||
| 286 | (should (equal (hierarchy-leafs animals 'animal) '(dove pigeon dolphin cow))) | ||
| 287 | (should (equal (hierarchy-leafs animals 'bird) '(dove pigeon))) | ||
| 288 | (should (equal (hierarchy-leafs animals 'dove) '())))) | ||
| 289 | |||
| 290 | (ert-deftest hierarchy-child-p () | ||
| 291 | (let ((animals (hierarchy-animals))) | ||
| 292 | (should (hierarchy-child-p animals 'dove 'bird)) | ||
| 293 | (should (hierarchy-child-p animals 'bird 'animal)) | ||
| 294 | (should (hierarchy-child-p animals 'cow 'animal)) | ||
| 295 | (should-not (hierarchy-child-p animals 'cow 'bird)) | ||
| 296 | (should-not (hierarchy-child-p animals 'bird 'cow)) | ||
| 297 | (should-not (hierarchy-child-p animals 'animal 'dove)) | ||
| 298 | (should-not (hierarchy-child-p animals 'animal 'bird)))) | ||
| 299 | |||
| 300 | (ert-deftest hierarchy-descendant () | ||
| 301 | (let ((animals (hierarchy-animals))) | ||
| 302 | (should (hierarchy-descendant-p animals 'dove 'animal)) | ||
| 303 | (should (hierarchy-descendant-p animals 'dove 'bird)) | ||
| 304 | (should (hierarchy-descendant-p animals 'bird 'animal)) | ||
| 305 | (should (hierarchy-descendant-p animals 'cow 'animal)) | ||
| 306 | (should-not (hierarchy-descendant-p animals 'cow 'bird)) | ||
| 307 | (should-not (hierarchy-descendant-p animals 'bird 'cow)) | ||
| 308 | (should-not (hierarchy-descendant-p animals 'animal 'dove)) | ||
| 309 | (should-not (hierarchy-descendant-p animals 'animal 'bird)))) | ||
| 310 | |||
| 311 | (ert-deftest hierarchy-descendant-if-not-same () | ||
| 312 | (let ((animals (hierarchy-animals))) | ||
| 313 | (should-not (hierarchy-descendant-p animals 'cow 'cow)) | ||
| 314 | (should-not (hierarchy-descendant-p animals 'dove 'dove)) | ||
| 315 | (should-not (hierarchy-descendant-p animals 'bird 'bird)) | ||
| 316 | (should-not (hierarchy-descendant-p animals 'animal 'animal)))) | ||
| 317 | |||
| 318 | ;; keywords supported: :test :key | ||
| 319 | (ert-deftest hierarchy--set-equal () | ||
| 320 | (should (hierarchy--set-equal '(1 2 3) '(1 2 3))) | ||
| 321 | (should (hierarchy--set-equal '(1 2 3) '(3 2 1))) | ||
| 322 | (should (hierarchy--set-equal '(3 2 1) '(1 2 3))) | ||
| 323 | (should-not (hierarchy--set-equal '(2 3) '(3 2 1))) | ||
| 324 | (should-not (hierarchy--set-equal '(1 2 3) '(2 3))) | ||
| 325 | (should-not (hierarchy--set-equal '("1" "2") '("2" "1") :test #'eq)) | ||
| 326 | (should (hierarchy--set-equal '("1" "2") '("2" "1") :test #'equal)) | ||
| 327 | (should-not (hierarchy--set-equal '(1 2) '(-1 -2))) | ||
| 328 | (should (hierarchy--set-equal '(1 2) '(-1 -2) :key #'abs)) | ||
| 329 | (should-not (hierarchy--set-equal '(("1" 1) ("2" 1)) '(("1" 2) ("2" 2)))) | ||
| 330 | (should-not (hierarchy--set-equal '(("1" 1) ("2" 1)) '(("1" 2) ("2" 2)) :key #'car)) | ||
| 331 | (should-not (hierarchy--set-equal '(("1" 1) ("2" 1)) '(("1" 2) ("2" 2)) :test #'equal)) | ||
| 332 | (should (hierarchy--set-equal '(("1" 1) ("2" 1)) '(("1" 2) ("2" 2)) :key #'car :test #'equal))) | ||
| 333 | |||
| 334 | (ert-deftest hierarchy-equal-returns-true-for-same-hierarchy () | ||
| 335 | (let ((animals (hierarchy-animals))) | ||
| 336 | (should (hierarchy-equal animals animals)) | ||
| 337 | (should (hierarchy-equal (hierarchy-animals) animals)))) | ||
| 338 | |||
| 339 | (ert-deftest hierarchy-equal-returns-true-for-hierarchy-copies () | ||
| 340 | (let ((animals (hierarchy-animals))) | ||
| 341 | (should (hierarchy-equal animals (hierarchy-copy animals))))) | ||
| 342 | |||
| 343 | (ert-deftest hierarchy-map-item-on-leaf () | ||
| 344 | (let* ((animals (hierarchy-animals)) | ||
| 345 | (result (hierarchy-map-item (lambda (item indent) (cons item indent)) | ||
| 346 | 'cow | ||
| 347 | animals))) | ||
| 348 | (should (equal result '((cow . 0)))))) | ||
| 349 | |||
| 350 | (ert-deftest hierarchy-map-item-on-leaf-with-indent () | ||
| 351 | (let* ((animals (hierarchy-animals)) | ||
| 352 | (result (hierarchy-map-item (lambda (item indent) (cons item indent)) | ||
| 353 | 'cow | ||
| 354 | animals | ||
| 355 | 2))) | ||
| 356 | (should (equal result '((cow . 2)))))) | ||
| 357 | |||
| 358 | (ert-deftest hierarchy-map-item-on-parent () | ||
| 359 | (let* ((animals (hierarchy-animals)) | ||
| 360 | (result (hierarchy-map-item (lambda (item indent) (cons item indent)) | ||
| 361 | 'bird | ||
| 362 | animals))) | ||
| 363 | (should (equal result '((bird . 0) (dove . 1) (pigeon . 1)))))) | ||
| 364 | |||
| 365 | (ert-deftest hierarchy-map-item-on-grand-parent () | ||
| 366 | (let* ((animals (hierarchy-animals)) | ||
| 367 | (result (hierarchy-map-item (lambda (item indent) (cons item indent)) | ||
| 368 | 'animal | ||
| 369 | animals))) | ||
| 370 | (should (equal result '((animal . 0) (bird . 1) (dove . 2) (pigeon . 2) | ||
| 371 | (cow . 1) (dolphin . 1)))))) | ||
| 372 | |||
| 373 | (ert-deftest hierarchy-map-conses () | ||
| 374 | (let* ((animals (hierarchy-animals)) | ||
| 375 | (result (hierarchy-map (lambda (item indent) | ||
| 376 | (cons item indent)) | ||
| 377 | animals))) | ||
| 378 | (should (equal result '((animal . 0) | ||
| 379 | (bird . 1) | ||
| 380 | (dove . 2) | ||
| 381 | (pigeon . 2) | ||
| 382 | (cow . 1) | ||
| 383 | (dolphin . 1)))))) | ||
| 384 | |||
| 385 | (ert-deftest hierarchy-map-tree () | ||
| 386 | (let ((animals (hierarchy-animals))) | ||
| 387 | (should (equal (hierarchy-map-tree (lambda (item indent children) | ||
| 388 | (list item indent children)) | ||
| 389 | animals) | ||
| 390 | '(animal | ||
| 391 | 0 | ||
| 392 | ((bird 1 ((dove 2 nil) (pigeon 2 nil))) | ||
| 393 | (cow 1 nil) | ||
| 394 | (dolphin 1 nil))))))) | ||
| 395 | |||
| 396 | (ert-deftest hierarchy-map-hierarchy-keeps-hierarchy () | ||
| 397 | (let* ((animals (hierarchy-animals)) | ||
| 398 | (result (hierarchy-map-hierarchy (lambda (item _) (identity item)) | ||
| 399 | animals))) | ||
| 400 | (should (hierarchy-equal animals result)))) | ||
| 401 | |||
| 402 | (ert-deftest hierarchy-map-applies-function () | ||
| 403 | (let* ((animals (hierarchy-animals)) | ||
| 404 | (parentfn (lambda (item) | ||
| 405 | (cond | ||
| 406 | ((equal item "bird") "animal") | ||
| 407 | ((equal item "dove") "bird") | ||
| 408 | ((equal item "pigeon") "bird") | ||
| 409 | ((equal item "cow") "animal") | ||
| 410 | ((equal item "dolphin") "animal")))) | ||
| 411 | (expected (hierarchy-new))) | ||
| 412 | (hierarchy-add-tree expected "dove" parentfn) | ||
| 413 | (hierarchy-add-tree expected "pigeon" parentfn) | ||
| 414 | (hierarchy-add-tree expected "cow" parentfn) | ||
| 415 | (hierarchy-add-tree expected "dolphin" parentfn) | ||
| 416 | (should (hierarchy-equal | ||
| 417 | (hierarchy-map-hierarchy (lambda (item _) (symbol-name item)) animals) | ||
| 418 | expected)))) | ||
| 419 | |||
| 420 | (ert-deftest hierarchy-extract-tree () | ||
| 421 | (let* ((animals (hierarchy-animals)) | ||
| 422 | (birds (hierarchy-extract-tree animals 'bird))) | ||
| 423 | (hierarchy-sort birds) | ||
| 424 | (should (equal (hierarchy-roots birds) '(animal))) | ||
| 425 | (should (equal (hierarchy-children birds 'animal) '(bird))) | ||
| 426 | (should (equal (hierarchy-children birds 'bird) '(dove pigeon))))) | ||
| 427 | |||
| 428 | (ert-deftest hierarchy-extract-tree-nil-if-not-in-hierarchy () | ||
| 429 | (let* ((animals (hierarchy-animals))) | ||
| 430 | (should-not (hierarchy-extract-tree animals 'foobar)))) | ||
| 431 | |||
| 432 | (ert-deftest hierarchy-items-of-empty-hierarchy-is-empty () | ||
| 433 | (should (seq-empty-p (hierarchy-items (hierarchy-new))))) | ||
| 434 | |||
| 435 | (ert-deftest hierarchy-items-returns-sequence-of-same-length () | ||
| 436 | (let* ((animals (hierarchy-animals)) | ||
| 437 | (result (hierarchy-items animals))) | ||
| 438 | (should (= (seq-length result) (hierarchy-length animals))))) | ||
| 439 | |||
| 440 | (ert-deftest hierarchy-items-return-all-elements-of-hierarchy () | ||
| 441 | (let* ((animals (hierarchy-animals)) | ||
| 442 | (result (hierarchy-items animals))) | ||
| 443 | (should (equal (seq-sort #'string< result) '(animal bird cow dolphin dove pigeon))))) | ||
| 444 | |||
| 445 | (ert-deftest hierarchy-labelfn-indent-no-indent-if-0 () | ||
| 446 | (let* ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 447 | (labelfn (hierarchy-labelfn-indent labelfn-base))) | ||
| 448 | (should (equal | ||
| 449 | (with-temp-buffer | ||
| 450 | (funcall labelfn "bar" 0) | ||
| 451 | (buffer-substring (point-min) (point-max))) | ||
| 452 | "foo")))) | ||
| 453 | |||
| 454 | (ert-deftest hierarchy-labelfn-indent-three-times-if-3 () | ||
| 455 | (let* ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 456 | (labelfn (hierarchy-labelfn-indent labelfn-base))) | ||
| 457 | (should (equal | ||
| 458 | (with-temp-buffer | ||
| 459 | (funcall labelfn "bar" 3) | ||
| 460 | (buffer-substring (point-min) (point-max))) | ||
| 461 | " foo")))) | ||
| 462 | |||
| 463 | (ert-deftest hierarchy-labelfn-indent-default-indent-string () | ||
| 464 | (let* ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 465 | (labelfn (hierarchy-labelfn-indent labelfn-base))) | ||
| 466 | (should (equal | ||
| 467 | (with-temp-buffer | ||
| 468 | (funcall labelfn "bar" 1) | ||
| 469 | (buffer-substring (point-min) (point-max))) | ||
| 470 | " foo")))) | ||
| 471 | |||
| 472 | (ert-deftest hierarchy-labelfn-indent-custom-indent-string () | ||
| 473 | (let* ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 474 | (labelfn (hierarchy-labelfn-indent labelfn-base "###")) | ||
| 475 | (content (with-temp-buffer | ||
| 476 | (funcall labelfn "bar" 1) | ||
| 477 | (buffer-substring (point-min) (point-max))))) | ||
| 478 | (should (equal content "###foo")))) | ||
| 479 | |||
| 480 | (ert-deftest hierarchy-labelfn-button-propertize () | ||
| 481 | (let* ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 482 | (actionfn #'identity) | ||
| 483 | (labelfn (hierarchy-labelfn-button labelfn-base actionfn)) | ||
| 484 | (properties (with-temp-buffer | ||
| 485 | (funcall labelfn "bar" 1) | ||
| 486 | (text-properties-at 1)))) | ||
| 487 | (should (equal (car properties) 'action)))) | ||
| 488 | |||
| 489 | (ert-deftest hierarchy-labelfn-button-execute-labelfn () | ||
| 490 | (let* ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 491 | (actionfn #'identity) | ||
| 492 | (labelfn (hierarchy-labelfn-button labelfn-base actionfn)) | ||
| 493 | (content (with-temp-buffer | ||
| 494 | (funcall labelfn "bar" 1) | ||
| 495 | (buffer-substring-no-properties (point-min) (point-max))))) | ||
| 496 | (should (equal content "foo")))) | ||
| 497 | |||
| 498 | (ert-deftest hierarchy-labelfn-button-if-does-not-button-unless-condition () | ||
| 499 | (let ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 500 | (spy-count 0) | ||
| 501 | (condition (lambda (_item _indent) nil))) | ||
| 502 | (cl-letf (((symbol-function 'hierarchy-labelfn-button) (lambda (_labelfn _actionfn) (lambda (_item _indent) (cl-incf spy-count))))) | ||
| 503 | (funcall (hierarchy-labelfn-button-if labelfn-base condition #'identity) nil nil) | ||
| 504 | (should (equal spy-count 0))))) | ||
| 505 | |||
| 506 | (ert-deftest hierarchy-labelfn-button-if-does-button-when-condition () | ||
| 507 | (let ((labelfn-base (lambda (_item _indent) (insert "foo"))) | ||
| 508 | (spy-count 0) | ||
| 509 | (condition (lambda (_item _indent) t))) | ||
| 510 | (cl-letf (((symbol-function 'hierarchy-labelfn-button) (lambda (_labelfn _actionfn) (lambda (_item _indent) (cl-incf spy-count))))) | ||
| 511 | (funcall (hierarchy-labelfn-button-if labelfn-base condition #'identity) nil nil) | ||
| 512 | (should (equal spy-count 1))))) | ||
| 513 | |||
| 514 | (ert-deftest hierarchy-labelfn-to-string () | ||
| 515 | (let ((labelfn (lambda (item _indent) (insert item)))) | ||
| 516 | (should (equal (hierarchy-labelfn-to-string labelfn "foo" 1) "foo")))) | ||
| 517 | |||
| 518 | (ert-deftest hierarchy-print () | ||
| 519 | (let* ((animals (hierarchy-animals)) | ||
| 520 | (result (with-temp-buffer | ||
| 521 | (hierarchy-print animals) | ||
| 522 | (buffer-substring-no-properties (point-min) (point-max))))) | ||
| 523 | (should (equal result "animal\n bird\n dove\n pigeon\n cow\n dolphin\n")))) | ||
| 524 | |||
| 525 | (ert-deftest hierarchy-to-string () | ||
| 526 | (let* ((animals (hierarchy-animals)) | ||
| 527 | (result (hierarchy-to-string animals))) | ||
| 528 | (should (equal result "animal\n bird\n dove\n pigeon\n cow\n dolphin\n")))) | ||
| 529 | |||
| 530 | (ert-deftest hierarchy-tabulated-display () | ||
| 531 | (let* ((animals (hierarchy-animals)) | ||
| 532 | (labelfn (lambda (item _indent) (insert (symbol-name item)))) | ||
| 533 | (contents (with-temp-buffer | ||
| 534 | (hierarchy-tabulated-display animals labelfn (current-buffer)) | ||
| 535 | (buffer-substring-no-properties (point-min) (point-max))))) | ||
| 536 | (should (equal contents "animal\nbird\ndove\npigeon\ncow\ndolphin\n")))) | ||
| 537 | |||
| 538 | (ert-deftest hierarchy-sort-non-root-nodes () | ||
| 539 | (let* ((animals (hierarchy-animals))) | ||
| 540 | (should (equal (hierarchy-roots animals) '(animal))) | ||
| 541 | (should (equal (hierarchy-children animals 'animal) '(bird cow dolphin))) | ||
| 542 | (should (equal (hierarchy-children animals 'bird) '(dove pigeon))))) | ||
| 543 | |||
| 544 | (ert-deftest hierarchy-sort-roots () | ||
| 545 | (let* ((organisms (hierarchy-new)) | ||
| 546 | (parentfn (lambda (item) | ||
| 547 | (cl-case item | ||
| 548 | (oak 'plant) | ||
| 549 | (bird 'animal))))) | ||
| 550 | (hierarchy-add-tree organisms 'oak parentfn) | ||
| 551 | (hierarchy-add-tree organisms 'bird parentfn) | ||
| 552 | (hierarchy-sort organisms) | ||
| 553 | (should (equal (hierarchy-roots organisms) '(animal plant))))) | ||
| 554 | |||
| 555 | (provide 'hierarchy-tests) | ||
| 556 | ;;; hierarchy-tests.el ends here | ||