Sfoglia il codice sorgente

Rewrite of org-mobile.org, for MobileOrg build 18

Carsten Dominik 15 anni fa
parent
commit
4a49e40daf
5 ha cambiato i file con 373 aggiunte e 133 eliminazioni
  1. 13 16
      doc/org.texi
  2. 5 59
      lisp/org-agenda.el
  3. 1 1
      lisp/org-archive.el
  4. 352 57
      lisp/org-mobile.el
  5. 2 0
      lisp/org.el

+ 13 - 16
doc/org.texi

@@ -7420,10 +7420,10 @@ This is a globally available command, and also available in the agenda menu.
 Write the agenda view to a file.  Depending on the extension of the selected
 file name, the view will be exported as HTML (extension @file{.html} or
 @file{.htm}), Postscript (extension @file{.ps}), PDF (extension @file{.pdf}),
-Org-mode (extension @file{.org}), and plain text (any other extension).  When
-called with a @kbd{C-u} prefix argument, immediately open the newly created
-file.  Use the variable @code{org-agenda-exporter-settings} to set options
-for @file{ps-print} and for @file{htmlize} to be used during export.
+and plain text (any other extension).  When called with a @kbd{C-u} prefix
+argument, immediately open the newly created file.  Use the variable
+@code{org-agenda-exporter-settings} to set options for @file{ps-print} and
+for @file{htmlize} to be used during export.
 
 @tsubheading{Quit and Exit}
 @kindex q
@@ -7650,10 +7650,9 @@ you want to do this only occasionally, use the command
 Write the agenda view to a file.  Depending on the extension of the selected
 file name, the view will be exported as HTML (extension @file{.html} or
 @file{.htm}), Postscript (extension @file{.ps}), iCalendar (extension
-@file{.ics}), Org-mode (extension @file{.org}), or plain text (any other
-extension).  Use the variable @code{org-agenda-exporter-settings} to set
-options for @file{ps-print} and for @file{htmlize} to be used during export,
-for example
+@file{.ics}), or plain text (any other extension).  Use the variable
+@code{org-agenda-exporter-settings} to set options for @file{ps-print} and
+for @file{htmlize} to be used during export, for example
 
 @vindex org-agenda-add-entry-text-maxlines
 @vindex htmlize-output-type
@@ -11684,17 +11683,15 @@ The following example counts the number of entries with TODO keyword
 @cindex MobileOrg
 
 @i{MobileOrg} is an application for the @i{iPhone/iPod Touch} series of
-devices, developed by Richard Moreland.  Instead of trying to implement the
-full feature set of Org and fighting with synchronization issues, this
-application chooses a different path.  @i{MobileOrg} provides offline viewing
-and capture support for an Org-mode system rooted on a ``real'' computer.
-Synchronization issues are avoided by making @i{MobileOrg} only @i{write} to
-a special capture file, that is only @i{read} by the computer-based system.
+devices, developed by Richard Moreland.  @i{MobileOrg} offers offline viewing
+and capture support for an Org-mode system rooted on a ``real'' computer.  It
+does also allow you to edit existing entries.
 
 This appendix describes the support Org has for creating agenda views in a
 format that can be displayed by @i{MobileOrg}, and for integrating notes
-captured by @i{MobileOrg} into the main system.  It does not cover the
-operation of @i{MobileOrg} itself (see @uref{http://mobileorg.ncogni.to/}).
+captured and changes made by @i{MobileOrg} into the main system.  It does not
+cover the operation of @i{MobileOrg} itself (see
+@uref{http://mobileorg.ncogni.to/}).
 
 @menu
 * Setting up the staging area::  Where to interact with the mobile device

+ 5 - 59
lisp/org-agenda.el

@@ -53,6 +53,7 @@
 (declare-function calendar-mayan-date-string    "cal-mayan"  (&optional date))
 (declare-function calendar-persian-date-string  "cal-persia" (&optional date))
 (declare-function org-columns-quit              "org-colview" ())
+(declare-function org-mobile-write-agenda-for-mobile "org-mobile" (file))
 (defvar calendar-mode-map)
 (defvar org-mobile-force-id-on-agenda-items) ; defined in org-mobile.el
 
@@ -2212,7 +2213,7 @@ so the export commands can easily use it."
       (put-text-property (point-at-bol) (point-at-eol)
 			 'org-agenda-title-append org-agenda-title-append))))
 
-
+(defvar org-mobile-creating-agendas)
 (defun org-write-agenda (file &optional open nosettings)
   "Write the current buffer (an agenda view) as a file.
 Depending on the extension of the file name, plain text (.txt),
@@ -2235,7 +2236,7 @@ higher priority settings."
     '(save-excursion
        (save-window-excursion
 	 (org-agenda-mark-filtered-text)
-	 (let ((bs (copy-sequence (buffer-string))) beg app)
+	 (let ((bs (copy-sequence (buffer-string))) beg)
 	   (org-agenda-unmark-filtered-text)
 	   (with-temp-buffer
 	     (insert bs)
@@ -2247,6 +2248,8 @@ higher priority settings."
 			(point-max))))
 	     (run-hooks 'org-agenda-before-write-hook)
 	     (cond
+	      ((org-bound-and-true-p org-mobile-creating-agendas)
+	       (org-mobile-write-agenda-for-mobile))
 	      ((string-match "\\.html?\\'" file)
 	       (set-buffer (htmlize-buffer (current-buffer)))
 
@@ -2275,63 +2278,6 @@ higher priority settings."
 			      (concat (file-name-sans-extension file) ".ps"))
 			     (expand-file-name file))
 	       (message "PDF written to %s" file))
-	      ((string-match "\\.org\\'" file)
-	       (let ((all (buffer-string)) in-date id pl prefix line)
-		 (with-temp-file file
-		   (org-mode)
-		   (insert all)
-		   (goto-char (point-min))
-		   (while (not (eobp))
-		     (cond
-		      ((looking-at "[ \t]*$")) ; keep empty lines
-		      ((looking-at "=+$")
-		       ;; remove underlining
-		       (delete-region (point) (point-at-eol)))
-		      ((get-text-property (point) 'org-agenda-structural-header)
-		       (setq in-date nil)
-		       (setq app (get-text-property (point)
-						     'org-agenda-title-append))
-		       (setq short (get-text-property (point)
-						      'short-heading))
-		       (when (and short (looking-at ".+"))
-			 (replace-match short)
-			 (beginning-of-line 1))
-		       (when app
-			 (end-of-line 1)
-			 (insert app)
-			 (beginning-of-line 1))
-		       (insert "* "))
-		      ((get-text-property (point) 'org-agenda-date-header)
-		       (setq in-date t)
-		       (insert "** "))
-		      ((setq m (or (get-text-property (point) 'org-hd-marker)
-				   (get-text-property (point) 'org-marker)))
-		       (if (setq pl (get-text-property (point) 'prefix-length))
-			   (progn
-			     (setq prefix (org-trim (buffer-substring
-						     (point) (+ (point) pl)))
-				   line (org-trim (buffer-substring
-						   (+ (point) pl)
-						   (point-at-eol))))
-			     (delete-region (point-at-bol) (point-at-eol))
-			     (insert line "<break>" prefix)
-			     (beginning-of-line 1))
-			 (and (looking-at "[ \t]+") (replace-match "")))
-		       (insert (if in-date "***  " "**  "))
-		       (end-of-line 1)
-		       (insert "\n")
-		       (insert (org-agenda-get-some-entry-text
-				m 10 "   " 'planning)
-			       "\n")
-		       (when (setq id
-				   (if (org-bound-and-true-p
-					org-mobile-force-id-on-agenda-items)
-				       (org-id-get m 'create)
-				     (org-entry-get m "ID")))
-			 (insert "   :PROPERTIES:\n   :ORIGINAL_ID: " id
-				 "\n   :END:\n"))))
-		     (beginning-of-line 2)))
-		 (message "Agenda written to Org file %s" file)))
 	      ((string-match "\\.ics\\'" file)
 	       (require 'org-icalendar)
 	       (let ((org-agenda-marker-table

+ 1 - 1
lisp/org-archive.el

@@ -447,7 +447,7 @@ the children that do not contain any open TODO items."
   "Archive the current subtree with the default command.
 This command is set with the variable `org-archive-default-command'."
   (interactive)
-  (call-interactively 'org-archive-default-command))
+  (call-interactively org-archive-default-command))
 
 (provide 'org-archive)
 

+ 352 - 57
lisp/org-mobile.el

@@ -28,14 +28,14 @@
 ;; This file contains the code to interact with Richard Moreland's iPhone
 ;; application MobileOrg.  This code is documented in Appendix B of the
 ;; Org-mode manual.  The code is not specific for the iPhone, however.
-;; Any external viewer and flagging application that uses the same
+;; Any external viewer/flagging/editing application that uses the same
 ;; conventions could be used.
 
 (require 'org)
 (require 'org-agenda)
 
 (defgroup org-mobile nil
-  "Options concerning support for a viewer on a mobile device."
+  "Options concerning support for a viewer/editor on a mobile device."
   :tag "Org Mobile"
   :group 'org)
 
@@ -88,12 +88,30 @@ should point to this file."
   :group 'org-mobile
   :type 'boolean)
 
+(defcustom org-mobile-force-mobile-change nil
+  "Non-nil means, force the change made on the mobile device.
+So even if there have been changes to the computer version of the entry,
+force the new value set on the mobile.
+When nil, mark the entry from the mobile with an error message.
+Instead of nil or t, this variable can also be a list of symbols, indicating
+the editing types for which the mobile version should always dominate."
+  :group 'org-mobile
+  :type '(choice
+	  (const :tag "Always" t)
+	  (const :tag "Never" nil)
+	  (set :greedy t :tag "Specify"
+	       (const todo)
+	       (const tags)
+	       (const priority)
+	       (const heading)
+	       (const body))))
+
 (defcustom org-mobile-action-alist
   '(("d" . (org-todo 'done))
     ("a" . (org-archive-subtree-default))
-    ("d-a" . (progn (org-todo 'done) (org-archive-subtree-default)))
-    ("todo" . (org-todo data))
-    ("tags" . (org-set-tags-to data)))
+    ("d-a" . (progn (org-todo 'done) (run-hooks 'post-command-hook)
+		    (org-archive-subtree-default)))
+    ("edit" . (org-mobile-edit data old new)))
   "Alist with flags and actions for mobile sync.
 When flagging an entry, MobileOrg will create entries that look like
 
@@ -208,7 +226,7 @@ agenda view showing the flagged items."
     (if (not (markerp insertion-marker))
 	(message "No new items")
       (org-with-point-at insertion-marker
-	(org-mobile-apply-flags (point) (point-max)))
+	(org-mobile-apply (point) (point-max)))
       (move-marker insertion-marker nil)
       (run-hooks 'org-mobile-post-pull-hook)
       (when org-mobile-last-flagged-files
@@ -240,7 +258,7 @@ agenda view showing the flagged items."
   (let ((files-alist org-mobile-files-alist)
 	(def-todo (default-value 'org-todo-keywords))
 	(def-tags (default-value 'org-tag-alist))
-	file link-name todo-kwds done-kwds tags drawers entry dwds twds)
+	file link-name todo-kwds done-kwds tags drawers entry kwds dwds twds)
     
     (org-prepare-agenda-buffers (mapcar 'car files-alist))
     (setq done-kwds (org-uniquify org-done-keywords-for-agenda))
@@ -287,6 +305,7 @@ agenda view showing the flagged items."
       (setq tags (append def-tags tags nil))
       (insert "#+TAGS: " (mapconcat 'identity tags " ") "\n")
       (insert "#+DRAWERS: " (mapconcat 'identity drawers " ") "\n")
+      (insert "#+ALLPRIORITIES: A B C" "\n")
       (insert "* [[file:agendas.org][Agenda Views]]\n")
       (while (setq entry (pop files-alist))
 	(setq file (car entry)
@@ -369,10 +388,10 @@ The table of checksums is written to the file mobile-checksums."
 	      settings (nth 4 e))
 	(setq settings
 	      (cons (list 'org-agenda-title-append
-			  (concat "<break>KEYS=" key " TITLE: "
+			  (concat "<after>KEYS=" key " TITLE: "
 				  (if (and (stringp desc) (> (length desc) 0))
 				      desc (symbol-name type))
-				  " " match))
+				  " " match "</after>"))
 		    settings))
 	(push (list type match settings) new))
        ((symbolp (nth 2 e))
@@ -387,13 +406,76 @@ The table of checksums is written to the file mobile-checksums."
 	  (setq settings (append gsettings settings))
 	  (setq settings
 		(cons (list 'org-agenda-title-append
-			    (concat "<break>KEYS=" gkey "#" (number-to-string
+			    (concat "<after>KEYS=" gkey "#" (number-to-string
 						      (setq cnt (1+ cnt)))
-				    " TITLE: " gdesc " " match))
+				    " TITLE: " gdesc " " match "</after>"))
 		      settings))
 	  (push (list type match settings) new)))))
     (list "X" "SUMO" (reverse new) nil)))
 
+(defvar org-mobile-creating-agendas nil)
+(defun org-mobile-write-agenda-for-mobile (file)
+  (let ((all (buffer-string)) in-date id pl prefix line app short m sexp)
+    (with-temp-file file
+      (org-mode)
+      (insert "#+READONLY\n")
+      (insert all)
+      (goto-char (point-min))
+      (while (not (eobp))
+	(cond
+	 ((looking-at "[ \t]*$")) ; keep empty lines
+	 ((looking-at "=+$")
+	  ;; remove underlining
+	  (delete-region (point) (point-at-eol)))
+	 ((get-text-property (point) 'org-agenda-structural-header)
+	  (setq in-date nil)
+	  (setq app (get-text-property (point)
+				       'org-agenda-title-append))
+	  (setq short (get-text-property (point)
+					 'short-heading))
+	  (when (and short (looking-at ".+"))
+	    (replace-match short)
+	    (beginning-of-line 1))
+	  (when app
+	    (end-of-line 1)
+	    (insert app)
+	    (beginning-of-line 1))
+	  (insert "* "))
+	 ((get-text-property (point) 'org-agenda-date-header)
+	  (setq in-date t)
+	  (insert "** "))
+	 ((setq m (or (get-text-property (point) 'org-hd-marker)
+		      (get-text-property (point) 'org-marker)))
+	  (setq sexp (member (get-text-property (point) 'type)
+			     '("diary" "sexp")))
+	  (if (setq pl (get-text-property (point) 'prefix-length))
+	      (progn
+		(setq prefix (org-trim (buffer-substring
+					(point) (+ (point) pl)))
+		      line (org-trim (buffer-substring
+				      (+ (point) pl)
+				      (point-at-eol))))
+		(delete-region (point-at-bol) (point-at-eol))
+		(insert line "<before>" prefix "</before>")
+		(beginning-of-line 1))
+	    (and (looking-at "[ \t]+") (replace-match "")))
+	  (insert (if in-date "***  " "**  "))
+	  (end-of-line 1)
+	  (insert "\n")
+	  (unless sexp
+	    (insert (org-agenda-get-some-entry-text
+		     m 10 "   " 'planning)
+		    "\n")
+	    (when (setq id
+			(if (org-bound-and-true-p
+			     org-mobile-force-id-on-agenda-items)
+			    (org-id-get m 'create)
+			  (org-entry-get m "ID")))
+	      (insert "   :PROPERTIES:\n   :ORIGINAL_ID: " id
+		      "\n   :END:\n")))))
+	(beginning-of-line 2)))
+    (message "Agenda written to Org file %s" file)))
+
 ;;;###autoload
 (defun org-mobile-create-sumo-agenda ()
   "Create a file that contains all custom agenda views."
@@ -402,7 +484,8 @@ The table of checksums is written to the file mobile-checksums."
 				 org-mobile-directory))
 	 (org-agenda-custom-commands
 	  (list (append (org-mobile-sumo-agenda-command)
-			(list (list file))))))
+			(list (list file)))))
+	 (org-mobile-write-agenda-for-mobile t))
     (unless (file-writable-p file)
       (error "Cannot write to file %s" file))
     (org-store-agenda-views)))
@@ -436,68 +519,131 @@ If nothing new has beed added, return nil."
     (kill-buffer capture-buffer)
     (if not-empty insertion-point)))
 
-(defun org-mobile-apply-flags (&optional beg end)
-  "Apply all flags in the current buffer.
+(defun org-mobile-apply (&optional beg end)
+  "Apply all change requests in the current buffer.
 If BEG and END are given, only do this in that region."
   (interactive)
   (require 'org-archive)
   (setq org-mobile-last-flagged-files nil)
   (setq beg (or beg (point-min)) end (or end (point-max)))
   (goto-char beg)
+
+  ;; First, find all the referenced entries
   (let ((marker (make-marker))
-	(org-inhibit-logging 'note)
+	(bos-marker (make-marker))
 	(end (move-marker (make-marker) end))
-	action data id id-pos cmd text)
+	buf-list
+	id-pos org-mobile-error)
     (while (re-search-forward
-	    "^\\*+[ \t]+F(\\([^():\n]*\\)\\(:\\([^()\n]*\\)\\)?)[ \t]+\\[\\[id:\\([^]\n ]+\\)" end t)
-      (goto-char (- (match-beginning 1) 2))
+	    "^\\*+[ \t]+F(\\([^():\n]*\\)\\(:\\([^()\n]*\\)\\)?)[ \t]+\\[\\[\\(\\(id\\|olp\\):\\([^]\n ]+\\)\\)" end t)
+      (setq id-pos (condition-case msg
+		       (org-mobile-locate-entry (match-string 4))
+		     (error (nth 1 msg))))
+      (when (and (markerp id-pos)
+		 (not (member (marker-buffer id-pos) buf-list)))
+	(org-mobile-timestamp-buffer (marker-buffer id-pos))
+	(push (marker-buffer id-pos) buf-list))
+				     
+      (if (or (not id-pos) (stringp id-pos))
+	  (progn
+	    (goto-char (+ 2 (point-at-bol)))
+	    (insert id-pos " "))
+	(add-text-properties (point-at-bol) (point-at-eol)
+			     (list 'org-mobile-marker
+				   (or id-pos "Linked entry not found")))))
+
+    ;; OK, now go back and start applying
+    (goto-char beg)
+    (while (re-search-forward "^\\*+[ \t]+F(\\([^():\n]*\\)\\(:\\([^()\n]*\\)\\)?)" end t)
       (catch 'next
-	(setq action (match-string 1)
-	      data (and (match-end 3) (match-string 3))
-	      id (match-string 4)
-	      cmd (if (equal action "")
-		      '(progn
-			 (org-toggle-tag "FLAGGED" 'on)
-			 (and text (org-entry-put nil "THEFLAGGINGNOTE" text)))
-		    (cdr (assoc action org-mobile-action-alist)))
-	      text (org-trim (buffer-substring (1+ (point-at-eol))
-					       (save-excursion
-						 (org-end-of-subtree t))))
-	      id-pos (org-id-find id 'marker))
-	(if (> (length text) 0)
-	    ;; Make TEXT into a single line, to fit into a property
-	    (setq text (mapconcat 'identity
-				  (org-split-string text "\n")
-				  "\\n"))
-	  (setq text nil))
-	(unless id-pos
-	  (insert "BAD ID REFERENCE ")
-	  (throw 'next t))
-	(unless cmd
-	  (insert "BAD FLAG ")
-	  (throw 'next t))
-	(move-marker marker (point))
-	(save-excursion
-	  (condition-case nil
-	      (org-with-point-at id-pos
-		(progn
+	(setq id-pos (get-text-property (point-at-bol) 'org-mobile-marker))
+	(if (not (markerp id-pos))
+	    (progn
+	      (insert "UNKNOWN PROBLEM"))
+	  (let* ((action (match-string 1))
+		 (data (and (match-end 3) (match-string 3)))
+		 (bos (point-at-bol))
+		 (eos (org-end-of-subtree t t))
+		 (cmd (if (equal action "")
+			  '(progn
+			     (org-toggle-tag "FLAGGED" 'on)
+			     (and note
+				  (org-entry-put nil "THEFLAGGINGNOTE" note)))
+			(cdr (assoc action org-mobile-action-alist))))
+		 (note (and (equal action "")
+			    (buffer-substring (1+ (point-at-eol)) eos)))
+		 (org-inhibit-logging 'note)
+		 old new)
+	    (goto-char bos)
+	    (move-marker bos-marker (point))
+	    (if (re-search-forward "^** Old value[ \t]*$" eos t)
+		(setq old (buffer-substring
+			   (1+ (match-end 0))
+			   (progn (outline-next-heading) (point)))))
+	    (if (re-search-forward "^** New value[ \t]*$" eos t)
+		(setq new (buffer-substring
+			   (1+ (match-end 0))
+			   (progn (outline-next-heading)
+				  (if (eobp) (org-back-over-empty-lines))
+				  (point)))))
+	    (setq old (if (string-match "\\S-" old) old nil))
+	    (setq new (if (string-match "\\S-" new) new nil))
+	    (if (and note (> (length note) 0))
+		;; Make Note into a single line, to fit into a property
+		(setq note (mapconcat 'identity
+				      (org-split-string (org-trim note) "\n")
+				      "\\n")))
+	    (unless (equal data "body")
+	      (setq new (and new (org-trim new))
+		    old (and old (org-trim old))))
+	    (goto-char (+ 2 bos-marker))
+	    (unless (markerp id-pos)
+	      (insert "BAD REFERENCE ")
+	      (throw 'next t))
+	    (unless cmd
+	      (insert "BAD FLAG ")
+	      (throw 'next t))
+	    ;; Remember this place so tha we can return
+	    (move-marker marker (point))
+	    (setq org-mobile-error nil)
+	    (save-excursion
+	      (condition-case msg
+		  (org-with-point-at id-pos
+		    (progn
 		  (eval cmd)
 		  (if (member "FLAGGED" (org-get-tags))
 		      (add-to-list 'org-mobile-last-flagged-files
 				   (buffer-file-name (current-buffer))))))
-	    (error
-	     (progn
-	       (switch-to-buffer (marker-buffer marker))
-	       (goto-char marker)
-	       (insert "EXECUTION FAILED ")
-	       (throw 'next t)))))
-	;; If we get here, the action has been applied successfully
-	;; So remove the entry
-	(org-back-to-heading t)
-	(delete-region (point) (org-end-of-subtree t t))))
+		(error (setq org-mobile-error msg))))
+	    (when org-mobile-error
+	      (switch-to-buffer (marker-buffer marker))
+	      (goto-char marker)
+	      (insert (if (stringp (nth 1 org-mobile-error))
+			  (nth 1 org-mobile-error)
+			"EXECUTION FAILED")
+		      " ")
+	      (throw 'next t))
+	    ;; If we get here, the action has been applied successfully
+	    ;; So remove the entry
+	    (goto-char bos-marker)
+	    (delete-region (point) (org-end-of-subtree t t))))))
     (move-marker marker nil)
     (move-marker end nil)))
 
+(defun org-mobile-timestamp-buffer (buf)
+  "Time stamp buffer BUF, just to make sure its checksum will change."
+  (with-current-buffer buf
+    (save-excursion
+      (save-restriction
+	(widen)
+	(goto-char (point-min))
+	(when (re-search-forward
+	       "^\\([ \t]*\\)#\\+LAST_MOBILE_CHANGE:.*\n?" nil t)
+	  (goto-char (match-end 1))
+	  (delete-region (point) (match-end 0)))
+	(insert "#+LAST_MOBILE_CHANGE: "
+		(format-time-string "%Y-%m-%d %T") "\n")))))
+
 (defun org-mobile-smart-read ()
   "Parse the entry at point for shortcuts and expand them.
 These shortcuts are meant for fast and easy typing on the limited
@@ -530,6 +676,155 @@ FIXME: Hmmm, not sure if we can make his work against the
 auto-correction feature.  Needs a bit more thinking.  So this function
 is currently a noop.")
 
+
+(defun org-find-olp (path)
+  "Return  a marker pointing to the entry at outline path OLP.
+If anything goes wrong, the return value will instead an error message,
+as a string."
+  (let* ((file (pop path))
+	 (buffer (find-file-noselect file))
+	 (level 1)
+	 (lmin 1)
+	 (lmax 1)
+	 limit re end found pos heading cnt)
+    (unless buffer (error "File not found :%s" file))
+    (with-current-buffer buffer
+      (save-excursion
+	(save-restriction
+	  (widen)
+	  (setq limit (point-max))
+	  (goto-char (point-min))
+	  (while (setq heading (pop path))
+	    (setq re (format org-complex-heading-regexp-format
+			     (regexp-quote heading)))
+	    (setq cnt 0 pos (point))
+	    (while (re-search-forward re end t)
+	      (setq level (- (match-end 1) (match-beginning 1)))
+	      (if (and (>= level lmin) (<= level lmax))
+		  (setq found (match-beginning 0) cnt (1+ cnt))))
+	    (when (= cnt 0) (error "Heading not found on level %d: %s"
+				   lmax heading))
+	    (when (> cnt 1) (error "Heading not unique on level %d: %s"
+				   lmax heading))
+	    (goto-char found)
+	    (setq lmin (1+ level) lmax (+ lmin (if org-odd-levels-only 1 0)))
+	    (setq end (save-excursion (org-end-of-subtree t t))))
+	  (when (org-on-heading-p)
+	    (throw 'exit (move-marker (make-marker) (point)))))))))
+
+(defun org-mobile-locate-entry (link)
+  (if (string-match "\\`id:\\(.*\\)$" link)
+      (org-id-find (match-string 1 link) 'marker)
+    (if (not (string-match "\\`olp:\\(.*?\\):\\(.*\\)$" link))
+	nil
+      (let ((file (match-string 1 link))
+	    (path (match-string 2 link))
+	    (table '((?: . "%3a") (?\[ . "%5b") (?\] . "%5d") (?/ . "%2f"))))
+	(setq file (org-link-unescape file table))
+	(setq path (mapcar (lambda (x) (org-link-unescape x table))
+			   (org-split-string path "/")))
+	(org-find-olp (cons file path))))))
+
+(defun org-mobile-edit (what old new)
+  "Edit item WHAT in the current entry by replacing OLD wih NEW.
+WHAT can be \"heading\", \"todo\", \"tags\", \"priority\", or \"body\".
+The edit only takes place if the current value is equal (except for
+white space) the OLD.  If this is so, OLD will be replace by NEW
+and the command will return t.  If something goes wrong, a string will
+be returned that indicates what went wrong."
+  (let (current old1 new1)
+    (if (stringp what) (setq what (intern what)))
+    (case what
+
+      ((todo todostate)
+       (setq current (org-get-todo-state))
+       (cond
+	((equal new current) t) ; nothing needs to be done
+	((or (equal current old)
+	     (eq org-mobile-force-mobile-change t)
+	     (memq 'todo org-mobile-force-mobile-change))
+	 (org-todo new) t)
+	(t (error "State before change was expected as \"%s\", but is \"%s\""
+		   old current))))
+      
+      (tags
+       (setq current (org-get-tags)
+	     new1 (and new (org-split-string new ":+"))
+	     old1 (and old (org-split-string old ":+")))
+       (cond
+	((org-mobile-tags-same-p current new1) t) ; no change needed
+	((or (org-mobile-tags-same-p current old1)
+	     (eq org-mobile-force-mobile-change t)
+	     (memq 'tags org-mobile-force-mobile-change))
+	 (org-set-tags-to new1) t)
+	(t (error "State before change was expected as \"%s\", but is \"%s\""
+		  (or old "") (or current "")))))
+
+      (priority
+       (when (looking-at org-complex-heading-regexp)
+	 (setq current (and (match-end 3) (substring (match-string 3) 2 3)))
+	 (cond
+	  ((equal current new) t) ; no action required
+	  ((or (equal current old)
+	       (eq org-mobile-force-mobile-change t)
+	       (memq 'tags org-mobile-force-mobile-change))
+	   (org-priority (and new (string-to-char new))))
+	  (t (error "Priority was expected to be %s, but is %s"
+		    old current)))))
+      (heading
+       (when (looking-at org-complex-heading-regexp)
+	 (setq current (match-string 4))
+	 (cond
+	  ((equal current new) t) ; no action required
+	  ((or (equal current old)
+	       (eq org-mobile-force-mobile-change t)
+	       (memq 'heading org-mobile-force-mobile-change))
+	   (goto-char (match-beginning 4))
+	   (insert new)
+	   (delete-region (point) (+ (point) (length current)))
+	   (org-set-tags nil 'align))
+	  (t (error "Heading changed in MobileOrg and on the computer")))))
+
+      (body
+       (setq current (buffer-substring (min (1+ (point-at-eol)) (point-max))
+				       (save-excursion (outline-next-heading)
+						       (point))))
+       (if (not (string-match "\\S-" current)) (setq current nil))
+       (cond
+	((org-mobile-bodies-same-p current new) t) ; no ation necesary
+	((or (org-mobile-bodies-same-p current old)
+	     (eq org-mobile-force-mobile-change t)
+	     (memq 'body org-mobile-force-mobile-change))
+	 (save-excursion
+	   (end-of-line 1)
+	   (insert "\n" new)
+	   (or (bolp) (insert "\n"))
+	   (delete-region (point) (progn (org-back-to-heading t)
+					 (outline-next-heading)
+					 (point))))
+	 t)
+	(t (error "Body was changed in MobileOrg and on the computer")))))))
+       
+
+(defun org-mobile-tags-same-p (list1 list2)
+  "Are the two tag lists the same?"
+  (not (or (org-delete-all list1 list2)
+	   (org-delete-all list2 list1))))
+
+(defun org-mobile-bodies-same-p (a b)
+  "Compare if A and B are visually equal strings.
+We first remove leading and trailing white space from the entire strings.
+Then we split the strings into lines and remove leading/trailing whitespace
+from each line.  Then we compare.
+A and B must be strings or nil."
+  (cond
+   ((and (not a) (not b)) t)
+   ((or (not a) (not b)) nil)
+   (t (setq a (org-trim a) b (org-trim b))
+      (setq a (mapconcat 'identity (org-split-string a "[ \t]*\n[ \t]*") "\n"))
+      (setq b (mapconcat 'identity (org-split-string b "[ \t]*\n[ \t]*") "\n"))
+      (equal a b))))
+
 (provide 'org-mobile)
 
 ;; arch-tag: ace0e26c-58f2-4309-8a61-05ec1535f658

+ 2 - 0
lisp/org.el

@@ -9511,6 +9511,8 @@ For calling through lisp, arg is also interpreted in the following way:
 			    (or (car (cdr (member head org-todo-heads)))
 				(car org-todo-heads))))
 			 ((car (member arg org-todo-keywords-1)))
+			 ((stringp arg)
+			  (error "State `%s' not valid in this file" arg))
 			 ((nth (1- (prefix-numeric-value arg))
 			       org-todo-keywords-1))))
 		       ((null member) (or head (car org-todo-keywords-1)))