Browse Source

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
Nicolas Goaziou 15 years ago
parent
commit
1829aa79b5
1 changed files with 191 additions and 173 deletions
  1. 191 173
      lisp/org-list.el

+ 191 - 173
lisp/org-list.el

@@ -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 the text below the heading, taking as reference the first item
 in subtree, ignoring drawers."
 in subtree, ignoring drawers."
   (interactive "P")
   (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)))
     (org-update-checkbox-count-maybe)))
 
 
 (defun org-reset-checkbox-state-subtree ()
 (defun org-reset-checkbox-state-subtree ()
@@ -1901,110 +1915,114 @@ information.")
 
 
 (defun org-update-checkbox-count (&optional all)
 (defun org-update-checkbox-count (&optional all)
   "Update the checkbox statistics in the current section.
   "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")
   (interactive "P")
   (save-excursion
   (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 ()
 (defun org-get-checkbox-statistics-face ()
   "Select the face for checkbox statistics.
   "Select the face for checkbox statistics.