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.
This commit is contained in:
Nicolas Goaziou 2010-08-19 16:09:45 +02:00
parent 2cca510276
commit b5eb7047f3
2 changed files with 234 additions and 75 deletions

View file

@ -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.

View file

@ -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
(unless (equal new-bul old-bul)
(save-excursion (replace-match new-bul nil nil nil 1)))
;; 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
;; 1. Shift item's body
(unless (= old-body-ind new-body-ind)
(org-shift-item-indentation (- new-body-ind old-body-ind))))))
(org-shift-item-indentation (- new-body-ind old-body-ind)))
;; 2. Replace bullet
(unless (equal new-bul old-bul)
(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)
(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)))