emacs-ipython-notebook/lisp/ein-notebook.el
2012-08-14 21:11:16 +02:00

1614 lines
62 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; ein-notebook.el --- Notebook module
;; Copyright (C) 2012- Takafumi Arakaki
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
;; This file is NOT part of GNU Emacs.
;; ein-notebook.el is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; ein-notebook.el is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with ein-notebook.el. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; * Coding rule about current buffer.
;; A lot of notebook and cell functions touches to current buffer and
;; it is not ideal to wrap all these functions by `with-current-buffer'.
;; Therefore, when the function takes `notebook' to the first argument
;; ("method" function), it is always assumed that the current buffer
;; is the notebook buffer. **However**, functions called as callback
;; (via `url-retrieve', for example) must protect themselves by
;; calling from unknown buffer.
;;; Code:
(eval-when-compile (require 'cl))
(require 'ewoc)
(eval-when-compile (require 'auto-complete nil t))
(require 'ein-utils)
(require 'ein-log)
(require 'ein-node)
(require 'ein-kernel)
(require 'ein-cell)
(require 'ein-completer)
(require 'ein-pager)
(require 'ein-events)
(require 'ein-notification)
(require 'ein-kill-ring)
(require 'ein-query)
(require 'ein-shared-output)
(require 'ein-pytools)
;;; 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.
`no' : symbol
Save output. This is the default.
`yes' : symbol
Always discard output.
a function
This function takes two arguments, notebook and cell. Return
`t' to discard output and return `nil' to save. For example,
if you don't want to save image output but other kind of
output, use `ein:notebook-cell-has-image-output-p'.
Note that using function needs EIN lisp API, which is not determined
yet. So be careful when using EIN functions. They may change."
:type '(choice (const :tag "No" 'no)
(const :tag "Yes" 'yes)
;; FIXME: this must be go to the customize UI after
;; clarifying the notebook lisp API.
;; (function :tag "Predicate" (lambda () t))
)
:group 'ein)
(defun ein:notebook-cell-has-image-output-p (-ignore- cell)
(ein:cell-has-image-ouput-p cell))
(defun ein:notebook-discard-output-p (notebook cell)
"Return non-`nil' if the output must be discarded, otherwise save."
(case ein:notebook-discard-output-on-save
(no nil)
(yes t)
(t (funcall ein:notebook-discard-output-on-save notebook cell))))
;; As opening/saving notebook treats possibly huge data, define these
;; timeouts separately:
(defcustom ein:notebook-querty-timeout-open (* 60 1000) ; 1 min
"Query timeout for opening notebook.
If you cannot open large notebook because of timeout error, try
to increase this value. Setting this value to `nil' means to use
global setting. For global setting and more information, see
`ein:query-timeout'."
:type '(choice (integer :tag "Timeout [ms]" 5000)
(const :tag "Use global setting" nil))
:group 'ein)
(defcustom ein:notebook-querty-timeout-save (* 60 1000) ; 1 min
"Query timeout for saving notebook.
Similar to `ein:notebook-querty-timeout-open', but for saving
notebook. For global setting and more information, see
`ein:query-timeout'."
:type '(choice (integer :tag "Timeout [ms]" 5000)
(const :tag "Use global setting" nil))
:group 'ein)
(defcustom ein:complete-on-dot t
"Start completion when inserting a dot. Note that
`ein:use-auto-complete' (or `ein:use-auto-complete-superpack')
must be `t' to enable this option. This variable has effect on
notebook buffers and connected buffers."
:type 'boolean
:group 'ein)
(defun ein:complete-on-dot-install (map func)
(if (and ein:complete-on-dot
(featurep 'auto-complete)
(or ein:use-auto-complete
ein:use-auto-complete-superpack))
(define-key map "." func)
(define-key map "." nil)))
(defvar ein:notebook-after-rename-hook nil
"Hooks to run after notebook is renamed successfully.
Current buffer for these functions is set to the notebook buffer.")
;;; Class and variable
(defvar ein:base-kernel-url "/")
;; Currently there is no way to know this setting. Maybe I should ask
;; IPython developers for an API to get this from notebook server.
(defvar ein:notebook-pager-buffer-name-template "*ein:pager %s/%s*")
(defvar ein:notebook-buffer-name-template "*ein: %s/%s*")
(defvar ein:notebook-save-retry-max 1
"Maximum retries for notebook saving.")
(defvar ein:notebook-opened-map (make-hash-table :test 'equal)
"A map: (URL-OR-PORT NOTEBOOK-ID) => notebook buffer.")
(defstruct ein:$notebook
"Hold notebook variables.
`ein:$notebook-url-or-port'
URL or port of IPython server.
`ein:$notebook-notebook-id' : string
uuid string
`ein:$notebook-data' - FIXME: remove this!
Original notebook JSON data sent from server. This slot exists
first for debugging reason and should be deleted later.
`ein:$notebook-ewoc' : `ewoc'
An instance of `ewoc'. Notebook is rendered using `ewoc'.
Also `ewoc' nodes are used for saving cell data.
`ein:$notebook-kernel' : `ein:$kernel'
`ein:$kernel' instance.
`ein:$notebook-pager'
Variable for `ein:pager-*' functions. See ein-pager.el.
`ein:$notebook-dirty' : boolean
Set to `t' if notebook has unsaved changes. Otherwise `nil'.
`ein:$notebook-metadata' : plist
Notebook meta data (e.g., notebook name).
`ein:$notebook-name' : string
Notebook name.
`ein:$notebook-nbformat' : integer
Notebook file format version.
`ein:$notebook-nbformat-minor' : integer
Notebook file format version.
`ein:$notebook-events' : `ein:$events'
Event handler instance.
`ein:$notebook-notification' : `ein:notification'
Notification widget.
"
url-or-port
notebook-id
data
ewoc
kernel
pager
dirty
metadata
notebook-name
nbformat
nbformat-minor
events
notification
)
(ein:deflocal ein:notebook nil
"Buffer local variable to store an instance of `ein:$notebook'.")
(defmacro ein:notebook-with-cell (cell-p &rest body)
"Execute BODY if in cell with a dynamically bound variable `cell'.
When CELL-P is non-`nil', it is called with the current cell object
and BODY will be executed only when it returns non-`nil'. If CELL-P
is `nil', BODY is executed with any cell types."
(declare (indent 1))
`(let ((cell (ein:notebook-get-current-cell)))
(if ,(if cell-p `(and cell (funcall ,cell-p cell)) 'cell)
(progn ,@body)
(ein:log 'warn "Not in cell"))))
(defmacro ein:notebook-with-cells-in-region (&rest body)
"Similar to `ein:notebook-with-cell' but sets a list of cells to `cells'.
Cells are fetched by `ein:notebook-get-cells-in-region-or-at-point'."
(declare (indent 0))
`(let* ((cells (ein:notebook-get-cells-in-region-or-at-point)))
(if cells
(progn ,@body)
(ein:log 'warn "Not in cell"))))
(defmacro ein:notebook-with-buffer (notebook &rest body)
"Execute BODY with current buffer setting at the one of NOTEBOOK."
;; FIXME: MANY functions can use this macro. Refactor them!
(declare (indent 1))
`(with-current-buffer (ein:notebook-buffer ,notebook)
,@body))
(defun ein:notebook-new (url-or-port notebook-id &rest args)
(let ((notebook (apply #'make-ein:$notebook
:url-or-port url-or-port
:notebook-id notebook-id
args)))
notebook))
(defun ein:notebook-init (notebook data)
"Initialize NOTEBOOK with DATA from the server."
(setf (ein:$notebook-data notebook) data)
(let* ((metadata (plist-get data :metadata))
(notebook-name (plist-get metadata :name)))
(setf (ein:$notebook-metadata notebook) metadata)
(setf (ein:$notebook-nbformat notebook) (plist-get data :nbformat))
(setf (ein:$notebook-nbformat-minor notebook)
(plist-get data :nbformat_minor))
(setf (ein:$notebook-notebook-name notebook) notebook-name)))
(defun ein:notebook-del (notebook)
"Destructor for `ein:$notebook'."
(ein:log-ignore-errors
(with-current-buffer (ein:notebook-buffer notebook)
(ein:log-del))
(ein:kernel-del (ein:$notebook-kernel notebook))))
(defun ein:notebook-get-buffer-name (notebook)
(format ein:notebook-buffer-name-template
(ein:$notebook-url-or-port notebook)
(ein:$notebook-notebook-name notebook)))
(defun ein:notebook-get-buffer (notebook)
(get-buffer-create (ein:notebook-get-buffer-name notebook)))
(defun ein:notebook-buffer (notebook)
"Return the buffer that is associated with NOTEBOOK."
(ewoc-buffer (ein:$notebook-ewoc notebook)))
(defalias 'ein:notebook-name 'ein:$notebook-notebook-name)
(defun ein:notebook-url (notebook)
(ein:notebook-url-from-url-and-id (ein:$notebook-url-or-port notebook)
(ein:$notebook-notebook-id notebook)))
(defun ein:notebook-url-from-url-and-id (url-or-port notebook-id)
(ein:url url-or-port "notebooks" notebook-id))
(defun ein:notebook-pop-to-current-buffer (&rest -ignore-)
"Default callback for `ein:notebook-open'."
(pop-to-buffer (current-buffer)))
(defun ein:notebook-open (url-or-port notebook-id &optional callback cbargs)
"Open notebook of NOTEBOOK-ID in the server URL-OR-PORT.
Opened notebook instance is returned. Note that notebook might not be
ready at the time when this function is executed.
After the notebook is opened, CALLBACK is called as::
\(apply CALLBACK notebook CREATED CBARGS)
where the second argument CREATED indicates whether the notebook
is newly created or not. When CALLBACK is specified, buffer is
**not** brought up by `pop-to-buffer'. It is caller's
responsibility to do so. The current buffer is set to the
notebook buffer when CALLBACK is called."
(unless callback (setq callback #'ein:notebook-pop-to-current-buffer))
(let* ((key (list url-or-port notebook-id))
(buffer (gethash key ein:notebook-opened-map)))
(if (buffer-live-p buffer)
(with-current-buffer buffer
(when callback
(apply callback ein:notebook nil cbargs))
ein:notebook)
(ein:notebook-request-open url-or-port notebook-id callback cbargs))))
(defun ein:notebook-request-open (url-or-port notebook-id
&optional callback cbargs)
"Request notebook of NOTEBOOK-ID to the server at URL-OR-PORT.
Return `ein:$notebook' instance. Notebook may not be ready at
the time of execution.
CALLBACK is called as \(apply CALLBACK notebook t CBARGS). The second
argument `t' indicates that the notebook is newly opened.
See `ein:notebook-open' for more information."
(let ((url (ein:notebook-url-from-url-and-id url-or-port notebook-id))
(notebook (ein:notebook-new url-or-port notebook-id)))
(ein:log 'debug "Opening notebook at %s" url)
(ein:query-singleton-ajax
(list 'notebook-open url-or-port notebook-id)
url
:timeout ein:notebook-querty-timeout-open
:parser #'ein:json-read
:success (cons #'ein:notebook-request-open-callback-with-callback
(list notebook callback cbargs)))
notebook))
(defun ein:notebook-request-open-callback-with-callback (packed &rest args)
(let ((notebook (nth 0 packed))
(callback (nth 1 packed))
(cbargs (nth 2 packed)))
(apply #'ein:notebook-request-open-callback notebook args)
(when callback
(with-current-buffer (ein:notebook-get-buffer notebook)
(apply callback notebook t cbargs)))))
(defun* ein:notebook-request-open-callback (notebook &key status data
&allow-other-keys)
(ein:log 'debug "URL-RETRIEVE nodtebook-id = %S, status = %S"
(ein:$notebook-notebook-id notebook)
status)
(let ((notebook-id (ein:$notebook-notebook-id notebook)))
(ein:notebook-init notebook data)
(with-current-buffer (ein:notebook-get-buffer notebook)
(ein:log-setup (ein:$notebook-notebook-id notebook))
(setq ein:notebook notebook)
(ein:notebook-render)
(set-buffer-modified-p nil)
(puthash (list (ein:$notebook-url-or-port ein:notebook) notebook-id)
(current-buffer)
ein:notebook-opened-map))))
(defun ein:notebook-render ()
"(Re-)Render the notebook."
(interactive)
(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)))
(ein:notebook-bind-events ein:notebook (ein:events-new (current-buffer)))
(ein:notebook--check-nbformat (ein:$notebook-data ein:notebook))
(ein:notebook-start-kernel)
(ein:log 'info "Notebook %s is ready"
(ein:$notebook-notebook-name ein:notebook)))
(defun ein:notebook-pp (ewoc-data)
(let ((path (ein:$node-path ewoc-data))
(data (ein:$node-data ewoc-data)))
(case (car path)
(cell (ein:cell-pp (cdr path) data)))))
(defun ein:notebook--different-number (n1 n2)
(and (numberp n1) (numberp n2) (not (= n1 n2))))
(defun ein:notebook--check-nbformat (data)
"Warn user when nbformat is changed on server side.
See https://github.com/ipython/ipython/pull/1934 for the purpose
of minor mode."
;; See `Notebook.prototype.load_notebook_success'
;; at IPython/frontend/html/notebook/static/js/notebook.js
(destructuring-bind (&key nbformat orig_nbformat
nbformat_minor orig_nbformat_minor
&allow-other-keys)
data
(cond
((ein:notebook--different-number nbformat orig_nbformat)
(ein:display-warning
(format "Notebook major version updated (v%d -> v%d).
To not update version, do not save this notebook."
orig_nbformat nbformat)))
((ein:notebook--different-number nbformat_minor orig_nbformat_minor)
(ein:display-warning
(format "This notebook is version v%s.%s, but IPython
server you are using only fully support up to v%s.%s.
Some features may not be available."
orig_nbformat orig_nbformat_minor
nbformat nbformat_minor))))))
;;; Initialization.
(defun ein:notebook-bind-events (notebook events)
"Bind events related to PAGER to the event handler EVENTS."
(setf (ein:$notebook-events ein:notebook) events)
(ein:events-on events
'set_next_input.Notebook
#'ein:notebook--set-next-input
notebook)
(ein:events-on events
'set_dirty.Notebook
(lambda (notebook data)
(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))
(ein:notification-bind-events (ein:$notebook-notification ein:notebook)
events)
(setf (ein:$notebook-pager notebook)
(ein:pager-new
(format ein:notebook-pager-buffer-name-template
(ein:$notebook-url-or-port notebook)
(ein:$notebook-notebook-name notebook))
(ein:$notebook-events notebook))))
(defun ein:notebook--set-next-input (notebook data)
(let* ((cell (plist-get data :cell))
(text (plist-get data :text))
(new-cell (ein:notebook-insert-cell-below notebook 'code cell)))
(ein:cell-set-text new-cell text)
(setf (ein:$notebook-dirty notebook) t)))
;;; Cell indexing, retrieval, etc.
(defun ein:notebook-cell-from-json (notebook data &rest args)
(apply #'ein:cell-from-json
data :ewoc (ein:$notebook-ewoc notebook) args))
(defun ein:notebook-cell-from-type (notebook type &rest args)
;; Note: TYPE can be a string.
;; FIXME: unify type of TYPE to symbol or string.
(apply #'ein:cell-from-type
(format "%s" type)
:ewoc (ein:$notebook-ewoc notebook)
:events (ein:$notebook-events notebook)
args))
(defun ein:notebook-get-cells (notebook)
(let* ((ewoc (ein:$notebook-ewoc notebook))
(nodes (ewoc-collect ewoc (lambda (n) (ein:cell-node-p n 'prompt)))))
(mapcar #'ein:$node-data nodes)))
(defun ein:notebook-ncells (notebook)
(length (ein:notebook-get-cells notebook)))
;; Insertion and deletion of cells
(defun ein:notebook-delete-cell (notebook cell)
(let ((inhibit-read-only t)
(buffer-undo-list t)) ; disable undo recording
(apply #'ewoc-delete
(ein:$notebook-ewoc notebook)
(ein:cell-all-element cell)))
(setf (ein:$notebook-dirty notebook) t)
(ein:notebook-empty-undo-maybe))
(defun ein:notebook-delete-cell-command ()
"Delete a cell. \(WARNING: no undo!)
This command has no key binding because there is no way to undo
deletion. Use kill to play on the safe side.
If you really want use this command, you can do something like this
\(but be careful when using it!)::
\(define-key ein:notebook-mode-map \"\\C-c\\C-d\"
'ein:notebook-delete-cell-command)"
(interactive)
(ein:notebook-with-cell nil
(ein:notebook-delete-cell ein:notebook cell)
(ein:aif (ein:notebook-get-current-cell) (ein:cell-goto it))))
(defun ein:notebook-kill-cells (notebook cells)
(when cells
(mapc (lambda (c)
(ein:cell-save-text c)
(ein:notebook-delete-cell notebook c)
(ein:cell-deactivate c))
cells)
(ein:kill-new cells)))
(defun ein:notebook-kill-cell-command ()
"Kill (\"cut\") the cell at point.
Note that the kill-ring for cells is not shared with the default
kill-ring of Emacs (kill-ring for texts)."
(interactive)
(ein:notebook-with-cells-in-region
(ein:notebook-kill-cells ein:notebook cells)
(deactivate-mark)
(ein:aif (ein:notebook-get-current-cell) (ein:cell-goto it))))
(defun ein:notebook-copy-cell-command ()
"Copy the cell at point. (Put the current cell into the kill-ring.)"
(interactive)
(ein:notebook-with-cells-in-region
(deactivate-mark)
(let ((cells (mapcar
(lambda (c)
(ein:cell-deactivate (ein:cell-copy c))) cells)))
(ein:log 'info "%s cells are copied." (length cells))
(ein:kill-new cells))))
(defun ein:notebook-insert-clone-below (notebook cell pivot)
(let ((clone (ein:cell-copy cell)))
;; Cell can be from another buffer, so reset `ewoc'.
(oset clone :ewoc (ein:$notebook-ewoc notebook))
(ein:notebook-insert-cell-below notebook clone pivot)
clone))
(defun ein:notebook-yank-cell-command (&optional arg)
"Insert (\"paste\") the latest killed cell.
Prefixes are act same as the normal `yank' command."
(interactive "*P")
;; Do not use `ein:notebook-with-cell'.
;; `ein:notebook-insert-cell-below' handles empty cell.
(let* ((cell (ein:notebook-get-current-cell))
(killed (ein:current-kill (cond
((listp arg) 0)
((eq arg '-) -2)
(t (1- arg))))))
(loop for c in killed
with last = cell
do (setq last (ein:notebook-insert-clone-below ein:notebook c last))
finally (ein:cell-goto last))))
(defun ein:notebook-maybe-new-cell (notebook type-or-cell)
"Return TYPE-OR-CELL as-is if it is a cell, otherwise return a new cell."
(let ((cell (if (ein:basecell-child-p type-or-cell)
type-or-cell
(ein:notebook-cell-from-type notebook type-or-cell))))
;; When newly created or copied, kernel is not attached or not the
;; kernel of this notebook. So reset it here.
(when (ein:codecell-p cell)
(oset cell :kernel (ein:$notebook-kernel notebook)))
(oset cell :events (ein:$notebook-events notebook))
cell))
(defun ein:notebook-insert-cell-below (notebook type-or-cell base-cell)
"Insert a cell just after BASE-CELL and return the new cell."
(let ((cell (ein:notebook-maybe-new-cell notebook type-or-cell)))
(when cell
(cond
((= (ein:notebook-ncells notebook) 0)
(ein:cell-enter-last cell))
(base-cell
(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))
(defun ein:notebook-insert-cell-below-command (&optional markdown)
"Insert cell below. Insert markdown cell instead of code cell
when the prefix argument is given."
(interactive "P")
(let ((cell (ein:notebook-get-current-cell)))
;; Do not use `ein:notebook-with-cell'. When there is no cell,
;; This command should add the first cell. So this clause must be
;; executed even if `cell' is `nil'.
(ein:cell-goto
(ein:notebook-insert-cell-below ein:notebook
(if markdown 'markdown 'code)
cell))))
(defun ein:notebook-insert-cell-above (notebook type-or-cell base-cell)
(let ((cell (ein:notebook-maybe-new-cell notebook type-or-cell)))
(when cell
(cond
((< (ein:notebook-ncells notebook) 2)
(ein:cell-enter-first cell))
(base-cell
(let ((prev-cell (ein:cell-prev base-cell)))
(if prev-cell
(ein:cell-insert-below prev-cell cell)
(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))
(defun ein:notebook-insert-cell-above-command (&optional markdown)
"Insert cell above. Insert markdown cell instead of code cell
when the prefix argument is given."
(interactive "P")
(let ((cell (ein:notebook-get-current-cell)))
(ein:cell-goto
(ein:notebook-insert-cell-above ein:notebook
(if markdown 'markdown 'code)
cell))))
(defun ein:notebook-toggle-cell-type ()
"Toggle the cell type of the cell at point.
Use `ein:notebook-change-cell-type' to change the cell type
directly."
(interactive)
(ein:notebook-with-cell nil
(let ((type (case (ein:$notebook-nbformat ein:notebook)
(2 (ein:case-equal (oref cell :cell-type)
(("code") "markdown")
(("markdown") "code")))
(3 (ein:case-equal (oref cell :cell-type)
(("code") "markdown")
(("markdown") "raw")
(("raw") "heading")
(("heading") "code"))))))
(let ((relpos (ein:cell-relative-point cell))
(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 relpos)))))
(defun ein:notebook-change-cell-type (type &optional level)
"Change the cell type of the current cell.
Prompt will appear in the minibuffer.
When used in as a Lisp function, TYPE (string) should be chose
from \"code\", \"markdown\", \"raw\" and \"heading\". LEVEL is
an integer used only when the TYPE is \"heading\"."
(interactive
(let* ((choices (case (ein:$notebook-nbformat ein:notebook)
(2 "cm")
(3 "cmr123456")))
(key (ein:ask-choice-char
(format "Cell type [%s]: " choices) choices))
(type (case key
(?c "code")
(?m "markdown")
(?r "raw")
(t "heading")))
(level (when (equal type "heading")
(string-to-number (char-to-string key)))))
(list type level)))
(ein:notebook-with-cell nil
(let ((relpos (ein:cell-relative-point cell))
(new (ein:cell-convert-inplace cell type)))
(when (ein:codecell-p new)
(oset new :kernel (ein:$notebook-kernel ein:notebook)))
(when level
(ein:cell-change-level new level))
(ein:notebook-empty-undo-maybe)
(ein:cell-goto new relpos))))
(defun ein:notebook-split-cell-at-point (&optional no-trim)
"Split cell at current position. Newlines at the splitting
point will be removed. This can be omitted by giving a prefix
argument \(C-u)."
(interactive "P")
(ein:notebook-with-cell nil
;; FIXME: should I inhibit undo?
(let* ((beg (set-marker (make-marker) (ein:cell-input-pos-min cell)))
(pos (point-marker))
(head (buffer-substring beg pos))
(new (ein:notebook-insert-cell-above ein:notebook
(oref cell :cell-type)
cell)))
(delete-region beg pos)
(unless no-trim
(setq head (ein:trim-right head "\n"))
(save-excursion
(goto-char pos)
(while (looking-at-p "\n")
(delete-char 1))))
(ein:cell-set-text new head)
(ein:notebook-empty-undo-maybe)
(ein:cell-goto cell))))
(defun ein:notebook-merge-cell-command (&optional next)
"Merge previous cell into current cell.
If prefix is given, merge current cell into next cell."
(interactive "P")
(ein:notebook-with-cell nil
(unless next
(setq cell (ein:cell-prev cell))
(unless cell (error "No previous cell"))
(ein:cell-goto cell))
(let* ((next-cell (ein:cell-next cell))
(head (ein:cell-get-text cell)))
(assert next-cell nil "No cell to merge.")
(ein:notebook-delete-cell ein:notebook cell)
(save-excursion
(goto-char (ein:cell-input-pos-min next-cell))
(insert head "\n"))
(ein:notebook-empty-undo-maybe)
(ein:cell-goto next-cell))))
;;; Cell selection.
(defun ein:notebook-goto-input (ewoc-node up)
(let* ((ewoc-data (ewoc-data ewoc-node))
(cell (ein:$node-data ewoc-data))
(path (ein:$node-path ewoc-data))
(element (nth 1 path)))
(ein:aif
(if (memql element (if up '(output footer) '(prompt)))
cell
(funcall (if up #'ein:cell-prev #'ein:cell-next) cell))
(ein:cell-goto it)
(ein:log 'warn "No %s input!" (if up "previous" "next")))))
(defun ein:notebook-goto-input-in-notebook-buffer (up)
(ein:aif (ein:notebook-get-current-ewoc-node)
(ein:notebook-goto-input it up)
(ein:log 'warn "Not in notebook buffer!")))
(defun ein:notebook-goto-next-input-command ()
(interactive)
(ein:notebook-goto-input-in-notebook-buffer nil))
(defun ein:notebook-goto-prev-input-command ()
(interactive)
(ein:notebook-goto-input-in-notebook-buffer t))
;;; Cell movement
(defun ein:notebook-move-cell (notebook cell up)
(ein:aif (if up (ein:cell-prev cell) (ein:cell-next cell))
(let ((inhibit-read-only t)
(pivot-cell it))
(ein:cell-save-text cell)
(ein:notebook-delete-cell ein:notebook cell)
(funcall (if up
#'ein:notebook-insert-cell-above
#'ein:notebook-insert-cell-below)
notebook cell pivot-cell)
(ein:cell-goto cell)
(setf (ein:$notebook-dirty notebook) t))
(ein:log 'warn "No %s cell" (if up "previous" "next"))))
(defun ein:notebook-move-cell-up-command ()
(interactive)
(ein:notebook-with-cell nil
(ein:notebook-move-cell ein:notebook cell t)))
(defun ein:notebook-move-cell-down-command ()
(interactive)
(ein:notebook-with-cell nil
(ein:notebook-move-cell ein:notebook cell nil)))
;;; Cell collapsing and output clearing
(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 ()
"Toggle the visibility of the output of the cell at point.
This does not alter the actual data stored in the cell."
(interactive)
(ein:notebook-with-cell #'ein:codecell-p
(ein:notebook-toggle-output ein:notebook cell)))
(defun ein:notebook-set-collapsed-all (notebook collapsed)
(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)
"Hide all cell output. When prefix is given, show all cell output."
(interactive "P")
(ein:notebook-set-collapsed-all ein:notebook (not show)))
(defun ein:notebook-clear-output-command (&optional preserve-input-prompt)
"Clear output from the current cell at point.
Do not clear input prompt when the prefix argument is given."
(interactive "P")
(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:notebook-empty-undo-maybe)))
(defun ein:notebook-clear-all-output-command (&optional preserve-input-prompt)
"Clear output from all cells.
Do not clear input prompts when the prefix argument is given."
(interactive "P")
(if ein:notebook
(loop for cell in (ein:notebook-get-cells ein:notebook)
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:notebook-empty-undo-maybe)))
(ein:log 'error "Not in notebook buffer!")))
(defun ein:notebook-show-in-shared-output ()
"Show truncated code cell ouput in shared-output buffer.
See also `ein:cell-max-num-outputs' to how to truncate long
output."
(interactive)
(ein:notebook-with-cell #'ein:codecell-p
(ein:shared-output-show-code-cell cell)))
;;; Kernel related things
(defun ein:notebook-start-kernel ()
(let* ((base-url (concat ein:base-kernel-url "kernels"))
(kernel (ein:kernel-new (ein:$notebook-url-or-port ein:notebook)
base-url
(ein:$notebook-events ein:notebook))))
(setf (ein:$notebook-kernel ein:notebook) kernel)
(ein:kernelinfo-init (ein:$kernel-kernelinfo kernel) (current-buffer))
(ein:kernelinfo-setup-hooks kernel)
(ein:pytools-setup-hooks kernel)
(ein:kernel-start kernel
(ein:$notebook-notebook-id ein:notebook))
(loop for cell in (ein:notebook-get-cells ein:notebook)
do (when (ein:codecell-p cell)
(ein:cell-set-kernel cell kernel)))))
(defun ein:notebook-restart-kernel (notebook)
(ein:kernel-restart (ein:$notebook-kernel notebook)))
(defun ein:notebook-restart-kernel-command ()
"Send request to the server to restart kernel."
(interactive)
(if ein:notebook
(when (y-or-n-p "Really restart kernel? ")
(ein:notebook-restart-kernel ein:notebook))
(ein:log 'error "Not in notebook buffer!")))
(defun ein:notebook-get-current-ewoc-node (&optional pos)
(ein:aand ein:notebook (ein:$notebook-ewoc it) (ewoc-locate it pos)))
(defun ein:notebook-get-nearest-cell-ewoc-node (&optional pos max cell-p)
(ein:aif (ein:notebook-get-current-ewoc-node pos)
(let ((ewoc-node it))
;; FIXME: can be optimized using the argument `max'
(while (and ewoc-node
(not (and (ein:cell-ewoc-node-p ewoc-node)
(if cell-p
(funcall cell-p
(ein:cell-from-ewoc-node ewoc-node))
t))))
(setq ewoc-node (ewoc-next (ein:$notebook-ewoc ein:notebook)
ewoc-node)))
ewoc-node)))
(defun ein:notebook-get-current-cell (&optional pos)
"Return CELL at POS. If POS is not given, it is assumed be the
current cursor position. When the current buffer is not notebook
buffer or there is no cell in the current buffer, return `nil'."
(let ((cell (ein:cell-from-ewoc-node
(ein:notebook-get-current-ewoc-node pos))))
(when (ein:basecell-child-p cell) cell)))
(defun ein:notebook-get-cells-in-region (beg end)
(ein:clip-list (ein:aand ein:notebook (ein:notebook-get-cells it))
(ein:notebook-get-current-cell beg)
(ein:notebook-get-current-cell end)))
(defun ein:notebook-get-cells-in-region-or-at-point ()
(if (region-active-p)
(ein:notebook-get-cells-in-region (region-beginning) (region-end))
(list (ein:notebook-get-current-cell))))
(defun ein:notebook-execute-cell (notebook cell)
(ein:kernel-if-ready (ein:$notebook-kernel notebook)
(ein:cell-execute cell)
(setf (ein:$notebook-dirty notebook) t)
cell))
(defun ein:notebook-execute-current-cell ()
"Execute cell at point."
(interactive)
(ein:notebook-with-cell #'ein:codecell-p
(ein:notebook-execute-cell ein:notebook cell)))
(defun ein:notebook-execute-current-cell-and-goto-next ()
"Execute cell at point if it is a code cell and move to the
next cell, or insert if none."
(interactive)
(ein:notebook-with-cell nil
(when (ein:codecell-p cell)
(ein:notebook-execute-cell ein:notebook cell))
(ein:aif (ein:cell-next cell)
(ein:cell-goto it)
(ein:notebook-insert-cell-below ein:notebook 'code cell))))
(defun ein:notebook-execute-all-cell ()
"Execute all cells in the current notebook buffer."
(interactive)
(if ein:notebook
(loop for cell in (ein:notebook-get-cells ein:notebook)
when (ein:codecell-p cell)
do (ein:cell-execute cell))
(ein:log 'error "Not in notebook buffer!")))
(defun ein:notebook-request-tool-tip (notebook cell func)
(let ((kernel (ein:$notebook-kernel notebook))
(callbacks
(list :object_info_reply
(cons #'ein:cell-finish-tooltip cell))))
(ein:kernel-object-info-request kernel func callbacks)))
(defun ein:notebook-request-tool-tip-command ()
(interactive)
(ein:notebook-with-cell #'ein:codecell-p
(ein:kernel-if-ready (ein:$notebook-kernel ein:notebook)
(let ((func (ein:object-at-point)))
(ein:notebook-request-tool-tip ein:notebook cell func)))))
(defun ein:notebook-request-help (notebook)
(ein:kernel-if-ready (ein:$notebook-kernel notebook)
(let ((func (ein:object-at-point)))
(when func
(ein:kernel-execute (ein:$notebook-kernel notebook)
(format "%s?" func) ; = code
nil ; = callbacks
;; It looks like that magic command does
;; not work in silent mode.
:silent nil)))))
(defun ein:notebook-request-help-command ()
(interactive)
(ein:notebook-request-help ein:notebook))
(defun ein:notebook-request-tool-tip-or-help-command (&optional pager)
"Show the help for the object at point using tooltip.
When the prefix argument ``C-u`` is given, open the help in the
pager buffer. You can explicitly specify the object by selecting it."
(interactive "P")
(if pager
(ein:notebook-request-help-command)
(ein:notebook-request-tool-tip-command)))
(defun ein:notebook-complete-at-point (notebook)
(let ((kernel (ein:$notebook-kernel notebook))
(callbacks
(list :complete_reply
(cons #'ein:completer-finish-completing nil))))
(ein:kernel-complete kernel
(thing-at-point 'line)
(current-column)
callbacks)))
(defun ein:notebook-complete-command ()
(interactive)
(ein:notebook-with-cell #'ein:codecell-p
(ein:kernel-if-ready (ein:$notebook-kernel ein:notebook)
(ein:notebook-complete-at-point ein:notebook))))
(defun ein:notebook-complete-dot ()
"Insert dot and request completion."
(interactive)
(insert ".")
(when (and ein:notebook
(not (ac-cursor-on-diable-face-p))
(ein:codecell-p (ein:notebook-get-current-cell))
(ein:kernel-live-p (ein:$notebook-kernel ein:notebook)))
(ein:notebook-complete-at-point ein:notebook)))
(defun ein:notebook-kernel-interrupt-command ()
"Interrupt the kernel.
This is equivalent to do ``C-c`` in the console program."
(interactive)
(ein:kernel-interrupt (ein:$notebook-kernel ein:notebook)))
(defun ein:notebook-kernel-kill-command ()
(interactive)
(when (y-or-n-p "Really kill kernel?")
(ein:kernel-kill (ein:$notebook-kernel ein:notebook))))
;; autoexec
(defun ein:notebook-toggle-autoexec ()
"Toggle auto-execution flag of the cell at point."
(interactive)
(ein:notebook-with-cell #'ein:codecell-p
(ein:cell-toggle-autoexec cell)))
(defun ein:notebook-turn-on-autoexec (cells &optional off)
"Turn on auto-execution flag of the cells in region or cell at point.
When the prefix argument is given, turn off the flag instead.
To use autoexec feature, you need to turn on auto-execution mode
in connected buffers, using the `ein:connect-toggle-autoexec'
command."
(interactive
(list (let ((cells
(ein:filter #'ein:codecell-p
(ein:notebook-get-cells-in-region-or-at-point))))
(if cells
cells
(error "Cell note found.")))
current-prefix-arg))
(mapc (lambda (c) (ein:cell-set-autoexec c (not off))) cells)
(ein:log 'info "Turn %s auto-execution flag of %s cells."
(if off "off" "on")
(length cells)))
(defun ein:notebook-execute-autoexec-cells (notebook)
"Execute cells of which auto-execution flag is on."
(interactive (if ein:notebook
(list ein:notebook)
(error "Not in notebook buffer!")))
(ein:kernel-if-ready (ein:$notebook-kernel notebook)
(mapc #'ein:cell-execute
(ein:filter #'ein:cell-autoexec-p
(ein:notebook-get-cells notebook)))))
;; misc kernel related
(defun ein:notebook-eval-string (code)
"Evaluate a code. Prompt will appear asking the code to run.
This is handy when you want to execute something quickly without
making a cell. If the code outputs something, it will go to the
shared output buffer. You can open the buffer by the command
`ein:shared-output-pop-to-buffer'."
(interactive "sIP[y]: ")
(let ((cell (ein:shared-output-get-cell))
(kernel (ein:$notebook-kernel ein:notebook))
(code (ein:trim-indent code)))
(ein:cell-execute cell kernel code))
(ein:log 'info "Code \"%s\" is sent to the kernel." code))
;; Followings are kernel related, but EIN specific
(defun ein:notebook-sync-directory (notebook)
(ein:kernel-sync-directory (ein:$notebook-kernel notebook)
(ein:notebook-buffer notebook)))
(defun ein:notebook-sync-directory-command ()
(interactive)
(when ein:notebook (ein:notebook-sync-directory ein:notebook)))
;;; Persistance and loading
(defun ein:notebook-set-notebook-name (notebook name)
"Check NAME and change the name of NOTEBOOK to it."
(if (ein:notebook-test-notebook-name name)
(setf (ein:$notebook-notebook-name notebook) name)
(ein:log 'error "%S is not a good notebook name." name)
(error "%S is not a good notebook name." name)))
(defun ein:notebook-test-notebook-name (name)
(and (stringp name)
(> (length name) 0)
(not (string-match "[\\/\\\\]" name))))
(defun ein:notebook-from-json (notebook data)
(let ((inhibit-read-only t))
(erase-buffer)
;; Enable nonsep for ewoc object (the last argument is non-nil).
;; This is for putting read-only text properties to the newlines.
(setf (ein:$notebook-ewoc notebook)
(ewoc-create 'ein:notebook-pp
(ein:propertize-read-only "\n")
nil t))
(mapc (lambda (cell-data)
(ein:cell-enter-last
(ein:notebook-cell-from-json ein:notebook cell-data)))
;; Only handle 1 worksheet for now, as in notebook.js
(plist-get (nth 0 (plist-get data :worksheets)) :cells))))
(defun ein:notebook-to-json (notebook)
"Return json-ready alist."
(let ((cells (mapcar (lambda (c)
(ein:cell-to-json
c (ein:notebook-discard-output-p notebook c)))
(ein:notebook-get-cells notebook))))
`((worksheets . [((cells . ,(apply #'vector cells)))])
(metadata . ,(ein:$notebook-metadata notebook)))))
(defun ein:notebook-save-notebook (notebook retry &optional callback cbarg)
(let ((data (ein:notebook-to-json notebook)))
(plist-put (cdr (assq 'metadata data))
:name (ein:$notebook-notebook-name notebook))
(push `(nbformat . ,(ein:$notebook-nbformat notebook)) data)
(ein:aif (ein:$notebook-nbformat-minor notebook)
;; Do not set nbformat when it is not given from server.
(push `(nbformat_minor . ,it) data))
(ein:events-trigger (ein:$notebook-events notebook)
'notebook_saving.Notebook)
(ein:query-singleton-ajax
(list 'notebook-save
(ein:$notebook-url-or-port notebook)
(ein:$notebook-notebook-id notebook))
(ein:notebook-url notebook)
:timeout ein:notebook-querty-timeout-save
:type "PUT"
:headers '(("Content-Type" . "application/json"))
:cache nil
:data (json-encode data)
:error (cons #'ein:notebook-save-notebook-error notebook)
:success (cons #'ein:notebook-save-notebook-workaround
(list notebook retry callback cbarg))
:status-code
`((204 . ,(cons (lambda (arg &rest rest)
(destructuring-bind (notebook callback cbarg)
arg
(apply #'ein:notebook-save-notebook-success
notebook rest)
(when callback
(apply callback cbarg rest))))
(list notebook callback cbarg)))))))
(defun ein:notebook-save-notebook-command ()
"Save the notebook."
(interactive)
(ein:notebook-save-notebook ein:notebook 0))
(defun* ein:notebook-save-notebook-workaround (packed &rest args
&key
status
response-status
&allow-other-keys)
;; IPython server returns 204 only when the notebook URL is
;; accessed via PUT or DELETE. As it seems Emacs failed to
;; choose PUT method every two times, let's check the response
;; here and fail when 204 is not returned.
(unless (eq response-status 204)
(destructuring-bind (notebook retry callback cbarg)
packed
(with-current-buffer (ein:notebook-buffer notebook)
(if (< retry ein:notebook-save-retry-max)
(progn
(ein:log 'info "Retry saving... Next count: %s" (1+ retry))
(ein:notebook-save-notebook notebook (1+ retry)
callback cbarg))
(ein:notebook-save-notebook-error notebook :status status)
(ein:log 'info
"Status code (=%s) is not 204 and retry exceeds limit (=%s)."
response-status ein:notebook-save-retry-max))))))
(defun ein:notebook-save-notebook-success (notebook &rest ignore)
(ein:log 'info "Notebook is saved.")
(setf (ein:$notebook-dirty notebook) nil)
(with-current-buffer (ein:notebook-buffer notebook)
(set-buffer-modified-p nil))
(ein:events-trigger (ein:$notebook-events notebook)
'notebook_saved.Notebook))
(defun* ein:notebook-save-notebook-error (notebook &key symbol-status
&allow-other-keys)
(if (eq symbol-status 'user-cancel)
(ein:log 'info "Cancel saving notebook.")
(ein:log 'info "Failed to save notebook!")
(ein:events-trigger (ein:$notebook-events notebook)
'notebook_save_failed.Notebook)))
(defun ein:notebook-rename-command (name)
"Rename current notebook and save it immediately.
NAME is any non-empty string that does not contain '/' or '\\'."
(interactive
(list (read-string "Rename notebook: "
(let ((name (ein:$notebook-notebook-name ein:notebook)))
(unless (string-match "Untitled[0-9]+" name)
name)))))
(ein:notebook-set-notebook-name ein:notebook name)
(rename-buffer (ein:notebook-get-buffer-name ein:notebook))
(ein:notebook-save-notebook
ein:notebook 0
(lambda (notebook &rest ignore)
(with-current-buffer (ein:notebook-buffer notebook)
(run-hooks 'ein:notebook-after-rename-hook)))
ein:notebook))
(defun ein:notebook-rename-to-scratch-command (name)
"Rename notebook based on `ein:scratch-notebook-name-template'
and save it immediately."
(interactive
(list (read-string "Rename notebook: "
(ein:scratch-notebook-name))))
(ein:notebook-rename-command name))
(defun ein:notebook-kill-kernel-then-close-command ()
"Kill kernel and then kill notebook buffer.
To close notebook without killing kernel, just close the buffer
as usual."
(interactive)
(when (ein:notebook-ask-before-kill-buffer)
(let ((kernel (ein:$notebook-kernel ein:notebook))
(close-notebook (lambda (notebook)
(let ((ein:notebook-kill-buffer-ask nil))
(kill-buffer (ein:notebook-buffer notebook))))))
;; If kernel is live, kill it before closing.
(if (ein:kernel-live-p kernel)
(ein:kernel-kill kernel close-notebook (list ein:notebook))
(funcall close-notebook ein:notebook)))))
;;; Generic getter
(defun ein:get-url-or-port--notebook ()
(when ein:notebook (ein:$notebook-url-or-port ein:notebook)))
(defun ein:get-notebook--notebook ()
ein:notebook)
(defun ein:get-kernel--notebook ()
(when (ein:$notebook-p ein:notebook)
(ein:$notebook-kernel ein:notebook)))
(defalias 'ein:get-cell-at-point--notebook 'ein:notebook-get-current-cell)
(defun ein:get-traceback-data--notebook ()
(ein:aand (ein:notebook-get-current-cell) (ein:cell-get-tb-data it)))
;;; Imenu
(defun ein:notebook-imenu-create-index ()
"`imenu-create-index-function' for notebook buffer."
;; As Imenu does not provide the way to represent level *and*
;; position, use #'s to do that.
(loop for cell in (ein:filter #'ein:headingcell-p
(ein:notebook-get-cells ein:notebook))
for sharps = (loop repeat (oref cell :level) collect "#")
for text = (ein:cell-get-text cell)
for name = (ein:join-str "" (append sharps (list " " text)))
collect (cons name (ein:cell-input-pos-min cell))))
(defun ein:notebook-imenu-setup ()
"Called via notebook mode hooks."
(setq imenu-create-index-function #'ein:notebook-imenu-create-index))
;;; Notebook mode
(defcustom ein:notebook-modes
'(ein:notebook-mumamo-mode ein:notebook-python-mode ein:notebook-plain-mode)
"Notebook modes to use \(in order of preference).
When the notebook is opened, mode in this value is checked one by one
and the first usable mode is used. By default, MuMaMo is used when
it is installed. If not, a simple mode derived from `python-mode' is
used.
Examples:
Avoid using MuMaMo even when it is installed::
(setq ein:notebook-modes (delq 'ein:notebook-mumamo-mode ein:notebook-modes))
Do not use `python-mode'. Use plain mode when MuMaMo is not installed::
(setq ein:notebook-modes '(ein:notebook-mumamo-mode ein:notebook-plain-mode))
"
:type '(repeat (choice (const :tag "MuMaMo" ein:notebook-mumamo-mode)
(const :tag "Only Python" ein:notebook-python-mode)
(const :tag "Plain" ein:notebook-plain-mode)))
:group 'ein)
(defun ein:notebook-choose-mode ()
"Return usable (defined) notebook mode."
;; So try to load extra modules here.
(when (require 'mumamo nil t)
(require 'ein-mumamo))
;; Return first matched mode
(loop for mode in ein:notebook-modes
if (functionp mode)
return mode))
(defvar ein:notebook-mode-map (make-sparse-keymap))
(let ((map ein:notebook-mode-map))
(define-key map "\C-c\C-c" 'ein:notebook-execute-current-cell)
(define-key map (kbd "M-RET")
'ein:notebook-execute-current-cell-and-goto-next)
(define-key map (kbd "C-c C-'") 'ein:notebook-turn-on-autoexec)
(define-key map "\C-c\C-e" 'ein:notebook-toggle-output-command)
(define-key map "\C-c\C-v" 'ein:notebook-set-collapsed-all-command)
(define-key map "\C-c\C-l" 'ein:notebook-clear-output-command)
(define-key map (kbd "C-c C-S-l") 'ein:notebook-clear-all-output-command)
(define-key map (kbd "C-c C-;") 'ein:notebook-show-in-shared-output)
(define-key map "\C-c\C-k" 'ein:notebook-kill-cell-command)
(define-key map "\C-c\M-w" 'ein:notebook-copy-cell-command)
(define-key map "\C-c\C-w" 'ein:notebook-copy-cell-command)
(define-key map "\C-c\C-y" 'ein:notebook-yank-cell-command)
(define-key map "\C-c\C-a" 'ein:notebook-insert-cell-above-command)
(define-key map "\C-c\C-b" 'ein:notebook-insert-cell-below-command)
(define-key map "\C-c\C-t" 'ein:notebook-toggle-cell-type)
(define-key map "\C-c\C-u" 'ein:notebook-change-cell-type)
(define-key map "\C-c\C-s" 'ein:notebook-split-cell-at-point)
(define-key map "\C-c\C-m" 'ein:notebook-merge-cell-command)
(define-key map "\C-c\C-n" 'ein:notebook-goto-next-input-command)
(define-key map "\C-c\C-p" 'ein:notebook-goto-prev-input-command)
(define-key map (kbd "C-<up>") 'ein:notebook-goto-prev-input-command)
(define-key map (kbd "C-<down>") 'ein:notebook-goto-next-input-command)
(define-key map (kbd "C-c <up>") 'ein:notebook-move-cell-up-command)
(define-key map (kbd "C-c <down>") 'ein:notebook-move-cell-down-command)
(define-key map (kbd "M-<up>") 'ein:notebook-move-cell-up-command)
(define-key map (kbd "M-<down>") 'ein:notebook-move-cell-down-command)
(define-key map "\C-c\C-f" 'ein:notebook-request-tool-tip-or-help-command)
(define-key map "\C-c\C-i" 'ein:notebook-complete-command)
(define-key map "\C-c\C-x" 'ein:tb-show)
(define-key map "\C-c\C-r" 'ein:notebook-restart-kernel-command)
(define-key map "\C-c\C-z" 'ein:notebook-kernel-interrupt-command)
(define-key map "\C-c\C-q" 'ein:notebook-kill-kernel-then-close-command)
(define-key map (kbd "C-:") 'ein:notebook-eval-string)
(define-key map "\C-c\C-o" 'ein:notebook-console-open)
(define-key map "\C-x\C-s" 'ein:notebook-save-notebook-command)
(define-key map "\C-x\C-w" 'ein:notebook-rename-command)
(define-key map "\M-." 'ein:pytools-jump-to-source-command)
(define-key map (kbd "C-c C-.") 'ein:pytools-jump-to-source-command)
(define-key map "\M-," 'ein:pytools-jump-back-command)
(define-key map (kbd "C-c C-,") 'ein:pytools-jump-back-command)
map)
(defun ein:notebook-mode ()
(funcall (ein:notebook-choose-mode))
(ein:complete-on-dot-install
ein:notebook-mode-map 'ein:notebook-complete-dot))
(define-derived-mode ein:notebook-plain-mode fundamental-mode "ein:notebook"
"IPython notebook mode without fancy coloring."
(font-lock-mode))
(add-hook 'ein:notebook-plain-mode-hook 'ein:notebook-imenu-setup)
(define-derived-mode ein:notebook-python-mode python-mode "ein:python"
"Use `python-mode' for whole notebook buffer.")
(add-hook 'ein:notebook-python-mode-hook 'ein:notebook-imenu-setup)
(set-keymap-parent ein:notebook-plain-mode-map ein:notebook-mode-map)
(set-keymap-parent ein:notebook-python-mode-map ein:notebook-mode-map)
(defun ein:notebook-open-in-browser (&optional print)
"Open current notebook in web browser.
When the prefix argument (``C-u``) is given, print page is opened.
Note that print page is not supported in IPython 0.12.1."
(interactive "P")
(let ((url (apply #'ein:url
(ein:$notebook-url-or-port ein:notebook)
(ein:$notebook-notebook-id ein:notebook)
(if print (list "print")))))
(message "Opening %s in browser" url)
(browse-url url)))
(defun ein:notebook-modified-p (&optional buffer)
(unless buffer (setq buffer (current-buffer)))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(and (ein:$notebook-p ein:notebook)
(or (ein:$notebook-dirty ein:notebook)
(buffer-modified-p))))))
(defcustom ein:notebook-kill-buffer-ask t
"Whether EIN should ask before killing unsaved notebook buffer."
:type '(choice (const :tag "Yes" t)
(const :tag "No" nil))
:group 'ein)
(defun ein:notebook-ask-before-kill-buffer ()
"Return `nil' to prevent killing the notebook buffer.
Called via `kill-buffer-query-functions'."
(not (and ein:notebook-kill-buffer-ask
(ein:notebook-modified-p)
(not (y-or-n-p "You have unsaved changes. Discard changes?")))))
(add-hook 'kill-buffer-query-functions 'ein:notebook-ask-before-kill-buffer)
(defun ein:notebook-force-kill-buffers (buffers)
"Kill notebook BUFFERS without confirmation."
(let ((ein:notebook-kill-buffer-ask nil))
(mapc #'kill-buffer buffers)))
(defun ein:notebook-opened-buffers ()
"Return list of opened buffers.
This function also cleans up closed buffers stores in
`ein:notebook-opened-map'."
(let (buffers)
(maphash (lambda (k b) (if (buffer-live-p b)
(push b buffers)
(remhash k ein:notebook-opened-map)))
ein:notebook-opened-map)
buffers))
(defun ein:notebook-opened-buffer-names ()
"Return list of opened notebook buffer names."
(mapcar #'buffer-name (ein:notebook-opened-buffers)))
(defun ein:notebook-ask-before-kill-emacs ()
"Return `nil' to prevent killing Emacs when unsaved notebook exists.
Called via `kill-emacs-query-functions'."
(let ((unsaved (ein:filter #'ein:notebook-modified-p
(ein:notebook-opened-buffers))))
(if (null unsaved)
t
(let ((answer
(y-or-n-p
(format "You have %s unsaved notebook(s). Discard changes?"
(length unsaved)))))
;; kill all unsaved buffers forcefully
(when answer
(ein:notebook-force-kill-buffers unsaved))
answer))))
(add-hook 'kill-emacs-query-functions 'ein:notebook-ask-before-kill-emacs)
(defun ein:notebook-kill-buffer-callback ()
"Call notebook destructor. This function is called via `kill-buffer-hook'."
(when (ein:$notebook-p ein:notebook)
(ein:notebook-del ein:notebook)))
(defun ein:notebook-setup-kill-buffer-hook ()
"Add \"notebook destructor\" to `kill-buffer-hook'."
(add-hook 'kill-buffer-hook 'ein:notebook-kill-buffer-callback))
(add-hook 'ein:notebook-plain-mode-hook 'ein:notebook-setup-kill-buffer-hook)
(defun ein:notebook-kill-all-buffers ()
"Close all opened notebooks."
(interactive)
(let* ((buffers (ein:notebook-opened-buffers))
(unsaved (ein:filter #'ein:notebook-modified-p buffers)))
(if buffers
(if (y-or-n-p
(format (concat "You have %s opened notebook(s). "
(when unsaved
(format "%s are UNSAVED. " (length unsaved)))
"Really kill all of them?")
(length buffers)))
(progn (ein:log 'info "Killing all notebook buffers...")
(ein:notebook-force-kill-buffers buffers)
(ein:log 'info "Killing all notebook buffers... Done!"))
(ein:log 'info "Canceled to kill all notebooks."))
(ein:log 'info "No opened notebooks."))))
;;; Console integration
(defcustom ein:notebook-console-security-dir ""
"Security directory setting.
Following types are valid:
string
Use this value as a path to security directory.
Handy when you have only one IPython server.
alist
An alist whose element is \"(URL-OR-PORT . DIR)\".
Key (URL-OR-PORT) can be string (URL), integer (port), or
`default' (symbol). The value of `default' is used when
other key does not much. Normally you should have this
entry.
function
Called with an argument URL-OR-PORT (integer or string).
You can have complex setting using this."
:type '(choice
(string :tag "Security directory"
"~/.config/ipython/profile_nbserver/security/")
(alist :tag "Security directory mapping"
:key-type (choice :tag "URL or PORT"
(string :tag "URL" "http://127.0.0.1:8888")
(integer :tag "PORT" 8888)
(const default))
:value-type (string :tag "Security directory"))
(function :tag "Security directory getter"
(lambda (url-or-port)
(format "~/.config/ipython/profile_%s/security/"
url-or-port))))
:group 'ein)
(defcustom ein:notebook-console-executable (executable-find "ipython")
"IPython executable used for console.
Example: ``\"/user/bin/ipython\"``.
Types same as `ein:notebook-console-security-dir' are valid."
:type '(choice
(string :tag "IPython executable" "/user/bin/ipython")
(alist :tag "IPython executable mapping"
:key-type (choice :tag "URL or PORT"
(string :tag "URL" "http://127.0.0.1:8888")
(integer :tag "PORT" 8888)
(const default))
:value-type (string :tag "IPython executable"
"/user/bin/ipython"))
(function :tag "IPython executable getter"
(lambda (url-or-port) (executable-find "ipython"))))
:group 'ein)
(defcustom ein:notebook-console-args "--profile nbserver"
"Additional argument when using console.
Example: ``\"--ssh HOSTNAME\"``.
Types same as `ein:notebook-console-security-dir' are valid."
:type '(choice
(string :tag "Arguments to IPython"
"--profile nbserver --ssh HOSTNAME")
(alist :tag "Arguments mapping"
:key-type (choice :tag "URL or PORT"
(string :tag "URL" "http://127.0.0.1:8888")
(integer :tag "PORT" 8888)
(const default))
:value-type (string :tag "Arguments to IPython"
"--profile nbserver --ssh HOSTNAME"))
(function :tag "Additional arguments getter"
(lambda (url-or-port)
(format "--ssh %s" url-or-port))))
:group 'ein)
(defun ein:notebook-console-security-dir-get (notebook)
(let ((dir (ein:choose-setting 'ein:notebook-console-security-dir
(ein:$notebook-url-or-port notebook))))
(if (equal dir "")
dir
(file-name-as-directory (expand-file-name dir)))))
(defun ein:notebook-console-executable-get (notebook)
(ein:choose-setting 'ein:notebook-console-executable
(ein:$notebook-url-or-port notebook)))
(defun ein:notebook-console-args-get (notebook)
(ein:choose-setting 'ein:notebook-console-args
(ein:$notebook-url-or-port notebook)))
;; `Fabian Gallina's python.el`_
(declare-function run-python "python")
(declare-function python-shell-parse-command "python")
(declare-function python-shell-switch-to-shell "python")
(defun ein:notebook-console-open ()
"Open IPython console.
To use this function, `ein:notebook-console-security-dir' and
`ein:notebook-console-args' must be set properly.
This function requires `Fabian Gallina's python.el`_ for now;
It should be possible to support python-mode.el. Patches are welcome!
.. _`Fabian Gallina's python.el`: https://github.com/fgallina/python.el"
;; FIXME: use %connect_info to get connection file, then I can get
;; rid of `ein:notebook-console-security-dir'.
(interactive)
(unless ein:notebook (error "Not in notebook buffer!"))
(if (fboundp 'python-shell-switch-to-shell)
(let* ((dir (ein:notebook-console-security-dir-get ein:notebook))
(kid (ein:$kernel-kernel-id
(ein:$notebook-kernel ein:notebook)))
(ipy (ein:notebook-console-executable-get ein:notebook))
(args (ein:notebook-console-args-get ein:notebook))
;; python.el settings:
(python-shell-setup-codes nil)
(python-shell-interpreter
(format "python %s console --existing %skernel-%s.json %s"
ipy dir kid args))
;; python.el makes dedicated process when
;; `buffer-file-name' has some value.
(buffer-file-name (buffer-name)))
(run-python t (python-shell-parse-command))
(python-shell-switch-to-shell))
(ein:log 'warn "python.el is not loaded!")))
(provide 'ein-notebook)
;;; ein-notebook.el ends here