mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-04 15:41:37 -05:00
2174 lines
93 KiB
EmacsLisp
2174 lines
93 KiB
EmacsLisp
;;; jupyter-repl-client.el --- A Jupyter REPL client -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C) 2018-2020 Nathaniel Nicandro
|
|
|
|
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
|
;; Created: 08 Jan 2018
|
|
|
|
;; This program 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, or (at
|
|
;; your option) any later version.
|
|
|
|
;; This program 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 GNU Emacs; see the file COPYING. If not, write to the
|
|
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|
;; Boston, MA 02111-1307, USA.
|
|
|
|
;;; Commentary:
|
|
|
|
;; A Jupyter REPL for Emacs.
|
|
;;
|
|
;; The main entry points are `jupyter-run-repl' and `jupyter-connect-repl'.
|
|
;;
|
|
;; When called interactively, `jupyter-run-repl' asks for a kernel to start
|
|
;; (based on the kernels found using `jupyter-available-kernelspecs'), connects
|
|
;; a `jupyter-repl-client' to the selected kernel, and pops up a REPL buffer.
|
|
;; The main difference of `jupyter-connect-repl' is that it will obtain the
|
|
;; kernel's connection info by asking for the JSON file containing it to start
|
|
;; connection to a kernel.
|
|
;;
|
|
;; Additionally, `jupyter-repl-associate-buffer' associates the
|
|
;; `current-buffer' with a REPL client appropriate for the buffer's
|
|
;; `major-mode'. Associating a buffer with a REPL client enables the minor
|
|
;; mode `jupyter-repl-interaction-mode'.
|
|
;;
|
|
;; `jupyter-repl-interaction-mode' adds the following keybindings for
|
|
;; interacting with a REPL client:
|
|
;;
|
|
;; C-c C-c `jupyter-eval-line-or-region'
|
|
;; C-c C-l `jupyter-eval-file'
|
|
;; M-i `jupyter-inspect-at-point'
|
|
;; C-c C-r `jupyter-repl-restart-kernel'
|
|
;; C-c C-i `jupyter-repl-interrupt-kernel'
|
|
;; C-c C-z `jupyter-repl-pop-to-buffer'
|
|
|
|
;;; Code:
|
|
|
|
(defgroup jupyter-repl nil
|
|
"A Jupyter REPL client"
|
|
:group 'jupyter)
|
|
|
|
(eval-when-compile (require 'subr-x))
|
|
(require 'jupyter-base)
|
|
(require 'jupyter-mime)
|
|
(require 'jupyter-client)
|
|
(require 'jupyter-kernelspec)
|
|
(require 'jupyter-widget-client)
|
|
(require 'jupyter-kernel-manager)
|
|
(require 'ring)
|
|
|
|
(declare-function jupyter-start-new-kernel "jupyter-kernel-process-manager")
|
|
|
|
;; TODO: Define `jupyter-kernel-manager-after-restart-hook' to update the
|
|
;; execution count after a restart. More generally, define more ways to hook
|
|
;; into differnt events of the client/kernel interaction.
|
|
|
|
;;; User variables
|
|
|
|
(defface jupyter-repl-input-prompt
|
|
'((((class color) (min-colors 88) (background light))
|
|
:foreground "darkseagreen2")
|
|
(((class color) (min-colors 88) (background dark))
|
|
:foreground "darkolivegreen"))
|
|
"Face used for the input prompt."
|
|
:group 'jupyter-repl)
|
|
|
|
(defface jupyter-repl-output-prompt
|
|
'((((class color) (min-colors 88) (background light))
|
|
:foreground "indianred3")
|
|
(((class color) (min-colors 88) (background dark))
|
|
:foreground "darkred"))
|
|
"Face used for the output prompt."
|
|
:group 'jupyter-repl)
|
|
|
|
(defface jupyter-repl-traceback
|
|
'((((class color) (min-colors 88) (background light))
|
|
:background "LightYellow2")
|
|
(((class color) (min-colors 88) (background dark))
|
|
:background "firebrick"))
|
|
"Face used for a traceback."
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-maximum-size 1024
|
|
"Maximum number of lines before the buffer is truncated."
|
|
:type 'integer
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-maximum-is-complete-timeout 2
|
|
"Maximum number of seconds to wait for an is-complete reply.
|
|
When no is-complete reply is received from the kernel within this
|
|
timeout, the built-in is-complete handler is used."
|
|
:type 'integer
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-history-maximum-length 100
|
|
"The maximum number of history elements to keep track of."
|
|
:type 'integer
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-prompt-margin-width 12
|
|
"The width of the margin which displays prompt strings."
|
|
:type 'integer
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-cell-pre-send-hook nil
|
|
"Hook run before sending the contents of an input cell to a kernel.
|
|
The hook is run with `point' at the cell code beginning position
|
|
and before the contents of the cell are extracted from the buffer
|
|
for sending to the kernel."
|
|
:type 'hook
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-cell-post-send-hook nil
|
|
"Hook run after sending the contents of an input cell to a kernel.
|
|
The hook is run with `point' at the cell code beginning
|
|
position."
|
|
:type 'hook
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-allow-RET-when-busy nil
|
|
"Allow RET to insert a newline when the kernel is busy.
|
|
Normally when the kernel is busy, pressing RET at an input cell
|
|
is disallowed. This is because, when the kernel is busy, it does
|
|
not respond to an `:is-complete-request' message and that message
|
|
is used to avoid sending incomplete code to the kernel.
|
|
|
|
If this variable is non-nil, RET is allowed to insert a newline.
|
|
In this case, pressing RET on an empty line, i.e. RET RET, will
|
|
send the code to the kernel."
|
|
:type 'boolean
|
|
:group 'jupyter-repl)
|
|
|
|
(defcustom jupyter-repl-echo-eval-p nil
|
|
"Copy evaluation input to a REPL cell if non-nil.
|
|
If non-nil, and when calling the `jupyter-eval-*' functions like
|
|
`jupyter-eval-line-or-region', copy the input into a REPL cell.
|
|
Otherwise the evaluation request is sent to the kernel without
|
|
displaying the code of the request in the REPL.
|
|
|
|
Note, output generated by requests will always be sent to the
|
|
REPL buffer whenever this variable is non-nil. When the REPL
|
|
buffer isn't visible, output will also be sent to pop-up buffers
|
|
as is done when this variable is nil."
|
|
:type 'boolean
|
|
:group 'jupyter-repl)
|
|
|
|
;;; Implementation
|
|
|
|
(defclass jupyter-repl-client (jupyter-widget-client jupyter-kernel-client)
|
|
((buffer
|
|
:type (or null buffer)
|
|
:initform nil
|
|
:documentation "The REPL buffer whose
|
|
`jupyter-current-client' is this client.")
|
|
(wait-to-clear
|
|
:type boolean
|
|
:initform nil
|
|
:documentation "Whether or not we should wait to clear the
|
|
current output of the cell. Set when the kernel sends a
|
|
`:clear-output' message.")))
|
|
|
|
(defvar-local jupyter-repl-lang-buffer nil
|
|
"A buffer with the `major-mode' set to the REPL language's `major-mode'.")
|
|
|
|
(defvar-local jupyter-repl-lang-mode nil
|
|
"The `major-mode' corresponding to the REPL's language.")
|
|
|
|
(defvar-local jupyter-repl-history nil
|
|
"The history of the current Jupyter REPL.")
|
|
|
|
(defvar-local jupyter-repl-use-builtin-is-complete nil
|
|
"Whether or not to send `:is-complete-request's to a kernel.
|
|
If a Jupyter kernel does not respond to an is_complete_request,
|
|
the buffer local value of this variable is set to t and code in a
|
|
cell is considered complete if the last line in a code cell is a
|
|
blank line, i.e. if RET is pressed twice in a row.")
|
|
|
|
(cl-generic-define-context-rewriter jupyter-repl-mode (mode &rest modes)
|
|
`(jupyter-repl-lang-mode (derived-mode ,mode ,@modes)))
|
|
|
|
;;; Macros
|
|
|
|
(defmacro jupyter-with-repl-buffer (client &rest body)
|
|
"Switch to CLIENT's REPL buffer and evaluate BODY.
|
|
`inhibit-read-only' is let bound to t while evaluating
|
|
BODY. After evaluation, if the current buffer is visible in some
|
|
window, set the window point to the value of `point' in the
|
|
buffer."
|
|
(declare (indent 1) (debug (symbolp &rest form)))
|
|
`(with-current-buffer (oref ,client buffer)
|
|
(let ((inhibit-read-only t))
|
|
(prog1 (progn ,@body)
|
|
(let ((win (get-buffer-window)))
|
|
(when win (set-window-point win (point))))))))
|
|
|
|
(defvar jupyter-repl-inhibit-continuation-prompts nil
|
|
"Non-nil when continuation prompts are suppressed.
|
|
See `jupyter-repl-insert-continuation-prompts'.")
|
|
|
|
(defmacro jupyter-repl-without-continuation-prompts (&rest body)
|
|
"Evaluate BODY without inserting continuation prompts."
|
|
(declare (debug (&rest form)))
|
|
`(let ((jupyter-repl-inhibit-continuation-prompts t))
|
|
,@body))
|
|
|
|
(defmacro jupyter-repl-append-output (client req &rest body)
|
|
"Switch to CLIENT's buffer, move to the end of REQ, and evaluate BODY.
|
|
REQ is a `jupyter-request' previously made using CLIENT, a REPL
|
|
client.
|
|
|
|
`point' is moved to the `jupyter-repl-cell-beginning-position' of
|
|
the cell *after* REQ, this position is where any newly generated
|
|
output of REQ should be inserted.
|
|
|
|
Also handles any terminal control codes in the appended output."
|
|
(declare (indent 2) (debug (symbolp &rest form)))
|
|
`(jupyter-with-repl-buffer ,client
|
|
(let ((buffer-undo-list t))
|
|
(save-excursion
|
|
(jupyter-repl-goto-cell ,req)
|
|
(jupyter-repl-next-cell)
|
|
(jupyter-with-insertion-bounds
|
|
beg end (jupyter-with-control-code-handling ,@body)
|
|
(put-text-property beg end 'read-only t)
|
|
(set-buffer-modified-p nil))))))
|
|
|
|
(defmacro jupyter-with-repl-lang-buffer (&rest body)
|
|
"Evaluate BODY in the `jupyter-repl-lang-buffer' of the `current-buffer'.
|
|
The contents of `jupyter-repl-lang-buffer' is erased before
|
|
evaluating BODY."
|
|
(declare (indent 0) (debug (&rest form)))
|
|
(let ((client (make-symbol "clientvar")))
|
|
`(let ((,client jupyter-current-client))
|
|
(with-current-buffer jupyter-repl-lang-buffer
|
|
(let ((inhibit-read-only t)
|
|
(jupyter-current-client ,client))
|
|
(erase-buffer)
|
|
,@body)))))
|
|
|
|
(defmacro jupyter-with-repl-cell (&rest body)
|
|
"Narrow to the current cell, evaluate BODY, then widen.
|
|
The cell is narrowed to the region between and including
|
|
`jupyter-repl-cell-code-beginning-position' and
|
|
`jupyter-repl-cell-code-end-position'."
|
|
(declare (indent 0) (debug (&rest form)))
|
|
`(save-excursion
|
|
(save-restriction
|
|
(narrow-to-region (jupyter-repl-cell-code-beginning-position)
|
|
(jupyter-repl-cell-code-end-position))
|
|
,@body)))
|
|
|
|
(defmacro jupyter-repl-inhibit-undo-when (cond &rest body)
|
|
"Evaluate BODY, disabling undo beforehand if COND is non-nil.
|
|
Undo is re-enabled after BODY is evaluated.
|
|
|
|
Note, any changes to `buffer-undo-list' during evaluation of BODY
|
|
will not be present when undo is re-enabled if COND is non-nil."
|
|
(declare (indent 1) (debug ([&or symbolp form] &rest form)))
|
|
(let ((new-undo-list (make-symbol "new"))
|
|
(disable-undo (make-symbol "disable")))
|
|
`(let ((,disable-undo ,cond) ,new-undo-list)
|
|
(let ((buffer-undo-list (if ,disable-undo t buffer-undo-list)))
|
|
(unwind-protect
|
|
(progn ,@body)
|
|
(unless ,disable-undo
|
|
(setq ,new-undo-list buffer-undo-list))))
|
|
(when ,new-undo-list
|
|
(setq buffer-undo-list ,new-undo-list)))))
|
|
|
|
(defmacro jupyter-repl-with-single-undo (&rest body)
|
|
"Evaluate BODY, remove all undo boundaries created during its evaluation."
|
|
(declare (indent 0) (debug (&rest form)))
|
|
(let ((handle (make-symbol "handle")))
|
|
`(let ((,handle (prepare-change-group)))
|
|
(unwind-protect
|
|
(progn
|
|
(activate-change-group ,handle)
|
|
,@body)
|
|
(undo-amalgamate-change-group ,handle)
|
|
(accept-change-group ,handle)))))
|
|
|
|
;;; Text insertion
|
|
|
|
(defun jupyter-repl-newline ()
|
|
"Insert a read-only newline into the `current-buffer'."
|
|
(insert (propertize "\n" 'read-only t)))
|
|
|
|
(cl-defmethod jupyter-insert :around (mime-or-plist
|
|
&context (major-mode jupyter-repl-mode) &rest _ignore)
|
|
"If MIME was inserted, mark the region that was inserted as read only.
|
|
Do this only when the `major-mode' is `jupyter-repl-mode'."
|
|
(if (listp mime-or-plist) (cl-call-next-method)
|
|
(jupyter-with-insertion-bounds
|
|
beg end (cl-call-next-method)
|
|
(add-text-properties beg end '(read-only t)))))
|
|
|
|
(cl-defmethod jupyter-insert ((_mime (eql :application/vnd.jupyter.widget-view+json)) data
|
|
&context ((and (require 'websocket nil t)
|
|
(require 'simple-httpd nil t)
|
|
(and jupyter-current-client
|
|
(object-of-class-p
|
|
jupyter-current-client
|
|
'jupyter-widget-client))
|
|
t)
|
|
(eql t))
|
|
&optional _metadata)
|
|
(jupyter-widgets-display-model jupyter-current-client (plist-get data :model_id)))
|
|
|
|
;;; Util
|
|
|
|
(defun jupyter-repl-completing-read-repl-buffer (&optional mode)
|
|
"Return a REPL buffer, selecting from all available ones.
|
|
MODE has the same meaning as in
|
|
`jupyter-repl-available-repl-buffers'."
|
|
(let* ((buffers (or (jupyter-repl-available-repl-buffers mode)
|
|
(error "No REPLs available")))
|
|
(buffer (completing-read "REPL buffer: " (mapcar #'buffer-name buffers) nil t)))
|
|
(when (equal buffer "")
|
|
(error "No REPL buffer selected"))
|
|
(get-buffer buffer)))
|
|
|
|
;;; Prompt
|
|
|
|
(defconst jupyter-repl-input-prompt-format "In [%d] ")
|
|
(defconst jupyter-repl-output-prompt-format "Out [%d] ")
|
|
(defconst jupyter-repl-busy-prompt "In [*] ")
|
|
|
|
(defsubst jupyter-repl--prompt-string (ov)
|
|
(nth 0 (overlay-get ov 'jupyter-prompt)))
|
|
|
|
(defsubst jupyter-repl--prompt-face (ov)
|
|
(nth 1 (overlay-get ov 'jupyter-prompt)))
|
|
|
|
(defun jupyter-repl--prompt-margin-alignment (str)
|
|
(- jupyter-repl-prompt-margin-width (length str)))
|
|
|
|
(defun jupyter-repl--prompt-display-value (str face)
|
|
"Return the margin display value for a prompt STR.
|
|
FACE is the `font-lock-face' to use for STR."
|
|
(list '(margin left-margin)
|
|
(propertize
|
|
(concat
|
|
(make-string (jupyter-repl--prompt-margin-alignment str) ?\s) str)
|
|
'fontified t
|
|
'font-lock-face face)))
|
|
|
|
(defun jupyter-repl--reset-prompt-display (ov)
|
|
(when-let* ((prompt (jupyter-repl--prompt-string ov))
|
|
(face (or (jupyter-repl--prompt-face ov)
|
|
'jupyter-repl-input-prompt))
|
|
(md (jupyter-repl--prompt-display-value prompt face)))
|
|
(overlay-put ov 'after-string (propertize " " 'display md))))
|
|
|
|
(defun jupyter-repl--reset-prompts ()
|
|
"Re-calculate all prompt strings in the buffer.
|
|
Also set the local value of `left-margin-width' to
|
|
`jupyter-repl-prompt-margin-width'."
|
|
(setq-local left-margin-width jupyter-repl-prompt-margin-width)
|
|
(dolist (ov (overlays-in (point-min) (point-max)))
|
|
(jupyter-repl--reset-prompt-display ov)))
|
|
|
|
(defun jupyter-repl--make-prompt (str face props)
|
|
"Make a prompt overlay for the character before POS.
|
|
STR is used as the prompt string and FACE is its
|
|
`font-lock-face'. Add PROPS as text properties to the character."
|
|
(when (< (jupyter-repl--prompt-margin-alignment str) 0)
|
|
(setq-local jupyter-repl-prompt-margin-width
|
|
(+ jupyter-repl-prompt-margin-width
|
|
(abs (jupyter-repl--prompt-margin-alignment str))))
|
|
(jupyter-repl--reset-prompts))
|
|
(let ((ov (make-overlay (1- (point)) (point) nil t)))
|
|
(overlay-put ov 'jupyter-prompt (list str face))
|
|
(overlay-put ov 'evaporate t)
|
|
(jupyter-repl--reset-prompt-display ov)
|
|
(add-text-properties (overlay-start ov) (overlay-end ov) props)
|
|
(overlay-recenter (point))))
|
|
|
|
(defun jupyter-repl-insert-prompt (&optional type)
|
|
"Insert a REPL prompt according to TYPE.
|
|
TYPE can either be `in', `out', or `continuation'. A nil TYPE is
|
|
interpreted as `in'."
|
|
(setq type (or type 'in))
|
|
(unless (memq type '(in out continuation))
|
|
(error "Prompt type can only be (`in', `out', or `continuation')"))
|
|
(jupyter-repl-without-continuation-prompts
|
|
(let ((inhibit-read-only t))
|
|
;; The newline that `jupyter-repl--make-prompt' will overlay.
|
|
(insert (propertize "\n" 'read-only (not (eq type 'continuation))))
|
|
(cond
|
|
((eq type 'in)
|
|
(let ((count (oref jupyter-current-client execution-count)))
|
|
(jupyter-repl--make-prompt
|
|
(format jupyter-repl-input-prompt-format count)
|
|
'jupyter-repl-input-prompt
|
|
`(jupyter-cell (beginning ,count))))
|
|
;; Prevent prompt overlay from inheriting text properties of code at the
|
|
;; beginning of a cell.
|
|
;;
|
|
;; rear-nonsticky is to prevent code inserted after this character to
|
|
;; inherit any of its text properties.
|
|
;;
|
|
;; front-sticky is to prevent `point' from being trapped between the
|
|
;; newline of the prompt overlay and this invisible character.
|
|
(insert (propertize " "
|
|
'read-only t 'invisible t
|
|
'rear-nonsticky t 'front-sticky t))
|
|
;; The insertion of a new prompt starts a new cell, don't consider the
|
|
;; buffer modified anymore. This is also an indicator for when undo's
|
|
;; can be made in the buffer.
|
|
(set-buffer-modified-p nil)
|
|
(setq buffer-undo-list '((t . 0))))
|
|
((eq type 'out)
|
|
;; Output is normally inserted by first going to the end of the output
|
|
;; for the request. The end of the ouput for a request is at the
|
|
;; beginning of the next cell after the request which is why we get the
|
|
;; cell count of the previous cell
|
|
(let ((count (jupyter-repl-previous-cell-count)))
|
|
(jupyter-repl--make-prompt
|
|
(format jupyter-repl-output-prompt-format count)
|
|
'jupyter-repl-output-prompt
|
|
`(jupyter-cell (out ,count))))
|
|
;; See the note above about the invisible character for input prompts
|
|
(insert (propertize " " 'read-only t 'invisible t 'front-sticky t)))
|
|
((eq type 'continuation)
|
|
(jupyter-repl--make-prompt
|
|
;; This needs to be two characters wide for some
|
|
;; reason, otherwise the continuation prompts will
|
|
;; be missing one character.
|
|
" " 'jupyter-repl-input-prompt
|
|
`(read-only nil rear-nonsticky t)))))))
|
|
|
|
(defun jupyter-repl-prompt-string ()
|
|
"Return the prompt string of the current input cell."
|
|
(jupyter-repl--prompt-string
|
|
(car (overlays-at (jupyter-repl-cell-beginning-position)))))
|
|
|
|
(defun jupyter-repl-cell-reset-prompt ()
|
|
"Reset the current prompt back to its default."
|
|
(jupyter-repl-cell-update-prompt
|
|
(format jupyter-repl-input-prompt-format (jupyter-repl-cell-count))))
|
|
|
|
(defun jupyter-repl-cell-update-prompt (str &optional face)
|
|
"Update the current cell's input prompt.
|
|
STR is the replacement prompt string. If FACE is non-nil, it
|
|
should be a face that the prompt will use and defaults to
|
|
`jupyter-repl-input-prompt'."
|
|
(when-let* ((ov (car (overlays-at (jupyter-repl-cell-beginning-position)))))
|
|
(overlay-put ov 'jupyter-prompt (list str face))
|
|
(jupyter-repl--reset-prompt-display ov)))
|
|
|
|
(defun jupyter-repl-cell-mark-busy ()
|
|
"Mark the current cell as busy."
|
|
(when (equal (jupyter-repl-prompt-string)
|
|
(format jupyter-repl-input-prompt-format
|
|
(jupyter-repl-cell-count)))
|
|
(jupyter-repl-cell-update-prompt jupyter-repl-busy-prompt)))
|
|
|
|
(defun jupyter-repl-cell-unmark-busy ()
|
|
"Un-mark the current cell as busy."
|
|
(when (equal (jupyter-repl-prompt-string) jupyter-repl-busy-prompt)
|
|
(jupyter-repl-cell-update-prompt
|
|
(format jupyter-repl-input-prompt-format
|
|
(jupyter-repl-cell-count)))))
|
|
|
|
(defun jupyter-repl-update-cell-count (n)
|
|
"Set the current cell count to N."
|
|
(when (or (jupyter-repl-cell-beginning-p)
|
|
(zerop (save-excursion (jupyter-repl-previous-cell))))
|
|
(setf (nth 1 (get-text-property
|
|
(jupyter-repl-cell-beginning-position)
|
|
'jupyter-cell))
|
|
n)
|
|
(when (string-match-p "In \\[[0-9]+\\]" (jupyter-repl-prompt-string))
|
|
(jupyter-repl-cell-reset-prompt))))
|
|
|
|
(defun jupyter-repl-cell-count ()
|
|
"Return the cell count of the cell at `point'."
|
|
(let ((pos (if (jupyter-repl-cell-beginning-p) (point)
|
|
(save-excursion
|
|
(jupyter-repl-previous-cell)
|
|
(point)))))
|
|
(nth 1 (get-text-property pos 'jupyter-cell))))
|
|
|
|
(defun jupyter-repl-previous-cell-count ()
|
|
"Return the cell count of the previous cell before `point'."
|
|
(save-excursion
|
|
(jupyter-repl-previous-cell)
|
|
(jupyter-repl-cell-count)))
|
|
|
|
(defun jupyter-repl-cell-request ()
|
|
"Return the `jupyter-request' of the current cell."
|
|
(get-text-property (jupyter-repl-cell-beginning-position) 'jupyter-request))
|
|
|
|
;;; Cell motions
|
|
|
|
(defun jupyter-repl-cell-beginning-position ()
|
|
"Return the cell beginning position of the current cell.
|
|
If `point' is already at the beginning of the current cell,
|
|
return `point'.
|
|
|
|
If the end of a cell is found before the beginning of one, i.e.
|
|
when `point' is somewhere inside the output of a cell, raise an
|
|
error.
|
|
|
|
If the beginning of the buffer is found before the beginning of a
|
|
cell, raise a `beginning-of-buffer' error."
|
|
(let ((pos (point)))
|
|
(while (not (jupyter-repl-cell-beginning-p pos))
|
|
(setq pos (previous-single-property-change pos 'jupyter-cell))
|
|
(if pos (when (jupyter-repl-cell-end-p pos)
|
|
(error "Found end of previous cell"))
|
|
(if (jupyter-repl-cell-beginning-p (point-min))
|
|
(setq pos (point-min))
|
|
(signal 'beginning-of-buffer nil))))
|
|
pos))
|
|
|
|
(defun jupyter-repl-cell-end-position ()
|
|
"Return the cell ending position of the current cell.
|
|
This is similar to `jupyter-repl-cell-beginning-position' except
|
|
the position at the end of the current cell is returned and an
|
|
error is raised if the beginning of a cell is found before an
|
|
end.
|
|
|
|
Note: If the current cell is the last cell in the buffer,
|
|
`point-max' is considered the end of the cell."
|
|
(let ((pos (point)))
|
|
(catch 'unfinalized
|
|
(while (not (jupyter-repl-cell-end-p pos))
|
|
(setq pos (next-single-property-change pos 'jupyter-cell))
|
|
(if pos (when (jupyter-repl-cell-beginning-p pos)
|
|
(error "Found beginning of next cell"))
|
|
;; Any unfinalized cell must be at the end of the buffer.
|
|
(throw 'unfinalized (point-max))))
|
|
pos)))
|
|
|
|
(defun jupyter-repl-cell-code-beginning-position ()
|
|
"Return the beginning of the current cell's code.
|
|
The code beginning position is
|
|
|
|
`jupyter-repl-cell-beginning-position' + 2
|
|
|
|
There is an extra invisible character after the prompt."
|
|
(+ (jupyter-repl-cell-beginning-position) 2))
|
|
|
|
(defun jupyter-repl-cell-code-end-position ()
|
|
"Return the end of the current cell's code.
|
|
In the case of the last cell in the REPL buffer, i.e. an
|
|
unfinalized cell, the code ending position is `point-max'."
|
|
(jupyter-repl-cell-end-position))
|
|
|
|
(defun jupyter-repl-next-cell (&optional N)
|
|
"Go to the beginning of the next cell.
|
|
Move N times where N defaults to 1. Return the count of cells
|
|
left to move."
|
|
(or N (setq N 1))
|
|
(catch 'done
|
|
(while (> N 0)
|
|
(let ((pos (next-single-property-change (point) 'jupyter-cell)))
|
|
(while (and pos (not (jupyter-repl-cell-beginning-p pos)))
|
|
(setq pos (next-single-property-change pos 'jupyter-cell)))
|
|
(unless (when pos (goto-char pos) (setq N (1- N)))
|
|
(goto-char (point-max))
|
|
(throw 'done t)))))
|
|
N)
|
|
|
|
(defun jupyter-repl-previous-cell (&optional N)
|
|
"Go to the beginning of the previous cell.
|
|
Move N times where N defaults to 1. Return the count of cells
|
|
left to move.
|
|
|
|
Note, if `point' is not at the beginning of the current cell, the
|
|
first move is to the beginning of the current cell."
|
|
(or N (setq N 1))
|
|
(catch 'done
|
|
(let ((starting-pos (point)))
|
|
(while (> N 0)
|
|
(let ((pos (previous-single-property-change (point) 'jupyter-cell)))
|
|
(while (and pos (not (jupyter-repl-cell-beginning-p pos)))
|
|
(setq pos (previous-single-property-change pos 'jupyter-cell)))
|
|
(unless (when pos (goto-char pos) (setq N (1- N)))
|
|
(goto-char (point-min))
|
|
;; Handle edge case when the first cell is at the beginning of the
|
|
;; buffer. This happens, for example, when erasing the buffer.
|
|
(when (and (/= (point) starting-pos)
|
|
(jupyter-repl-cell-beginning-p (point)))
|
|
(setq N (1- N)))
|
|
(throw 'done t))))))
|
|
N)
|
|
|
|
(defun jupyter-repl-goto-cell (req)
|
|
"Go to the cell beginning position of REQ.
|
|
REQ should be a `jupyter-request' that corresponds to one of the
|
|
`jupyter-send-execute-request's created by a cell in the
|
|
`current-buffer'. Note that the `current-buffer' is assumed to be
|
|
a Jupyter REPL buffer."
|
|
(goto-char (point-max))
|
|
(unless (catch 'done
|
|
(while (= (jupyter-repl-previous-cell) 0)
|
|
(when (eq (jupyter-repl-cell-request) req)
|
|
(throw 'done t))))
|
|
(error "Cell for request not found")))
|
|
|
|
(defun jupyter-repl-forward-cell (&optional arg)
|
|
"Go to the code beginning of the cell after the current one.
|
|
ARG is the number of cells to move and defaults to 1."
|
|
(interactive "^p")
|
|
(or arg (setq arg 1))
|
|
(jupyter-repl-next-cell arg)
|
|
(goto-char (jupyter-repl-cell-code-beginning-position)))
|
|
|
|
(defun jupyter-repl-backward-cell (&optional arg)
|
|
"Go to the code beginning of the cell before the current one.
|
|
ARG is the number of cells to move and defaults to 1."
|
|
(interactive "^p")
|
|
(or arg (setq arg 1))
|
|
;; Ignore the case when `point' is in the output of a cell, in this case
|
|
;; `jupyter-repl-previous-cell' will go to the previous cell.
|
|
(ignore-errors (goto-char (jupyter-repl-cell-beginning-position)))
|
|
(jupyter-repl-previous-cell arg)
|
|
(goto-char (jupyter-repl-cell-code-beginning-position)))
|
|
|
|
(defun jupyter-repl-map-cells (beg end input output)
|
|
"Call INPUT or OUTPUT on the corresponding cells between BEG and END.
|
|
For every input or output cell between BEG and END, call INPUT or
|
|
OUTPUT, respectively, with the buffer narrowed to the cell.
|
|
INPUT and OUTPUT are functions of no arguments.
|
|
|
|
Note the narrowed regions may not be full input/output cells if
|
|
BEG and END are within an input/output cell."
|
|
(declare (indent 2))
|
|
(save-excursion
|
|
(save-restriction
|
|
(let (next)
|
|
(while (/= beg end)
|
|
(widen)
|
|
(cond
|
|
((eq (get-text-property beg 'field) 'cell-code)
|
|
(setq next (min end (field-end beg t)))
|
|
(narrow-to-region beg next)
|
|
(funcall input))
|
|
(t
|
|
(setq next (or (text-property-any
|
|
beg end 'field 'cell-code)
|
|
end))
|
|
(narrow-to-region beg next)
|
|
(funcall output)))
|
|
(setq beg next))))))
|
|
|
|
;;; Predicates
|
|
|
|
(defun jupyter-repl-cell-beginning-p (&optional pos)
|
|
"Is POS the beginning of a cell?
|
|
POS defaults to `point'."
|
|
(setq pos (or pos (point)))
|
|
(eq (nth 0 (get-text-property pos 'jupyter-cell)) 'beginning))
|
|
|
|
(defun jupyter-repl-cell-end-p (&optional pos)
|
|
"Is POS the end of a cell?
|
|
POS defaults to `point'."
|
|
(setq pos (or pos (point)))
|
|
(or (= pos (point-max))
|
|
(eq (nth 0 (get-text-property pos 'jupyter-cell)) 'end)))
|
|
|
|
(defun jupyter-repl-multiline-p (text)
|
|
"Is TEXT a multi-line string?"
|
|
(string-match-p "\n" text))
|
|
|
|
(defun jupyter-repl-cell-line-p ()
|
|
"Is the current line a cell input line?"
|
|
(let ((pos (point)))
|
|
(ignore-errors
|
|
(save-excursion
|
|
(unless (= pos (jupyter-repl-cell-beginning-position))
|
|
(jupyter-repl-previous-cell))
|
|
(<= (jupyter-repl-cell-code-beginning-position)
|
|
pos
|
|
(jupyter-repl-cell-code-end-position))))))
|
|
|
|
(defun jupyter-repl-cell-finalized-p ()
|
|
"Has the current cell been finalized?"
|
|
(or (not (jupyter-repl-cell-line-p))
|
|
(/= (jupyter-repl-cell-end-position) (point-max))))
|
|
|
|
(defun jupyter-repl-connected-p ()
|
|
"Is the `jupyter-current-client' connected to its kernel?"
|
|
(when jupyter-current-client
|
|
(jupyter-kernel-alive-p jupyter-current-client)))
|
|
|
|
;;; Modifying cell code, truncating REPL buffer
|
|
|
|
(defun jupyter-repl-cell-output ()
|
|
"Return the output of the current cell."
|
|
(unless (jupyter-repl-cell-finalized-p)
|
|
(error "Cell not finalized"))
|
|
(let ((beg (jupyter-repl-cell-end-position))
|
|
(end (save-excursion
|
|
(jupyter-repl-next-cell)
|
|
(jupyter-repl-cell-beginning-position))))
|
|
(buffer-substring beg end)))
|
|
|
|
(defun jupyter-repl-cell-code ()
|
|
"Return the code of the current cell."
|
|
(buffer-substring
|
|
(jupyter-repl-cell-code-beginning-position)
|
|
(jupyter-repl-cell-code-end-position)))
|
|
|
|
(defun jupyter-repl-cell-code-position ()
|
|
"Return the relative position of `point' with respect to the cell code."
|
|
(unless (jupyter-repl-cell-line-p)
|
|
(error "Not in code of cell"))
|
|
(1+ (- (point) (jupyter-repl-cell-code-beginning-position))))
|
|
|
|
(defun jupyter-repl-finalize-cell (req)
|
|
"Finalize the current input cell.
|
|
REQ is the `jupyter-request' to associate with the current cell.
|
|
Place `point' at `point-max'."
|
|
(goto-char (point-max))
|
|
(let ((beg (jupyter-repl-cell-beginning-position))
|
|
(count (jupyter-repl-cell-count)))
|
|
(jupyter-repl-newline)
|
|
(put-text-property (1- (point)) (point) 'jupyter-cell `(end ,count))
|
|
(put-text-property beg (1+ beg) 'jupyter-request req)
|
|
;; Remove this property so that text can't be inserted at the start of the
|
|
;; cell or after any continuation prompts. See
|
|
;; `jupyter-repl-insert-prompt'.
|
|
(remove-text-properties beg (point) '(rear-nonsticky))
|
|
(add-text-properties beg (point) '(read-only t))
|
|
;; reset the undo list so that a completed cell doesn't get undone.
|
|
(setq buffer-undo-list '((t . 0)))))
|
|
|
|
(defun jupyter-repl-replace-cell-code (new-code)
|
|
"Replace the current cell code with NEW-CODE.
|
|
If NEW-CODE is a buffer use `replace-buffer-contents' to replace
|
|
the cell code. Otherwise NEW-CODE should be a string, the current
|
|
cell code will be erased and NEW-CODE inserted in its place."
|
|
(if (bufferp new-code)
|
|
(jupyter-with-repl-cell
|
|
(jupyter-repl-with-single-undo
|
|
;; Need to create a single undo step here because
|
|
;; `replace-buffer-contents' adds in unwanted undo boundaries.
|
|
;;
|
|
;; Tests failing on Appveyor due to `replace-buffer-contents' not
|
|
;; supplying the right arguments to `after-change-functions' so call
|
|
;; the change functions manually. Seen on Emacs 26.1.
|
|
;;
|
|
;; For reference see https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278
|
|
(let ((inhibit-modification-hooks t)
|
|
(beg (point-min))
|
|
(end (point-max))
|
|
(new-len (with-current-buffer new-code
|
|
(- (point-max) (point-min)))))
|
|
(run-hook-with-args
|
|
'before-change-functions beg end)
|
|
(replace-buffer-contents new-code)
|
|
(run-hook-with-args
|
|
'after-change-functions
|
|
beg (+ beg new-len) (- end beg)))))
|
|
(goto-char (jupyter-repl-cell-code-beginning-position))
|
|
(delete-region (point) (jupyter-repl-cell-code-end-position))
|
|
(insert-and-inherit new-code)))
|
|
|
|
(defun jupyter-repl-truncate-buffer ()
|
|
"Truncate the `current-buffer' based on `jupyter-repl-maximum-size'.
|
|
The `current-buffer' is assumed to be a Jupyter REPL buffer. If
|
|
the `current-buffer' is larger than `jupyter-repl-maximum-size'
|
|
lines, truncate it to something less than
|
|
`jupyter-repl-maximum-size' lines."
|
|
(save-excursion
|
|
(when (= (forward-line (- jupyter-repl-maximum-size)) 0)
|
|
(jupyter-repl-next-cell)
|
|
(delete-region (point-min) (point)))))
|
|
|
|
(defun jupyter-repl-clear-cells ()
|
|
"Clear the input and output cells of the current buffer."
|
|
(interactive)
|
|
(jupyter-repl-without-continuation-prompts
|
|
(let ((inhibit-read-only t))
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(when (get-text-property (point) 'jupyter-banner)
|
|
(goto-char (next-single-property-change (point) 'jupyter-banner)))
|
|
(delete-region (point) (point-max))
|
|
(jupyter-repl-insert-prompt 'in))))
|
|
(goto-char (point-max)))
|
|
|
|
;;; Handlers
|
|
|
|
(defun jupyter-repl-history-add (code)
|
|
"Add CODE as the newest element in the REPL history."
|
|
;; Ensure the newest element is actually the newest element and not the most
|
|
;; recently navigated history element.
|
|
(while (not (eq (ring-ref jupyter-repl-history -1) 'jupyter-repl-history))
|
|
(ring-insert jupyter-repl-history (ring-remove jupyter-repl-history)))
|
|
;; Remove the second to last element when the ring is full to preserve the
|
|
;; sentinel.
|
|
(when (eq (ring-length jupyter-repl-history)
|
|
(ring-size jupyter-repl-history))
|
|
(ring-remove jupyter-repl-history -2))
|
|
(ring-remove+insert+extend jupyter-repl-history code))
|
|
|
|
(cl-defmethod jupyter-send-execute-request ((client jupyter-repl-client)
|
|
&key code
|
|
(silent nil)
|
|
(store-history t)
|
|
(user-expressions nil)
|
|
(allow-stdin t)
|
|
(stop-on-error nil))
|
|
(if code (cl-call-next-method)
|
|
(jupyter-with-repl-buffer client
|
|
(jupyter-repl-truncate-buffer)
|
|
(save-excursion
|
|
(goto-char (jupyter-repl-cell-code-beginning-position))
|
|
(run-hooks 'jupyter-repl-cell-pre-send-hook))
|
|
(setq code (string-trim (jupyter-repl-cell-code)))
|
|
;; Handle empty code cells as just an update of the prompt number
|
|
(if (= (length code) 0)
|
|
(setq silent t)
|
|
;; Needed by the prompt insertion below
|
|
(oset client execution-count (1+ (oref client execution-count)))
|
|
(jupyter-repl-history-add code))
|
|
(let ((req (cl-call-next-method
|
|
client :code code :silent silent :store-history store-history
|
|
:user-expressions user-expressions :allow-stdin allow-stdin
|
|
:stop-on-error stop-on-error)))
|
|
(jupyter-repl-without-continuation-prompts
|
|
(jupyter-repl-cell-mark-busy)
|
|
(jupyter-repl-finalize-cell req)
|
|
(jupyter-repl-insert-prompt 'in))
|
|
(save-excursion
|
|
(jupyter-repl-backward-cell)
|
|
(run-hooks 'jupyter-repl-cell-post-send-hook))
|
|
req))))
|
|
|
|
(cl-defmethod jupyter-handle-payload ((_source (eql set_next_input)) pl
|
|
&context (major-mode jupyter-repl-mode))
|
|
(goto-char (point-max))
|
|
(jupyter-repl-previous-cell)
|
|
(jupyter-repl-replace-cell-code (plist-get pl :text)))
|
|
|
|
(cl-defmethod jupyter-handle-execute-reply ((client jupyter-repl-client) _req msg)
|
|
(jupyter-with-repl-buffer client
|
|
(jupyter-with-message-content msg (payload)
|
|
(when payload
|
|
(jupyter-handle-payload payload)))))
|
|
|
|
(cl-defmethod jupyter-handle-execute-result ((client jupyter-repl-client) req msg)
|
|
;; Only handle our results
|
|
(when req
|
|
(jupyter-repl-append-output client req
|
|
(jupyter-repl-insert-prompt 'out)
|
|
(jupyter-with-message-content msg (data metadata)
|
|
(jupyter-insert data metadata)))))
|
|
|
|
(cl-defmethod jupyter-handle-display-data ((client jupyter-repl-client) req msg)
|
|
(let ((clear (prog1 (oref client wait-to-clear)
|
|
(oset client wait-to-clear nil)))
|
|
(req (if (eq (jupyter-message-parent-type msg) :comm-msg)
|
|
;; For comm messages which produce a `:display-data' message,
|
|
;; the request is assumed to be the most recently completed
|
|
;; one.
|
|
(jupyter-with-repl-buffer client
|
|
(save-excursion
|
|
(goto-char (point-max))
|
|
(jupyter-repl-previous-cell 2)
|
|
(jupyter-repl-cell-request)))
|
|
req)))
|
|
(jupyter-repl-append-output client req
|
|
(jupyter-with-message-content msg (data metadata transient)
|
|
(cl-destructuring-bind (&key display_id &allow-other-keys)
|
|
transient
|
|
(if display_id
|
|
(jupyter-insert display_id data metadata)
|
|
(let ((inhibit-redisplay (not debug-on-error)))
|
|
(when clear
|
|
(jupyter-repl-clear-last-cell-output client)
|
|
;; Prevent slight flickering of prompt margin and text, this is
|
|
;; needed in addition to `inhibit-redisplay'. It also seems that
|
|
;; it can be placed anywhere within this let and it will prevent
|
|
;; flickering.
|
|
(sit-for 0.1 t))
|
|
(jupyter-insert data metadata))))))))
|
|
|
|
(cl-defmethod jupyter-handle-update-display-data ((client jupyter-repl-client) _req msg)
|
|
(jupyter-with-message-content msg (data metadata transient)
|
|
(cl-destructuring-bind (&key display_id &allow-other-keys)
|
|
transient
|
|
(unless display_id
|
|
(error "No display ID in `:update-display-data' message"))
|
|
(jupyter-with-repl-buffer client
|
|
(jupyter-update-display display_id data metadata)))))
|
|
|
|
(defun jupyter-repl-clear-last-cell-output (client)
|
|
"In CLIENT's REPL buffer, clear the output of the last completed cell."
|
|
(jupyter-with-repl-buffer client
|
|
(goto-char (point-max))
|
|
(jupyter-repl-previous-cell 2)
|
|
(delete-region (1+ (jupyter-repl-cell-end-position))
|
|
(progn
|
|
(jupyter-repl-next-cell)
|
|
(point)))))
|
|
|
|
(cl-defmethod jupyter-handle-clear-output ((client jupyter-repl-client) _req msg)
|
|
(unless (oset client wait-to-clear
|
|
(jupyter-with-message-content msg (wait)
|
|
(eq wait t)))
|
|
(cond
|
|
((eq (jupyter-message-parent-type msg) :comm-msg)
|
|
(with-current-buffer (jupyter-get-buffer-create "output")
|
|
(erase-buffer)))
|
|
(t
|
|
(jupyter-repl-clear-last-cell-output client)))))
|
|
|
|
(cl-defmethod jupyter-handle-status ((client jupyter-repl-client) req msg)
|
|
(when (equal "idle"
|
|
(jupyter-with-message-content msg (execution_state)
|
|
execution_state))
|
|
(jupyter-with-repl-buffer client
|
|
(save-excursion
|
|
(when (ignore-errors
|
|
(progn (jupyter-repl-goto-cell req) t))
|
|
(jupyter-repl-cell-unmark-busy))
|
|
;; Update the cell count and reset the prompt
|
|
(goto-char (point-max))
|
|
(jupyter-repl-update-cell-count (oref client execution-count)))))
|
|
(force-mode-line-update))
|
|
|
|
(defun jupyter-repl-display-other-output (client stream text)
|
|
"Display output not originating from CLIENT.
|
|
STREAM is the name of a stream which will be used to select the
|
|
buffer to display TEXT."
|
|
(let* ((bname (buffer-name (oref client buffer)))
|
|
(stream-buffer
|
|
(concat (substring bname 0 (1- (length bname))) "-" stream "*")))
|
|
;; FIXME: Reset this on the next request
|
|
(jupyter-with-display-buffer stream-buffer nil
|
|
(let ((pos (point)))
|
|
(jupyter-insert-ansi-coded-text text)
|
|
(fill-region pos (point)))
|
|
(jupyter-display-current-buffer-reuse-window))))
|
|
|
|
(cl-defmethod jupyter-handle-stream ((client jupyter-repl-client) req msg)
|
|
(jupyter-with-message-content msg (name text)
|
|
(if (null req)
|
|
(jupyter-repl-display-other-output client name text)
|
|
(cond
|
|
((eq (jupyter-message-parent-type
|
|
(jupyter-request-last-message req))
|
|
:comm-msg)
|
|
(jupyter-with-display-buffer "output" req
|
|
(jupyter-insert-ansi-coded-text text)
|
|
(jupyter-display-current-buffer-reuse-window)))
|
|
(t
|
|
(jupyter-repl-append-output client req
|
|
(jupyter-insert-ansi-coded-text text)))))))
|
|
|
|
(cl-defmethod jupyter-handle-error ((client jupyter-repl-client) req msg)
|
|
(when req
|
|
(jupyter-with-message-content msg (traceback)
|
|
(cond
|
|
((eq (jupyter-message-parent-type msg) :comm-msg)
|
|
(jupyter-display-traceback traceback))
|
|
(t
|
|
(jupyter-repl-append-output client req
|
|
(jupyter-with-insertion-bounds
|
|
beg end (jupyter-insert-ansi-coded-text
|
|
(concat (mapconcat #'identity traceback "\n") "\n"))
|
|
(font-lock-prepend-text-property
|
|
beg end 'font-lock-face 'jupyter-repl-traceback))))))))
|
|
|
|
(defun jupyter-repl-history--rotate (n)
|
|
"Rotate the REPL history ring N times.
|
|
The direction of rotation is determined by the sign of N. For N
|
|
positive rotate to newer history elements, for N negative rotate
|
|
to older elements.
|
|
|
|
Return nil if the sentinel value is found before completing the
|
|
required number of rotations, otherwise return the element
|
|
rotated to, i.e. the one at index 0."
|
|
(let (ifun cidx ridx)
|
|
(if (> n 0)
|
|
(setq ifun 'ring-insert cidx -1 ridx -1)
|
|
(setq ifun 'ring-insert-at-beginning cidx 1 ridx 0))
|
|
(cl-loop
|
|
repeat (abs n)
|
|
;; Check that the next index to rotate to is not the sentinel
|
|
if (eq (ring-ref jupyter-repl-history cidx) 'jupyter-repl-history)
|
|
return nil else do
|
|
;; if it isn't, remove an element at RIDX and insert it using IFUN back
|
|
;; into the history ring, thereby rotating the history
|
|
(funcall ifun jupyter-repl-history
|
|
(ring-remove jupyter-repl-history ridx))
|
|
;; after N successful rotations, return the element rotated to
|
|
finally return (let ((el (ring-ref jupyter-repl-history 0)))
|
|
(unless (eq el 'jupyter-repl-history)
|
|
el)))))
|
|
|
|
(defun jupyter-repl-history--match-input (regexp arg)
|
|
"Return the index of the ARGth REGEXP match.
|
|
Or nil, on failure. If ARG is positive, search backward from the most
|
|
recent history element. If negative, search forward through items
|
|
previously visited during this input session. If ARG is zero, do
|
|
nothing."
|
|
;; Adapted from `comint-previous-matching-input-string-position'
|
|
(let* ((direction (if (> arg 0) +1 -1))
|
|
(i (if (= direction -1) 0 -1)) ; adjust for initial increment
|
|
(failed (zerop arg))
|
|
;;
|
|
code)
|
|
;; Search ARG times
|
|
(while (not (or failed (zerop arg)))
|
|
(while (not (or (setq code (ring-ref jupyter-repl-history
|
|
(cl-incf i direction))
|
|
failed (eq 'jupyter-repl-history code))
|
|
(string-match-p regexp code))))
|
|
(setq arg (- arg direction)))
|
|
(unless failed i)))
|
|
|
|
(defun jupyter-repl-history-previous-matching (&optional n)
|
|
"Search input history for the input pending before point.
|
|
On success, replace the current input with the matching code element
|
|
while preserving and returning point. Ding on failure. If N is negative,
|
|
find the Nth next match; if positive, the Nth previous. If N is zero or
|
|
nil, pretend it's one."
|
|
;; Adapted from: `comint-previous-matching-input-from-input' and friends
|
|
(interactive "p")
|
|
(when (or (null n) (zerop n)) (setq n 1))
|
|
(let ((opoint (point))
|
|
(code (jupyter-repl-cell-code))
|
|
(is-prev (> n 0))
|
|
(input-string (buffer-substring
|
|
(jupyter-repl-cell-code-beginning-position) (point)))
|
|
found)
|
|
;; Look past an initial duplicate
|
|
(when (equal code (ring-ref jupyter-repl-history (if is-prev 0 -1)))
|
|
(cl-incf n (if is-prev 1 -1)))
|
|
(if (not (setq found (jupyter-repl-history--match-input
|
|
(concat "^" (regexp-quote input-string)) n)))
|
|
(user-error "No %s matching input" (if is-prev "earlier" "later"))
|
|
(setq code (ring-ref jupyter-repl-history found))
|
|
(jupyter-repl-history--rotate (- found))
|
|
(jupyter-repl-replace-cell-code code)
|
|
(goto-char opoint))))
|
|
|
|
(defun jupyter-repl-history-next-matching (&optional n)
|
|
"Search existing history session for an element matching input.
|
|
Only consider the text before point. If N is negative, find the Nth
|
|
previous match, otherwise the Nth next. If N is zero or nil, make it
|
|
one. \"Existing history session\" means those history elements already
|
|
visited while forming the current input."
|
|
(interactive "p")
|
|
(when (or (null n) (zerop n)) (setq n 1))
|
|
(jupyter-repl-history-previous-matching (- n)))
|
|
|
|
(defun jupyter-repl-history-next (&optional n)
|
|
"Go to the next history element.
|
|
Navigate through the REPL history to the next (newer) history
|
|
element and insert it as the last code cell. For N positive move
|
|
forward in history that many times. If N is negative, move to
|
|
older history elements."
|
|
(interactive "p")
|
|
(or n (setq n 1))
|
|
(if (< n 0) (jupyter-repl-history-previous (- n))
|
|
(goto-char (point-max))
|
|
(let ((code (jupyter-repl-history--rotate n)))
|
|
(if (and (null code) (equal (jupyter-repl-cell-code) ""))
|
|
(error "End of history")
|
|
(if (null code)
|
|
;; When we have reached the last history element in the forward
|
|
;; direction and the cell code is not empty, make it empty.
|
|
(jupyter-repl-replace-cell-code "")
|
|
(jupyter-repl-replace-cell-code code))))))
|
|
|
|
(defun jupyter-repl-history-previous (&optional n)
|
|
"Go to the previous history element.
|
|
Similar to `jupyter-repl-history-next' but for older history
|
|
elements. If N is negative in this case, move to newer history
|
|
elements."
|
|
(interactive "p")
|
|
(or n (setq n 1))
|
|
(if (< n 0) (jupyter-repl-history-next (- n))
|
|
(goto-char (point-max))
|
|
(unless (equal (jupyter-repl-cell-code)
|
|
(ring-ref jupyter-repl-history 0))
|
|
(setq n (1- n)))
|
|
(let ((code (jupyter-repl-history--rotate (- n))))
|
|
(if (null code)
|
|
(error "Beginning of history")
|
|
(jupyter-repl-replace-cell-code code)))))
|
|
|
|
(cl-defmethod jupyter-handle-history-reply ((client jupyter-repl-client) _req msg)
|
|
(jupyter-with-repl-buffer client
|
|
(cl-loop for elem across (jupyter-with-message-content msg (history) history)
|
|
for input-output = (aref elem 2)
|
|
do (ring-remove+insert+extend jupyter-repl-history input-output))))
|
|
|
|
(cl-defmethod jupyter-handle-is-complete-reply ((client jupyter-repl-client) _req msg)
|
|
(jupyter-with-repl-buffer client
|
|
(jupyter-with-message-content msg (status indent)
|
|
(pcase status
|
|
("complete"
|
|
(jupyter-send-execute-request client))
|
|
("incomplete"
|
|
(insert "\n")
|
|
(if (= (length indent) 0) (jupyter-repl-indent-line)
|
|
(insert indent)))
|
|
("invalid"
|
|
;; Force an execute to produce a traceback
|
|
(jupyter-send-execute-request client))
|
|
("unknown"
|
|
;; Let the kernel decide if the code is complete
|
|
(jupyter-send-execute-request client))))))
|
|
|
|
(defun jupyter-repl--insert-banner-and-prompt (client)
|
|
(jupyter-with-repl-buffer client
|
|
(goto-char (point-max))
|
|
(unless (jupyter-repl-cell-finalized-p)
|
|
(jupyter-repl-finalize-cell nil))
|
|
(jupyter-repl-newline)
|
|
(jupyter-repl-insert-banner
|
|
(plist-get (jupyter-kernel-info client) :banner))
|
|
(jupyter-repl-insert-prompt 'in)
|
|
(jupyter-repl-update-cell-count 1)))
|
|
|
|
(cl-defmethod jupyter-handle-shutdown-reply ((client jupyter-repl-client) _req msg)
|
|
(jupyter-with-repl-buffer client
|
|
(jupyter-repl-without-continuation-prompts
|
|
(goto-char (point-max))
|
|
(let ((shutdown-handled-p (jupyter-repl-cell-finalized-p)))
|
|
(unless (jupyter-repl-cell-finalized-p)
|
|
(jupyter-repl-finalize-cell nil))
|
|
;; Only run the following once. The Python kernel sends a shutdown-reply
|
|
;; on both the shell and iopub which is mainly the reason why this is
|
|
;; needed.
|
|
(unless shutdown-handled-p
|
|
(jupyter-repl-newline)
|
|
(jupyter-repl-newline)
|
|
(jupyter-with-message-content msg (restart)
|
|
;; TODO: Add a slot mentioning that the kernel is shutdown so that we can
|
|
;; block sending requests or delay until it has restarted.
|
|
(insert (propertize (concat "kernel " (if restart "restart" "shutdown"))
|
|
'read-only t 'font-lock-face 'warning))
|
|
(jupyter-repl-newline)
|
|
(when restart
|
|
(jupyter-repl--insert-banner-and-prompt client))))))))
|
|
|
|
(defun jupyter-repl-ret (&optional force)
|
|
"Send the current cell code to the kernel.
|
|
If `point' is before the last cell in the REPL buffer move to
|
|
`point-max', i.e. move to the last cell. Otherwise if `point' is
|
|
at some position within the last cell, either insert a newline or
|
|
ask the kernel to execute the cell code depending on the kernel's
|
|
response to an `:is-complete-request'.
|
|
|
|
If a prefix argument is given, FORCE the kernel to execute the
|
|
current cell code without sending an `:is-complete-request'. See
|
|
`jupyter-repl-use-builtin-is-complete' for yet another way to
|
|
execute the current cell."
|
|
(interactive "P")
|
|
(condition-case nil
|
|
(let ((cell-beginning (save-excursion
|
|
(goto-char (point-max))
|
|
(jupyter-repl-cell-beginning-position))))
|
|
(if (< (point) cell-beginning)
|
|
(goto-char (point-max))
|
|
(unless (jupyter-repl-connected-p)
|
|
(error "Kernel not alive"))
|
|
;; NOTE: kernels allow execution requests to queue up, but we prevent
|
|
;; sending a request when the kernel is busy because of the
|
|
;; is-complete request. Some kernels don't respond to this request
|
|
;; when the kernel is busy.
|
|
(when (and (jupyter-kernel-busy-p jupyter-current-client)
|
|
(not jupyter-repl-allow-RET-when-busy))
|
|
(error "Kernel busy"))
|
|
(cond
|
|
(force (jupyter-send-execute-request jupyter-current-client))
|
|
((or jupyter-repl-use-builtin-is-complete
|
|
(and jupyter-repl-allow-RET-when-busy
|
|
(jupyter-kernel-busy-p jupyter-current-client)))
|
|
(goto-char (point-max))
|
|
(let ((complete-p (equal (buffer-substring-no-properties
|
|
(line-beginning-position) (point))
|
|
"")))
|
|
(jupyter-handle-is-complete-reply
|
|
jupyter-current-client
|
|
nil (if complete-p "complete" "incomplete") "")))
|
|
(t
|
|
(let ((res (jupyter-wait-until-received :is-complete-reply
|
|
(let ((jupyter-inhibit-handlers '(not :is-complete-reply)))
|
|
(jupyter-send-is-complete-request
|
|
jupyter-current-client
|
|
:code (jupyter-repl-cell-code)))
|
|
jupyter-repl-maximum-is-complete-timeout)))
|
|
(unless res
|
|
(message "\
|
|
Kernel did not respond to is-complete-request, using built-in is-complete.
|
|
Reset `jupyter-repl-use-builtin-is-complete' to nil if this is only temporary.")
|
|
(setq jupyter-repl-use-builtin-is-complete t)
|
|
(jupyter-repl-ret force)))))))
|
|
(beginning-of-buffer
|
|
;; No cells in the current buffer, just insert one
|
|
(jupyter-repl-insert-prompt 'in))))
|
|
|
|
(cl-defgeneric jupyter-indent-line ()
|
|
(call-interactively #'indent-for-tab-command))
|
|
|
|
(defun jupyter-repl-indent-line ()
|
|
"Indent the line according to the language of the REPL."
|
|
(when-let* ((pos (and (jupyter-repl-cell-line-p)
|
|
(jupyter-repl-cell-code-position)))
|
|
(code (jupyter-repl-cell-code))
|
|
(replacement
|
|
(jupyter-with-repl-lang-buffer
|
|
(insert code)
|
|
(goto-char pos)
|
|
(let ((tick (buffer-chars-modified-tick)))
|
|
(jupyter-indent-line)
|
|
(unless (eq tick (buffer-chars-modified-tick))
|
|
(setq pos (point))
|
|
(current-buffer))))))
|
|
;; Don't modify the buffer when unnecessary, this allows
|
|
;; `company-indent-or-complete-common' to work.
|
|
(when replacement
|
|
(jupyter-repl-replace-cell-code replacement)
|
|
(goto-char (+ pos (jupyter-repl-cell-code-beginning-position))))))
|
|
|
|
;;; Buffer change functions
|
|
|
|
(defun jupyter-repl-yank-handle-field-property (val beg end)
|
|
"If VAL is not cell-code, remove the field property between BEG and END.
|
|
Yanking text into a REPL cell normally removes the field
|
|
property (see `yank-excluded-properties') but this property is
|
|
added in `jupyter-repl-after-change' to mark text in an input cell.
|
|
|
|
The problem is that the after change functions run *before*
|
|
`insert-for-yank' removes the field property. This function is
|
|
added to `yank-handled-properties' to prevent the removal of
|
|
field when the associated text is part of the input to a REPL
|
|
cell."
|
|
;; Assume that text with a field value of cell-code is due to
|
|
;; `jupyter-repl-mark-as-cell-code'.
|
|
(unless (eq val 'cell-code)
|
|
(remove-text-properties beg end '(field))))
|
|
|
|
(defun jupyter-repl-insert-continuation-prompts (bound)
|
|
"Insert continuation prompts if needed, stopping at BOUND.
|
|
Return the new BOUND since inserting continuation prompts may add
|
|
more characters than were initially in the buffer.
|
|
|
|
If `jupyter-repl-inhibit-continuation-prompts' is non-nil return
|
|
BOUND without inserting any continuation prompts."
|
|
(if jupyter-repl-inhibit-continuation-prompts
|
|
bound
|
|
(setq bound (set-marker (make-marker) bound))
|
|
(set-marker-insertion-type bound t)
|
|
;; Don't record these changes as it adds unnecessary undo information which
|
|
;; interferes with undo.
|
|
(let ((buffer-undo-list t))
|
|
(while (and (< (point) bound)
|
|
(search-forward "\n" bound 'noerror))
|
|
;; Delete the newline that is re-added by prompt insertion
|
|
;; FIXME: Why not just overlay the newline?
|
|
(delete-char -1)
|
|
(jupyter-repl-insert-prompt 'continuation)))
|
|
(prog1 (marker-position bound)
|
|
(set-marker bound nil))))
|
|
|
|
(defun jupyter-repl-mark-as-cell-code (beg end)
|
|
"Add the field property to text between (BEG . END) if within a code cell."
|
|
;; Handle field boundary at the front of the cell code
|
|
(when (< beg end)
|
|
(when (= beg (jupyter-repl-cell-code-beginning-position))
|
|
(put-text-property beg (1+ beg) 'front-sticky t))
|
|
(when (text-property-not-all beg end 'field 'cell-code)
|
|
(font-lock-fillin-text-property beg end 'field 'cell-code))))
|
|
|
|
(defun jupyter-repl-do-after-change (beg end len)
|
|
"Call `jupyter-repl-after-change' when the current cell code is changed.
|
|
`jupyter-repl-after-change' is only called when BEG is a position
|
|
on a `jupyter-repl-cell-line-p'. BEG, END, and LEN have the same
|
|
meaning as in `after-change-functions'."
|
|
(when (eq major-mode 'jupyter-repl-mode)
|
|
(with-demoted-errors "Jupyter error after buffer change: %S"
|
|
(save-restriction
|
|
;; Take into account insertions that may have the buffer narrowed since
|
|
;; functions like `jupyter-repl-cell-code-beginning-position' need to
|
|
;; look at parts of the buffer not necessarily within the narrowed
|
|
;; region. See #38.
|
|
;;
|
|
;; TODO: Does it really make sense to widen the buffer? To get around
|
|
;; this, how can functions like
|
|
;; `jupyter-repl-cell-code-beginning-position' and
|
|
;; `jupyter-repl-cell-line-p' only rely on the `field' text property?
|
|
(widen)
|
|
(when (save-excursion
|
|
(goto-char beg)
|
|
(jupyter-repl-cell-line-p))
|
|
(cond
|
|
((= len 0)
|
|
(jupyter-repl-after-change 'insert beg end))
|
|
((and (= beg end) (not (zerop len)))
|
|
(jupyter-repl-after-change 'delete beg len))
|
|
;; Text property changes
|
|
((= (- end beg) len)
|
|
;; Revert changes made by `insert-for-yank'. See #14.
|
|
(when (and (= len 1)
|
|
(get-text-property beg 'rear-nonsticky)
|
|
(= end (jupyter-repl-cell-end-position)))
|
|
(remove-text-properties beg end '(rear-nonsticky))))
|
|
;; Post change inserted text in the region
|
|
((> (- end beg) len)
|
|
(jupyter-repl-after-change 'insert beg end))
|
|
;; Post change deleted text
|
|
(t
|
|
;; FIXME: This is probably wrong.
|
|
(jupyter-repl-after-change 'delete beg (- len (- end beg))))))))))
|
|
|
|
(cl-defgeneric jupyter-repl-after-change (_type _beg _end-or-len)
|
|
"Called from the `after-change-functions' of a REPL buffer.
|
|
Modify the text just inserted or deleted. TYPE is either insert
|
|
or delete to signify if the change was due to insertion or
|
|
deletion of text. BEG is always the beginning of the insertion or
|
|
deletion. END-OR-LEN is the end of the insertion when TYPE is
|
|
insert and is the length of the deleted text when TYPE is delete.
|
|
|
|
The `after-change-functions' of the REPL buffer are only called
|
|
for changes to input cells and not for output generated by the
|
|
kernel.
|
|
|
|
Note, the overriding method should call `cl-call-next-method'.
|
|
|
|
Also note, any buffer narrowing will be temporarily removed when
|
|
this method is called."
|
|
nil)
|
|
|
|
(cl-defmethod jupyter-repl-after-change ((_type (eql insert)) beg end)
|
|
(goto-char beg)
|
|
;; Avoid doing anything on self insertion
|
|
(unless (and (= (point) (1- end))
|
|
(not (eq (char-after) ?\n)))
|
|
(setq end (jupyter-repl-insert-continuation-prompts end)))
|
|
(jupyter-repl-mark-as-cell-code beg end)
|
|
(goto-char end))
|
|
|
|
(cl-defmethod jupyter-repl-after-change ((_type (eql delete)) beg _len)
|
|
;; Ensure that the `front-sticky' property at the beginning of cell code is
|
|
;; added after deleting text at the beginning of a cell.
|
|
(jupyter-repl-mark-as-cell-code beg (min (point-max) (+ beg 1))))
|
|
|
|
(defun jupyter-repl--deactivate-interaction-buffers ()
|
|
(cl-loop
|
|
with client = jupyter-current-client
|
|
for buffer in (buffer-list)
|
|
do (with-current-buffer buffer
|
|
(when (and jupyter-repl-interaction-mode
|
|
(eq jupyter-current-client client))
|
|
(jupyter-repl-interaction-mode -1)))))
|
|
|
|
(defun jupyter-repl-kill-buffer-query-function ()
|
|
"Ask before killing a Jupyter REPL buffer.
|
|
If the REPL buffer is killed, stop the client's channels. When
|
|
the client is connected to a managed kernel, ask to also shutdown
|
|
the kernel.
|
|
|
|
In addition to the above, call the function
|
|
`jupyter-repl-interaction-mode' in all buffers associated with
|
|
the REPL to disable that mode in those buffers. See
|
|
`jupyter-repl-associate-buffer'."
|
|
(when (eq major-mode 'jupyter-repl-mode)
|
|
(if (not (jupyter-channels-running-p jupyter-current-client)) t
|
|
(when (y-or-n-p (format "Jupyter REPL (%s) still connected. Kill it? "
|
|
(buffer-name (current-buffer))))
|
|
(prog1 t
|
|
(jupyter-stop-channels jupyter-current-client)
|
|
(when (and (jupyter-client-has-manager-p)
|
|
(yes-or-no-p (format "Shutdown the client's kernel? ")))
|
|
(jupyter-shutdown-kernel (oref jupyter-current-client manager))))))))
|
|
|
|
(defun jupyter-repl-error-before-major-mode-change ()
|
|
"Error if attempting to change the `major-mode' in a REPL buffer."
|
|
(when (eq major-mode 'jupyter-repl-mode)
|
|
(error "Attempting to change `major-mode' in the REPL buffer!")))
|
|
|
|
(defun jupyter-repl-preserve-window-margins (&optional window)
|
|
"Ensure that the margins of a REPL window are present.
|
|
If WINDOW is showing a REPL buffer and the margins are not set to
|
|
`jupyter-repl-prompt-margin-width', set them to the proper
|
|
value."
|
|
;; NOTE: Sometimes the margins will disappear after the window configuration
|
|
;; changes which is why `window-configuration-change-hook' is not used.
|
|
(when (and (eq major-mode 'jupyter-repl-mode)
|
|
(let ((margins (window-margins window)))
|
|
(not (and (consp margins)
|
|
(car margins)
|
|
(= (car margins) jupyter-repl-prompt-margin-width)))))
|
|
(set-window-buffer window (current-buffer))))
|
|
|
|
;;; Completion
|
|
|
|
(cl-defmethod jupyter-code-context ((_type (eql completion))
|
|
&context (major-mode jupyter-repl-mode))
|
|
(list (jupyter-repl-cell-code)
|
|
(1- (jupyter-repl-cell-code-position))))
|
|
|
|
(cl-defmethod jupyter-completion-prefix (&context (major-mode jupyter-repl-mode))
|
|
(and (not (get-text-property (point) 'read-only))
|
|
(cl-call-next-method)))
|
|
|
|
;;; Evaluation
|
|
|
|
(cl-defmethod jupyter-read-expression (&context ((and
|
|
jupyter-current-client
|
|
(object-of-class-p
|
|
jupyter-current-client
|
|
'jupyter-repl-client)
|
|
t)
|
|
(eql t)))
|
|
(jupyter-with-repl-buffer jupyter-current-client
|
|
(jupyter-set jupyter-current-client 'jupyter-eval-expression-history
|
|
(delq 'jupyter-repl-history
|
|
(ring-elements jupyter-repl-history)))
|
|
(let ((ex (cl-call-next-method)))
|
|
(prog1 ex
|
|
(jupyter-repl-history-add ex)))))
|
|
|
|
(cl-defmethod jupyter-eval-string (str &context (jupyter-current-client jupyter-repl-client)
|
|
&optional beg end)
|
|
(let (req
|
|
cell-previous-code)
|
|
(jupyter-with-repl-buffer jupyter-current-client
|
|
(when jupyter-repl-echo-eval-p
|
|
(goto-char (point-max))
|
|
(setq cell-previous-code (jupyter-repl-cell-code))
|
|
(jupyter-repl-replace-cell-code str)
|
|
(setq str nil))
|
|
(let* ((jupyter-inhibit-handlers
|
|
;; When copying the input to the REPL we need the handlers to
|
|
;; update the REPL state
|
|
(unless jupyter-repl-echo-eval-p
|
|
'(not :input-request))))
|
|
(setq req (jupyter-send-execute-request jupyter-current-client
|
|
:code str
|
|
:store-history jupyter-repl-echo-eval-p))
|
|
(if jupyter-repl-echo-eval-p
|
|
(jupyter-repl-replace-cell-code cell-previous-code))))
|
|
;; Add callbacks to display evaluation output in pop-up buffers either when
|
|
;; we aren't copying the input to a REPL cell or, if we are, when the REPL
|
|
;; buffer isn't visible.
|
|
;;
|
|
;; Make sure we do this in the original buffer where STR originated from
|
|
;; when BEG and END are non-nil.
|
|
(prog1 req
|
|
(unless (and jupyter-repl-echo-eval-p
|
|
(get-buffer-window (oref jupyter-current-client buffer) 'visible))
|
|
(jupyter-eval-add-callbacks req beg end)))))
|
|
|
|
;;; Kernel management
|
|
|
|
(defun jupyter-repl-interrupt-kernel ()
|
|
"Interrupt the kernel if possible.
|
|
A kernel can be interrupted if it was started using a kernel
|
|
manager. See `jupyter-start-new-kernel'."
|
|
(interactive)
|
|
(if (not (jupyter-client-has-manager-p))
|
|
(user-error "Can only interrupt managed kernels")
|
|
(message "Interrupting kernel")
|
|
(jupyter-interrupt-kernel
|
|
(oref jupyter-current-client manager))))
|
|
|
|
(defun jupyter-repl-restart-kernel (&optional shutdown client)
|
|
"Restart the kernel `jupyter-current-client' is connected to.
|
|
With a prefix argument, SHUTDOWN the kernel completely instead.
|
|
If CLIENT is non-nil, it should by a REPL client to use instead
|
|
of `jupyter-current-client'.
|
|
|
|
If CLIENT is nil, `jupyter-current-client' will be restarted
|
|
instead. If `jupyter-current-client' is nil or is not a REPL
|
|
client, prompt for a REPL client to restart. Otherwise restart
|
|
the kernel `jupyter-current-client' is connected to."
|
|
(interactive
|
|
(list current-prefix-arg nil))
|
|
(unless client
|
|
(setq client
|
|
(or jupyter-current-client
|
|
;; Also allow this command to be called from an Org mode buffer by
|
|
;; selecting a client based on the REPL buffer.
|
|
(buffer-local-value
|
|
'jupyter-current-client
|
|
(jupyter-repl-completing-read-repl-buffer)))))
|
|
(cl-check-type client jupyter-repl-client)
|
|
(unless shutdown
|
|
;; This may have been set to t due to a non-responsive kernel so make sure
|
|
;; that we try again when restarting.
|
|
(jupyter-with-repl-buffer client
|
|
(setq jupyter-repl-use-builtin-is-complete nil)))
|
|
(jupyter-hb-pause client)
|
|
(let ((manager (oref client manager)))
|
|
(cond
|
|
((and (not shutdown)
|
|
(jupyter-client-has-manager-p client)
|
|
(not (jupyter-kernel-alive-p manager)))
|
|
(message "Starting dead kernel...")
|
|
(jupyter-repl--insert-banner-and-prompt client))
|
|
(t
|
|
(message "%s kernel..." (if shutdown "Shutting down"
|
|
"Restarting"))
|
|
(if manager (jupyter-shutdown-kernel manager (not shutdown))
|
|
;; NOTE: It's not possible to restart a kernel without a kernel manager
|
|
;; unless the kernel is able to restart on its own.
|
|
(when (and (null (jupyter-wait-until-received :shutdown-reply
|
|
(let ((jupyter-inhibit-handlers '(not :shutdown-reply)))
|
|
(jupyter-send-shutdown-request client
|
|
:restart (not shutdown)))))
|
|
(not shutdown))
|
|
;; Handle the case of a restart that does not send a shutdown-reply
|
|
;;
|
|
;; TODO: Clean up the logic of when to insert a new prompt. We insert
|
|
;; a new prompt before we know if the kernel is ready, but this should
|
|
;; be done after we know if the kernel is ready or not, e.g. on the
|
|
;; next status: starting message. Generalize the stuff in
|
|
;; `jupyter-start-new-kernel' that handles the status: starting message
|
|
;; so its easier to hook into that message.
|
|
(message "Kernel did not send shutdown-reply")
|
|
(jupyter-repl--insert-banner-and-prompt client)))))
|
|
(unless shutdown
|
|
(jupyter-hb-unpause client))))
|
|
|
|
(defun jupyter-repl-display-kernel-buffer ()
|
|
"Display the kernel processes stdout."
|
|
(interactive)
|
|
(if (jupyter-client-has-manager-p)
|
|
(let ((manager (oref jupyter-current-client manager)))
|
|
(if (jupyter-kernel-alive-p manager)
|
|
(if (and (slot-boundp manager 'kernel)
|
|
(processp (oref manager kernel)))
|
|
(display-buffer (process-buffer (oref manager kernel)))
|
|
(error "Manager needs a kernel slot"))
|
|
(error "Kernel is not alive")))
|
|
(user-error "Kernel not a subprocess")))
|
|
|
|
;;; Isearch
|
|
;; Adapted from isearch in `comint', see `comint-history-isearch-search' for
|
|
;; details
|
|
|
|
(defun jupyter-repl-isearch-setup ()
|
|
"Setup Isearch to search through the input history."
|
|
(setq-local isearch-search-fun-function
|
|
#'jupyter-repl-history-isearch-search)
|
|
(setq-local isearch-wrap-function
|
|
#'jupyter-repl-history-isearch-wrap)
|
|
(setq-local isearch-push-state-function
|
|
#'jupyter-repl-history-isearch-push-state))
|
|
|
|
;; Adapted from `comint-history-isearch-search'
|
|
(defun jupyter-repl-history-isearch-search ()
|
|
"Return a search function to search through a REPL's input history."
|
|
(lambda (string bound noerror)
|
|
(let ((search-fun (isearch-search-fun-default)) found)
|
|
(setq isearch-lazy-highlight-start-limit
|
|
(jupyter-repl-cell-beginning-position))
|
|
(or
|
|
;; 1. First try searching in the initial cell text
|
|
(funcall search-fun string
|
|
(or bound
|
|
(unless isearch-forward
|
|
(jupyter-repl-cell-code-beginning-position)))
|
|
noerror)
|
|
;; 2. If the above search fails, start putting next/prev history
|
|
;; elements in the cell successively, and search the string in them. Do
|
|
;; this only when bound is nil (i.e. not while lazy-highlighting search
|
|
;; strings in the current cell text).
|
|
(unless bound
|
|
(condition-case err
|
|
(progn
|
|
(while (not found)
|
|
(cond (isearch-forward
|
|
;; `jupyter-repl-history-next' clears the cell if the
|
|
;; last element is the sentinel, prevent that.
|
|
(if (eq (ring-ref jupyter-repl-history -1)
|
|
'jupyter-repl-history)
|
|
(error "End of history")
|
|
(jupyter-repl-history-next))
|
|
(goto-char (jupyter-repl-cell-code-beginning-position)))
|
|
(t
|
|
(jupyter-repl-history-previous)
|
|
(goto-char (point-max))))
|
|
;; After putting the next/prev history element, search the
|
|
;; string in them again, until an error is thrown at the
|
|
;; beginning/end of history.
|
|
(setq found (funcall search-fun string
|
|
(unless isearch-forward
|
|
(jupyter-repl-cell-code-beginning-position))
|
|
'noerror)))
|
|
;; Return point of the new search result
|
|
(point))
|
|
(error
|
|
(unless noerror
|
|
(signal (car err) (cdr err))))))))))
|
|
|
|
(defun jupyter-repl-history-isearch-wrap ()
|
|
"Wrap the input history search when search fails.
|
|
Go to the oldest history element for a forward search or to the
|
|
newest history element for a backward search."
|
|
(jupyter-repl-history--rotate
|
|
(* (if isearch-forward -1 1)
|
|
(ring-length jupyter-repl-history)))
|
|
(jupyter-repl-replace-cell-code (ring-ref jupyter-repl-history 0))
|
|
(goto-char (if isearch-forward (jupyter-repl-cell-code-beginning-position)
|
|
(point-max))))
|
|
|
|
(defun jupyter-repl-history-isearch-push-state ()
|
|
"Save a function restoring the state of input history search.
|
|
Save the element at index 0 in `jupyter-repl-history'. When
|
|
restoring the state, the `jupyter-repl-history' ring is rotated,
|
|
in the appropriate direction, to the saved element."
|
|
(let ((code (jupyter-repl-cell-code)))
|
|
(cond
|
|
((equal code (ring-ref jupyter-repl-history 0))
|
|
(let ((elem (ring-ref jupyter-repl-history 0)))
|
|
(lambda (_cmd)
|
|
(when isearch-wrapped
|
|
(jupyter-repl-history--rotate
|
|
(* (if isearch-forward 1 -1)
|
|
(ring-length jupyter-repl-history))))
|
|
(let ((dir (if isearch-forward -1 1)))
|
|
(while (not (eq (ring-ref jupyter-repl-history 0) elem))
|
|
(jupyter-repl-history--rotate dir)))
|
|
(jupyter-repl-replace-cell-code (ring-ref jupyter-repl-history 0)))))
|
|
(t
|
|
(let ((elem code))
|
|
(lambda (_cmd)
|
|
(jupyter-repl-replace-cell-code elem)))))))
|
|
|
|
;;; `jupyter-repl-mode'
|
|
|
|
(defun jupyter-repl-scratch-buffer ()
|
|
"Switch to a scratch buffer connected to the current REPL in another window.
|
|
Return the buffer switched to."
|
|
(interactive)
|
|
(if (jupyter-repl-connected-p)
|
|
(let* ((client jupyter-current-client)
|
|
(name (replace-regexp-in-string "^*jupyter-repl"
|
|
"*jupyter-scratch"
|
|
(buffer-name) (oref client buffer))))
|
|
(unless (get-buffer name)
|
|
(with-current-buffer (get-buffer-create name)
|
|
(funcall (jupyter-kernel-language-mode client))
|
|
(jupyter-repl-associate-buffer client)
|
|
(insert
|
|
(substitute-command-keys
|
|
"Jupyter scratch buffer for evaluation.
|
|
\\[jupyter-eval-line-or-region] to evaluate the line or region.
|
|
\\[jupyter-eval-buffer] to evaluate the whole buffer.
|
|
\\[jupyter-repl-pop-to-buffer] to show the REPL buffer."))
|
|
(comment-region (point-min) (point-max))
|
|
(insert "\n\n")))
|
|
(switch-to-buffer-other-window name))
|
|
(error "Not in a valid REPL buffer")))
|
|
|
|
(defvar jupyter-repl-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map "q" nil)
|
|
(define-key map [remap backward-sentence] #'jupyter-repl-backward-cell)
|
|
(define-key map [remap forward-sentence] #'jupyter-repl-forward-cell)
|
|
(define-key map (kbd "RET") #'jupyter-repl-ret)
|
|
(define-key map (kbd "M-n") #'jupyter-repl-history-next)
|
|
(define-key map (kbd "M-p") #'jupyter-repl-history-previous)
|
|
(define-key map (kbd "C-c C-o") #'jupyter-repl-clear-cells)
|
|
(define-key map (kbd "C-c M-r") #'jupyter-repl-history-previous-matching)
|
|
(define-key map (kbd "C-c M-s") #'jupyter-repl-history-next-matching)
|
|
map))
|
|
|
|
(put 'jupyter-repl-mode 'mode-class 'special)
|
|
(define-derived-mode jupyter-repl-mode fundamental-mode
|
|
"Jupyter-REPL"
|
|
"A Jupyter REPL major mode."
|
|
(cl-check-type jupyter-current-client jupyter-repl-client)
|
|
;; This is a better setting for rendering language banners.
|
|
(setq-local show-trailing-whitespace nil)
|
|
;; This is a better setting when rendering HTML tables
|
|
(setq-local truncate-lines t)
|
|
(setq-local indent-line-function #'jupyter-repl-indent-line)
|
|
(setq-local left-margin-width jupyter-repl-prompt-margin-width)
|
|
(setq-local yank-handled-properties
|
|
(append '((field . jupyter-repl-yank-handle-field-property))
|
|
yank-handled-properties))
|
|
(setq-local yank-excluded-properties (remq 'field yank-excluded-properties))
|
|
;; Initialize a buffer using the major-mode correponding to the kernel's
|
|
;; language. This will be used for indentation and to capture font lock
|
|
;; properties.
|
|
(let* ((info (jupyter-kernel-info jupyter-current-client))
|
|
(language (plist-get (plist-get info :language_info) :name)))
|
|
(jupyter-load-language-support jupyter-current-client)
|
|
(cl-destructuring-bind (mode syntax)
|
|
(jupyter-kernel-language-mode-properties jupyter-current-client)
|
|
(setq jupyter-repl-lang-mode mode)
|
|
(setq jupyter-repl-lang-buffer
|
|
(get-buffer-create
|
|
(format " *jupyter-repl-lang-%s*" language)))
|
|
(set-syntax-table syntax)
|
|
(jupyter-with-repl-lang-buffer
|
|
(unless (eq major-mode mode)
|
|
(funcall mode))))
|
|
;; Get history from kernel
|
|
(setq jupyter-repl-history
|
|
(make-ring (1+ jupyter-repl-history-maximum-length)))
|
|
;; The sentinel value keeps track of the newest/oldest elements of the
|
|
;; history since next/previous navigation is implemented by rotations on the
|
|
;; ring.
|
|
(ring-insert jupyter-repl-history 'jupyter-repl-history)
|
|
(let ((jupyter-inhibit-handlers '(:status)))
|
|
(jupyter-send-history-request jupyter-current-client
|
|
:n jupyter-repl-history-maximum-length :raw nil :unique t))
|
|
(erase-buffer)
|
|
;; Add local hooks
|
|
(add-hook 'kill-buffer-query-functions #'jupyter-repl-kill-buffer-query-function nil t)
|
|
(add-hook 'kill-buffer-hook #'jupyter-repl--deactivate-interaction-buffers nil t)
|
|
(add-hook 'after-change-functions 'jupyter-repl-do-after-change nil t)
|
|
(add-hook 'pre-redisplay-functions 'jupyter-repl-preserve-window-margins nil t)
|
|
;; Initialize the REPL
|
|
(jupyter-repl-initialize-fontification)
|
|
(jupyter-repl-isearch-setup)
|
|
(jupyter-repl-sync-execution-state)
|
|
(jupyter-repl-interaction-mode)
|
|
;; Do this last so that it runs before any other `change-major-mode-hook's.
|
|
(add-hook 'change-major-mode-hook #'jupyter-repl-error-before-major-mode-change nil t)))
|
|
|
|
(cl-defgeneric jupyter-repl-after-init ()
|
|
"Hook function called whenever `jupyter-repl-mode' is enabled/disabled.
|
|
You may override this function for a particular language using a
|
|
jupyter-lang &context specializer. For example, to do something
|
|
when the language of the REPL is python the method signature
|
|
would look like
|
|
|
|
(cl-defmethod jupyter-repl-after-init (&context (jupyter-lang python)))"
|
|
nil)
|
|
|
|
(add-hook 'jupyter-repl-mode-hook 'jupyter-repl-after-init)
|
|
|
|
(defun jupyter-repl-font-lock-fontify-region (fontify-fun beg end &optional verbose)
|
|
"Use FONTIFY-FUN to fontify input cells between BEG and END.
|
|
VERBOSE has the same meaning as in
|
|
`font-lock-fontify-region-function'."
|
|
(jupyter-repl-map-cells beg end
|
|
;; Ensure that the buffer is narrowed to the actual cell code before calling
|
|
;; the REPL language's `major-mode' specific fontification functions since
|
|
;; those functions don't know anything about input cells or output cells and
|
|
;; may traverse cell boundaries.
|
|
;;
|
|
;; It is OK that we do not update BEG and END using the return value of this
|
|
;; function as long as the default value of
|
|
;; `font-lock-extend-region-functions' is used since an input cell always
|
|
;; starts at the beginning of a line and ends at the end of a line and does
|
|
;; not use the font-lock-multiline property (2018-12-20).
|
|
(lambda () (funcall fontify-fun (point-min) (point-max) verbose))
|
|
;; Unfontify the region mainly to remove the font-lock-multiline property in
|
|
;; the output, e.g. added by markdown. These regions will get highlighted
|
|
;; syntactically in some scenarios.
|
|
(lambda () (font-lock-unfontify-region (point-min) (point-max))))
|
|
`(jit-lock-bounds ,beg . ,end))
|
|
|
|
(defun jupyter-repl-syntax-propertize-function (propertize-fun beg end)
|
|
"Use PROPERTIZE-FUN to syntax propertize text between BEG and END."
|
|
(jupyter-repl-map-cells beg end
|
|
;; See note in `jupyter-repl-font-lock-fontify-region' on why the buffer
|
|
;; should be narrowed to the input cell before calling this function.
|
|
(lambda () (funcall propertize-fun (point-min) (point-max)))
|
|
;; Treat parenthesis and string characters as punctuation when parsing the
|
|
;; syntax of the output. Although we don't fontify output regions,
|
|
;; `syntax-ppss' still looks at the whole contents of the buffer. If there
|
|
;; are unmatched parenthesis or string delimiters in the output, it will
|
|
;; interfere with `syntax-ppss'. Note, this requires
|
|
;; `parse-sexp-lookup-properties' to be non-nil so that `syntax-ppss' will
|
|
;; look at the `syntax-table' property.
|
|
(lambda ()
|
|
(goto-char (point-min))
|
|
(skip-syntax-forward "^()\"")
|
|
(while (not (eobp))
|
|
(put-text-property (point) (1+ (point)) 'syntax-table '(1 . ?.))
|
|
(forward-char)
|
|
(skip-syntax-forward "^()\"")))))
|
|
|
|
(cl-defgeneric jupyter-repl-initialize-fontification ()
|
|
"Initialize fontification for the current REPL buffer."
|
|
(let (fld frf sff spf comment)
|
|
(jupyter-with-repl-lang-buffer
|
|
(setq fld font-lock-defaults
|
|
frf (or font-lock-fontify-region-function #'ignore)
|
|
sff (or font-lock-syntactic-face-function #'ignore)
|
|
spf (or syntax-propertize-function #'ignore)
|
|
comment comment-start))
|
|
;; Set `font-lock-defaults' to a copy of the font lock defaults for the
|
|
;; REPL language but with a modified syntactic fontification function
|
|
(cl-destructuring-bind (kws &optional kws-only case-fold syntax-alist
|
|
&rest vars)
|
|
(or fld (list nil))
|
|
(setq vars
|
|
(append vars
|
|
(list
|
|
;; See `jupyter-repl-font-lock-fontify-region'
|
|
(cons 'parse-sexp-lookup-properties t)
|
|
(cons 'syntax-propertize-function
|
|
(apply-partially
|
|
#'jupyter-repl-syntax-propertize-function spf))
|
|
(cons 'font-lock-fontify-region-function
|
|
(apply-partially
|
|
#'jupyter-repl-font-lock-fontify-region frf))
|
|
(cons 'font-lock-syntactic-face-function sff))))
|
|
(setq-local comment-start comment)
|
|
(setq font-lock-defaults
|
|
(apply #'list kws kws-only case-fold syntax-alist vars)))
|
|
(font-lock-mode)))
|
|
|
|
(defun jupyter-repl-insert-banner (banner)
|
|
"Insert BANNER into the `current-buffer'.
|
|
Make the text of BANNER read only and apply the `shadow' face to
|
|
it."
|
|
(jupyter-repl-without-continuation-prompts
|
|
(insert (propertize banner
|
|
'read-only t 'jupyter-banner t
|
|
'font-lock-face 'shadow 'fontified t
|
|
'font-lock-fontified t))
|
|
(jupyter-repl-newline)))
|
|
|
|
(defun jupyter-repl-sync-execution-state ()
|
|
"Synchronize the `jupyter-current-client's kernel state.
|
|
Also update the cell count of the current REPL input prompt using
|
|
the updated state."
|
|
(let* ((client jupyter-current-client)
|
|
(req (let ((jupyter-inhibit-handlers t))
|
|
(jupyter-send-execute-request client :code "" :silent t))))
|
|
(jupyter-add-callback req
|
|
:execute-reply
|
|
(jupyter-message-lambda (execution_count)
|
|
(oset client execution-count (1+ execution_count))
|
|
(unless (equal (jupyter-execution-state client) "busy")
|
|
;; Set the cell count and update the prompt
|
|
(jupyter-with-repl-buffer client
|
|
(save-excursion
|
|
(goto-char (point-max))
|
|
(jupyter-repl-update-cell-count
|
|
(oref client execution-count)))))))
|
|
;; Waiting longer here to account for initial startup of the Jupyter
|
|
;; kernel. Sometimes the idle message won't be received if another long
|
|
;; running execute request is sent right after.
|
|
(jupyter-wait-until-idle req jupyter-long-timeout)))
|
|
|
|
;;; `jupyter-repl-interaction-mode'
|
|
|
|
(defvar jupyter-repl-interaction-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map (kbd "C-x C-e") #'jupyter-eval-line-or-region)
|
|
(define-key map (kbd "C-c C-c") #'jupyter-eval-line-or-region)
|
|
(define-key map (kbd "C-M-x") #'jupyter-eval-defun)
|
|
(define-key map (kbd "C-c C-o") #'jupyter-eval-remove-overlays)
|
|
(define-key map (kbd "C-c C-s") #'jupyter-repl-scratch-buffer)
|
|
(define-key map (kbd "C-c C-b") #'jupyter-eval-buffer)
|
|
(define-key map (kbd "C-c C-l") #'jupyter-load-file)
|
|
(define-key map (kbd "C-c M-:") #'jupyter-eval-string-command)
|
|
(define-key map (kbd "M-i") #'jupyter-inspect-at-point)
|
|
(define-key map (kbd "C-c C-r") #'jupyter-repl-restart-kernel)
|
|
(define-key map (kbd "C-c C-i") #'jupyter-repl-interrupt-kernel)
|
|
(define-key map (kbd "C-c C-z") #'jupyter-repl-pop-to-buffer)
|
|
map))
|
|
|
|
(define-minor-mode jupyter-repl-interaction-mode
|
|
"Minor mode for interacting with a Jupyter REPL.
|
|
When this minor mode is enabled you may evaluate code from the
|
|
current buffer using the associated REPL (see
|
|
`jupyter-repl-associate-buffer' to associate a REPL).
|
|
|
|
In addition any new buffers opened with the same `major-mode' as
|
|
the `current-buffer' will automatically have
|
|
`jupyter-repl-interaction-mode' enabled for them.
|
|
|
|
\\{jupyter-repl-interaction-mode-map}"
|
|
:group 'jupyter-repl
|
|
:lighter (:eval (jupyter-repl-interaction-mode-line))
|
|
:init-value nil
|
|
(cond
|
|
(jupyter-repl-interaction-mode
|
|
(add-hook 'completion-at-point-functions 'jupyter-completion-at-point nil t)
|
|
(add-hook 'after-revert-hook 'jupyter-repl-interaction-mode nil t))
|
|
(t
|
|
(remove-hook 'completion-at-point-functions 'jupyter-completion-at-point t)
|
|
(remove-hook 'after-revert-hook 'jupyter-repl-interaction-mode t)
|
|
(unless (eq major-mode 'jupyter-repl-mode)
|
|
(kill-local-variable 'jupyter-current-client)))))
|
|
|
|
(defun jupyter-repl-interaction-mode-reenable ()
|
|
"Re-enable `jupyter-repl-interaction-mode' in the current buffer.
|
|
Do so only if possible in the `current-buffer'."
|
|
(when (and (not jupyter-repl-interaction-mode)
|
|
(cl-typep jupyter-current-client 'jupyter-repl-client)
|
|
(eq major-mode
|
|
(jupyter-kernel-language-mode jupyter-current-client)))
|
|
(jupyter-repl-interaction-mode)))
|
|
|
|
(defun jupyter-repl-interaction-mode-line ()
|
|
"Return a mode line string with the status of the kernel.
|
|
'*' means the kernel is busy, '-' means the kernel is idle and
|
|
the REPL is connected, 'x' means the REPL is disconnected
|
|
from the kernel."
|
|
(and (cl-typep jupyter-current-client 'jupyter-repl-client)
|
|
(concat " JuPy["
|
|
(cond
|
|
((not (jupyter-hb-beating-p jupyter-current-client)) "x")
|
|
((equal (jupyter-execution-state jupyter-current-client) "busy")
|
|
"*")
|
|
(t "-"))
|
|
"]")))
|
|
|
|
(defun jupyter-repl-pop-to-buffer ()
|
|
"Switch to the REPL buffer of the `jupyter-current-client'."
|
|
(interactive)
|
|
(if jupyter-current-client
|
|
(jupyter-with-repl-buffer jupyter-current-client
|
|
(goto-char (point-max))
|
|
(pop-to-buffer (current-buffer)))
|
|
(error "Buffer not associated with a REPL, see `jupyter-repl-associate-buffer'")))
|
|
|
|
(defun jupyter-repl-available-repl-buffers (&optional mode first)
|
|
"Return a list of REPL buffers that are connected to live kernels.
|
|
If MODE is non-nil, return all REPL buffers whose
|
|
`jupyter-repl-lang-mode' is MODE.
|
|
|
|
If FIRST is non-nil, only return the first REPL buffer that matches."
|
|
(cl-loop
|
|
for client in (jupyter-clients)
|
|
for match =
|
|
(when (and (object-of-class-p client 'jupyter-repl-client)
|
|
(buffer-live-p (oref client buffer)))
|
|
(with-current-buffer (oref client buffer)
|
|
(and (or (null mode)
|
|
(provided-mode-derived-p mode jupyter-repl-lang-mode))
|
|
(jupyter-repl-connected-p)
|
|
(buffer-name))))
|
|
if (and match first) return (oref client buffer)
|
|
else if match collect (oref client buffer)))
|
|
|
|
;;;###autoload
|
|
(defun jupyter-repl-associate-buffer (client)
|
|
"Associate the `current-buffer' with a REPL CLIENT.
|
|
If the `major-mode' of the `current-buffer' is the
|
|
`jupyter-repl-lang-mode' of CLIENT, call the function
|
|
`jupyter-repl-interaction-mode' to enable the corresponding mode.
|
|
|
|
CLIENT should be the symbol `jupyter-repl-client' or the symbol
|
|
of a subclass. If CLIENT is a buffer or the name of a buffer, use
|
|
the `jupyter-current-client' local to the buffer."
|
|
(interactive
|
|
(list
|
|
(let ((buffer
|
|
(when (jupyter-repl-available-repl-buffers major-mode)
|
|
(jupyter-repl-completing-read-repl-buffer major-mode))))
|
|
(when buffer
|
|
(buffer-local-value 'jupyter-current-client buffer)))))
|
|
(if (not client)
|
|
(when (y-or-n-p "No REPL for `major-mode' exists. Start one? ")
|
|
(call-interactively #'jupyter-run-repl))
|
|
(setq client (if (or (bufferp client) (stringp client))
|
|
(with-current-buffer client
|
|
jupyter-current-client)
|
|
client))
|
|
(unless (object-of-class-p client 'jupyter-repl-client)
|
|
(error "Not a REPL client (%s)" client))
|
|
(unless (eq (jupyter-kernel-language-mode client) major-mode)
|
|
(error "Cannot associate buffer to REPL. Wrong `major-mode'"))
|
|
(setq-local jupyter-current-client client)
|
|
(unless jupyter-repl-interaction-mode
|
|
(jupyter-repl-interaction-mode))))
|
|
|
|
(defun jupyter-repl-propagate-client (buffer &rest _)
|
|
"Propagate the `jupyter-current-client' to BUFFER.
|
|
If BUFFER's value of the variable `jupyter-repl-interaction-mode'
|
|
is nil and the buffer has the same `major-mode' as the
|
|
`jupyter-current-client's language mode, set the buffer local
|
|
value of `jupyter-current-client' in BUFFER to the current value
|
|
of that variable."
|
|
(when (and jupyter-current-client
|
|
(cl-typep jupyter-current-client 'jupyter-repl-client)
|
|
(or (bufferp buffer) (stringp buffer))
|
|
(setq buffer (get-buffer buffer))
|
|
(buffer-live-p buffer)
|
|
(null (buffer-local-value 'jupyter-repl-interaction-mode buffer))
|
|
(eq (buffer-local-value 'major-mode buffer)
|
|
(jupyter-kernel-language-mode jupyter-current-client)))
|
|
(let ((client jupyter-current-client))
|
|
(with-current-buffer buffer
|
|
(jupyter-repl-associate-buffer client)))))
|
|
|
|
(defun jupyter-repl--before-switch-to-buffer (buffer &rest _)
|
|
"Call `jupyter-repl-propagate-client' on BUFFER, handling a nil BUFFER.
|
|
When BUFFER is nil use `other-buffer'."
|
|
(jupyter-repl-propagate-client (or buffer (other-buffer))))
|
|
|
|
(defun jupyter-repl--before-set-window-buffer (_ buffer &rest __)
|
|
"Call `jupyter-repl-propagate-client' on BUFFER."
|
|
(jupyter-repl-propagate-client buffer))
|
|
|
|
;;; `jupyter-repl-persistent-mode'
|
|
|
|
;;;###autoload
|
|
(define-minor-mode jupyter-repl-persistent-mode
|
|
"Global minor mode to persist Jupyter REPL connections.
|
|
When the `jupyter-current-client' of the current buffer is a REPL
|
|
client, its value is propagated to all buffers switched to that
|
|
have the same `major-mode' as the client's kernel language and
|
|
`jupyter-repl-interaction-mode' is enabled in those buffers."
|
|
:group 'jupyter-repl
|
|
:global t
|
|
:keymap nil
|
|
:init-value nil
|
|
(cond
|
|
(jupyter-repl-persistent-mode
|
|
(advice-add 'switch-to-buffer :before #'jupyter-repl--before-switch-to-buffer)
|
|
(advice-add 'display-buffer :before #'jupyter-repl-propagate-client)
|
|
(advice-add 'set-window-buffer :before #'jupyter-repl--before-set-window-buffer)
|
|
(add-hook 'after-change-major-mode-hook 'jupyter-repl-interaction-mode-reenable))
|
|
(t
|
|
(advice-remove 'switch-to-buffer #'jupyter-repl--before-switch-to-buffer)
|
|
(advice-remove 'display-buffer #'jupyter-repl-propagate-client)
|
|
(advice-remove 'set-window-buffer #'jupyter-repl--before-set-window-buffer)
|
|
(remove-hook 'after-change-major-mode-hook 'jupyter-repl-interaction-mode-reenable))))
|
|
|
|
;;; Starting a REPL
|
|
|
|
(cl-defgeneric jupyter-bootstrap-repl ((client jupyter-repl-client)
|
|
&optional repl-name associate-buffer display)
|
|
"Initialize a new REPL buffer based on CLIENT, return CLIENT.
|
|
CLIENT should be a REPL client already connected to its kernel.
|
|
|
|
A new REPL buffer communicating with CLIENT's kernel is created
|
|
and set as CLIENT's buffer slot. If CLIENT already has a non-nil
|
|
buffer slot, do nothing.
|
|
|
|
REPL-NAME is a string that will be used to generate the buffer
|
|
name. If nil or empty, a default will be used.
|
|
|
|
If ASSOCIATE-BUFFER is non-nil, attempt to \"connect\" the
|
|
`current-buffer' to the REPL (see
|
|
`jupyter-repl-associate-buffer') if it is compatible with the
|
|
underlying kernel.
|
|
|
|
If DISPLAY is non-nil, display the REPL buffer after
|
|
completing all of the above.")
|
|
|
|
(cl-defmethod jupyter-bootstrap-repl :before ((_client jupyter-repl-client)
|
|
&optional _repl-name _associate-buffer _display)
|
|
"Enable `jupyter-repl-persistent-mode' if needed."
|
|
(unless jupyter-repl-persistent-mode (jupyter-repl-persistent-mode)))
|
|
|
|
(cl-defmethod jupyter-bootstrap-repl :after ((client jupyter-repl-client)
|
|
&optional _repl-name associate-buffer display)
|
|
(when (and associate-buffer
|
|
(eq major-mode (jupyter-kernel-language-mode client)))
|
|
(jupyter-repl-associate-buffer client))
|
|
(when display
|
|
(pop-to-buffer (oref client buffer))))
|
|
|
|
(cl-defmethod jupyter-bootstrap-repl ((client jupyter-repl-client)
|
|
&optional repl-name _associate-buffer _display)
|
|
(prog1 client
|
|
(unless (oref client buffer)
|
|
(cl-destructuring-bind (&key language_info
|
|
banner
|
|
&allow-other-keys)
|
|
(jupyter-kernel-info client)
|
|
(let ((language-name (plist-get language_info :name))
|
|
(language-version (plist-get language_info :version)))
|
|
(oset client buffer
|
|
(generate-new-buffer
|
|
(format "*jupyter-repl[%s]*"
|
|
(if (zerop (length repl-name))
|
|
(format "%s %s" language-name language-version)
|
|
repl-name))))
|
|
(jupyter-with-repl-buffer client
|
|
(setq-local jupyter-current-client client)
|
|
(jupyter-repl-mode)
|
|
(jupyter-repl-insert-banner banner)
|
|
(jupyter-repl-insert-prompt 'in)))))))
|
|
|
|
;;;###autoload
|
|
(defun jupyter-run-repl (kernel-name &optional repl-name associate-buffer client-class display)
|
|
"Run a Jupyter REPL connected to a kernel with name, KERNEL-NAME.
|
|
KERNEL-NAME will be passed to `jupyter-guess-kernelspec' and the
|
|
first kernel found will be used to start the new kernel.
|
|
|
|
With a prefix argument give a new REPL-NAME for the REPL.
|
|
|
|
Optional argument ASSOCIATE-BUFFER, if non-nil, means to enable
|
|
the REPL interaction mode by calling the function
|
|
`jupyter-repl-interaction-mode' in the `current-buffer' and
|
|
associate it with the REPL created. When called interactively,
|
|
ASSOCIATE-BUFFER is set to t. If the `current-buffer's
|
|
`major-mode' does not correspond to the language of the kernel
|
|
started, ASSOCIATE-BUFFER has no effect.
|
|
|
|
Optional argument CLIENT-CLASS is the class that will be passed
|
|
to `jupyter-start-new-kernel' and should be a class symbol like
|
|
the symbol `jupyter-repl-client', which is the default.
|
|
|
|
When called interactively, DISPLAY the new REPL buffer.
|
|
Otherwise, in a non-interactive call, return the REPL client
|
|
connected to the kernel.
|
|
|
|
Note, if `default-directory' is a remote directory, a kernel will
|
|
start on the remote host by using the \"jupyter kernel\" shell
|
|
command on the host."
|
|
(interactive (list (car (jupyter-completing-read-kernelspec
|
|
nil current-prefix-arg))
|
|
(when current-prefix-arg
|
|
(read-string "REPL Name: "))
|
|
t nil t))
|
|
(or client-class (setq client-class 'jupyter-repl-client))
|
|
(jupyter-error-if-not-client-class-p client-class 'jupyter-repl-client)
|
|
;; For `jupyter-start-new-kernel', we don't require this at top-level since
|
|
;; there are many ways to interact with a kernel, e.g. through a notebook
|
|
;; server, and we don't want to load any unnecessary files.
|
|
(require 'jupyter-kernel-process-manager)
|
|
(cl-destructuring-bind (_manager client)
|
|
(jupyter-start-new-kernel kernel-name client-class)
|
|
(jupyter-bootstrap-repl client repl-name associate-buffer display)))
|
|
|
|
;;;###autoload
|
|
(defun jupyter-connect-repl (file-or-plist &optional repl-name associate-buffer client-class display)
|
|
"Run a Jupyter REPL using a kernel's connection FILE-OR-PLIST.
|
|
FILE-OR-PLIST can be either a file holding the connection
|
|
information or a property list of connection information.
|
|
ASSOCIATE-BUFFER has the same meaning as in `jupyter-run-repl'.
|
|
|
|
With a prefix argument give a new REPL-NAME for the REPL.
|
|
|
|
Optional argument CLIENT-CLASS is the class of the client that
|
|
will be used to initialize the REPL and should be a class symbol
|
|
like the symbol `jupyter-repl-client', which is the default.
|
|
|
|
Return the REPL client connected to the kernel. When called
|
|
interactively, DISPLAY the new REPL buffer as well."
|
|
(interactive (list (read-file-name "Connection file: ")
|
|
(when current-prefix-arg
|
|
(read-string "REPL Name: "))
|
|
t nil t))
|
|
(or client-class (setq client-class 'jupyter-repl-client))
|
|
(jupyter-error-if-not-client-class-p client-class 'jupyter-repl-client)
|
|
(let ((client (make-instance client-class)))
|
|
;; FIXME: See note in `jupyter-make-client'
|
|
(require 'jupyter-channel-ioloop-comm)
|
|
(require 'jupyter-zmq-channel-ioloop)
|
|
(oset client kcomm (make-instance
|
|
'jupyter-channel-ioloop-comm
|
|
:ioloop-class 'jupyter-zmq-channel-ioloop))
|
|
(jupyter-comm-initialize client file-or-plist)
|
|
(jupyter-start-channels client)
|
|
(jupyter-hb-unpause client)
|
|
(jupyter-bootstrap-repl client repl-name associate-buffer display)))
|
|
|
|
(provide 'jupyter-repl)
|
|
|
|
;;; jupyter-repl.el ends here
|