org-list: Fix infloop when inserting item in post sub-list area

* lisp/org-list.el (org-list-insert-item): Do not infloop when
insertion happens in an item, after a sub-list.  Small refactoring.
* testing/lisp/test-org-list.el (test-org-list/insert-item): Add
tests.
This commit is contained in:
Nicolas Goaziou 2020-04-09 15:42:36 +02:00
parent acd1635966
commit 1ae1f8f2df
2 changed files with 135 additions and 123 deletions

View File

@ -1233,125 +1233,122 @@ after the bullet. Cursor will be after this text once the
function ends. function ends.
This function modifies STRUCT." This function modifies STRUCT."
(let ((case-fold-search t)) (let* ((case-fold-search t)
;; 1. Get information about list: ITEM containing POS, position of ;; Get information about list: ITEM containing POS, position
;; point with regards to item start (BEFOREP), blank lines ;; of point with regards to item start (BEFOREP), blank lines
;; number separating items (BLANK-NB), if we're allowed to ;; number separating items (BLANK-NB), if we're allowed to
;; (SPLIT-LINE-P). ;; (SPLIT-LINE-P).
(let* ((item (goto-char (catch :exit (item
(let ((inner-item 0)) (catch :exit
(pcase-dolist (`(,i . ,_) struct) ;; Do not initialize I as top item as we don't know if the
(cond ;; list is correctly structured.
((= i pos) (throw :exit i)) (let ((i nil))
((< i pos) (setq inner-item i)) (pcase-dolist (`(,start ,_ ,_ ,_ ,_ ,_ ,end) struct)
(t (throw :exit inner-item)))) (cond
inner-item)))) ((> start pos) (throw :exit i))
(item-end (org-list-get-item-end item struct)) ((< end pos) nil) ;skip sub-lists before point
(item-end-no-blank (org-list-get-item-end-before-blank item struct)) (t (setq i start))))
(beforep (or i (org-list-get-top-point struct)))))
(progn (item-end (org-list-get-item-end item struct))
(looking-at org-list-full-item-re) (item-end-no-blank (org-list-get-item-end-before-blank item struct))
(<= pos (beforep
(cond (progn
((not (match-beginning 4)) (match-end 0)) (goto-char item)
;; Ignore tag in a non-descriptive list. (looking-at org-list-full-item-re)
((save-match-data (string-match "[.)]" (match-string 1))) (<= pos
(match-beginning 4)) (cond
(t (save-excursion ((not (match-beginning 4)) (match-end 0))
(goto-char (match-end 4)) ;; Ignore tag in a non-descriptive list.
(skip-chars-forward " \t") ((save-match-data (string-match "[.)]" (match-string 1)))
(point))))))) (match-beginning 4))
(split-line-p (org-get-alist-option org-M-RET-may-split-line 'item)) (t (save-excursion
(blank-nb (org-list-separating-blank-lines-number (goto-char (match-end 4))
pos struct prevs)) (skip-chars-forward " \t")
;; 2. Build the new item to be created. Concatenate same (point)))))))
;; bullet as item, checkbox, text AFTER-BULLET if (split-line-p (org-get-alist-option org-M-RET-may-split-line 'item))
;; provided, and text cut from point to end of item (blank-nb (org-list-separating-blank-lines-number pos struct prevs))
;; (TEXT-CUT) to form item's BODY. TEXT-CUT depends on ;; Build the new item to be created. Concatenate same bullet
;; BEFOREP and SPLIT-LINE-P. The difference of size ;; as item, checkbox, text AFTER-BULLET if provided, and text
;; between what was cut and what was inserted in buffer ;; cut from point to end of item (TEXT-CUT) to form item's
;; is stored in SIZE-OFFSET. ;; BODY. TEXT-CUT depends on BEFOREP and SPLIT-LINE-P. The
(ind (org-list-get-ind item struct)) ;; difference of size between what was cut and what was
(ind-size (if indent-tabs-mode ;; inserted in buffer is stored in SIZE-OFFSET.
(+ (/ ind tab-width) (mod ind tab-width)) (ind (org-list-get-ind item struct))
ind)) (ind-size (if indent-tabs-mode
(bullet (org-list-bullet-string (org-list-get-bullet item struct))) (+ (/ ind tab-width) (mod ind tab-width))
(box (when checkbox "[ ]")) ind))
(text-cut (bullet (org-list-bullet-string (org-list-get-bullet item struct)))
(and (not beforep) split-line-p (box (and checkbox "[ ]"))
(progn (text-cut
(goto-char pos) (and (not beforep)
;; If POS is greater than ITEM-END, then point is split-line-p
;; in some white lines after the end of the list. (progn
;; Those must be removed, or they will be left, (goto-char pos)
;; stacking up after the list. ;; If POS is greater than ITEM-END, then point is in
(when (< item-end pos) ;; some white lines after the end of the list. Those
(delete-region (1- item-end) (point-at-eol))) ;; must be removed, or they will be left, stacking up
(skip-chars-backward " \r\t\n") ;; after the list.
(setq pos (point)) (when (< item-end pos)
(delete-and-extract-region pos item-end-no-blank)))) (delete-region (1- item-end) (point-at-eol)))
(body (concat bullet (when box (concat box " ")) after-bullet (skip-chars-backward " \r\t\n")
(and text-cut (setq pos (point))
(if (string-match "\\`[ \t]+" text-cut) (delete-and-extract-region pos item-end-no-blank))))
(replace-match "" t t text-cut) (body
text-cut)))) (concat bullet
(item-sep (make-string (1+ blank-nb) ?\n)) (and box (concat box " "))
(item-size (+ ind-size (length body) (length item-sep))) after-bullet
(size-offset (- item-size (length text-cut)))) (and text-cut
;; 4. Insert effectively item into buffer. (if (string-match "\\`[ \t]+" text-cut)
(goto-char item) (replace-match "" t t text-cut)
(indent-to-column ind) text-cut))))
(insert body item-sep) (item-sep (make-string (1+ blank-nb) ?\n))
;; 5. Add new item to STRUCT. (item-size (+ ind-size (length body) (length item-sep)))
(mapc (lambda (e) (size-offset (- item-size (length text-cut))))
(let ((p (car e)) (end (nth 6 e))) ;; Insert effectively item into buffer.
(cond (goto-char item)
;; Before inserted item, positions don't change but (indent-to-column ind)
;; an item ending after insertion has its end shifted (insert body item-sep)
;; by SIZE-OFFSET. ;; Add new item to STRUCT.
((< p item) (dolist (e struct)
(when (> end item) (setcar (nthcdr 6 e) (+ end size-offset)))) (let ((p (car e)) (end (nth 6 e)))
;; Trivial cases where current item isn't split in (cond
;; two. Just shift every item after new one by ;; Before inserted item, positions don't change but an item
;; ITEM-SIZE. ;; ending after insertion has its end shifted by SIZE-OFFSET.
((or beforep (not split-line-p)) ((< p item)
(setcar e (+ p item-size)) (when (> end item)
(setcar (nthcdr 6 e) (+ end item-size))) (setcar (nthcdr 6 e) (+ end size-offset))))
;; Item is split in two: elements before POS are just ;; Item where insertion happens may be split in two parts.
;; shifted by ITEM-SIZE. In the case item would end ;; In this case, move start by ITEM-SIZE and end by
;; after split POS, ending is only shifted by ;; SIZE-OFFSET.
;; SIZE-OFFSET. ((and (= p item) (not beforep) split-line-p)
((< p pos) (setcar e (+ p item-size))
(setcar e (+ p item-size)) (setcar (nthcdr 6 e) (+ end size-offset)))
(if (< end pos) ;; Items starting after modified item fall into two
(setcar (nthcdr 6 e) (+ end item-size)) ;; categories. If item was split, and current item was
(setcar (nthcdr 6 e) (+ end size-offset)))) ;; located after split point, it was moved to the new
;; Elements after POS are moved into new item. ;; item. Practically speaking, this means that the part
;; Length of ITEM-SEP has to be removed as ITEM-SEP ;; between body start of body and split point was removed.
;; doesn't appear in buffer yet. ;; So we compute the offset and shit item's positions
((< p item-end) ;; accordingly. In any other case, the item was simply
(setcar e (+ p size-offset (- item pos (length item-sep)))) ;; shifted by ITEM-SIZE.
(if (= end item-end) ((and split-line-p (not beforep) (>= p pos))
(setcar (nthcdr 6 e) (+ item item-size)) (let ((offset (- pos item ind (length bullet) (length after-bullet))))
(setcar (nthcdr 6 e) (setcar e (- p offset))
(+ end size-offset (setcar (nthcdr 6 e) (- end offset))))
(- item pos (length item-sep)))))) (t
;; Elements at ITEM-END or after are only shifted by (setcar e (+ p item-size))
;; SIZE-OFFSET. (setcar (nthcdr 6 e) (+ end item-size))))))
(t (setcar e (+ p size-offset)) (push (list item ind bullet nil box nil (+ item item-size)) struct)
(setcar (nthcdr 6 e) (+ end size-offset)))))) (setq struct (sort struct #'car-less-than-car))
struct) ;; If not BEFOREP, new item must appear after ITEM, so exchange
(push (list item ind bullet nil box nil (+ item item-size)) struct) ;; ITEM with the next item in list. Position cursor after bullet,
(setq struct (sort struct (lambda (e1 e2) (< (car e1) (car e2))))) ;; counter, checkbox, and label.
;; 6. If not BEFOREP, new item must appear after ITEM, so (if beforep
;; exchange ITEM with the next item in list. Position cursor (goto-char item)
;; after bullet, counter, checkbox, and label. (setq struct (org-list-swap-items item (+ item item-size) struct))
(if beforep (goto-char (org-list-get-next-item
(goto-char item) item struct (org-list-prevs-alist struct))))
(setq struct (org-list-swap-items item (+ item item-size) struct)) struct))
(goto-char (org-list-get-next-item
item struct (org-list-prevs-alist struct))))
struct)))
(defun org-list-delete-item (item struct) (defun org-list-delete-item (item struct)
"Remove ITEM from the list and return the new structure. "Remove ITEM from the list and return the new structure.

View File

@ -607,9 +607,11 @@ b. Item 2<point>"
(should-error (org-move-item-up))) (should-error (org-move-item-up)))
;; ... unless `org-list-use-circular-motion' is non-nil. In this ;; ... unless `org-list-use-circular-motion' is non-nil. In this
;; case, move to the first item. ;; case, move to the first item.
(org-test-with-temp-text "- item 1\n- item 2\n- item 3" (should
(let ((org-list-use-circular-motion t)) (org-move-item-up)) (equal "- item 2\n- item 3\n- item 1"
(should (equal (buffer-string) "- item 2\n- item 3\n- item 1"))) (org-test-with-temp-text "- item 1\n- item 2\n- item 3"
(let ((org-list-use-circular-motion t)) (org-move-item-up))
(buffer-string))))
;; Preserve item visibility. ;; Preserve item visibility.
(org-test-with-temp-text "* Headline\n- item 1\n body 1\n- item 2\n body 2" (org-test-with-temp-text "* Headline\n- item 1\n body 1\n- item 2\n body 2"
(let ((org-cycle-include-plain-lists t)) (let ((org-cycle-include-plain-lists t))
@ -784,7 +786,20 @@ b. Item 2<point>"
(org-cycle) (org-cycle)
(org-insert-item) (org-insert-item)
(list (get-char-property (line-beginning-position 0) 'invisible) (list (get-char-property (line-beginning-position 0) 'invisible)
(get-char-property (line-end-position 2) 'invisible))))))) (get-char-property (line-end-position 2) 'invisible))))))
;; Test insertion in area after a sub-list. In particular, if point
;; is right at the end of the previous sub-list, still insert
;; a sub-item in that list.
(should
(= 2
(org-test-with-temp-text "- item\n - sub-list\n<point> resume item"
(org-insert-item)
(current-indentation))))
(should
(= 0
(org-test-with-temp-text "- item\n - sub-list\n resume item<point>"
(org-insert-item)
(current-indentation)))))
(ert-deftest test-org-list/repair () (ert-deftest test-org-list/repair ()
"Test `org-list-repair' specifications." "Test `org-list-repair' specifications."