org-element: Refactor navigation tools

* contrib/lisp/org-element.el (org-element-at-point,
  org-element-backward, org-element-up, org-element-down,
  org-element-drag-backward): Refactor.
(org-element-swap-A-B): Handle the case of the first paragraph in an
item.
(org-element-transpose): New function.
(org-transpose-elements): Removed function.
(org-element-unindent-buffer): Correctly un-indent contents of
headlines.
* testing/lisp/test-org-element.el: Add tests.
This commit is contained in:
Nicolas Goaziou 2012-05-05 09:14:39 +02:00
parent b137cdb296
commit ed9a748057
2 changed files with 183 additions and 244 deletions

View File

@ -3821,19 +3821,18 @@ first row.
If optional argument KEEP-TRAIL is non-nil, the function returns If optional argument KEEP-TRAIL is non-nil, the function returns
a list of of elements leading to element at point. The list's a list of of elements leading to element at point. The list's
CAR is always the element at point. Its last item will be the CAR is always the element at point. Following positions contain
element's parent, unless element was either the first in its element's siblings, then parents, siblings of parents, until the
section (in which case the last item in the list is the first first element of current section."
element of section) or an headline (in which case the list
contains that headline as its single element). Elements
in-between, if any, are siblings of the element at point."
(org-with-wide-buffer (org-with-wide-buffer
;; If at an headline, parse it. It is the sole element that ;; If at an headline, parse it. It is the sole element that
;; doesn't require to know about context. Be sure to disallow ;; doesn't require to know about context. Be sure to disallow
;; secondary string parsing, though. ;; secondary string parsing, though.
(if (org-with-limited-levels (org-at-heading-p)) (if (org-with-limited-levels (org-at-heading-p))
(progn
(beginning-of-line)
(if (not keep-trail) (org-element-headline-parser t) (if (not keep-trail) (org-element-headline-parser t)
(list (org-element-headline-parser t))) (list (org-element-headline-parser t))))
;; Otherwise move at the beginning of the section containing ;; Otherwise move at the beginning of the section containing
;; point. ;; point.
(let ((origin (point)) element type special-flag trail struct prevs) (let ((origin (point)) element type special-flag trail struct prevs)
@ -3843,72 +3842,39 @@ in-between, if any, are siblings of the element at point."
(forward-line))) (forward-line)))
(org-skip-whitespace) (org-skip-whitespace)
(beginning-of-line) (beginning-of-line)
;; Starting parsing successively each element with ;; Parse successively each element, skipping those ending
;; `org-element-current-element'. Skip those ending before ;; before original position.
;; original position.
(catch 'exit (catch 'exit
(while t (while t
(setq element (org-element-current-element (setq element (org-element-current-element
'element special-flag struct) 'element special-flag struct)
type (car element)) type (car element))
(when keep-trail (push element trail)) (push element trail)
(cond (cond
;; 1. Skip any element ending before point or at point. ;; 1. Skip any element ending before point or at point.
((let ((end (org-element-property :end element))) ((let ((end (org-element-property :end element)))
(when (<= end origin) (when (<= end origin)
(if (> (point-max) end) (goto-char end) (if (> (point-max) end) (goto-char end)
(throw 'exit (or trail element)))))) (throw 'exit (if keep-trail trail element))))))
;; 2. An element containing point is always the element at ;; 2. An element containing point is always the element at
;; point. ;; point.
((not (memq type org-element-greater-elements)) ((not (memq type org-element-greater-elements))
(throw 'exit (if keep-trail trail element))) (throw 'exit (if keep-trail trail element)))
;; 3. At a plain list. ;; 3. At any other greater element type, if point is
((eq type 'plain-list)
(setq struct (org-element-property :structure element)
prevs (or prevs (org-list-prevs-alist struct)))
(let ((beg (org-element-property :contents-begin element)))
(if (<= origin beg) (throw 'exit (or trail element))
;; Find the item at this level containing ORIGIN.
(let ((items (org-list-get-all-items beg struct prevs))
parent)
(catch 'local
(mapc
(lambda (pos)
(cond
;; Item ends before point: skip it.
((<= (org-list-get-item-end pos struct) origin))
;; Item contains point: store is in PARENT.
((<= pos origin) (setq parent pos))
;; We went too far: return PARENT.
(t (throw 'local nil)))) items))
;; No parent: no item contained point, though the
;; plain list does. Point is in the blank lines
;; after the list: return plain list.
(if (not parent) (throw 'exit (or trail element))
(setq special-flag 'item)
(goto-char parent))))))
;; 4. At a table.
((eq type 'table)
(if (eq (org-element-property :type element) 'table.el)
(throw 'exit (or trail element))
(let ((beg (org-element-property :contents-begin element))
(end (org-element-property :contents-end element)))
(if (or (<= origin beg) (>= origin end))
(throw 'exit (or trail element))
(when keep-trail (setq trail (list element)))
(setq special-flag 'table-row)
(narrow-to-region beg end)))))
;; 4. At any other greater element type, if point is
;; within contents, move into it. Otherwise, return ;; within contents, move into it. Otherwise, return
;; that element. ;; that element.
(t (t
(when (eq type 'item) (setq special-flag nil))
(let ((beg (org-element-property :contents-begin element)) (let ((beg (org-element-property :contents-begin element))
(end (org-element-property :contents-end element))) (end (org-element-property :contents-end element)))
(if (or (not beg) (not end) (> beg origin) (< end origin)) (if (or (not beg) (not end) (> beg origin) (<= end origin)
(throw 'exit (or trail element)) (and (= beg origin) (memq type '(plain-list table))))
;; Reset trail, since we found a parent. (throw 'exit (if keep-trail trail element))
(when keep-trail (setq trail (list element))) (case type
(plain-list
(setq special-flag 'item
struct (org-element-property :structure element)))
(table (setq special-flag 'table-row))
(otherwise (setq special-flag nil)))
(narrow-to-region beg end) (narrow-to-region beg end)
(goto-char beg))))))))))) (goto-char beg)))))))))))
@ -3942,10 +3908,27 @@ in-between, if any, are siblings of the element at point."
(defun org-element-swap-A-B (elem-A elem-B) (defun org-element-swap-A-B (elem-A elem-B)
"Swap elements ELEM-A and ELEM-B. "Swap elements ELEM-A and ELEM-B.
Assume ELEM-B is after ELEM-A in the buffer. Leave point at the
Leave point at the end of ELEM-A." end of ELEM-A."
(goto-char (org-element-property :begin elem-A)) (goto-char (org-element-property :begin elem-A))
(let* ((beg-A (org-element-property :begin elem-A)) ;; There are two special cases when an element doesn't start at bol:
;; the first paragraph in an item or in a footnote definition.
(let ((specialp (not (bolp))))
;; Only a paragraph without any affiliated keyword can be moved at
;; ELEM-A position in such a situation. Note that the case of
;; a footnote definition is impossible: it cannot contain two
;; paragraphs in a row because it cannot contain a blank line.
(if (and specialp
(or (not (eq (org-element-type elem-B) 'paragraph))
(/= (org-element-property :begin elem-B)
(org-element-property :contents-begin elem-B))))
(error "Cannot swap elements"))
;; In a special situation, ELEM-A will have no indentation. We'll
;; give it ELEM-B's (which will in, in turn, have no indentation).
(let* ((ind-B (when specialp
(goto-char (org-element-property :begin elem-B))
(org-get-indentation)))
(beg-A (org-element-property :begin elem-A))
(end-A (save-excursion (end-A (save-excursion
(goto-char (org-element-property :end elem-A)) (goto-char (org-element-property :end elem-A))
(skip-chars-backward " \r\t\n") (skip-chars-backward " \r\t\n")
@ -3958,68 +3941,106 @@ Leave point at the end of ELEM-A."
(body-A (buffer-substring beg-A end-A)) (body-A (buffer-substring beg-A end-A))
(body-B (delete-and-extract-region beg-B end-B))) (body-B (delete-and-extract-region beg-B end-B)))
(goto-char beg-B) (goto-char beg-B)
(when specialp
(setq body-B (replace-regexp-in-string "\\`[ \t]*" "" body-B))
(org-indent-to-column ind-B))
(insert body-A) (insert body-A)
(goto-char beg-A) (goto-char beg-A)
(delete-region beg-A end-A) (delete-region beg-A end-A)
(insert body-B) (insert body-B)
(goto-char (org-element-property :end elem-B)))) (goto-char (org-element-property :end elem-B)))))
(defun org-element-forward ()
"Move forward by one element.
Move to the next element at the same level, when possible."
(interactive)
(if (org-with-limited-levels (org-at-heading-p))
(let ((origin (point)))
(org-forward-same-level 1)
(unless (org-with-limited-levels (org-at-heading-p))
(goto-char origin)
(error "Cannot move further down")))
(let* ((trail (org-element-at-point 'keep-trail))
(elem (pop trail))
(end (org-element-property :end elem))
(parent (loop for prev in trail
when (>= (org-element-property :end prev) end)
return prev)))
(cond
((eobp) (error "Cannot move further down"))
((and parent (= (org-element-property :contents-end parent) end))
(goto-char (org-element-property :end parent)))
(t (goto-char end))))))
(defun org-element-backward () (defun org-element-backward ()
"Move backward by one element. "Move backward by one element.
Move to the previous element at the same level, when possible." Move to the previous element at the same level, when possible."
(interactive) (interactive)
(if (save-excursion (skip-chars-backward " \r\t\n") (bobp)) (if (org-with-limited-levels (org-at-heading-p))
(error "Cannot move further up") ;; At an headline, move to the previous one, if any, or stay
;; here.
(let ((origin (point)))
(org-backward-same-level 1)
(unless (org-with-limited-levels (org-at-heading-p))
(goto-char origin)
(error "Cannot move further up")))
(let* ((trail (org-element-at-point 'keep-trail)) (let* ((trail (org-element-at-point 'keep-trail))
(element (car trail)) (elem (car trail))
(beg (org-element-property :begin element))) (prev-elem (nth 1 trail))
;; Move to beginning of current element if point isn't there. (beg (org-element-property :begin elem)))
(if (/= (point) beg) (goto-char beg)
(let ((type (org-element-type element)))
(cond (cond
;; At an headline: move to previous headline at the same ;; Move to beginning of current element if point isn't there
;; level, a parent, or BOB. ;; already.
((eq type 'headline) ((/= (point) beg) (goto-char beg))
(let ((dest (save-excursion (org-backward-same-level 1) (point)))) ((not prev-elem) (error "Cannot move further up"))
(if (= (point-min) dest) (error "Cannot move further up") (t (goto-char (org-element-property :begin prev-elem)))))))
(goto-char dest))))
;; At an item: try to move to the previous item, if any. (defun org-element-up ()
((and (eq type 'item) "Move to upper element."
(let* ((struct (org-element-property :structure element)) (interactive)
(prev (org-list-get-prev-item (if (org-with-limited-levels (org-at-heading-p))
beg struct (org-list-prevs-alist struct)))) (unless (org-up-heading-safe)
(when prev (goto-char prev))))) (error "No surrounding element"))
;; In any other case, find the previous element in the (let* ((trail (org-element-at-point 'keep-trail))
;; trail and move to its beginning. If no previous element (elem (pop trail))
;; can be found, move to headline. (end (org-element-property :end elem))
(t (let ((prev (nth 1 trail))) (parent (loop for prev in trail
(if prev (goto-char (org-element-property :begin prev)) when (>= (org-element-property :end prev) end)
(org-back-to-heading)))))))))) return prev)))
(cond
(parent (goto-char (org-element-property :begin parent)))
((org-before-first-heading-p) (error "No surrounding element"))
(t (org-back-to-heading))))))
(defun org-element-down ()
"Move to inner element."
(interactive)
(let ((element (org-element-at-point)))
(cond
((memq (org-element-type element) '(plain-list table))
(goto-char (org-element-property :contents-begin element))
(forward-char))
((memq (org-element-type element) org-element-greater-elements)
;; If contents are hidden, first disclose them.
(when (org-element-property :hiddenp element) (org-cycle))
(goto-char (org-element-property :contents-begin element)))
(t (error "No inner element")))))
(defun org-element-drag-backward () (defun org-element-drag-backward ()
"Drag backward element at point." "Move backward element at point."
(interactive) (interactive)
(let* ((pos (point)) (if (org-with-limited-levels (org-at-heading-p)) (org-move-subtree-up)
(elem (org-element-at-point))) (let* ((trail (org-element-at-point 'keep-trail))
(when (= (progn (goto-char (point-min)) (elem (car trail))
(org-skip-whitespace) (prev-elem (nth 1 trail)))
(point-at-bol)) ;; Error out if no previous element or previous element is
(org-element-property :end elem)) ;; a parent of the current one.
(error "Cannot drag element backward")) (if (or (not prev-elem) (org-element-nested-p elem prev-elem))
(goto-char (org-element-property :begin elem)) (error "Cannot drag element backward")
(org-element-backward) (let ((pos (point)))
(let ((prev-elem (org-element-at-point)))
(when (or (org-element-nested-p elem prev-elem)
(and (eq (org-element-type elem) 'headline)
(not (eq (org-element-type prev-elem) 'headline))))
(goto-char pos)
(error "Cannot drag element backward"))
;; Compute new position of point: it's shifted by PREV-ELEM
;; body's length.
(let ((size-prev (- (org-element-property :end prev-elem)
(org-element-property :begin prev-elem))))
(org-element-swap-A-B prev-elem elem) (org-element-swap-A-B prev-elem elem)
(goto-char (- pos size-prev)))))) (goto-char (+ (org-element-property :begin prev-elem)
(- pos (org-element-property :begin elem)))))))))
(defun org-element-drag-forward () (defun org-element-drag-forward ()
"Move forward element at point." "Move forward element at point."
@ -4042,7 +4063,9 @@ Move to the previous element at the same level, when possible."
(goto-char (org-element-property :end next-elem)) (goto-char (org-element-property :end next-elem))
(skip-chars-backward " \r\t\n") (skip-chars-backward " \r\t\n")
(forward-line) (forward-line)
(point)) ;; Small correction if buffer doesn't end
;; with a newline character.
(if (and (eolp) (not (bolp))) (1+ (point)) (point)))
(org-element-property :begin next-elem))) (org-element-property :begin next-elem)))
(size-blank (- (org-element-property :end elem) (size-blank (- (org-element-property :end elem)
(save-excursion (save-excursion
@ -4053,43 +4076,6 @@ Move to the previous element at the same level, when possible."
(org-element-swap-A-B elem next-elem) (org-element-swap-A-B elem next-elem)
(goto-char (+ pos size-next size-blank)))))) (goto-char (+ pos size-next size-blank))))))
(defun org-element-forward ()
"Move forward by one element.
Move to the next element at the same level, when possible."
(interactive)
(if (eobp) (error "Cannot move further down")
(let* ((trail (org-element-at-point 'keep-trail))
(element (car trail))
(type (org-element-type element))
(end (org-element-property :end element)))
(cond
;; At an headline, move to next headline at the same level.
((eq type 'headline) (goto-char end))
;; At an item. Move to the next item, if possible.
((and (eq type 'item)
(let* ((struct (org-element-property :structure element))
(prevs (org-list-prevs-alist struct))
(beg (org-element-property :begin element))
(next-item (org-list-get-next-item beg struct prevs)))
(when next-item (goto-char next-item)))))
;; In any other case, move to element's end, unless this
;; position is also the end of its parent's contents, in which
;; case, directly jump to parent's end.
(t
(let ((parent
;; Determine if TRAIL contains the real parent of ELEMENT.
(and (> (length trail) 1)
(let* ((parent-candidate (car (last trail))))
(and (memq (org-element-type parent-candidate)
org-element-greater-elements)
(>= (org-element-property
:contents-end parent-candidate) end)
parent-candidate)))))
(cond ((not parent) (goto-char end))
((= (org-element-property :contents-end parent) end)
(goto-char (org-element-property :end parent)))
(t (goto-char end)))))))))
(defun org-element-mark-element () (defun org-element-mark-element ()
"Put point at beginning of this element, mark at end. "Put point at beginning of this element, mark at end.
@ -4127,29 +4113,18 @@ ones already marked."
(org-element-property :begin elem) (org-element-property :begin elem)
(org-element-property :end elem)))))) (org-element-property :end elem))))))
(defun org-transpose-elements () (defun org-element-transpose ()
"Transpose current and previous elements, keeping blank lines between. "Transpose current and previous elements, keeping blank lines between.
Point is moved after both elements." Point is moved after both elements."
(interactive) (interactive)
(org-skip-whitespace) (org-skip-whitespace)
(let ((pos (point)) (let ((end (org-element-property :end (org-element-at-point))))
(cur (org-element-at-point))) (org-element-drag-backward)
(when (= (save-excursion (goto-char (point-min)) (goto-char end)))
(org-skip-whitespace)
(point-at-bol))
(org-element-property :begin cur))
(error "No previous element"))
(goto-char (org-element-property :begin cur))
(forward-line -1)
(let ((prev (org-element-at-point)))
(when (org-element-nested-p cur prev)
(goto-char pos)
(error "Cannot transpose nested elements"))
(org-element-swap-A-B prev cur))))
(defun org-element-unindent-buffer () (defun org-element-unindent-buffer ()
"Un-indent the visible part of the buffer. "Un-indent the visible part of the buffer.
Relative indentation \(between items, inside blocks, etc.\) isn't Relative indentation (between items, inside blocks, etc.) isn't
modified." modified."
(interactive) (interactive)
(unless (eq major-mode 'org-mode) (unless (eq major-mode 'org-mode)
@ -4159,10 +4134,10 @@ modified."
(unindent-tree (unindent-tree
(function (function
(lambda (contents) (lambda (contents)
(mapc (lambda (element) (mapc
(if (eq (org-element-type element) 'headline) (lambda (element)
(funcall unindent-tree (if (memq (org-element-type element) '(headline section))
(org-element-contents element)) (funcall unindent-tree (org-element-contents element))
(save-excursion (save-excursion
(save-restriction (save-restriction
(narrow-to-region (narrow-to-region
@ -4172,57 +4147,6 @@ modified."
(reverse contents)))))) (reverse contents))))))
(funcall unindent-tree (org-element-contents parse-tree)))) (funcall unindent-tree (org-element-contents parse-tree))))
(defun org-element-up ()
"Move to upper element."
(interactive)
(cond
((bobp) (error "No surrounding element"))
((org-with-limited-levels (org-at-heading-p))
(or (org-up-heading-safe) (error "No surronding element")))
(t
(let* ((trail (org-element-at-point 'keep-trail))
(element (car trail))
(type (org-element-type element)))
(cond
;; At an item, with a parent in the list: move to that parent.
((and (eq type 'item)
(let* ((beg (org-element-property :begin element))
(struct (org-element-property :structure element))
(parents (org-list-parents-alist struct))
(parentp (org-list-get-parent beg struct parents)))
(and parentp (goto-char parentp)))))
;; Determine parent in the trail.
(t
(let ((parent
(and (> (length trail) 1)
(let ((parentp (car (last trail))))
(and (memq (org-element-type parentp)
org-element-greater-elements)
(>= (org-element-property :contents-end parentp)
(org-element-property :end element))
parentp)))))
(cond
;; When parent is found move to its beginning.
(parent (goto-char (org-element-property :begin parent)))
;; If no parent was found, move to headline above, if any
;; or return an error.
((org-before-first-heading-p) (error "No surrounding element"))
(t (org-back-to-heading))))))))))
(defun org-element-down ()
"Move to inner element."
(interactive)
(let ((element (org-element-at-point)))
(cond
((memq (org-element-type element) '(plain-list table))
(goto-char (org-element-property :contents-begin element))
(forward-char))
((memq (org-element-type element) org-element-greater-elements)
;; If contents are hidden, first disclose them.
(when (org-element-property :hiddenp element) (org-cycle))
(goto-char (org-element-property :contents-begin element)))
(t (error "No inner element")))))
(provide 'org-element) (provide 'org-element)
;;; org-element.el ends here ;;; org-element.el ends here

View File

@ -1673,7 +1673,22 @@ Paragraph \\alpha."
;;; Test Navigation Tools. ;;; Test Navigation Tools.
(ert-deftest test-org-element/forward-element () (ert-deftest test-org-element/at-point ()
"Test `org-element-at-point' specifications."
;; Special case: at the very beginning of a table, return `table'
;; object instead of `table-row'.
(should
(eq 'table
(org-test-with-temp-text "| a | b |"
(org-element-type (org-element-at-point)))))
;; Special case: at the very beginning of a list or sub-list, return
;; `plain-list' object instead of `item'.
(should
(eq 'plain-list
(org-test-with-temp-text "- item"
(org-element-type (org-element-at-point))))))
(ert-deftest test-org-element/forward ()
"Test `org-element-forward' specifications." "Test `org-element-forward' specifications."
;; 1. At EOB: should error. ;; 1. At EOB: should error.
(org-test-with-temp-text "Some text\n" (org-test-with-temp-text "Some text\n"
@ -1753,7 +1768,7 @@ Outside."
(org-element-forward) (org-element-forward)
(should (looking-at " - sub3")))) (should (looking-at " - sub3"))))
(ert-deftest test-org-element/backward-element () (ert-deftest test-org-element/backward ()
"Test `org-element-backward' specifications." "Test `org-element-backward' specifications."
;; 1. At BOB (modulo some white spaces): should error. ;; 1. At BOB (modulo some white spaces): should error.
(org-test-with-temp-text " \nParagraph." (org-test-with-temp-text " \nParagraph."
@ -1832,7 +1847,7 @@ Outside."
(org-element-backward) (org-element-backward)
(should (looking-at "- item1")))) (should (looking-at "- item1"))))
(ert-deftest test-org-element/up-element () (ert-deftest test-org-element/up ()
"Test `org-element-up' specifications." "Test `org-element-up' specifications."
;; 1. At BOB or with no surrounding element: should error. ;; 1. At BOB or with no surrounding element: should error.
(org-test-with-temp-text "Paragraph." (org-test-with-temp-text "Paragraph."
@ -1883,7 +1898,7 @@ Outside."
(org-element-up) (org-element-up)
(should (looking-at "\\* Top")))) (should (looking-at "\\* Top"))))
(ert-deftest test-org-element/down-element () (ert-deftest test-org-element/down ()
"Test `org-element-down' specifications." "Test `org-element-down' specifications."
;; 1. Error when the element hasn't got a recursive type. ;; 1. Error when the element hasn't got a recursive type.
(org-test-with-temp-text "Paragraph." (org-test-with-temp-text "Paragraph."