branch: externals/cm-mode
commit 6a66026a9b183acd35b2ad91dc8c8cf2579b7584
Author: Joost Kremers <[email protected]>
Commit: Joost Kremers <[email protected]>
Improve follow changes mode.
This involved changes to several other parts of the code.
---
cm-mode.el | 266 +++++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 181 insertions(+), 85 deletions(-)
diff --git a/cm-mode.el b/cm-mode.el
index 5429e5e997..075e34757c 100644
--- a/cm-mode.el
+++ b/cm-mode.el
@@ -143,8 +143,8 @@
(defvar cm-follow-changes nil
"Flag indicating whether follow changes mode is active.")
-(defvar cm-change-text nil
- "Deleted text in follow changes mode.")
+(defvar cm-current-change nil
+ "Details about the deleted text in follow changes mode.")
(defvar cm-change-no-record nil
"Flag indicating whether to actually record a change.
@@ -259,28 +259,23 @@ flag to indicate this. (Though they should actually use
the macro
(message "Follow changes mode activated.")))
(defun cm-before-change (beg end)
- "Function to execute before a buffer change.
-In the case of an addition, this function adds the relevant
-markup. For a deletion, the deleted text is stored so that
-cm-after-change can insert it again."
- (unless (or cm-change-no-record ; do not record this change
- (and (= beg (point-min)) (= end (point-max))) ; this happens on
buffer switches
- (cm-markup-at-point)) ; if we're already inside a change, don't
do anything special
- (if (not (= beg end)) ; deletion
- (setq cm-change-text (buffer-substring beg end))
- (insert "{++++}")
- (forward-char -3))))
+ "Function to execute before a buffer change."
+ (unless (or cm-change-no-record ; do not record this change
+ (and (= beg (point-min)) (= end (point-max)))) ; this happens on
buffer switches
+ ;; (message "Point: %s; beg: %s; end: %s" (point) beg end)
+ (let ((change (cm-markup-at-point)))
+ (if (= beg end) ; addition
+ (cm-make-addition change)
+ (setq cm-current-change (list (buffer-substring beg end) change))))))
(defun cm-after-change (beg end length)
"Function to execute after a buffer change.
This function marks deletions. See cm-before-change for
details."
(unless (or cm-change-no-record
- (not cm-change-text))
- (save-excursion
- (goto-char beg)
- (insert (concat "{--" cm-change-text "--}"))
- (setq cm-change-text nil))))
+ (not cm-current-change))
+ (apply 'cm-make-deletion cm-current-change)
+ (setq cm-current-change nil)))
(defmacro cm-without-following-changes (&rest body)
"Execute BODY without following changes."
@@ -304,32 +299,72 @@ details."
(interactive)
(cm-mode -1))
+;; Making an addition is fairly simple: we just need to add markup if point
+;; isn't already at an addition markup, and then position point
+;; appropriately. The user can then type new text. A deletion is more
+;; difficult, because it also needs to (re)insert the deleted text and do
+;; something sensible with point. This is especially difficult in follow
+;; changes mode, because the deletion may be made with DEL or BACKSPACE.
+
(defun cm-addition ()
- "Make an addition."
+ "Make an addition at point.
+If point is at an addition markup already, the new addition is
+combined with it. If point is inside any other markup, no
+addition can be made."
(interactive)
- (when (cm-markup-at-point)
- (error "Already inside a change"))
- (cm-without-following-changes
- (insert "{++++}")
- (backward-char 3)))
+ (let ((change (cm-markup-at-point)))
+ (if (or (not (cm-point-inside-change-p change))
+ (eq (car change) 'cm-addition))
+ (cm-without-following-changes
+ (cm-make-addition change))
+ (error "Cannot make an addition here"))))
(defun cm-deletion (beg end)
"Mark text for deletion."
(interactive "r")
- (when (cm-markup-at-point)
- (error "Already inside a change"))
- (cm-without-following-changes
- (let ((text (delete-and-extract-region beg end)))
- (insert (concat "{--" text "--}")))))
+ (let ((change (cm-markup-at-point)))
+ (when (cm-point-inside-change-p change)
+ (error "Cannot make a deletion here")) ; TODO we should check whether
the region contains markup.
+ (when (use-region-p)
+ (cm-without-following-changes
+ (cm-make-deletion (delete-and-extract-region beg end) change)))))
+
+(defun cm-make-addition (change)
+ "Position point for an addition and insert addition markup if necessary.
+CHANGE is the change markup at point, if any, as returned by
+cm-markup-at-point. If this is an addition, the new addition is
+combined with it, even if point is right outside it. (That avoids
+having two additions adjacent to each other.) If it is another
+kind of markup, and point is inside the curly braces, we make
+sure point is not in the delimiter before adding text."
+ (if (or (eq (car change) 'cm-addition)
+ (cm-point-inside-change-p change))
+ (cm-move-into-markup (car change))
+ (insert "{++++}")
+ (backward-char 3)))
+
+(defun cm-make-deletion (text change)
+ "Insert deletion markup.
+TEXT is the text that's being deleted, CHANGE the change at
+point, if any."
+ ;; TODO: we should check whether the text to be deleted contains part of
+ ;; a change.
+ (unless (cm-point-inside-change-p change (length text))
+ (if (not (or change
+ (eq (car change) 'cm-deletion)))
+ (insert (concat "{--" text "--}"))
+ (save-excursion
+ (cm-move-into-markup 'cm-deletion)
+ (insert text)))))
(defun cm-substitution (beg end)
"Mark a substitution."
(interactive "r")
- (when (cm-markup-at-point)
- (error "Already inside a change"))
+ (when (cm-point-inside-change-p (cm-markup-at-point))
+ (error "Cannot make a substitution here")) ; TODO we should check whether
the region contains markup.
(cm-without-following-changes
(let ((text (delete-and-extract-region beg end)))
- (insert (concat "{~~" text "~>~~}"))
+ (insert (concat "{~~" text "~>~~}"))
(backward-char 3))))
(defun cm-comment (beg end)
@@ -343,48 +378,85 @@ If point is in an existing change, the comment is added
after it."
(cond
(change
(deactivate-mark) ; we don't want the region active
- (cm-forward-markup (car change)))
+ (cm-end-of-markup (car change)))
;; note: we do not account for the possibility that the region
;; contains a change but point is outside of it...
((use-region-p)
(setq text (delete-and-extract-region beg end))))
(insert (if text (concat "{{" text "}}") "") "{>><<}")
(backward-char 3))))
-
+
+(defun cm-point-at-delim (delim &optional end strict)
+ "Return non-NIL if point is at a delimiter.
+If DELIM is an end delimiter, optional argument END must be T.
+
+Point counts as being at delim if it is in a delimiter or
+directly outside, but not when it is directly inside. So `|{++',
+`{|++', `{+|+', return 0, 1, and 2 respectively, while `{++|'
+returns NIL. Similarly, `++}|', `++|}', `+|+}' return 0, 1, and
+2, while `|++}' returns NIL.
+
+If STRICT is non-NIL, point must be inside the delimiter. That
+is, instead of 0, the return value will be NIL."
+ (save-excursion
+ (if end
+ (let ((distance (skip-chars-forward (substring delim 1) (+ (point)
2))))
+ (if (looking-back (regexp-quote delim))
+ (if (> distance 0)
+ distance
+ (and (not strict) 0))))
+ (let ((distance (skip-chars-backward (substring delim 0 -1) (- (point)
2))))
+ (if (looking-at (regexp-quote delim))
+ (if (< distance 0)
+ (abs distance)
+ (and (not strict) 0)))))))
+
(defun cm-forward-markup (type &optional n)
"Move forward N markups of TYPE.
-If N is negative, move backward."
- (if (eq type 'cm-highlight) ; highlights have a delimiter of two characters
- (cm-forward-highlight n) ; therefore they have their own forward function
- (or n (setq n 1))
- ;; note that the delimiters are all three characters long. we must
- ;; therefore allow for the possibility that point is *within* a
- ;; delimiter. in the exx below, point is indicated with `|'.
- (cond
- ((> n 0) ; moving forward
- (let ((delim (third (assq type cm-delimiters))))
- ;; if point is inside the delimiter `+|+}':
- (when (looking-at (regexp-quote (substring delim -2)))
- (backward-char))
- (re-search-forward (regexp-quote delim) nil t n)))
- (t ; moving backward
- (let ((delim (second (assq type cm-delimiters))))
- ;; if point is inside the delimiter `{|++':
- (when (and (looking-back (regexp-quote (substring delim 0 1)) (1-
(point)))
- (looking-at (regexp-quote (substring delim 1))))
- (forward-char 2))
- ;; if point is inside the delimiter `{+|+':
- (when (looking-back (regexp-quote (substring delim 0 2)) (- (point)
2))
- (forward-char))
- (re-search-backward (regexp-quote delim) nil t (abs n)))))))
+If N is negative, move backward. If point is inside a delimiter,
+this function moves point to the previous/next markup. If it's
+inside a markup, it moves it to the edge. If point is at the edge
+of a markup, it moves to the end of the next markup of the same
+type."
+ (or n (setq n 1))
+ (cond
+ ((> n 0) ; moving forward
+ (let ((delim (third (assq type cm-delimiters))))
+ (backward-char (- 3 (or (cm-point-at-delim delim t t) 3))) ; adjust
point if it's inside a delim
+ (re-search-forward (regexp-quote delim) nil t n)))
+ (t ; moving backward
+ (let ((delim (second (assq type cm-delimiters))))
+ (forward-char (- 3 (or (cm-point-at-delim delim nil t) 3))) ; adjust
point if it's inside a delim
+ (re-search-backward (regexp-quote delim) nil t (abs n))))))
(defun cm-beginning-of-markup (type)
"Move to the beginning of a markup of TYPE."
+ ;; first move out of the delimiter, if we're in one.
+ (cm-move-past-delim (second (assq type cm-delimiters)))
(cm-forward-markup type -1))
(defun cm-end-of-markup (type)
"Move to the end of a markup of TYPE."
- (cm-forward-markup type 1))
+ ;; first move out of the delimiter, if we're in one.
+ (cm-move-past-delim (third (assq type cm-delimiters)) t)
+ (cm-forward-markup type))
+
+(defun cm-move-past-delim (delim &optional end)
+ "Move point past DELIM into the markup.
+If DELIM is an end delimiter, END must be T. If point is not at a
+delimiter, do not move."
+ (if end
+ (backward-char (- 3 (or (cm-point-at-delim delim end)
+ 3)))
+ (forward-char (- 3 (or (cm-point-at-delim delim)
+ 3)))))
+
+(defun cm-move-into-markup (type)
+ "Make sure point is inside the delimiters of TYPE."
+ ;; we simply call cm-move-past-delim twice, since it's harmless if we're
+ ;; not on the right delimiter.
+ (cm-move-past-delim (second (assq type cm-delimiters)))
+ (cm-move-past-delim (third (assq type cm-delimiters)) t))
(defun cm-forward-addition (&optional n)
"Move forward N addition markups.
@@ -457,23 +529,25 @@ If N is negative, move backward."
(defun cm-forward-highlight (&optional n)
"Move forward N highlight markups.
If N is negative, move backward."
- (or n (setq n 1))
- (cond
- ((> n 0)
- (re-search-forward "}}" nil t n))
- (t
- (when (and (looking-back "{" (1- (point)))
- (looking-at "{"))
- (forward-char))
- (re-search-backward "{{" nil t (abs n)))))
+ (cm-forward-markup 'cm-highlight n))
+
+ ;; (or n (setq n 1))
+ ;; (cond
+ ;; ((> n 0)
+ ;; (re-search-forward "}}" nil t n))
+ ;; (t
+ ;; (when (and (looking-back "{" (1- (point)))
+ ;; (looking-at "{"))
+ ;; (forward-char))
+ ;; (re-search-backward "{{" nil t (abs n)))))
(defun cm-beginning-of-highlight ()
"Move to the beginning of an highlight."
- (cm-forward-highlight -1))
+ (cm-forward-markup 'cm-highlight -1))
(defun cm-end-of-highlight ()
"Move to the end of an highlight."
- (cm-forward-highlight 1))
+ (cm-forward-markup 'cm-highlight 1))
(put 'cm-highlight 'forward-op 'cm-forward-highlight)
(put 'cm-highlight 'beginning-op 'cm-beginning-of-highlight)
@@ -496,12 +570,16 @@ is not included."
(end (save-excursion
(cm-end-of-markup type)
(point))))
- (list (1- beg) (1+ end))))) ; adjust (see comment at
cm-beginning-of-markup)
+ (list beg end))))
(defun cm-markup-at-point ()
"Find the markup at point.
Return a list of the form (TYPE TEXT START-POS END-POS), or NIL
-if point is not inside a markup."
+if point is not at a markup."
+ ;; if point is in between two markups, the one that is first in
+ ;; cm-delimiters will be returned, regardless whether it's before or
+ ;; after point. this is not very pretty, but it does no harm, so no need
+ ;; to change it.
(let ((type (catch 'found
(dolist (type (mapcar #'car cm-delimiters))
(when (thing-at-point type)
@@ -509,6 +587,20 @@ if point is not inside a markup."
(when type
(append (list type) (list (thing-at-point type))
(cm-bounds-of-markup-at-point type)))))
+(defun cm-point-inside-change-p (change &optional correction)
+ "Return T if point is inside CHANGE.
+CHANGE is a change as returned by `cm-markup-at-point'. Point is
+within a change if it's inside the curly braces, not directly
+outside of them. The latter counts as being AT a change.
+
+If non-NIL, CORRECTION is added to the value of point; this is
+useful if `cm-point-inside-change-p' is used after a deletion but
+with a change that follows that deletion but was extracted before
+it."
+ (and change ; if there *is* no change, we're not inside one...
+ (not (or (= (+ (or correction 0) (point)) (third change))
+ (= (+ (or correction 0) (point)) (fourth change))))))
+
(defun cm-expand-change (change)
"Expand CHANGE with a following comment or, if a comment, with a preceding
change.
If CHANGE is a comment, check if there's another change preceding
@@ -519,20 +611,19 @@ is of any other type, check if there's a commend and
include it."
(save-excursion
(cm-beginning-of-comment)
(skip-chars-backward "[:space:]") ; allow for any whitespace between
change and comment
- (backward-char 3) ; adjust point
+ (backward-char 3) ; adjust point
(let ((preceding (cm-markup-at-point)))
(if preceding
(list (car preceding) (concat (second preceding) (second change))
(third preceding) (fourth change))
change))))
- (t
- (save-excursion
- (cm-end-of-markup (car change))
- (skip-chars-forward "[:space:]") ; allow for any whitespace between
change and comment
- (forward-char 3) ; adjust point
- (let ((comment (cm-markup-at-point)))
- (if (eq (car comment) 'cm-comment)
- (list 'cm-highlight (concat (second change) (second comment))
(third change) (fourth comment))
- change))))))
+ (t (save-excursion
+ (cm-end-of-markup (car change))
+ (skip-chars-forward "[:space:]") ; allow for any whitespace between
change and comment
+ (forward-char 3) ; adjust point
+ (let ((comment (cm-markup-at-point)))
+ (if (eq (car comment) 'cm-comment)
+ (list (car change) (concat (second change) (second comment))
(third change) (fourth comment))
+ change))))))
(defun cm-accept/reject-change-at-point (&optional interactive)
"Accept or reject change at point interactively.
@@ -577,10 +668,15 @@ substitutions, `d' for comments and highlights."
(text (delete ?\n (second change)))) ; delete newlines because they
mess up string-match below.
(cond
((eq type 'cm-addition)
- (if action (substring text 3 -3)
- ""))
+ (if (not action)
+ ""
+ (string-match "{\\+\\+\\(.*?\\)\\+\\+}" text)
+ (match-string 1 text)))
((eq type 'cm-deletion)
- (if action "" (substring text 3 -3)))
+ (if action
+ ""
+ (string-match "{--\\(.*?\\)--}" text)
+ (match-string 1 text)))
((eq type 'cm-substitution)
(string-match "{~~\\(.*?\\)~>\\(.*?\\)~~}" text)
(match-string (if action 2 1) text))