Better support for undo.

This commit is contained in:
Magnar Sveen 2012-07-20 12:10:22 +02:00
parent 4078570320
commit 7ab8a8c977
2 changed files with 71 additions and 13 deletions

View File

@ -57,6 +57,14 @@ Feature: Multiple cursors core
And I press "C-!" And I press "C-!"
Then I should see "This aatext contains the word text twice" Then I should see "This aatext contains the word text twice"
Scenario: Undo
Given I have cursors at "text" in "This text contains the word text twice"
When I press "M-f"
And I press "M-DEL"
And I press "C-_"
And I type "!"
Then I should see "This text! contains the word text! twice"
Scenario: Setting and popping mark Scenario: Setting and popping mark
Given I have cursors at "text" in "This text contains the word text twice" Given I have cursors at "text" in "This text contains the word text twice"
And I press "C-SPC" And I press "C-SPC"

View File

@ -1,3 +1,5 @@
(eval-when-compile (require 'cl))
(defface mc/cursor-face (defface mc/cursor-face
'((t (:inverse-video t))) '((t (:inverse-video t)))
"The face used for fake cursors" "The face used for fake cursors"
@ -77,10 +79,18 @@ highlights the entire width of the window."
(ignore-errors (ignore-errors
(delete-overlay (overlay-get o 'region-overlay)))) (delete-overlay (overlay-get o 'region-overlay))))
(defun mc/create-fake-cursor-at-point () (defvar mc--current-cursor-id 0
"Var to store increasing id of fake cursors, used to keep track of them for undo.")
(defun mc/create-cursor-id ()
"Returns a unique cursor id"
(incf mc--current-cursor-id))
(defun mc/create-fake-cursor-at-point (&optional id)
"Add a fake cursor and possibly a fake active region overlay based on point and mark. "Add a fake cursor and possibly a fake active region overlay based on point and mark.
Saves the current state in the overlay to be restored later." Saves the current state in the overlay to be restored later."
(let ((overlay (mc/make-cursor-overlay-at-point))) (let ((overlay (mc/make-cursor-overlay-at-point)))
(overlay-put overlay 'mc-id (or id (mc/create-cursor-id)))
(overlay-put overlay 'type 'additional-cursor) (overlay-put overlay 'type 'additional-cursor)
(overlay-put overlay 'priority 100) (overlay-put overlay 'priority 100)
(mc/store-current-state-in-overlay overlay) (mc/store-current-state-in-overlay overlay)
@ -88,23 +98,62 @@ Saves the current state in the overlay to be restored later."
(overlay-put overlay 'region-overlay (overlay-put overlay 'region-overlay
(mc/make-region-overlay-between-point-and-mark))))) (mc/make-region-overlay-between-point-and-mark)))))
(defun mc/execute-command-for-all-fake-cursors (cmd) (defun mc/execute-command (cmd)
"Calls CMD interactively for each cursor. "Run command, simulating the parts of the command loop that makes sense for fake cursors."
It works by moving point to the fake cursor, setting
up the proper kill-ring, and then removing the cursor.
After executing the command, it sets up a new fake
cursor with updated info."
(let ((annoying-arrows-mode nil))
(mc/save-excursion
(mc/for-each-fake-cursor
(mc/pop-state-from-overlay cursor)
(ignore-errors
(setq this-command cmd) (setq this-command cmd)
(run-hooks 'pre-command-hook) (run-hooks 'pre-command-hook)
(unless (eq this-command 'ignore) (unless (eq this-command 'ignore)
(call-interactively cmd)) (call-interactively cmd))
(when deactivate-mark (deactivate-mark)) (when deactivate-mark (deactivate-mark)))
(mc/create-fake-cursor-at-point))))))
(defun mc/execute-command-for-all-fake-cursors (cmd)
"Calls CMD interactively for each cursor.
It works by moving point to the fake cursor, setting
up the proper environment, and then removing the cursor.
After executing the command, it sets up a new fake
cursor with updated info."
(mc/save-excursion
(mc/for-each-fake-cursor
(let ((id (overlay-get cursor 'mc-id))
(annoying-arrows-mode nil))
(mc/add-fake-cursor-to-undo-list
(mc/pop-state-from-overlay cursor)
(ignore-errors
(mc/execute-command cmd)
(mc/create-fake-cursor-at-point id)))))))
(defmacro mc/add-fake-cursor-to-undo-list (&rest forms)
"Make sure point is in the right place when undoing"
`(let ((undo-cleaner (cons 'apply (cons 'deactivate-cursor-after-undo (list id)))))
(setq buffer-undo-list (cons undo-cleaner buffer-undo-list))
,@forms
(if (eq undo-cleaner (car buffer-undo-list)) ;; if nothing has been added to the undo-list
(setq buffer-undo-list (cdr buffer-undo-list)) ;; then pop the cleaner right off again
(setq buffer-undo-list ;; otherwise add a function to activate this cursor
(cons (cons 'apply (cons 'activate-cursor-for-undo (list id))) buffer-undo-list)))))
(defun mc/cursor-with-id (id)
"Find the first cursor with the given id, or nil"
(find-if #'(lambda (o) (= id (overlay-get o 'mc-id)))
(overlays-in (point-min) (point-max))))
(defvar mc--stored-state-for-undo nil
"Variable to keep the state of the real cursor while undoing a fake one")
(defun activate-cursor-for-undo (id)
"Called when undoing to temporarily activate the fake cursor which action is being undone."
(let ((cursor (mc/cursor-with-id id)))
(when cursor
(setq mc--stored-state-for-undo (mc/store-current-state-in-overlay
(make-overlay (point) (point) nil nil t)))
(mc/pop-state-from-overlay cursor))))
(defun deactivate-cursor-after-undo (id)
"Called when undoing to reinstate the real cursor after undoing a fake one."
(when mc--stored-state-for-undo
(mc/create-fake-cursor-at-point id)
(mc/pop-state-from-overlay mc--stored-state-for-undo)
(setq mc--stored-state-for-undo nil)))
(defmacro mc/for-each-fake-cursor (&rest forms) (defmacro mc/for-each-fake-cursor (&rest forms)
"Runs the body for each fake cursor, bound to the name cursor" "Runs the body for each fake cursor, bound to the name cursor"
@ -122,6 +171,7 @@ cursor with updated info."
(mc/pop-state-from-overlay current-state))) (mc/pop-state-from-overlay current-state)))
(defun mc/prompt-for-inclusion-in-whitelist (original-command) (defun mc/prompt-for-inclusion-in-whitelist (original-command)
"Asks the user, then adds the command either to the once-list or the all-list."
(if (y-or-n-p (format "Do %S for all cursors?" original-command)) (if (y-or-n-p (format "Do %S for all cursors?" original-command))
(add-to-list 'mc--cmds original-command) (add-to-list 'mc--cmds original-command)
(add-to-list 'mc--cmds-run-once original-command) (add-to-list 'mc--cmds-run-once original-command)