org-list: use list structure to update checkboxes and cookies

* lisp/org-list.el (org-toggle-checkbox): use structures to fix
  checkboxes of a list
(org-update-checkbox-count): use structures to update cookies
This commit is contained in:
Nicolas Goaziou 2010-12-17 19:54:25 +01:00
parent 8a3a81c08e
commit 1829aa79b5
1 changed files with 191 additions and 173 deletions

View File

@ -1798,77 +1798,91 @@ If the cursor is in a headline, apply this to all checkbox items
in the text below the heading, taking as reference the first item
in subtree, ignoring drawers."
(interactive "P")
;; Bounds is a list of type (beg end single-p) where single-p is t
;; when `org-toggle-checkbox' is applied to a single item. Only
;; toggles on single items will return errors.
(let* ((bounds
(cond
((org-region-active-p)
(let ((rbeg (region-beginning))
(rend (region-end)))
(save-excursion
(goto-char rbeg)
(if (org-search-forward-unenclosed org-item-beginning-re rend 'move)
(list (point-at-bol) rend nil)
(error "No item in region")))))
((org-on-heading-p)
;; In this case, reference line is the first item in
;; subtree outside drawers
(let ((pos (point))
(limit (save-excursion (outline-next-heading) (point))))
(save-excursion
(goto-char limit)
(org-search-backward-unenclosed ":END:" pos 'move)
(org-search-forward-unenclosed
org-item-beginning-re limit 'move)
(list (point) limit nil))))
((org-at-item-p)
(list (point-at-bol) (1+ (point-at-eol)) t))
(t (error "Not at an item or heading, and no active region"))))
(beg (car bounds))
;; marker is needed because deleting or inserting checkboxes
;; will change bottom point
(end (copy-marker (nth 1 bounds)))
(single-p (nth 2 bounds))
(ref-presence (save-excursion
(goto-char beg)
(org-at-item-checkbox-p)))
(ref-status (equal (match-string 1) "[X]"))
(act-on-item
(lambda (ref-pres ref-stat)
(if (equal toggle-presence '(4))
(cond
((and ref-pres (org-at-item-checkbox-p))
(replace-match ""))
((and (not ref-pres)
(not (org-at-item-checkbox-p))
(org-at-item-p))
(goto-char (match-end 0))
;; Ignore counter, if any
(when (looking-at "\\(?:\\[@\\(?:start:\\)?[0-9]+\\][ \t]*\\)?")
(goto-char (match-end 0)))
(let ((desc-p (and (org-at-item-description-p)
(cdr (assq 'checkbox org-list-automatic-rules)))))
(cond
((and single-p desc-p)
(error "Cannot add a checkbox in a description list"))
((not desc-p) (insert "[ ] "))))))
(let ((blocked (org-checkbox-blocked-p)))
(cond
((and blocked single-p)
(error "Checkbox blocked because of unchecked box in line %d" blocked))
(blocked nil)
((org-at-item-checkbox-p)
(replace-match
(cond ((equal toggle-presence '(16)) "[-]")
(ref-stat "[ ]")
(t "[X]"))
t t nil 1))))))))
(save-excursion
(goto-char beg)
(while (< (point) end)
(funcall act-on-item ref-presence ref-status)
(org-search-forward-unenclosed org-item-beginning-re end 'move)))
(save-excursion
(let* (singlep
block-item
lim-up
lim-down
(orderedp (ignore-errors (org-entry-get nil "ORDERED")))
(bounds
;; In a region, start at first item in region
(cond
((org-region-active-p)
(let ((limit (region-end)))
(goto-char (region-beginning))
(if (org-search-forward-unenclosed org-item-beginning-re
limit t)
(setq lim-up (point-at-bol))
(error "No item in region"))
(setq lim-down (copy-marker limit))))
((org-on-heading-p)
;; On an heading, start at first item after drawers
(let ((limit (save-excursion (outline-next-heading) (point))))
(forward-line 1)
(when (looking-at org-drawer-regexp)
(re-search-forward "^[ \t]*:END:" limit nil))
(if (org-search-forward-unenclosed org-item-beginning-re
limit t)
(setq lim-up (point-at-bol))
(error "No item in subtree"))
(setq lim-down (copy-marker limit))))
;; Just one item: set singlep flag
((org-at-item-p)
(setq singlep t)
(setq lim-up (point-at-bol)
lim-down (point-at-eol)))
(t (error "Not at an item or heading, and no active region"))))
;; determine the checkbox going to be applied to all items
;; within bounds
(ref-checkbox
(progn
(goto-char lim-up)
(let ((cbox (and (org-at-item-checkbox-p) (match-string 1))))
(cond
((equal toggle-presence '(16)) "[-]")
((equal toggle-presence '(4))
(unless cbox "[ ]"))
((equal "[ ]" cbox) "[X]")
(t "[ ]"))))))
;; When an item is found within bounds, grab the full list at
;; point structure, then: 1. set checkbox of all its items
;; within bounds to ref-checkbox; 2. fix checkboxes of the whole
;; list; 3. move point after the list.
(goto-char lim-up)
(while (and (< (point) lim-down)
(org-search-forward-unenclosed
org-item-beginning-re lim-down 'move))
(let* ((struct (org-list-struct))
(struct-copy (mapcar (lambda (e) (copy-alist e)) struct))
(parents (org-list-struct-parent-alist struct))
(bottom (copy-marker (org-list-get-bottom-point struct)))
(items-to-toggle (org-remove-if
(lambda (e) (or (< e lim-up) (> e lim-down)))
(mapcar 'car (cdr struct)))))
(mapc (lambda (e) (org-list-set-checkbox
e struct
;; if there is no box at item, leave as-is
;; unless function was called with C-u prefix
(let ((cur-box (org-list-get-checkbox e struct)))
(if (or cur-box (equal toggle-presence '(4)))
ref-checkbox
cur-box))))
items-to-toggle)
(setq block-item (org-list-struct-fix-box struct parents orderedp))
;; Report some problems due to ORDERED status of subtree. If
;; only one box was being checked, throw an error, else,
;; only signal problems.
(cond
((and singlep block-item (> lim-up block-item))
(error
"Checkbox blocked because of unchecked box at line %d"
(org-current-line block-item)))
(block-item
(message
"Checkboxes were removed due to unchecked box at line %d"
(org-current-line block-item))))
(goto-char bottom)
(org-list-struct-apply-struct struct struct-copy))))
(org-update-checkbox-count-maybe)))
(defun org-reset-checkbox-state-subtree ()
@ -1901,110 +1915,114 @@ information.")
(defun org-update-checkbox-count (&optional all)
"Update the checkbox statistics in the current section.
This will find all statistic cookies like [57%] and [6/12] and update them
with the current numbers. With optional prefix argument ALL, do this for
the whole buffer."
This will find all statistic cookies like [57%] and [6/12] and
update them with the current numbers.
With optional prefix argument ALL, do this for the whole buffer."
(interactive "P")
(save-excursion
(let ((cstat 0))
(catch 'exit
(while t
(let* ((buffer-invisibility-spec (org-inhibit-invisibility)) ; Emacs 21
(beg (condition-case nil
(progn (org-back-to-heading) (point))
(error (point-min))))
(end (copy-marker (save-excursion
(outline-next-heading) (point))))
(re-cookie "\\(\\(\\[[0-9]*%\\]\\)\\|\\(\\[[0-9]*/[0-9]*\\]\\)\\)")
(re-box "^[ \t]*\\([-+*]\\|[0-9]+[.)]\\)[ \t]+\\(?:\\[@\\(?:start:\\)?[0-9]+\\][ \t]*\\)?\\(\\[[- X]\\]\\)")
beg-cookie end-cookie is-percent c-on c-off lim new
curr-ind next-ind continue-from startsearch list-beg list-end
(recursive
(or (not org-hierarchical-checkbox-statistics)
(string-match "\\<recursive\\>"
(or (ignore-errors
(org-entry-get nil "COOKIE_DATA"))
"")))))
(goto-char end)
;; find each statistics cookie
(while (and (org-search-backward-unenclosed re-cookie beg 'move)
(not (save-match-data
(and (org-on-heading-p)
(string-match "\\<todo\\>"
(downcase
(or (org-entry-get
nil "COOKIE_DATA")
"")))))))
(setq beg-cookie (match-beginning 1)
end-cookie (match-end 1)
cstat (+ cstat (if end-cookie 1 0))
startsearch (point-at-eol)
continue-from (match-beginning 0)
is-percent (match-beginning 2)
lim (cond
((org-on-heading-p) (outline-next-heading) (point))
;; Ensure many cookies in the same list won't imply
;; computing list boundaries as many times.
((org-at-item-p)
(unless (and list-beg (>= (point) list-beg))
(setq list-beg (org-list-top-point)
list-end (copy-marker
(org-list-bottom-point))))
(org-get-end-of-item list-end))
(t nil))
c-on 0
c-off 0)
(when lim
;; find first checkbox for this cookie and gather
;; statistics from all that are at this indentation level
(goto-char startsearch)
(if (org-search-forward-unenclosed re-box lim t)
(progn
(beginning-of-line)
(setq curr-ind (org-get-indentation))
(setq next-ind curr-ind)
(while (and (bolp) (org-at-item-p)
(if recursive
(<= curr-ind next-ind)
(= curr-ind next-ind)))
(when (org-at-item-checkbox-p)
(if (member (match-string 1) '("[ ]" "[-]"))
(setq c-off (1+ c-off))
(setq c-on (1+ c-on))))
(if (not recursive)
;; org-get-next-item goes through list-enders
;; with proper limit.
(goto-char (or (org-get-next-item (point) lim) lim))
(end-of-line)
(when (org-search-forward-unenclosed
org-item-beginning-re lim t)
(beginning-of-line)))
(setq next-ind (org-get-indentation)))))
(goto-char continue-from)
;; update cookie
(when end-cookie
(setq new (if is-percent
(format "[%d%%]" (/ (* 100 c-on)
(max 1 (+ c-on c-off))))
(format "[%d/%d]" c-on (+ c-on c-off))))
(goto-char beg-cookie)
(insert new)
(delete-region (point) (+ (point) (- end-cookie beg-cookie))))
;; update items checkbox if it has one
(when (and (org-at-item-checkbox-p)
(> (+ c-on c-off) 0))
(setq beg-cookie (match-beginning 1)
end-cookie (match-end 1))
(delete-region beg-cookie end-cookie)
(goto-char beg-cookie)
(cond ((= c-off 0) (insert "[X]"))
((= c-on 0) (insert "[ ]"))
(t (insert "[-]")))))
(goto-char continue-from)))
(unless (and all (outline-next-heading)) (throw 'exit nil))))
(when (interactive-p)
(message "Checkbox statistics updated %s (%d places)"
(if all "in entire file" "in current outline entry") cstat)))))
(let ((cookie-re "\\(\\(\\[[0-9]*%\\]\\)\\|\\(\\[[0-9]*/[0-9]*\\]\\)\\)")
(box-re "^[ \t]*\\([-+*]\\|[0-9]+[.)]\\)[ \t]+\\(?:\\[@\\(?:start:\\)?[0-9]+\\][ \t]*\\)?\\(\\[[- X]\\]\\)")
(recursivep
(or (not org-hierarchical-checkbox-statistics)
(string-match "\\<recursive\\>"
(or (ignore-errors
(org-entry-get nil "COOKIE_DATA"))
""))))
(bounds (if all
(cons (point-min) (point-max))
(cons (or (ignore-errors (org-back-to-heading) (point))
(point-min))
(save-excursion (outline-next-heading) (point)))))
(count-boxes
(function
;; add checked boxes and boxes of all types in all
;; structures in STRUCTS to c-on and c-all, respectively.
;; This looks at RECURSIVEP value. If ITEM is nil, count
;; across the whole structure, else count only across
;; subtree whose ancestor is ITEM.
(lambda (item structs)
(mapc
(lambda (s)
(let* ((pre (org-list-struct-prev-alist s))
(items
(if recursivep
(or (and item (org-list-get-subtree item s pre))
(mapcar 'car s))
(or (and item (org-list-get-all-children item s pre))
(org-list-get-all-items
(org-list-get-top-point s) s pre))))
(cookies (delq nil (mapcar
(lambda (e)
(org-list-get-checkbox e s))
items))))
(setq c-all (+ (length cookies) c-all)
c-on (+ (org-count "[X]" cookies) c-on))))
structs))))
cookies-list backup-end structs-backup)
(goto-char (car bounds))
;; 1. Build an alist for each cookie found within BOUNDS. The
;; key will be position at beginning of cookie and values
;; ending position, format of cookie, number of checked boxes
;; to report, and total number of boxes.
(while (re-search-forward cookie-re (cdr bounds) t)
(save-excursion
(let ((c-on 0) (c-all 0))
(save-match-data
;; There are two types of cookies: those at headings and those
;; at list items.
(cond
((and (org-on-heading-p)
(string-match "\\<todo\\>"
(downcase
(or (org-entry-get nil "COOKIE_DATA") "")))))
;; This cookie is at an heading, but specifically for
;; todo, not for checkboxes: skip it.
((org-on-heading-p)
(setq backup-end (save-excursion
(outline-next-heading) (point)))
;; This cookie is at an heading. Grab structure of
;; every list containing a checkbox between point and
;; next headline, and save them in STRUCTS-BACKUP
(while (org-search-forward-unenclosed box-re backup-end 'move)
(let* ((struct (org-list-struct))
(bottom (org-list-get-bottom-point struct)))
(setq structs-backup (cons struct structs-backup))
(goto-char bottom)))
(funcall count-boxes nil structs-backup))
((org-at-item-p)
;; This cookie is at an item. Look in STRUCTS-BACKUP
;; to see if we have the structure of list at point in
;; it. Else compute the structure.
(let ((item (point-at-bol)))
(if (and backup-end (< item backup-end))
(funcall count-boxes item structs-backup)
(setq end-entry bottom
structs-backup (list (org-list-struct)))
(funcall count-boxes item structs-backup))))))
;; Build the cookies list, with appropriate information
(setq cookies-list (cons (list (match-beginning 1) ; cookie start
(match-end 1) ; cookie end
(match-beginning 2) ; percent?
c-on ; checked boxes
c-all) ; total boxes
cookies-list)))))
;; 2. Apply alist to buffer, in reverse order so positions stay
;; unchanged after cookie modifications.
(mapc (lambda (cookie)
(let* ((beg (car cookie))
(end (nth 1 cookie))
(percentp (nth 2 cookie))
(checked (nth 3 cookie))
(total (nth 4 cookie))
(new (if percentp
(format "[%d%%]" (/ (* 100 checked)
(max 1 total)))
(format "[%d/%d]" checked total))))
(goto-char beg)
(insert new)
(delete-region (point) (+ (point) (- end beg)))))
cookies-list))))
(defun org-get-checkbox-statistics-face ()
"Select the face for checkbox statistics.