瀏覽代碼

Now both indentation and regexps can end lists

* org-list.el (org-list-ending-method): New customizable variable to
  tell Org Mode how lists end. See docstring.
Nicolas Goaziou 14 年之前
父節點
當前提交
b5eb7047f3
共有 2 個文件被更改,包括 233 次插入74 次删除
  1. 2 1
      lisp/org-exp.el
  2. 231 73
      lisp/org-list.el

+ 2 - 1
lisp/org-exp.el

@@ -1638,7 +1638,8 @@ These special cookies will later be interpreted by the backend.
 	   (goto-char (point-min))
 	   (while (org-search-forward-unenclosed org-item-beginning-re nil t)
 	     (goto-char (org-list-bottom-point))
-	     (when (looking-at (org-list-end-re))
+	     (when (and (not (eq org-list-ending-method 'indent))
+			(looking-at (org-list-end-re)))
 	       (replace-match "\n"))
 	     (insert end-list-marker)))))
   ;; We need to divide backends into 3 categories.

+ 231 - 73
lisp/org-list.el

@@ -149,9 +149,32 @@ spaces instead of one after the bullet in each item of the list."
 	  (const :tag "never" nil)
 	  (regexp)))
 
+(defcustom org-list-ending-method 'regexp
+  "Determine where plain lists should end.
+
+Valid values are symbols 'regexp, 'indent or 'both.
+
+When set to 'regexp, Org will look into two variables,
+`org-empty-line-terminates-plain-lists' and the more general
+`org-list-end-regexp', to know what will end lists. This is the
+default value.
+
+When set to 'indent, indentation of the last non-blank line will
+determine if point is in a list. If that line is less indented
+than the previous item in the section, if any, list has ended.
+
+When set to 'both, each of the preceding methods must confirm
+that point is in a list."
+  :group 'org-plain-lists
+  :type '(choice
+	  (const :tag "With a well defined ending (recommended)" regexp)
+	  (const :tag "With indentation of the current line" indent)
+	  (const :tag "With both methods" both)))
+
 (defcustom org-empty-line-terminates-plain-lists nil
   "Non-nil means an empty line ends all plain list levels.
-Otherwise, look for `org-list-end-regexp'."
+This variable only makes sense if `org-list-ending-method' is set
+to 'regexp or 'both."
   :group 'org-plain-lists
   :type 'boolean)
 
@@ -295,34 +318,164 @@ the end of the nearest terminator from max."
 	   ;; we want to be on the first line of the list ender
 	   (match-beginning 0)))))
 
-(defun org-list-search-unenclosed-generic (search skip len re bound noerr)
+(defun org-list-maybe-skip-block (search limit)
+  "Return non-nil value if point is in a block, skipping it on the way.
+
+It looks for the boundary of the block in SEARCH direction."
+  (save-match-data
+    (let ((case-fold-search t)
+	  (boundary (if (eq search 're-search-forward) 3 5)))
+    (when (save-excursion
+	    (and (funcall search "^[ \t]*#\\+\\(begin\\|end\\)_" limit t)
+		 (= (length (match-string 1)) boundary)))
+      ;; We're in a block: get out of it
+      (goto-char (match-beginning 0))))))
+
+(defun org-list-search-unenclosed-generic (search re bound noerr)
   "Search for RE with SEARCH outside blocks and protected places."
-  (let ((in-block-p
-	 (lambda ()
-	   (let ((case-fold-search t))
-	     (when (save-excursion
-		     (and (funcall search "^[ \t]*#\\+\\(begin\\|end\\)_" bound t)
-			  (= (length (match-string 1)) len)))
-	       ;; We're in a block: get out of it and resume searching
-	       (goto-char (funcall skip 0)))))))
-    (catch 'exit
-      (let ((origin (point)))
-	(while t
-	  (unless (funcall search re bound noerr)
-	    (throw 'exit (and (goto-char (if (booleanp noerr) origin bound)) nil)))
-	  (unless (or (get-text-property (match-beginning 0) 'org-protected)
-		      (save-match-data (funcall in-block-p)))
-	    (throw 'exit (point))))))))
+  (catch 'exit
+    (let ((origin (point)))
+      (while t
+	(unless (funcall search re bound noerr)
+	  (throw 'exit (and (goto-char (if (booleanp noerr) origin bound))
+			    nil)))
+	(unless (or (get-text-property (match-beginning 0) 'org-protected)
+		    (org-list-maybe-skip-block search bound))
+	  (throw 'exit (point)))))))
 
 (defun org-search-backward-unenclosed (regexp &optional bound noerror)
-  "Like `re-search-backward' but don't stop inside blocks or at protected places."
+  "Like `re-search-backward' but don't stop inside blocks or protected places."
   (org-list-search-unenclosed-generic
-   #'re-search-backward #'match-beginning 5 regexp (or bound (point-min)) noerror))
+   #'re-search-backward regexp (or bound (point-min)) noerror))
 
 (defun org-search-forward-unenclosed (regexp &optional bound noerror)
-  "Like `re-search-forward' but don't stop inside blocks or at protected places."
+  "Like `re-search-forward' but don't stop inside blocks or protected places."
   (org-list-search-unenclosed-generic
-   #'re-search-forward #'match-end 3 regexp (or bound (point-max)) noerror))
+   #'re-search-forward regexp (or bound (point-max)) noerror))
+
+(defun org-list-in-item-p-with-indent (limit)
+  "Is the cursor inside a plain list?
+
+Plain lists are considered ending when a non-blank line is less
+indented than the previous item within LIMIT.
+
+Return the position of the previous item, if applicable."
+  (save-excursion
+    (beginning-of-line)
+    ;; do not start searching at a blank line or inside a block
+    (while (or (and (org-list-maybe-skip-block #'re-search-backward limit)
+		    (goto-char (1- (point-at-bol))))
+	       (looking-at "^[ \t]*$"))
+      (skip-chars-backward " \r\t\n")
+      (beginning-of-line))
+    (or (and (org-at-item-p) (point-at-bol))
+	(let ((ind (org-get-indentation)))
+	  (catch 'exit
+	    (while t
+	      (cond
+	       ((or (bobp) (< (point) limit)) (throw 'exit nil))
+	       ((and (not (looking-at "[ \t]*$"))
+		     (not (org-list-maybe-skip-block
+			   #'re-search-backward limit))
+		     (< (org-get-indentation) ind))
+		(throw 'exit (and (org-at-item-p) (point-at-bol))))
+	       (t (beginning-of-line 0)))))))))
+
+(defun org-list-in-item-p-with-regexp (limit)
+  "Is the cursor inside a plain list?
+
+Plain lists end when `org-list-end-regexp' is matched, or at a
+blank line if `org-empty-line-terminates-plain-lists' is true."
+  (save-excursion
+    (let* ((actual-pos (goto-char (point-at-eol)))
+	   ;; Moved to eol so current line can be matched by
+	   ;; `org-item-re'.
+	   (last-item-start (save-excursion
+			      (org-search-backward-unenclosed
+			       org-item-beginning-re limit t)))
+	   (list-ender (org-list-terminator-between
+			last-item-start actual-pos)))
+      ;; We are in a list when we are on an item line or when we can
+      ;; find an item before point and there is no valid list ender
+      ;; between it and the point.
+      (and last-item-start
+	   (not list-ender)))))
+
+(defun org-list-top-point-with-regexp (limit)
+  "Return point at the top level item in a list, or nil if not in a list.
+
+List ending is determined by regexp. See
+`org-list-ending-method'. for more information."
+  (save-excursion
+    (and (org-list-in-item-p-with-regexp limit)
+	 (let ((pos (point-at-eol)))
+	   ;; Is there some list above this one ? If so, go to its ending.
+	   ;; Otherwise, go back to the heading above or bob.
+	   (goto-char (or (org-list-terminator-between limit pos) limit))
+	   ;; From there, search down our list.
+	   (org-search-forward-unenclosed org-item-beginning-re pos t)
+	   (point-at-bol)))))
+
+(defun org-list-bottom-point-with-regexp (limit)
+  "Return point just before list ending or nil if not in a list.
+
+List ending is determined by regexp. See
+`org-list-ending-method'. for more information."
+  (save-excursion
+    (and (org-in-item-p)
+	 (let ((pos (point)))
+	   ;; The list ending is either first point matching
+	   ;; `org-list-end-re', point at first white-line before next
+	   ;; heading, or eob.
+	   (or (org-list-terminator-between (min pos limit) limit t) limit)))))
+
+(defun org-list-top-point-with-indent (limit)
+  "Return point just before list ending or nil if not in a list.
+
+List ending is determined by indentation of text. See
+`org-list-ending-method'. for more information."
+  (save-excursion
+    (let ((prev-p (org-list-in-item-p-with-indent limit)))
+      (and prev-p
+           (catch 'exit
+             (while t
+               (cond
+		((not prev-p) (throw 'exit (1+ (point-at-eol))))
+		((= limit prev-p) (throw 'exit limit))
+		(t
+		 (goto-char prev-p)
+		 (beginning-of-line 0)
+		 (setq prev-p (org-list-in-item-p-with-indent limit))))))))))
+
+(defun org-list-bottom-point-with-indent (limit)
+  "Return point just before list ending or nil if not in a list.
+
+List ending is determined by the indentation of text. See
+`org-list-ending-method' for more information."
+  (save-excursion
+    (let* ((ind (save-excursion
+		  (ignore-errors (org-beginning-of-item))
+		  (org-get-indentation)))
+	   (end-item (lambda ()
+		       (save-excursion
+			 (catch 'end
+			   (while t
+			     (beginning-of-line 2)
+			     (cond
+			      ((>= (point) limit) (throw 'end limit))
+			      ((or (looking-at "^[ \t]*$")
+				   (org-list-maybe-skip-block
+				    #'re-search-forward limit)
+				   (> (org-get-indentation) ind)))
+			      (t (throw 'end (point-at-bol))))))))))
+      (and (org-in-item-p)
+	   (catch 'exit
+	     (while t
+	       (goto-char (funcall end-item))
+	       (if (looking-at org-item-beginning-re)
+		   (setq ind (org-get-indentation))
+		 (skip-chars-backward " \r\t\n")
+		 (throw 'exit (1+ (point-at-eol))))))))))
 
 (defun org-list-at-regexp-after-bullet-p (regexp)
   "Is point at a list item with REGEXP after bullet?"
@@ -393,7 +546,8 @@ function ends."
 		usr-blank)
 	    (cond
 	     ;; Trivial cases where there should be none.
-	     ((or org-empty-line-terminates-plain-lists
+	     ((or (and (not (eq org-list-ending-method 'indent))
+		       org-empty-line-terminates-plain-lists)
 		  (not insert-blank-p)) 0)
 	     ;; When `org-blank-before-new-entry' says so, it is 1.
 	     ((eq insert-blank-p t) 1)
@@ -465,20 +619,18 @@ function ends."
 ;;; Predicates
 
 (defun org-in-item-p ()
-  "Is the cursor inside a plain list ?"
+  "Is the cursor inside a plain list?
+This checks `org-list-ending-method'."
   (unless (let ((outline-regexp org-outline-regexp)) (org-at-heading-p))
-    (save-excursion
-      (let* ((limit (save-excursion (outline-previous-heading)))
-	     ;; Move to eol so current line can be matched by `org-item-re'.
-	     (actual-pos (goto-char (point-at-eol)))
-	     (last-item-start (save-excursion
-				(org-search-backward-unenclosed org-item-beginning-re limit t)))
-	     (list-ender (org-list-terminator-between last-item-start actual-pos)))
-	;; We are in a list when we are on an item line or when we can
-	;; find an item before point and there is no valid list ender
-	;; between it and the point.
-	(and last-item-start
-	     (not list-ender))))))
+    (let ((bound (or (save-excursion (outline-previous-heading))
+		     (point-min))))
+      (cond
+       ((eq org-list-ending-method 'indent)
+	(org-list-in-item-p-with-indent bound))
+       ((eq org-list-ending-method 'both)
+	(and (org-list-in-item-p-with-indent bound)
+	     (org-list-in-item-p-with-regexp bound)))
+       (t (org-list-in-item-p-with-regexp bound))))))
 
 (defun org-list-first-item-p ()
   "Is this item the first item in a plain list?
@@ -486,7 +638,8 @@ Assume point is at an item."
   (save-excursion
     (beginning-of-line)
     (let ((ind (org-get-indentation)))
-      (or (not (org-search-backward-unenclosed org-item-beginning-re (org-list-top-point) t))
+      (or (not (org-search-backward-unenclosed
+		org-item-beginning-re (org-list-top-point) t))
 	  (< (org-get-indentation) ind)))))
 
 (defun org-at-item-p ()
@@ -502,7 +655,8 @@ Assume point is at an item."
 
 (defun org-at-item-timer-p ()
   "Is point at a line starting a plain list item with a timer?"
-  (org-list-at-regexp-after-bullet-p "\\([0-9]+:[0-9]+:[0-9]+\\)[ \t]+::[ \t]+"))
+  (org-list-at-regexp-after-bullet-p
+   "\\([0-9]+:[0-9]+:[0-9]+\\)[ \t]+::[ \t]+"))
 
 (defun org-at-item-description-p ()
   "Is point at a description list item?"
@@ -537,34 +691,32 @@ A checkbox is blocked if all of the following conditions are fulfilled:
 ;;; Navigate
 
 (defun org-list-top-point ()
-  "Return point at the top level item in a list, or nil if not in a list."
-  (save-excursion
-    (and (org-in-item-p)
-	 (let ((pos (point-at-eol))
-	       (bound (or (outline-previous-heading) (point-min))))
-	   ;; Is there some list above this one ? If so, go to its ending.
-	   ;; Otherwise, go back to the heading above or bob.
-	   (goto-char (or (org-list-terminator-between bound pos) bound))
-	   ;; From there, search down our list.
-	   (org-search-forward-unenclosed org-item-beginning-re pos t)
-	   (point-at-bol)))))
+  (let ((limit (or (save-excursion (outline-previous-heading))
+		   (point-min))))
+    (cond
+     ((eq org-list-ending-method 'indent)
+      (org-list-top-point-with-indent limit))
+     ((eq org-list-ending-method 'both)
+      (max (org-list-top-point-with-regexp limit)
+	   (org-list-top-point-with-indent limit)))
+     (t (org-list-top-point-with-regexp limit)))))
 
 (defun org-list-bottom-point ()
-  "Return point just before list ending or nil if not in a list."
-  (save-excursion
-    (and (org-in-item-p)
-	 (let ((pos (point))
-	       (bound (or (and (let ((outline-regexp org-outline-regexp))
-				 ;; Use default regexp because folding
-				 ;; changes OUTLINE-REGEXP.
-				 (outline-next-heading))
-			       (skip-chars-backward " \t\r\n")
-			       (1+ (point-at-eol)))
-			  (point-max))))
-	   ;; The list ending is either first point matching
-	   ;; `org-list-end-re', point at first white-line before next
-	   ;; heading, or eob.
-	   (or (org-list-terminator-between (min pos bound) bound t) bound)))))
+  (let ((limit (or (save-excursion
+		     (and (let ((outline-regexp org-outline-regexp))
+			  ;; Use default regexp because folding
+			  ;; changes OUTLINE-REGEXP.
+			  (outline-next-heading))
+			(skip-chars-backward " \r\t\n")
+			(1+ (point-at-eol))))
+		   (point-max))))
+    (cond
+     ((eq org-list-ending-method 'indent)
+      (org-list-bottom-point-with-indent limit))
+     ((eq org-list-ending-method 'both)
+      (min (org-list-bottom-point-with-regexp limit)
+	   (org-list-bottom-point-with-indent limit)))
+     (t (org-list-bottom-point-with-regexp limit)))))
 
 (defun org-beginning-of-item ()
   "Go to the beginning of the current hand-formatted item.
@@ -1032,16 +1184,21 @@ Initial position is restored after the changes."
 			      (match-string 1)))
 		   (old-body-ind (+ (length old-bul) old-ind))
 		   (new-body-ind (+ (length new-bul) new-ind)))
-	      ;; Replace bullet
+	      ;; 1. Shift item's body
+	      (unless (= old-body-ind new-body-ind)
+		(org-shift-item-indentation (- new-body-ind old-body-ind)))
+	      ;; 2. Replace bullet
 	      (unless (equal new-bul old-bul)
-		(save-excursion (replace-match new-bul nil nil nil 1)))
-	      ;; Indent item to appropriate column
+		(save-excursion
+		  (looking-at "[ \t]*\\(\\S-+[ \t]*\\)")
+		  (replace-match new-bul nil nil nil 1)))
+	      ;; 3. Indent item to appropriate column
 	      (unless (= new-ind old-ind)
-		(delete-region (point-at-bol) (match-beginning 1))
-		(indent-to new-ind))
-	      ;; Shift item's body
-	      (unless (= old-body-ind new-body-ind)
-		(org-shift-item-indentation (- new-body-ind old-body-ind))))))
+		(delete-region (point-at-bol)
+			       (progn
+				 (skip-chars-forward " \t")
+				 (point)))
+		(indent-to new-ind)))))
 	 ;; Remove ancestor if it is left.
 	 (struct-to-apply (if (or (not ancestor) (= 0 ancestor))
 			      (cdr struct)
@@ -1680,7 +1837,8 @@ sublevels as a list of strings."
     (when delete
       (delete-region start end)
       (save-match-data
-	(when (looking-at (org-list-end-re))
+	(when (and (not (eq org-list-ending-method 'indent))
+		   (looking-at (org-list-end-re)))
 	  (replace-match "\n"))))
     (setq output (nreverse output))
     (push ltype output)))