Browse Source

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.
Nicolas Goaziou 8 months ago
parent
commit
17e28d6467
2 changed files with 295 additions and 57 deletions
  1. 103 57
      lisp/org-capture.el
  2. 192 0
      testing/lisp/test-org-capture.el

+ 103 - 57
lisp/org-capture.el

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

+ 192 - 0
testing/lisp/test-org-capture.el

@@ -162,6 +162,15 @@
 	(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 |"
 	    (let* ((file (buffer-file-name))
@@ -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.