Browse Source

Merge branch 'master' of code.orgmode.org:bzg/org-mode

Bastien 2 years ago
parent
commit
f979ce47ca
6 changed files with 558 additions and 159 deletions
  1. 23 10
      doc/org-manual.org
  2. 164 113
      lisp/org-capture.el
  3. 56 33
      lisp/org-macs.el
  4. 14 0
      lisp/ox-ascii.el
  5. 298 2
      testing/lisp/test-org-capture.el
  6. 3 1
      testing/lisp/test-org-macs.el

+ 23 - 10
doc/org-manual.org

@@ -3026,14 +3026,17 @@ or alternatively
 
 : [[LINK]]
 
-Once a link in the buffer is complete (all brackets present), Org
+Once a link in the buffer is complete---all brackets present---, Org
 changes the display so that =DESCRIPTION= is displayed instead of
-=[[LINK][DESCRIPTION]]= and =LINK= is displayed instead of =[[LINK]]=.  Links are be
-highlighted in the face ~org-link~, which by default is an underlined
-face.  You can directly edit the visible part of a link.  Note that
-this can be either the LINK part, if there is no description, or the
-{{{var(DESCRIPTION)}}} part.  To edit also the invisible
-{{{var(LINK)}}} part, use {{{kbd(C-c C-l)}}} with point on the link.
+=[[LINK][DESCRIPTION]]= and =LINK= is displayed instead of =[[LINK]]=.
+Links are highlighted in the ~org-link~ face, which, by default, is an
+underlined face.
+
+You can directly edit the visible part of a link.  This can be either
+the {{{var(LINK)}}} part, if there is no description, or the
+{{{var(DESCRIPTION)}}} part otherwise.  To also edit the invisible
+{{{var(LINK)}}} part, use {{{kbd(C-c C-l)}}} with point on the link
+(see [[*Handling Links]]).
 
 If you place point at the beginning or just behind the end of the
 displayed text and press {{{kbd(BS)}}}, you remove
@@ -11089,7 +11092,7 @@ Org only loads back-ends for the following formats by default: ASCII,
 HTML, iCalendar, LaTeX, and ODT.  Additional back-ends can be loaded
 in either of two ways: by configuring the ~org-export-backends~
 variable, or by requiring libraries in the Emacs init file.  For
-example, to load the markdown back-end, add this to your Emacs config:
+example, to load the Markdown back-end, add this to your Emacs config:
 
 #+begin_src emacs-lisp
 (require 'ox-md)
@@ -15235,6 +15238,16 @@ regions.  A convenient feature of this in-place conversion is that the
 exported output replaces the original source.  Here are such
 functions:
 
+- ~org-ascii-convert-region-to-ascii~ ::
+
+     #+findex: org-ascii-convert-region-to-ascii
+     Convert the selected region into ASCII.
+
+- ~org-ascii-convert-region-to-utf8~ ::
+
+     #+findex: org-ascii-convert-region-to-utf8
+     Convert the selected region into UTF-8.
+
 - ~org-html-convert-region-to-html~ ::
 
      #+findex: org-html-convert-region-to-html
@@ -19279,10 +19292,10 @@ locally just set the variable to point to that directory:
 
 Alternatively, by using TRAMP (see [[info:tramp][TRAMP User Manual]]),
 ~org-mobile-directory~ may point to a remote directory accessible
-through, for example, SSH and SCP:
+through, for example, SSH, SCP, or DAVS:
 
 #+begin_src emacs-lisp
-(setq org-mobile-directory "/scpc:user@remote.host:org/webdav/")
+(setq org-mobile-directory "/davs:user@remote.host:/org/webdav/")
 #+end_src
 
 #+vindex: org-mobile-encryption

+ 164 - 113
lisp/org-capture.el

@@ -1111,8 +1111,10 @@ may have been stored before."
 
 (defun org-capture-place-entry ()
   "Place the template as a new Org entry."
-  (let ((reversed? (org-capture-get :prepend))
+  (let ((template (org-capture-get :template))
+	(reversed? (org-capture-get :prepend))
 	(level 1))
+    (org-capture-verify-tree template)
     (when (org-capture-get :exact-position)
       (goto-char (org-capture-get :exact-position)))
     (cond
@@ -1128,81 +1130,126 @@ may have been stored before."
       (unless (org-at-heading-p) (outline-next-heading)))
      ;; Otherwise, insert as a top-level entry at the end of the file.
      (t (goto-char (point-max))))
-    (unless (bolp) (insert "\n"))
-    (org-capture-empty-lines-before)
-    (let ((beg (point))
-	  (template (org-capture-get :template)))
-      (org-capture-verify-tree template)
-      (org-paste-subtree level template 'for-yank)
-      (org-capture-empty-lines-after)
-      (org-capture-position-for-last-stored beg)
-      (unless (org-at-heading-p) (outline-next-heading))
-      (let ((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 ((origin (point)))
+      (unless (bolp) (insert "\n"))
+      (org-capture-empty-lines-before)
+      (org-capture-position-for-last-stored (point))
+      (let ((beg (point)))
+	(org-paste-subtree level template 'for-yank)
+	(let ((end (if (org-at-heading-p) (line-end-position 0) (point))))
+	  (org-capture-empty-lines-after)
+	  (unless (org-at-heading-p) (outline-next-heading))
+	  (org-capture-mark-kill-region origin (point))
+	  (org-capture-narrow beg end)
+	  (when (or (search-backward "%?" beg t)
+		    (search-forward "%?" end t))
+	    (replace-match "")))))))
 
 (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."
@@ -1270,18 +1317,21 @@ may have been stored before."
      (t
       (goto-char (org-table-end))))
     ;; Insert text and position point according to template.
-    (unless (bolp) (insert "\n"))
-    (let ((beg (point))
-	  (end (save-excursion
-		 (insert text)
-		 (point))))
-      (org-capture-position-for-last-stored 'table-line)
-      (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 "")))
-    (org-table-align)))
+    (let ((origin (point)))
+      (unless (bolp) (insert "\n"))
+      (let ((beg (point))
+	    (end (save-excursion
+		   (insert text)
+		   (point))))
+	(org-capture-position-for-last-stored 'table-line)
+	(org-capture-mark-kill-region origin end)
+	;; TEXT is guaranteed to end with a newline character.  Ignore
+	;; it when narrowing so as to not alter data on the next line.
+	(org-capture-narrow beg (1- end))
+	(when (or (search-backward "%?" beg t)
+		  (search-forward "%?" end t))
+	  (replace-match "")))
+      (org-table-align))))
 
 (defun org-capture-place-plain-text ()
   "Place the template plainly.
@@ -1289,35 +1339,36 @@ If the target locator points at an Org node, place the template into
 the text of the entry, before the first child.  If not, place the
 template at the beginning or end of the file.
 Of course, if exact position has been required, just put it there."
-  (let* ((txt (org-capture-get :template))
-	 beg end)
-    (cond
-     ((org-capture-get :exact-position)
-      (goto-char (org-capture-get :exact-position)))
-     ((and (org-capture-get :target-entry-p)
-	   (bolp)
-	   (looking-at org-outline-regexp))
-      ;; we should place the text into this entry
-      (if (org-capture-get :prepend)
-	  ;; Skip meta data and drawers
-	  (org-end-of-meta-data t)
-	;; go to ent of the entry text, before the next headline
-	(outline-next-heading)))
-     (t
-      ;; beginning or end of file
-      (goto-char (if (org-capture-get :prepend) (point-min) (point-max)))))
-    (or (bolp) (newline))
+  (cond
+   ((org-capture-get :exact-position)
+    (goto-char (org-capture-get :exact-position)))
+   ((org-capture-get :target-entry-p)
+    ;; Place the text into this entry.
+    (if (org-capture-get :prepend)
+	;; Skip meta data and drawers.
+	(org-end-of-meta-data t)
+      ;; Go to end of the entry text, before the next headline.
+      (outline-next-heading)))
+   (t
+    ;; Beginning or end of file.
+    (goto-char (if (org-capture-get :prepend) (point-min) (point-max)))))
+  (let ((origin (point)))
+    (unless (bolp) (insert "\n"))
     (org-capture-empty-lines-before)
-    (setq beg (point))
-    (insert txt)
-    (org-capture-empty-lines-after)
-    (org-capture-position-for-last-stored beg)
-    (setq end (point))
-    (org-capture-mark-kill-region beg (1- end))
-    (org-capture-narrow beg (1- end))
-    (when (or (re-search-backward "%\\?" beg t)
-	      (re-search-forward "%\\?" end t))
-      (replace-match ""))))
+    (org-capture-position-for-last-stored (point))
+    (let ((beg (point)))
+      (insert (org-capture-get :template))
+      (unless (bolp) (insert "\n"))
+      ;; Ignore the final newline character so as to not alter data
+      ;; after inserted text.  Yet, if the template is empty, make
+      ;; sure END matches BEG instead of pointing before it.
+      (let ((end (max beg (1- (point)))))
+	(org-capture-empty-lines-after)
+	(org-capture-mark-kill-region origin (point))
+	(org-capture-narrow beg end)
+	(when (or (search-backward "%?" beg t)
+		  (search-forward "%?" end t))
+	  (replace-match ""))))))
 
 (defun org-capture-mark-kill-region (beg end)
   "Mark the region that will have to be killed when aborting capture."

+ 56 - 33
lisp/org-macs.el

@@ -826,47 +826,70 @@ end of string are ignored."
 		      results		;skip trailing separator
 		    (cons (substring string i) results)))))))
 
-(defun org--string-from-props (s property)
-  "Return visible string according to text properties in string S.
-PROPERTY is either `invisible' or `display'."
-  (let ((len (length s))
-	(new nil)
-	(i 0)
-	(cursor 0))
-    (while (setq i (text-property-not-all i len property nil s))
-      (let* ((end (next-single-property-change i property s len))
-	     (props (text-properties-at i s))
+(defun org--string-from-props (s property beg end)
+  "Return the visible part of string S.
+Visible part is determined according to text PROPERTY, which is
+either `invisible' or `display'.  BEG and END are 0-indices
+delimiting S."
+  (let ((width 0)
+	(cursor beg))
+    (while (setq beg (text-property-not-all beg end property nil s))
+      (let* ((next (next-single-property-change beg property s end))
+	     (props (text-properties-at beg s))
+	     (spec (plist-get props property))
 	     (value
-	      (if (eq property 'invisible)
-		  ;; If `invisible' property in PROPS means text is to
-		  ;; be invisible, return the empty string.  Otherwise
-		  ;; return nil so that the part is skipped.
-		  (and (or (eq t buffer-invisibility-spec)
-			   (assoc-string (plist-get props 'invisible)
-					 buffer-invisibility-spec))
-		       "")
-		(let ((display (plist-get props 'display)))
-		  (pcase (if (stringp display) display
-			   (cl-some #'stringp display))
-		    (`nil nil)
+	      (pcase property
+		(`invisible
+		 ;; If `invisible' property in PROPS means text is to
+		 ;; be invisible, return 0.  Otherwise return nil so
+		 ;; as to resume search.
+		 (and (or (eq t buffer-invisibility-spec)
+			  (assoc-string spec buffer-invisibility-spec))
+		      0))
+		(`display
+		 (pcase spec
+		   (`nil nil)
+		   (`(space . ,props)
+		    (let ((width (plist-get props :width)))
+		      (and (wholenump width) width)))
+		   (`(image . ,_)
+		    (ceiling (car (image-size spec))))
+		   ((pred stringp)
 		    ;; Displayed string could contain invisible parts,
 		    ;; but no nested display.
-		    (s (org--string-from-props s 'invisible)))))))
+		    (org--string-from-props spec 'invisible 0 (length spec)))
+		   (_
+		    ;; Un-handled `display' value.  Ignore it.
+		    ;; Consider the original string instead.
+		    nil)))
+		(_ (error "Unknown property: %S" property)))))
 	(when value
-	  (setq new (concat new (substring s cursor i) value))
-	  (setq cursor end))
-	(setq i end)))
-    (if new (concat new (substring s cursor))
-      ;; If PROPERTY was not found, return S as-is.
-      s)))
+	  (cl-incf width
+		   ;; When looking for `display' parts, we still need
+		   ;; to look for `invisible' property elsewhere.
+		   (+ (cond ((eq property 'display)
+			     (org--string-from-props s 'invisible cursor beg))
+			    ((= cursor beg) 0)
+			    (t (string-width (substring s cursor beg))))
+		      value))
+	  (setq cursor next))
+	(setq beg next)))
+    (+ width
+       ;; Look for `invisible' property in the last part of the
+       ;; string.  See above.
+       (cond ((eq property 'display)
+	      (org--string-from-props s 'invisible cursor end))
+	     ((= cursor end) 0)
+	     (t (string-width (substring s cursor end)))))))
 
 (defun org-string-width (string)
   "Return width of STRING when displayed in the current buffer.
 Unlike `string-width', this function takes into consideration
-`invisible' and `display' text properties."
-  (string-width
-   (org--string-from-props (org--string-from-props string 'display)
-			   'invisible)))
+`invisible' and `display' text properties.  It supports the
+latter in a limited way, mostly for combinations used in Org.
+Results may be off sometimes if it cannot handle a given
+`display' value."
+  (org--string-from-props string 'display 0 (length string)))
 
 (defun org-not-nil (v)
   "If V not nil, and also not the string \"nil\", then return V.

+ 14 - 0
lisp/ox-ascii.el

@@ -2064,6 +2064,20 @@ a communication channel."
 
 ;;; End-user functions
 
+;;;###autoload
+(defun org-ascii-convert-region-to-ascii ()
+  "Assume region has Org syntax, and convert it to plain ASCII."
+  (interactive)
+  (let ((org-ascii-charset 'ascii))
+    (org-export-replace-region-by 'ascii)))
+
+;;;###autoload
+(defun org-ascii-convert-region-to-utf8 ()
+  "Assume region has Org syntax, and convert it to UTF-8."
+  (interactive)
+  (let ((org-ascii-charset 'utf-8))
+    (org-export-replace-region-by 'ascii)))
+
 ;;;###autoload
 (defun org-ascii-export-as-ascii
   (&optional async subtreep visible-only body-only ext-plist)

+ 298 - 2
testing/lisp/test-org-capture.el

@@ -145,8 +145,10 @@
 	     (org-capture-refile)
 	     (list file1 file2 (buffer-file-name)))))))))
 
-(ert-deftest test-org-capture/insert-at-end-abort ()
-  "Test that capture can be aborted after inserting at end of capture buffer."
+(ert-deftest test-org-capture/abort ()
+  "Test aborting a capture process."
+  ;; Test that capture can be aborted after inserting at end of
+  ;; capture buffer.
   (should
    (equal
     "* A\n* B\n"
@@ -158,6 +160,235 @@
 	(goto-char (point-max))
 	(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 |"
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Table" table-line (file ,file) "| x |"))))
+	      (org-capture nil "t")
+	      (org-capture-kill))
+	    (buffer-string))))
+  ;; Test aborting a capture that split the line.
+  (should
+   (equal
+    "* AB\n"
+    (org-test-with-temp-text-in-file "* AB\n"
+      (let* ((file (buffer-file-name))
+	     (org-capture-templates
+	      `(("t" "Todo" entry
+		 (file+function ,file (lambda () (goto-char 4))) "** H1 %?"))))
+	(org-capture nil "t")
+	(org-capture-kill))
+      (buffer-string)))))
+
+(ert-deftest test-org-caputre/entry ()
+  "Test `entry' type in capture template."
+  ;; Do not break next headline.
+  (should
+   (equal
+    "* A\n** H1 Capture text\n* B\n"
+    (org-test-with-temp-text-in-file "* A\n* B\n"
+      (let* ((file (buffer-file-name))
+	     (org-capture-templates
+	      `(("t" "Todo" entry (file+headline ,file "A") "** H1 %?"))))
+	(org-capture nil "t")
+	(goto-char (point-max))
+	(insert "Capture text")
+	(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 ()
@@ -334,6 +565,71 @@
 	(org-capture nil "t")
 	(org-table-get-stored-formulas))))))
 
+(ert-deftest test-org-capture/plain ()
+  "Test `plain' type in capture template."
+  ;; Insert at end of the file, unless `:prepend' is non-nil.
+  (should
+   (equal "Some text.\nFoo\n"
+	  (org-test-with-temp-text-in-file "Some text."
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Text" plain (file ,file) "Foo"
+		       :immediate-finish t))))
+	      (org-capture nil "t")
+	      (buffer-string)))))
+  (should
+   (equal "Foo\nSome text."
+	  (org-test-with-temp-text-in-file "Some text."
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Text" plain (file ,file) "Foo"
+		       :immediate-finish t :prepend t))))
+	      (org-capture nil "t")
+	      (buffer-string)))))
+  ;; When a headline is specified, add it at the beginning of the
+  ;; entry, past any meta-data, or at its end, depending on
+  ;; `:prepend'.
+  (should
+   (equal "* A\nSCHEDULED: <2012-03-29 Thu>\nSome text.\nFoo\n* B"
+	  (org-test-with-temp-text-in-file
+	      "* A\nSCHEDULED: <2012-03-29 Thu>\nSome text.\n* B"
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Text" plain (file+headline ,file "A") "Foo"
+		       :immediate-finish t))))
+	      (org-capture nil "t")
+	      (buffer-string)))))
+  (should
+   (equal "* A\nSCHEDULED: <2012-03-29 Thu>\nFoo\nSome text.\n* B"
+	  (org-test-with-temp-text-in-file
+	      "* A\nSCHEDULED: <2012-03-29 Thu>\nSome text.\n* B"
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Text" plain (file+headline ,file "A") "Foo"
+		       :immediate-finish t :prepend t))))
+	      (org-capture nil "t")
+	      (buffer-string)))))
+  ;; At an exact position, in the middle of a line, make sure to
+  ;; insert text on a line on its own.
+  (should
+   (equal "A\nX\nB"
+	  (org-test-with-temp-text-in-file "AB"
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Text" plain (file+function ,file forward-char) "X"
+		       :immediate-finish t))))
+	      (org-capture nil "t")
+	      (buffer-string)))))
+  ;; Pathological case: insert an empty template in an empty file.
+  (should
+   (equal ""
+	  (org-test-with-temp-text-in-file ""
+	    (let* ((file (buffer-file-name))
+		   (org-capture-templates
+		    `(("t" "Text" plain (file ,file) ""
+		       :immediate-finish t))))
+	      (org-capture nil "t")
+	      (buffer-string))))))
 
 (provide 'test-org-capture)
 ;;; test-org-capture.el ends here

+ 3 - 1
testing/lisp/test-org-macs.el

@@ -63,7 +63,9 @@
   (should (= 5 (org-string-width #("1a3" 1 2 (display "abc")))))
   ;; `display' string can also contain invisible characters.
   (should (= 4 (org-string-width
-		#("123" 1 2 (display #("abc" 1 2 (invisible t))))))))
+		#("123" 1 2 (display #("abc" 1 2 (invisible t)))))))
+  ;; Test `space' property in `display'.
+  (should (= 2 (org-string-width #(" " 0 1 (display (space :width 2)))))))
 
 
 ;;; Regexp