Merge branch 'configurable-undo'

This commit is contained in:
Takafumi Arakaki 2012-06-17 03:34:55 +02:00
commit 4a7c81869f
7 changed files with 238 additions and 61 deletions

View file

@ -2,8 +2,9 @@
Emacs IPython Notebook (and more)
===================================
.. warning:: This is **very** early version.
Make sure you have backup!
.. note:: It is stable enough for my day to day work, but I can't
grantee the safety for your notebook data. So please make
sure you have backup.
Screenshot
==========

View file

@ -239,6 +239,7 @@ Notebook list
Notebook
^^^^^^^^
.. el:variable:: ein:notebook-enable-undo
.. el:variable:: ein:notebook-discard-output-on-save
.. el:variable:: ein:notebook-modes
.. el:variable:: ein:notebook-kill-buffer-ask

View file

@ -543,7 +543,9 @@ Called from ewoc pretty printer via `ein:cell-pp'."
(defmethod ein:cell-location ((cell ein:basecell) &optional elm end)
"Return the starting location of CELL.
ELM is a name (keyword) of element in the `:element-names' slot of CELL.
ELM is a name (keyword) of element that `ein:cell-element-get'
understands. Note that you can't use `:output' since it returns
a list. Use `:after-input' instead.
If END is non-`nil', return the location of next element."
(unless elm (setq elm :prompt))
(let ((element (oref cell :element)))
@ -780,23 +782,27 @@ Called from ewoc pretty printer via `ein:cell-insert-output'."
(ein:cell-set-input-prompt cell "*")
(ein:cell-running-set cell t)
(oset cell :dynamic t)
(let* ((callbacks
(apply #'ein:kernel-execute kernel code (ein:cell-make-callbacks cell) args))
(defmethod ein:cell-make-callbacks ((cell ein:codecell))
(list
:execute_reply (cons #'ein:cell--handle-execute-reply cell)
:output (cons #'ein:cell--handle-output cell)
:clear_output (cons #'ein:cell--handle-clear-output cell)
:set_next_input (cons #'ein:cell--handle-set-next-input cell))))
(apply #'ein:kernel-execute kernel code callbacks args)))
:set_next_input (cons #'ein:cell--handle-set-next-input cell)))
(defmethod ein:cell--handle-execute-reply ((cell ein:codecell) content)
(ein:cell-set-input-prompt cell (plist-get content :execution_count))
(ein:cell-running-set cell nil)
(ein:events-trigger
(oref cell :events) 'set_dirty.Notebook '(:value t)))
(let ((events (oref cell :events)))
(ein:events-trigger events 'set_dirty.Notebook '(:value t))
(ein:events-trigger events 'maybe_reset_undo.Notebook)))
(defmethod ein:cell--handle-set-next-input ((cell ein:codecell) text)
(ein:events-trigger
(oref cell :events) 'set_next_input.Notebook (list :cell cell :text text)))
(let ((events (oref cell :events)))
(ein:events-trigger events 'set_next_input.Notebook
(list :cell cell :text text))
(ein:events-trigger events 'maybe_reset_undo.Notebook)))
@ -823,7 +829,7 @@ Called from ewoc pretty printer via `ein:cell-insert-output'."
(plist-put json :traceback (plist-get content :traceback))))
(ein:cell-append-output cell json t)
;; (oset cell :dirty t)
))
(ein:events-trigger (oref cell :events) 'maybe_reset_undo.Notebook)))
(defun ein:output-area-convert-mime-types (json data)
@ -845,7 +851,8 @@ Called from ewoc pretty printer via `ein:cell-insert-output'."
(ein:cell-clear-output cell
(plist-get content :stdout)
(plist-get content :stderr)
(plist-get content :other)))
(plist-get content :other))
(ein:events-trigger (oref cell :events) 'maybe_reset_undo.Notebook))
;;; Misc.

View file

@ -62,7 +62,9 @@
When EVENT-TYPE is triggered on the event handler EVENTS,
CALLBACK is called. CALLBACK must take two arguments:
ARG as the first argument and DATA, which is passed via
`ein:events-trigger', as the second."
`ein:events-trigger', as the second. When calling the function,
current buffer is set to the configured buffer. `ein:events-new'
is used to configure the buffer."
(assert (symbolp event-type))
(let* ((table (oref events :callbacks))
(cbs (gethash event-type table)))

View file

@ -54,6 +54,34 @@
;;; Configuration
(defcustom ein:notebook-enable-undo 'yes
"Configure undo in notebook buffers.
`no' : symbol
Do not use undo in notebook buffers. It is the safest option.
`yes' : symbol
Enable undo in notebook buffers. You can't undo after
modification of cell (execution, add, remove, etc.). This
is default.
`full' : symbol
Enable full undo in notebook buffers. It is powerful but
sometime (typically after the cell specific commands) undo
mess up notebook buffer. Use it on your own risk. When the
buffer is messed up, you can just redo and continue editing,
or save it once and reopen it if you want to be careful.
You need to reopen the notebook buffer to reflect the change of
this value."
:type '(choice (const :tag "No" no)
(const :tag "Yes" yes)
(const :tag "Full" full))
:group 'ein)
(defun ein:notebook-empty-undo-maybe ()
"Empty `buffer-undo-list' if `ein:notebook-enable-undo' is `yes'."
(when (eq ein:notebook-enable-undo 'yes)
(setq buffer-undo-list nil)))
(defcustom ein:notebook-discard-output-on-save 'no
"Configure if the output part of the cell should be saved or not.
@ -295,6 +323,8 @@ See `ein:notebook-open' for more information."
(assert ein:notebook) ; make sure in a notebook buffer
(ein:notebook-from-json ein:notebook (ein:$notebook-data ein:notebook))
(setq buffer-undo-list nil) ; clear undo history
(when (eq ein:notebook-enable-undo 'no)
(setq buffer-undo-list t))
(ein:notebook-mode)
(setf (ein:$notebook-notification ein:notebook)
(ein:notification-setup (current-buffer)))
@ -326,6 +356,10 @@ See `ein:notebook-open' for more information."
(setf (ein:$notebook-dirty notebook)
(plist-get data :value)))
notebook)
(ein:events-on events
'maybe_reset_undo.Notebook
(lambda (&rest -ignore-)
(ein:notebook-empty-undo-maybe)))
;; Bind events for sub components:
(mapc (lambda (cell) (oset cell :events (ein:$notebook-events notebook)))
(ein:notebook-get-cells notebook))
@ -385,7 +419,8 @@ See `ein:notebook-open' for more information."
(apply #'ewoc-delete
(ein:$notebook-ewoc notebook)
(ein:cell-all-element cell)))
(setf (ein:$notebook-dirty notebook) t))
(setf (ein:$notebook-dirty notebook) t)
(ein:notebook-empty-undo-maybe))
(defun ein:notebook-delete-cell-command ()
"Delete a cell. \(WARNING: no undo!)
@ -475,6 +510,7 @@ Prefixes are act same as the normal `yank' command."
(ein:cell-insert-below base-cell cell))
(t (error (concat "`base-cell' is `nil' but ncells != 0. "
"There is something wrong..."))))
(ein:notebook-empty-undo-maybe)
(ein:cell-goto cell)
(setf (ein:$notebook-dirty notebook) t))
cell))
@ -505,6 +541,7 @@ when the prefix argument is given."
(ein:cell-enter-first cell))))
(t (error (concat "`base-cell' is `nil' but ncells > 1. "
"There is something wrong..."))))
(ein:notebook-empty-undo-maybe)
(ein:cell-goto cell)
(setf (ein:$notebook-dirty notebook) t))
cell))
@ -537,6 +574,7 @@ directly."
(let ((new (ein:cell-convert-inplace cell type)))
(when (ein:codecell-p new)
(oset new :kernel (ein:$notebook-kernel ein:notebook)))
(ein:notebook-empty-undo-maybe)
(ein:cell-goto new)))))
(defun ein:notebook-change-cell-type ()
@ -560,7 +598,8 @@ Prompt will appear in the minibuffer."
(when (ein:codecell-p new)
(oset new :kernel (ein:$notebook-kernel ein:notebook)))
(when level
(ein:cell-change-level new type))))))
(ein:cell-change-level new type))
(ein:notebook-empty-undo-maybe)))))
(defun ein:notebook-split-cell-at-point (&optional no-trim)
"Split cell at current position. Newlines at the splitting
@ -663,6 +702,7 @@ If prefix is given, merge current cell into previous cell."
(defun ein:notebook-toggle-output (notebook cell)
(ein:cell-toggle-output cell)
(ein:notebook-empty-undo-maybe)
(setf (ein:$notebook-dirty notebook) t))
(defun ein:notebook-toggle-output-command ()
@ -676,6 +716,7 @@ This does not alter the actual data stored in the cell."
(mapc (lambda (c)
(when (ein:codecell-p c) (ein:cell-set-collapsed c collapsed)))
(ein:notebook-get-cells notebook))
(ein:notebook-empty-undo-maybe)
(setf (ein:$notebook-dirty notebook) t))
(defun ein:notebook-set-collapsed-all-command (&optional show)
@ -690,7 +731,8 @@ Do not clear input prompt when the prefix argument is given."
(ein:notebook-with-cell #'ein:codecell-p
(ein:cell-clear-output cell t t t)
(unless preserve-input-prompt
(ein:cell-set-input-prompt cell))))
(ein:cell-set-input-prompt cell))
(ein:notebook-empty-undo-maybe)))
(defun ein:notebook-clear-all-output-command (&optional preserve-input-prompt)
"Clear output from all cells.
@ -701,7 +743,8 @@ Do not clear input prompts when the prefix argument is given."
do (when (ein:codecell-p cell)
(ein:cell-clear-output cell t t t)
(unless preserve-input-prompt
(ein:cell-set-input-prompt cell))))
(ein:cell-set-input-prompt cell))
(ein:notebook-empty-undo-maybe)))
(ein:log 'error "Not in notebook buffer!")))

View file

@ -109,8 +109,9 @@
ein:@shared-output)))
(defun ein:shared-output-bind-events (events)
(ein:events-on events 'set_dirty.Notebook
(lambda (&rest ignore))))
"Add dummy event handlers."
(ein:events-on events 'set_dirty.Notebook (lambda (&rest ignore)))
(ein:events-on events 'maybe_reset_undo.Notebook (lambda (&rest ignore))))
(defun ein:shared-output-get-cell ()
"Get the singleton shared output cell.

View file

@ -49,8 +49,7 @@
(with-current-buffer (ein:notebook-request-open-callback
(ein:notebook-new "DUMMY-URL" notebook-id)
:data (ein:json-read-from-string json-string))
(let ((events (ein:events-new (current-buffer))))
(setf (ein:$notebook-events ein:notebook) events)
(let ((events (ein:$notebook-events ein:notebook)))
(setf (ein:$notebook-kernel ein:notebook)
(ein:kernel-new 8888 "/kernels" events)))
(current-buffer))))
@ -70,6 +69,38 @@
(defun eintest:notebook-enable-mode (buffer)
(with-current-buffer buffer (ein:notebook-plain-mode) buffer))
(defun eintest:kernel-fake-execute-reply (kernel msg-id execution-count)
(let* ((payload nil)
(content (list :execution_count 1 :payload payload))
(packet (list :header (list :msg_type "execute_reply")
:parent_header (list :msg_id msg-id)
:content content)))
(ein:kernel--handle-shell-reply kernel (json-encode packet))))
(defun eintest:kernel-fake-stream (kernel msg-id data)
(let* ((content (list :data data
:name "stdout"))
(packet (list :header (list :msg_type "stream")
:parent_header (list :msg_id msg-id)
:content content)))
(ein:kernel--handle-iopub-reply kernel (json-encode packet))))
(defun eintest:check-search-forward-from (start string &optional null-string)
"Search STRING from START and check it is found.
When non-`nil' NULL-STRING is given, it is searched from the
position where the search of the STRING ends and check that it
is not found."
(save-excursion
(goto-char start)
(should (search-forward string nil t))
(when null-string
(should-not (search-forward null-string nil t)))))
(defun eintest:cell-check-output (cell regexp)
(save-excursion
(goto-char (ein:cell-location cell :after-input))
(should (looking-at-p (concat "\\=" regexp "\n")))))
;; from-json
@ -325,23 +356,12 @@ some text
;; Kernel related things
(ert-deftest ein:notebook-execute-current-cell ()
(with-current-buffer (eintest:notebook-make-empty)
(ein:notebook-insert-cell-below-command)
(let* ((text "print 'Hello World'")
(cell (ein:notebook-get-current-cell))
(kernel (ein:$notebook-kernel ein:notebook))
(msg-id "DUMMY-MSG-ID")
(callbacks
(list
:execute_reply (cons #'ein:cell--handle-execute-reply cell)
:output (cons #'ein:cell--handle-output cell)
:clear_output (cons #'ein:cell--handle-clear-output cell)
:set_next_input (cons #'ein:cell--handle-set-next-input cell))))
(defun eintest:notebook-check-kernel-and-codecell (kernel cell)
(should (ein:$kernel-p kernel))
(should (ein:codecell-p cell))
(should (ein:$kernel-p (oref cell :kernel)))
(insert text)
(should (ein:$kernel-p (oref cell :kernel))))
(defun eintest:notebook-fake-execution (kernel text msg-id callbacks)
(mocker-let ((ein:kernel-execute
(kernel code callbacks kwd-silent silent)
((:input (list kernel text callbacks :silent nil))))
@ -349,34 +369,136 @@ some text
(kernel)
((:input (list kernel) :output t))))
(ein:notebook-execute-current-cell))
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
(save-excursion
(goto-char (point-min))
(should-not (search-forward "In [1]:" nil t)))
(let* ((payload nil)
(content (list :execution_count 1 :payload payload))
(packet (list :header (list :msg_type "execute_reply")
:parent_header (list :msg_id msg-id)
:content content)))
(ein:kernel--handle-shell-reply kernel (json-encode packet)))
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks))
(ert-deftest ein:notebook-execute-current-cell ()
(with-current-buffer (eintest:notebook-make-empty)
(ein:notebook-insert-cell-below-command)
(let* ((text "print 'Hello World'")
(cell (ein:notebook-get-current-cell))
(kernel (ein:$notebook-kernel ein:notebook))
(msg-id "DUMMY-MSG-ID")
(callbacks (ein:cell-make-callbacks cell)))
(eintest:notebook-check-kernel-and-codecell kernel cell)
;; Execute
(insert text)
(eintest:notebook-fake-execution kernel text msg-id callbacks)
;; Execute reply
(should-error (eintest:check-search-forward-from (point-min) "In [1]:"))
(eintest:kernel-fake-execute-reply kernel msg-id 1)
(should (= (oref cell :input-prompt-number) 1))
(save-excursion
(goto-char (point-min))
(should (search-forward "In [1]:" nil t)))
(let* ((content (list :data "'Hello World'"
:name "stdout"))
(packet (list :header (list :msg_type "stream")
:parent_header (list :msg_id msg-id)
:content content)))
(ein:kernel--handle-iopub-reply kernel (json-encode packet)))
(eintest:check-search-forward-from (point-min) "In [1]:")
;; Stream output
(eintest:kernel-fake-stream kernel msg-id "Hello World")
(should (= (ein:cell-num-outputs cell) 1))
(save-excursion
(goto-char (point-min))
(should (search-forward "In [1]:" nil t))
(should (search-forward "print 'Hello World'" nil t))
(should (search-forward "Hello World" nil t)) ; stream output
(should (search-forward "\nHello World\n" nil t)) ; stream output
(should-not (search-forward "Hello World" nil t))))))
;; Notebook undo
(defun eintest:notebook-undo-after-execution-1-cell ()
(with-current-buffer (eintest:notebook-make-empty)
(ein:notebook-insert-cell-below-command)
(let* ((text "print 'Hello World'")
(output-text "Hello World\n")
(cell (ein:notebook-get-current-cell))
(kernel (ein:$notebook-kernel ein:notebook))
(msg-id "DUMMY-MSG-ID")
(callbacks (ein:cell-make-callbacks cell))
(check-output
(lambda ()
(eintest:cell-check-output cell output-text))))
(eintest:notebook-check-kernel-and-codecell kernel cell)
;; Execute
(insert text)
(undo-boundary)
(eintest:notebook-fake-execution kernel text msg-id callbacks)
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
;; Stream output
(eintest:kernel-fake-stream kernel msg-id output-text)
(funcall check-output)
;; Undo
(should (equal (ein:cell-get-text cell) text))
(if (eq ein:notebook-enable-undo 'full)
(undo)
(should-error (undo)))
(when (eq ein:notebook-enable-undo 'full)
(should (equal (ein:cell-get-text cell) ""))
;; FIXME: Known bug. (it must succeed.)
(should-error (funcall check-output))))))
(defun eintest:notebook-undo-after-execution-2-cells ()
(with-current-buffer (eintest:notebook-make-empty)
(ein:notebook-insert-cell-below-command)
(ein:notebook-insert-cell-above-command)
(let* ((text "print 'Hello World\\n' * 10")
(next-text "something")
(output-text
(apply #'concat (loop repeat 10 collect "Hello World\n")))
(cell (ein:notebook-get-current-cell))
(next-cell (ein:cell-next cell))
(kernel (ein:$notebook-kernel ein:notebook))
(msg-id "DUMMY-MSG-ID")
(callbacks (ein:cell-make-callbacks cell))
(check-output
(lambda ()
(eintest:cell-check-output cell output-text))))
(eintest:notebook-check-kernel-and-codecell kernel cell)
;; Execute
(insert text)
(undo-boundary)
(let ((pos (point)))
;; Do not use `save-excursion' because it does not record undo.
(ein:notebook-goto-next-input-command)
(insert next-text)
(undo-boundary)
(goto-char pos))
(eintest:notebook-fake-execution kernel text msg-id callbacks)
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
;; Stream output
(eintest:kernel-fake-stream kernel msg-id output-text)
(funcall check-output)
;; Undo
(should (equal (ein:cell-get-text cell) text))
(should (equal (ein:cell-get-text next-cell) next-text))
(if (eq ein:notebook-enable-undo 'full)
(undo)
(should-error (undo)))
(when (eq ein:notebook-enable-undo 'full)
(should (equal (ein:cell-get-text cell) text))
;; FIXME: Known bug. (these two must succeed.)
(should-error (should (equal (ein:cell-get-text next-cell) "")))
(should-error (funcall check-output))))))
(ert-deftest ein:notebook-undo-after-execution-1-cell/no ()
(let ((ein:notebook-enable-undo 'no))
(eintest:notebook-undo-after-execution-1-cell)))
(ert-deftest ein:notebook-undo-after-execution-1-cell/yes ()
(let ((ein:notebook-enable-undo 'yes))
(eintest:notebook-undo-after-execution-1-cell)))
(ert-deftest ein:notebook-undo-after-execution-1-cell/full ()
(let ((ein:notebook-enable-undo 'full))
(eintest:notebook-undo-after-execution-1-cell)))
(ert-deftest ein:notebook-undo-after-execution-2-cells/no ()
(let ((ein:notebook-enable-undo 'no))
(eintest:notebook-undo-after-execution-2-cells)))
(ert-deftest ein:notebook-undo-after-execution-2-cells/yes ()
(let ((ein:notebook-enable-undo 'yes))
(eintest:notebook-undo-after-execution-2-cells)))
(ert-deftest ein:notebook-undo-after-execution-2-cells/full ()
(let ((ein:notebook-enable-undo 'full))
(eintest:notebook-undo-after-execution-2-cells)))
;; Notebook mode