org-capture: Various fixes to item capture

* lisp/org-capture.el (org-capture-place-item): Rewrite function.
* testing/lisp/test-org-capture.el (test-org-capture/abort): Add test.
(test-org-capture/item): New test.
This commit is contained in:
Nicolas Goaziou 2018-10-23 23:27:28 +02:00
parent 9dbd2993f6
commit 17e28d6467
2 changed files with 295 additions and 57 deletions

View File

@ -1147,63 +1147,109 @@ may have been stored before."
(defun org-capture-place-item ()
"Place the template as a new plain list item."
(let* ((txt (org-capture-get :template))
(target-entry-p (org-capture-get :target-entry-p))
(ind 0)
beg end)
(if (org-capture-get :exact-position)
(goto-char (org-capture-get :exact-position))
(cond
((not target-entry-p)
;; Insert as top-level entry, either at beginning or at end of file
(setq beg (point-min) end (point-max)))
(t
(setq beg (1+ (point-at-eol))
end (save-excursion (outline-next-heading) (point)))))
(setq ind nil)
(if (org-capture-get :prepend)
(progn
(goto-char beg)
(when (org-list-search-forward (org-item-beginning-re) end t)
(goto-char (match-beginning 0))
(setq ind (current-indentation))))
(goto-char end)
(when (org-list-search-backward (org-item-beginning-re) beg t)
(setq ind (current-indentation))
(org-end-of-item)))
(unless ind (goto-char end)))
;; Remove common indentation
(setq txt (org-remove-indentation txt))
;; Make sure this is indeed an item
(unless (string-match (concat "\\`" (org-item-re)) txt)
(setq txt (concat "- "
(mapconcat 'identity (split-string txt "\n")
"\n "))))
;; Prepare surrounding empty lines.
(unless (bolp) (insert "\n"))
(org-capture-empty-lines-before)
(setq beg (point))
(unless (eolp) (save-excursion (insert "\n")))
(unless ind
(org-indent-line)
(setq ind (current-indentation))
(delete-region beg (point)))
;; Set the correct indentation, depending on context
(setq ind (make-string ind ?\ ))
(setq txt (concat ind
(mapconcat 'identity (split-string txt "\n")
(concat "\n" ind))
"\n"))
;; Insert item.
(insert txt)
(org-capture-empty-lines-after)
(org-capture-position-for-last-stored beg)
(setq end (point))
(org-capture-mark-kill-region beg end)
(org-capture-narrow beg end)
(when (or (re-search-backward "%\\?" beg t)
(re-search-forward "%\\?" end t))
(replace-match ""))))
(let ((prepend? (org-capture-get :prepend))
(template (org-remove-indentation (org-capture-get :template)))
item)
;; Make template suitable for insertion. In particular, add
;; a main bullet if it is missing.
(unless (string-match-p (concat "\\`" (org-item-re)) template)
(setq template (concat "- " (mapconcat #'identity
(split-string template "\n")
"\n "))))
;; Delimit the area where we should look for a plain list.
(pcase-let ((`(,beg . ,end)
(cond ((org-capture-get :exact-position)
;; User gave a specific position. Start
;; looking for lists from here.
(cons (save-excursion
(goto-char (org-capture-get :exact-position))
(line-beginning-position))
(org-entry-end-position)))
((org-capture-get :target-entry-p)
;; At a heading, limit search to its body.
(cons (line-beginning-position 2)
(org-entry-end-position)))
(t
;; Table is not necessarily under a heading.
;; Search whole buffer.
(cons (point-min) (point-max))))))
;; Find the first plain list in the delimited area.
(goto-char beg)
(let ((item-regexp (org-item-beginning-re)))
(catch :found
(while (re-search-forward item-regexp end t)
(when (setq item (org-element-lineage
(org-element-at-point) '(plain-list) t))
(goto-char (org-element-property (if prepend? :post-affiliated
:contents-end)
item))
(throw :found t)))
;; No list found. Move to the location when to insert
;; template.
(goto-char (if prepend? beg end)))))
;; Insert template.
(let ((origin (point)))
(unless (bolp) (insert "\n"))
;; When a new list is created, always obey to `:empty-lines' and
;; friends.
;;
;; When capturing in an existing list, do not change blank lines
;; above or below the list; consider it to be a stable
;; structure. However, we can control how many blank lines
;; separate items. So obey to `:empty-lines' between items as
;; long as it does not insert more than one empty line. In the
;; specific case of empty lines above, it means we only obey the
;; parameter when appending an item.
(unless (and item prepend?)
(org-capture-empty-lines-before
(and item
(not prepend?)
(min 1 (or (org-capture-get :empty-lines-before)
(org-capture-get :empty-lines)
0)))))
(org-capture-position-for-last-stored (point))
(let ((beg (line-beginning-position))
(end (progn
(insert (org-trim template) "\n")
(point-marker))))
(when item
(let ((i (save-excursion
(goto-char (org-element-property :post-affiliated item))
(current-indentation))))
(save-excursion
(goto-char beg)
(save-excursion
(while (< (point) end)
(indent-to i)
(forward-line)))
;; Pre-pending an item could change the type of the list
;; if there is a mismatch. In this situation,
;; prioritize the existing list.
(when prepend?
(let ((ordered? (eq 'ordered (org-element-property :type item))))
(when (org-xor ordered?
(string-match-p "\\`[A-Za-z0-9]\\([.)]\\)"
template))
(org-cycle-list-bullet (if ordered? "1." "-")))))
;; Eventually repair the list for proper indentation and
;; bullets.
(org-list-repair))))
;; Limit number of empty lines. See above for details.
(unless (and item (not prepend?))
(org-capture-empty-lines-after
(and item
prepend?
(min 1 (or (org-capture-get :empty-lines-after)
(org-capture-get :empty-lines)
0)))))
(org-capture-mark-kill-region origin (point))
;; ITEM always end with a newline character. Make sure we do
;; not narrow at the beginning of the next line, possibly
;; altering its structure (e.g., when it is a headline).
(org-capture-narrow beg (1- end))
(when (or (search-backward "%?" beg t)
(search-forward "%?" end t))
(replace-match ""))))))
(defun org-capture-place-table-line ()
"Place the template as a table line."

View File

@ -161,6 +161,15 @@
(insert "Capture text")
(org-capture-kill))
(buffer-string))))
(should
(equal "- A\n - B"
(org-test-with-temp-text-in-file "- A\n - B"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X"))))
(org-capture nil "t")
(org-capture-kill))
(buffer-string))))
(should
(equal "| a |\n| b |"
(org-test-with-temp-text-in-file "| a |\n| b |"
@ -199,6 +208,189 @@
(org-capture-finalize))
(buffer-string)))))
(ert-deftest test-org-capture/item ()
"Test `item' type in capture template."
;; Insert item in the first plain list found at the target location.
(should
(equal
"* A\n- list 1\n- X\n\n\n1. list 2"
(org-test-with-temp-text-in-file "* A\n- list 1\n\n\n1. list 2"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file+headline ,file "A") "- X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
(should
(equal
"Text\n- list 1\n- X\n\n\n1. list 2"
(org-test-with-temp-text-in-file "Text\n- list 1\n\n\n1. list 2"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; When targeting a specific location, start looking for plain lists
;; from there.
(should
(equal
"* A\n- skip\n\n\n1. here\n2. X\n"
(org-test-with-temp-text-in-file "* A\n- skip\n\n\n1. here"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file+regexp ,file "here") "1. X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; If there is no such list, create it.
(should
(equal
"* A\n- X\n"
(org-test-with-temp-text-in-file "* A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file+headline ,file "A") "- X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; When `:prepend' is non-nil, insert new item as the first item.
(should
(equal
"* A\n- X\n- 1\n- 2"
(org-test-with-temp-text-in-file "* A\n- 1\n- 2"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file+headline ,file "A") "- X"
:prepend t))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; When `:prepend' is nil, insert new item as the last top-level
;; item.
(should
(equal
"* A\n- 1\n - 2\n- X\n"
(org-test-with-temp-text-in-file "* A\n- 1\n - 2"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file+headline ,file "A") "- X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; When targeting a specific location, one can insert in a sub-list.
(should
(equal
"* A\n- skip\n - here\n - X\n- skip"
(org-test-with-temp-text-in-file "* A\n- skip\n - here\n- skip"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file+regexp ,file "here") "- X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; Obey `:empty-lines' when creating a new list.
(should
(equal
"\n- X\n\n\n* H"
(org-test-with-temp-text-in-file "\n* H"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X"
:empty-lines-before 1 :empty-lines-after 2 :prepend t))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; Obey `:empty-lines' in an existing list only between items, and
;; only if the value doesn't break the list.
(should
(equal
"- A\n\n- X\nText"
(org-test-with-temp-text-in-file "- A\nText"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X" :empty-lines 1))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
(should
(equal
"Text\n- X\n\n- A"
(org-test-with-temp-text-in-file "Text\n- A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X"
:prepend t :empty-lines 1))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
(should-not
(equal
"- A\n\n\n- X"
(org-test-with-temp-text-in-file "- A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X" :empty-lines 2))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; Preserve list type when pre-pending.
(should
(equal
"1. X\n2. A"
(org-test-with-temp-text-in-file "1. A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X" :prepend t))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; Handle indentation. Handle multi-lines templates.
(should
(equal
" - A\n - X\n"
(org-test-with-temp-text-in-file " - A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
(should
(equal
" - A\n - X\n Line 2\n"
(org-test-with-temp-text-in-file " - A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X\n Line 2"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; Handle incomplete templates.
(should
(equal
"- A\n- X\n"
(org-test-with-temp-text-in-file "- A"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "X"))))
(org-capture nil "t")
(org-capture-finalize))
(buffer-string))))
;; Do not break next headline.
(should-not
(equal
"- A\n- X\nFoo* H"
(org-test-with-temp-text-in-file "- A\n* H"
(let* ((file (buffer-file-name))
(org-capture-templates
`(("t" "Item" item (file ,file) "- X"))))
(org-capture nil "t")
(goto-char (point-max))
(insert "Foo")
(org-capture-finalize))
(buffer-string)))))
(ert-deftest test-org-capture/table-line ()
"Test `table-line' type in capture template."
;; When a only file is specified, use the first table available.