emacs-jupyter/jupyter-repl.el

2624 lines
108 KiB
EmacsLisp
Raw Normal View History

2018-01-08 21:38:32 -06:00
;;; jupyter-repl-client.el --- A Jupyter REPL client -*- lexical-binding: t -*-
;; Copyright (C) 2018 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 08 Jan 2018
;; Version: 0.0.1
;; 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 2, 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:
2018-02-04 18:18:19 -06:00
;; A Jupyter REPL for Emacs.
2018-01-08 21:38:32 -06:00
;;
2018-02-04 18:18:19 -06:00
;; The main entry points are `run-jupyter-repl' and `connect-jupyter-repl'.
2018-05-12 14:52:35 -05:00
;;
;; When called interactively, `run-jupyter-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 `connect-jupyter-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.
2018-05-12 14:52:35 -05:00
;;
;; 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' and, if `company-mode' is installed, enables
;; code completion using the associated REPL client.
2018-05-12 14:52:35 -05:00
;;
;; `jupyter-repl-interaction-mode' adds the following keybindings for
;; interacing a REPL client:
;;
;; C-c C-c `jupyter-repl-eval-line-or-region'
;; C-c C-l `jupyter-repl-eval-file'
;; C-c C-f `jupyter-inspect-at-point'
2018-05-12 14:52:35 -05:00
;; 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'
2018-01-08 21:38:32 -06:00
;;; Code:
(defgroup jupyter-repl nil
2018-01-08 21:38:32 -06:00
"A Jupyter REPL client"
:group 'jupyter)
2018-01-08 21:38:32 -06:00
2018-01-13 22:53:19 -06:00
(require 'jupyter-base)
2017-12-23 15:34:28 -06:00
(require 'jupyter-client)
2018-05-22 21:58:33 -05:00
(require 'jupyter-widget-client)
(require 'jupyter-kernel-manager)
(require 'shr)
2018-01-13 22:53:19 -06:00
(require 'ring)
2018-05-30 22:15:17 -05:00
(require 'ansi-color)
2017-12-23 15:34:28 -06:00
2018-01-22 19:42:36 -06:00
(declare-function company-begin-backend "company" (backend &optional callback))
(declare-function company-doc-buffer "company" (&optional string))
2018-05-30 22:15:17 -05:00
(declare-function company-grab-symbol-cons "company" (idle-begin-after-re &optional max-len))
2018-01-22 19:42:36 -06:00
(declare-function org-format-latex "org" (prefix &optional beg end dir overlays msg forbuffer processing-type))
2018-05-30 22:15:17 -05:00
(declare-function markdown-link-at-pos "markdown-mode" (pos))
(declare-function markdown-follow-link-at-point "markdown-mode")
2018-01-22 19:42:36 -06:00
2017-12-23 15:34:28 -06:00
;; TODO: Fallbacks for when the language doesn't have a major mode installed.
2018-02-04 18:18:19 -06:00
;; TODO: Define `jupyter-kernel-manager-after-restart-hook' to update the
2018-05-15 17:47:33 -05:00
;; execution count after a restart. More generally, define more ways to hook
;; into differnt events of the client/kernel interaction.
2018-01-13 23:18:15 -06:00
;;; User variables
2017-12-23 15:34:28 -06:00
(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"))
2018-01-13 23:20:50 -06:00
"Face used for the output prompt."
2017-12-23 15:34:28 -06:00
:group 'jupyter-repl)
(defcustom jupyter-repl-maximum-size 1024
"Maximum number of lines before the buffer is truncated."
2018-05-26 20:02:16 -05:00
:type 'integer
2017-12-23 15:34:28 -06:00
:group 'jupyter-repl)
(defcustom jupyter-repl-maximum-is-complete-timeout 2
2018-02-12 11:03:41 -06:00
"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."
2018-05-26 20:02:16 -05:00
:type 'integer
:group 'jupyter-repl)
(defcustom jupyter-repl-history-maximum-length 100
"The maximum number of history elements to keep track of."
2018-05-26 20:02:16 -05:00
:type 'integer
:group 'jupyter-repl)
(defcustom jupyter-repl-prompt-margin-width 12
"The width of the margin which displays prompt strings."
2018-05-26 20:02:16 -05:00
:type 'integer
:group 'jupyter-repl)
2018-01-13 23:18:15 -06:00
;;; Implementation
2018-05-22 21:58:33 -05:00
(defclass jupyter-repl-client (jupyter-widget-client)
2018-05-26 20:04:02 -05:00
((buffer
:type (or null buffer)
:initform nil
:documentation "The REPL buffer whose
`jupyter-current-client' is this client.")
2018-05-22 21:58:33 -05:00
(wait-to-clear
:type boolean
:initform nil
2018-05-22 21:58:33 -05:00
:documentation "Whether or not we should wait to clear the
current output of the cell. Set when the kernel sends a
`:clear-output' message.")
2018-05-26 20:04:02 -05:00
(execution-state
:type string
:initform "idle"
:documentation "The current state of the kernel. Can be
either \"idle\", \"busy\", or \"starting\".")
(execution-count
:type integer
:initform 1
:documentation "The current execution count of the kernel.")))
(defvar jupyter-repl-lang-buffer nil
"A buffer with the `major-mode' set to the REPL language's `major-mode'.")
2017-12-23 15:34:28 -06:00
(make-variable-buffer-local 'jupyter-repl-lang-buffer)
(defvar jupyter-repl-lang-mode nil
"The `major-mode' corresponding to the REPL's language.")
(make-variable-buffer-local 'jupyter-repl-lang-mode)
2017-12-23 15:34:28 -06:00
(defvar jupyter-repl-history nil
2017-12-31 10:16:41 -06:00
"The history of the current Jupyter REPL.")
2018-01-12 18:28:23 -06:00
(defvar jupyter-repl-fontify-buffers nil
2018-01-13 23:20:50 -06:00
"An alist of (MODE . BUFFER) pairs used for fontification.
See `jupyter-repl-fontify-according-to-mode'.")
2018-01-12 18:28:23 -06:00
(defvar-local jupyter-repl-use-builtin-is-complete nil
2018-05-26 20:04:02 -05:00
"Whether or not to send `:is-complete-request's to a kernel.
If a Jupyter kernel does not respond to an is_complete_request,
2018-01-13 23:20:50 -06:00
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.")
2018-05-26 17:45:41 -05:00
(defvar jupyter-repl-display-ids nil
"A hash table of display IDs.
Display IDs are implemented by setting the text property,
`jupyter-display', to the display ID requested by a
`:display-data' message. When a display is updated from an
`:update-display-data' message, the display ID from the initial
2018-05-26 20:04:02 -05:00
`:display-data' message is retrieved from this table and used to
find the display in the REPL buffer. See
`jupyter-repl-update-display'.")
2018-05-26 17:45:41 -05:00
(cl-generic-define-context-rewriter jupyter-repl-mode (mode &rest modes)
`(jupyter-repl-lang-mode (derived-mode ,mode ,@modes)))
2018-01-16 11:34:22 -06:00
;;; Macros
2017-12-27 21:55:58 -06:00
(defmacro jupyter-with-repl-buffer (client &rest body)
"Switch to CLIENT's REPL buffer before running BODY.
2018-01-08 18:11:08 -06:00
This switches to CLIENT's buffer slot, sets `inhibit-read-only'
to t, and then runs BODY. Afterwards, if CLIENT's buffer is
currently being shown in a window, move windows `point' to the
value of `point' in the buffer."
2017-12-23 15:34:28 -06:00
(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))))))))
(defmacro jupyter-repl-without-continuation-prompts (&rest body)
"Run BODY without inserting continuation prompts.
Normally a continuation prompt is inserted for every newline
inserted into the REPL buffer through a function in
`after-change-functions'. Prevent the function from running while
executing BODY."
(declare (debug (&rest form)))
`(let ((inhibit-modification-hooks t))
,@body))
;; Taken from `eshell-handle-control-codes'
(defun jupyter-repl-handle-control-codes (beg end)
"Handle any control sequences between BEG and END."
(save-excursion
(goto-char beg)
(while (< (point) end)
(let ((char (char-after)))
(cond
((eq char ?\r)
(if (< (1+ (point)) end)
(if (memq (char-after (1+ (point)))
'(?\n ?\r))
(delete-char 1)
(let ((end (1+ (point))))
(beginning-of-line)
(delete-region (point) end)))
(add-text-properties (point) (1+ (point))
'(invisible t))
(forward-char)))
((eq char ?\a)
(delete-char 1)
(beep))
((eq char ?\C-h)
(delete-region (1- (point)) (1+ (point))))
(t
(forward-char)))))))
(defmacro jupyter-repl-append-output (client req &rest body)
"Switch to CLIENT's buffer, move to the end of REQ, and run BODY.
REQ is a `jupyter-request' previously made using CLIENT, a
`jupyter-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
;; Don't mess with font-lock and don't insert
;; continuation prompts
(with-silent-modifications
(save-excursion
(jupyter-repl-goto-cell ,req)
(jupyter-repl-next-cell)
(let ((beg (point-marker))
(end (point-marker)))
(set-marker-insertion-type end t)
,@body
(jupyter-repl-handle-control-codes beg end)
(set-marker beg nil)
(set-marker end nil))))))
(defmacro jupyter-with-repl-lang-buffer (&rest body)
2018-01-08 18:11:08 -06:00
"Run BODY in the `jupyter-repl-lang-buffer' of the `current-buffer'.
The contents of `jupyter-repl-lang-buffer' is erased before
running BODY."
2017-12-23 15:34:28 -06:00
(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)))))
2017-12-23 15:34:28 -06:00
(defmacro jupyter-with-repl-cell (&rest body)
2018-01-08 18:11:08 -06:00
"Narrow to the current cell, run 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'. When BODY is run, `point' will
be at the `jupyter-repl-cell-code-beginning-position'."
2017-12-23 15:34:28 -06:00
(declare (indent 0) (debug (&rest form)))
2017-12-27 21:25:29 -06:00
`(save-excursion
2017-12-23 15:34:28 -06:00
(save-restriction
(narrow-to-region (jupyter-repl-cell-code-beginning-position)
(jupyter-repl-cell-code-end-position))
(goto-char (jupyter-repl-cell-code-beginning-position))
2017-12-23 15:34:28 -06:00
,@body)))
;; TODO: Rename to `jupyter-get-doc-buffer' since they are
;; not limited to the REPL.
(defun jupyter-get-doc-buffer (name)
"Return the REPL documentation buffer for NAME.
A REPL documentation buffer has the following characteristics:
- `major-mode' is `special-mode'
- local keybindings to quit the window (q), and scroll the
window (SPC and <backtab>).
The buffer returned will have a `buffer-name' of
\"*jupyter-repl-NAME*\". If a buffer with this name already
exists, it is returned."
2018-01-16 11:34:22 -06:00
(let* ((bname (format "*jupyter-repl-%s*" name))
(buffer (get-buffer bname)))
(unless buffer
(setq buffer (get-buffer-create bname))
(with-current-buffer buffer
(special-mode)
(local-set-key "q" #'quit-window)
(local-set-key (kbd "SPC") #'scroll-down)
(local-set-key (kbd "<backtab>") #'scroll-up)))
buffer))
(defmacro jupyter-with-doc-buffer (name &rest body)
"With the REPL documentation buffer corresponding to NAME, run BODY.
NAME should be a string representing the purpose of the
documentation buffer. The buffer corresponding to NAME will be
2018-09-10 00:22:20 -05:00
obtained by a call to `jupyter-repl-get-doc-buffer'. Before
running BODY, the doc buffer is set as the
`other-window-scroll-buffer' and the contents of the buffer are
erased."
(declare (indent 1))
(let ((buffer (make-symbol "buffer")))
`(let ((,buffer (jupyter-get-doc-buffer ,name)))
(setq other-window-scroll-buffer ,buffer)
(with-current-buffer ,buffer
(let ((inhibit-read-only t))
(erase-buffer)
,@body)))))
2018-02-09 17:21:10 -06:00
;;; Convenience functions
(defun jupyter-repl-language-mode (client)
"Return the `major-mode' of CLIENT's kernel language."
(jupyter-with-repl-buffer client
2018-02-09 17:21:10 -06:00
jupyter-repl-lang-mode))
2018-01-16 11:34:22 -06:00
;;; Text insertion
2017-12-27 21:55:58 -06:00
(defun jupyter-repl-add-font-lock-properties (start end &optional object)
"Add font lock text properties between START and END in the `current-buffer'.
START, END, and OBJECT have the same meaning as in
2018-01-16 11:34:22 -06:00
`add-text-properties'. The properties added are the ones that
mark the text between START and END as fontified according to
font lock. Any text between START and END that does not have a
`font-lock-face' property will have the `default' face filled in
for the property."
(jupyter-repl-fixup-font-lock-properties start end object)
(add-text-properties
start end '(fontified t font-lock-fontified t font-lock-multiline t) object))
(defun jupyter-repl-fixup-font-lock-properties (beg end &optional object)
"Fixup the text properties in the `current-buffer' between BEG END.
If OBJECT is non-nil, fixup the text properties of OBJECT. Fixing
the text properties involves substituting any `face' property
with `font-lock-face' for insertion into the REPL buffer."
(let ((next beg) val)
(while (/= beg end)
(setq val (get-text-property beg 'face object)
next (next-single-property-change beg 'face object end))
(remove-text-properties beg next '(face) object)
(put-text-property beg next 'font-lock-face (or val 'default) object)
(setq beg next))))
2018-01-12 18:28:23 -06:00
(defun jupyter-repl-get-fontify-buffer (mode)
"Return the buffer used to fontify text for MODE.
Retrieve the buffer for MODE from `jupyter-repl-fontify-buffers'.
If no buffer for MODE exists, create a new one."
2018-01-12 18:28:23 -06:00
(let ((buf (alist-get mode jupyter-repl-fontify-buffers)))
(unless buf
(setq buf (get-buffer-create
(format " *jupyter-repl-fontify[%s]*" mode)))
(with-current-buffer buf
(funcall mode))
(setf (alist-get mode jupyter-repl-fontify-buffers) buf))
buf))
2017-12-27 22:17:38 -06:00
(defun jupyter-repl-fontify-according-to-mode (mode str)
2018-01-08 18:11:08 -06:00
"Fontify a string according to MODE.
2018-01-16 11:34:22 -06:00
In addition to fontifying STR, if MODE has a non-default
`fill-forward-paragraph-function', STR will be filled using
`fill-region'."
2018-01-12 18:28:23 -06:00
(with-current-buffer (jupyter-repl-get-fontify-buffer mode)
2017-12-27 22:17:38 -06:00
(let ((inhibit-modification-hooks nil))
2018-01-12 18:28:23 -06:00
(erase-buffer)
2017-12-27 22:17:38 -06:00
(insert str)
(font-lock-ensure))
(jupyter-repl-add-font-lock-properties (point-min) (point-max))
(when (not (memq fill-forward-paragraph-function
'(forward-paragraph)))
2018-05-06 10:51:18 -05:00
(fill-region (point-min) (point-max) t 'nosqueeze))
2017-12-27 22:17:38 -06:00
(buffer-string)))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-insert (&rest args)
"Insert text into the `current-buffer', possibly with text properties.
This acts like `insert' except that the leading elements of ARGS
can contain the following keywords along with their values:
- `:read-only' :: A non-nil value makes the text to be inserted,
read only. This is t by default, so to make text editable you
will have to do something like:
(jupyter-repl-insert :read-only nil \"<editable text>\")
- `:properties' :: A list of text properties and their values to
be added to the inserted text. This defaults to an empty list.
- `:inherit' :: A non-nil value will use
`insert-and-inherit' instead of `insert' for the function used
to insert the text. This is nil by default."
2017-12-23 15:34:28 -06:00
(let ((arg nil)
(read-only t)
(properties nil)
(insert-fun #'insert))
2017-12-23 15:34:28 -06:00
(while (keywordp (setq arg (car args)))
(cl-case arg
(:read-only (setq read-only (cadr args)))
(:properties (setq properties (cadr args)))
(:inherit
(setq insert-fun (if (cadr args) #'insert-and-inherit #'insert)))
(otherwise
(error "Keyword not one of `:read-only', `:properties', `:inherit-' (`%s')" arg)))
2017-12-23 15:34:28 -06:00
(setq args (cddr args)))
(setq properties (append (when read-only '(read-only t))
properties))
(apply insert-fun (mapcar (lambda (s)
(prog1 s
(when properties
(add-text-properties
0 (length s) properties s))))
args))))
2017-12-27 22:17:38 -06:00
(defun jupyter-repl-newline ()
"Insert a read-only newline into the `current-buffer'."
2017-12-27 22:17:38 -06:00
(jupyter-repl-insert "\n"))
;;; Handling rich output
(defvar jupyter-repl-graphic-mimetypes '(:image/png :image/svg+xml :text/latex)
"Mimetypes that display graphics in the REPL buffer.")
(defun jupyter-repl-graphic-data-p (msg)
"Check to see if MSG has mimetypes for graphics."
(cl-loop
with graphic-types = jupyter-repl-graphic-mimetypes
for (mimetype _value) on (jupyter-message-get msg :data) by #'cddr
thereis (memq mimetype graphic-types)))
2017-12-27 22:17:38 -06:00
(defun jupyter-repl-insert-html (html)
"Parse and insert the HTML string using `shr'."
(jupyter-repl-without-continuation-prompts
(let ((beg (point)))
(insert html)
;; NOTE: Parsing takes a very long time when the text
;; is > ~500000 characters.
(shr-render-region beg (point))
(jupyter-repl-add-font-lock-properties beg (point))
(add-text-properties beg (point) '(read-only t)))))
2018-01-12 18:39:27 -06:00
;; Markdown integration
2018-05-06 10:51:18 -05:00
(defvar markdown-hide-markup)
(defvar markdown-hide-urls)
(defvar markdown-fontify-code-blocks-natively)
(defvar jupyter-repl-markdown-mouse-map
(let ((map (make-sparse-keymap)))
(define-key map [return] 'jupyter-repl-markdown-follow-link-at-point)
(define-key map [follow-link] 'mouse-face)
(define-key map [mouse-2] 'jupyter-repl-markdown-follow-link-at-point)
map))
(cl-defgeneric jupyter-markdown-follow-link (_link-text _url _ref-label _title-text _bang)
"Follow the markdown link at `point'."
(markdown-follow-link-at-point))
(cl-defmethod jupyter-markdown-follow-link (link-text url _ref-label _title-text _bang
&context (jupyter-lang julia))
"Send a help query to the Julia REPL for LINK-TEXT if URL is \"@ref\".
Otherwise follow the link normally."
(if (string= url "@ref")
;; Links have the form `fun`
(let ((fun (substring link-text 1 -1)))
(if (not (eq major-mode 'jupyter-repl-mode))
(jupyter-inspect fun (1- (length fun)))
(goto-char (point-max))
(jupyter-repl-replace-cell-code (concat "?" fun))
(jupyter-repl-ret)))
(cl-call-next-method)))
(defun jupyter-repl-markdown-follow-link-at-point ()
"Handle markdown links specially."
(interactive)
(let ((link (markdown-link-at-pos (point))))
(when (car link)
(apply #'jupyter-markdown-follow-link (cddr link)))))
2018-01-12 18:39:45 -06:00
(defun jupyter-repl-insert-markdown (text)
"Insert TEXT, fontifying it using `markdown-mode' first."
(let ((pos (point)))
(jupyter-repl-insert
(let ((markdown-hide-markup t)
(markdown-hide-urls t)
(markdown-fontify-code-blocks-natively t))
(jupyter-repl-fontify-according-to-mode 'markdown-mode text)))
;; Update keymaps
(let ((limit (point)) next)
(setq pos (next-single-property-change pos 'keymap nil limit))
(while (/= pos limit)
(setq next (next-single-property-change pos 'keymap nil limit))
(when (eq (get-text-property pos 'keymap) markdown-mode-mouse-map)
(put-text-property pos next 'keymap jupyter-repl-markdown-mouse-map))
(setq pos next)))))
2017-12-27 22:17:38 -06:00
2018-05-06 10:51:18 -05:00
(defvar org-format-latex-options)
(defvar org-preview-latex-image-directory)
2018-05-06 10:51:18 -05:00
2017-12-27 22:17:38 -06:00
(defun jupyter-repl-insert-latex (tex)
"Generate and insert a LaTeX image based on TEX.
Note that this uses `org-format-latex' to generate the LaTeX
image."
2018-01-17 21:25:16 -06:00
;; FIXME: Getting a weird error when killing the temp buffers created by
;; `org-format-latex'. When generating the image, it seems that the temp
;; buffers created have the same major mode and local variables as the REPL
;; buffer which causes the query function to ask to kill the kernel client
;; when the temp buffers are killed!
(let ((kill-buffer-query-functions nil)
(org-format-latex-options
`(:foreground
default
:background default :scale 2.0
:matchers ,(plist-get org-format-latex-options :matchers)))
beg end)
2017-12-27 22:17:38 -06:00
(setq beg (point))
(jupyter-repl-insert tex)
2017-12-27 22:17:38 -06:00
(setq end (point))
(org-format-latex
org-preview-latex-image-directory
beg end org-babel-jupyter-resource-directory
2017-12-27 22:17:38 -06:00
'overlays "Creating LaTeX image...%s"
'forbuffer
;; Use the default method for creating image files
2018-01-17 21:25:16 -06:00
org-preview-latex-default-process)
(goto-char end)))
2017-12-27 22:17:38 -06:00
(defun jupyter-repl-insert-ansi-coded-text (text)
"Insert TEXT, converting ANSI color codes to font lock faces."
2018-05-13 09:09:13 -05:00
(setq text (ansi-color-apply text))
(jupyter-repl-add-font-lock-properties 0 (length text) text)
;; NOTE: Mark text with a specific syntax class so that string characters do
;; not get registered as strings. This requires
;; `parse-sexp-lookup-properties' to be non-nil.
(add-text-properties 0 (length text) '(syntax-table (3)) text)
(jupyter-repl-insert text))
(defun jupyter-repl-insert-data (data metadata)
"Insert DATA into the REPL buffer in order of decreasing richness.
DATA is a plist mapping mimetypes to their content. METADATA is a
plist similar to data, but with values describing extra
information for inserting each kind of mimetype. For example the
value of `image/png' can be a plist with the keys `:width',
`:height'.
Attempt to insert a recognized mimetype, trying each one in order
of decreasing richness of the mimetype. The current order is
- application/vnd.jupyter.widget-view+json
- text/html
- text/markdown (only if `markdown-mode' is available)
- text/latex
- image/png
- image/svg+xml
- text/plain
As a special case, since Emacs is currently unable to embed a web
browser in a sufficient way, inserting a widget does not actually
insert it into the buffer. Instead the widget is displayed in a
browser.
When no valid mimetype is present in DATA, a warning is shown."
(let ((mimetypes (cl-loop
for (k d) on data by #'cddr
when
(and d (not (equal d ""))
(or (display-graphic-p)
(not (memq k jupyter-repl-graphic-mimetypes))))
collect k)))
2017-12-31 09:37:56 -06:00
(cond
((and (memq :application/vnd.jupyter.widget-view+json mimetypes)
(require 'websocket nil t)
(require 'simple-httpd nil t))
(jupyter-widgets-display-model
jupyter-current-client
(plist-get (plist-get data :application/vnd.jupyter.widget-view+json)
:model_id)))
((and (memq :text/html mimetypes)
(functionp 'libxml-parse-html-region))
(let ((html (plist-get data :text/html)))
(when (string-match-p "^<img" html)
(jupyter-repl-newline))
(jupyter-repl-insert-html html)
(jupyter-repl-newline)))
((and (memq :text/markdown mimetypes)
(require 'markdown-mode nil t))
(jupyter-repl-insert-markdown (plist-get data :text/markdown)))
((and (memq :text/latex mimetypes)
(require 'org nil t))
(jupyter-repl-insert-latex (plist-get data :text/latex))
(jupyter-repl-newline))
((and (memq :image/svg+xml mimetypes)
(image-type-available-p 'svg))
(cl-destructuring-bind (&key width height)
(plist-get metadata :image/svg+xml)
(let* ((data (plist-get data :image/svg+xml))
2018-10-04 18:31:44 -05:00
(img (create-image data 'svg 'data :width width :height height)))
2018-10-04 18:32:30 -05:00
(insert-image img (propertize " " 'read-only t))))
(jupyter-repl-newline))
((memq :image/png mimetypes)
(cl-destructuring-bind (&key width height)
(plist-get metadata :image/png)
(let* ((data (base64-decode-string (plist-get data :image/png)))
(img (create-image data nil 'data :width width :height height)))
(insert-image img (propertize " " 'read-only t))))
(jupyter-repl-newline))
2017-12-31 09:37:56 -06:00
((memq :text/plain mimetypes)
(jupyter-repl-insert-ansi-coded-text
(plist-get data :text/plain))
(jupyter-repl-newline))
(t (warn "No supported mimetype found %s" mimetypes)))))
2017-12-31 09:37:56 -06:00
2018-05-26 17:45:41 -05:00
(defun jupyter-repl-insert-data-with-id (display-id data metadata)
"Associate DISPLAY-ID with DATA when inserting DATA.
DATA and METADATA have the same meaning as in
`jupyter-repl-insert-data'.
Currently there is no support for associating a DISPLAY-ID if
DATA is displayed as a widget."
2018-05-26 17:45:41 -05:00
(unless jupyter-repl-display-ids
(setq-local jupyter-repl-display-ids
(make-hash-table :test #'equal
:weakness 'value)))
(let ((beg (point))
(id (gethash display-id jupyter-repl-display-ids))
(widget-p (memq :application/vnd.jupyter.widget-view+json data)))
(or id widget-p (setq id (puthash display-id
display-id
jupyter-repl-display-ids)))
2018-05-26 17:45:41 -05:00
(jupyter-repl-insert-data data metadata)
(unless widget-p
(put-text-property beg (point) 'jupyter-display id))))
2018-05-26 17:45:41 -05:00
;;; Prompt
(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-width
(length str))
? )
str)
'fontified t
'font-lock-face face)))
(defun jupyter-repl--make-prompt (str face props)
"Make a prompt overlay for the character at `point'.
STR is used as the prompt string and FACE is its
`font-lock-face'. Add PROPS as text properties to the character."
(let ((ov (make-overlay (1- (point)) (point) nil t))
(md (jupyter-repl--prompt-display-value str face)))
(overlay-put ov 'after-string (propertize " " 'display md))
(overlay-put ov 'evaporate t)
(add-text-properties (overlay-start ov) (overlay-end ov) props)
(overlay-recenter (point))))
2017-12-23 15:34:28 -06:00
(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'."
2017-12-23 15:34:28 -06:00
(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))
2018-09-09 21:33:05 -05:00
;; The newline that `jupyter-repl--make-prompt' will overlay.
(jupyter-repl-newline)
(cond
((eq type 'in)
(let ((count (oref jupyter-current-client execution-count)))
(jupyter-repl--make-prompt
(format "In [%d] " 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.
;;
;; field is so that text motions will not move past this invisible
;; character.
(jupyter-repl-insert
:properties '(invisible t rear-nonsticky t front-sticky t) " "))
((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 (save-excursion
(jupyter-repl-previous-cell)
(jupyter-repl-cell-count))))
(jupyter-repl--make-prompt
(format "Out [%d] " count) 'jupyter-repl-output-prompt
`(jupyter-cell (out ,count))))
;; See the note above about the invisible character for input prompts
(jupyter-repl-insert
:properties '(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-cell-update-prompt (str)
2018-01-13 23:20:50 -06:00
"Update the current cell's input prompt.
STR is the replacement prompt string."
(let ((ov (car (overlays-at (jupyter-repl-cell-beginning-position)))))
(when ov
(overlay-put ov 'after-string
(propertize
" " 'display (jupyter-repl--prompt-display-value
str 'jupyter-repl-input-prompt))))))
(defun jupyter-repl-cell-mark-busy ()
"Mark the current cell as busy."
2018-05-27 23:29:58 -05:00
(jupyter-repl-cell-update-prompt "In [*] "))
(defun jupyter-repl-cell-unmark-busy ()
"Un-mark the current cell as busy."
(jupyter-repl-cell-update-prompt
2018-05-27 23:29:58 -05:00
(format "In [%d] " (jupyter-repl-cell-count))))
(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))))
2017-12-27 21:25:29 -06:00
(defun jupyter-repl-cell-request ()
"Return the `jupyter-request' of the current cell."
(get-text-property (jupyter-repl-cell-beginning-position) 'jupyter-request))
2017-12-27 21:25:29 -06:00
;;; Cell motions
(defun jupyter-repl-cell-beginning-position ()
2018-01-08 18:11:08 -06:00
"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."
2017-12-27 21:25:29 -06:00
(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)
2018-01-08 22:31:33 -06:00
(error "Found end of previous cell"))
(if (jupyter-repl-cell-beginning-p (point-min))
(setq pos (point-min))
(signal 'beginning-of-buffer nil))))
2017-12-27 21:25:29 -06:00
pos))
(defun jupyter-repl-cell-end-position ()
2018-01-08 18:11:08 -06:00
"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."
2017-12-27 21:25:29 -06:00
(let ((pos (point)))
(catch 'unfinalized
(while (not (jupyter-repl-cell-end-p pos))
2017-12-27 21:25:29 -06:00
(setq pos (next-single-property-change pos 'jupyter-cell))
(if pos (when (jupyter-repl-cell-beginning-p pos)
2018-01-08 22:31:33 -06:00
(error "Found beginning of next cell"))
;; Any unfinalized cell must be at the end of the buffer.
2017-12-27 21:25:29 -06:00
(throw 'unfinalized (point-max))))
pos)))
(defun jupyter-repl-cell-code-beginning-position ()
2018-01-08 18:11:08 -06:00
"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))
2017-12-27 21:25:29 -06:00
(defun jupyter-repl-cell-code-end-position ()
2018-01-08 18:11:08 -06:00
"Return the end of the current cell's code.
The code ending position is
2018-01-16 11:34:22 -06:00
`jupyter-repl-cell-end-position' - 1
In the case of the last cell in the REPL buffer, i.e. an
unfinalized cell, the code ending position is `point-max'."
(let ((pos (jupyter-repl-cell-end-position)))
(if (= pos (point-max)) (point-max)
(1- pos))))
2017-12-27 21:25:29 -06:00
(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)
2017-12-27 21:25:29 -06:00
(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)
2018-01-13 23:02:12 -06:00
(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
2018-01-13 23:02:12 -06:00
`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))))
2018-01-17 21:29:54 -06:00
(error "Cell for request not found")))
2018-01-13 23:02:12 -06:00
(defun jupyter-repl-forward-cell (&optional arg)
"Go to the code beginning of the cell after the current one.
2018-01-13 23:02:12 -06:00
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.
2018-01-13 23:02:12 -06:00
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)))
2018-01-13 23:02:12 -06:00
(jupyter-repl-previous-cell arg)
(goto-char (jupyter-repl-cell-code-beginning-position)))
2017-12-27 21:25:29 -06:00
2018-01-16 11:34:22 -06:00
;;; Predicates
2017-12-27 21:25:29 -06:00
(defun jupyter-repl-cell-beginning-p (&optional pos)
2018-01-08 18:11:08 -06:00
"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))
2017-12-27 21:25:29 -06:00
(defun jupyter-repl-cell-end-p (&optional pos)
2018-01-08 18:11:08 -06:00
"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)))
2017-12-27 21:25:29 -06:00
(defun jupyter-repl-multiline-p (text)
2018-01-08 18:11:08 -06:00
"Is TEXT a multi-line string?"
(string-match-p "\n" text))
2017-12-27 21:25:29 -06:00
(defun jupyter-repl-cell-line-p ()
2018-01-08 18:11:08 -06:00
"Is the current line a cell input line?"
2018-02-06 17:45:12 -06:00
(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))))))
2017-12-27 21:25:29 -06:00
(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))))
2018-02-03 21:49:46 -06:00
(defun jupyter-repl-client-has-manager-p ()
"Does the `jupyter-current-client' have a `jupyter-kernel-manager'?"
(and jupyter-current-client
(oref jupyter-current-client manager)))
2018-02-03 21:49:46 -06:00
(defun jupyter-repl-connected-p ()
"Is the `jupyter-current-client' connected to its kernel?"
(when jupyter-current-client
(or (and (jupyter-repl-client-has-manager-p)
;; Check if the kernel is local
(jupyter-kernel-alive-p
(oref jupyter-current-client manager)))
(let ((hb (oref jupyter-current-client hb-channel)))
(and (jupyter-channel-alive-p hb)
(jupyter-hb-beating-p hb))))))
2018-02-03 21:49:46 -06:00
;;; Modifying cell code, truncating REPL buffer
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-filter-substring (beg end delete)
"Return text between BEG and END with invisible text removed.
If DELETE is non-nil, delete text between BEG and END also."
;; Include the last character at END. `buffer-substring'
;; excludes the last character.
(setq end (min (point-max) (1+ end)))
(let ((next beg)
(str ""))
(while (/= beg end)
(setq next (next-single-property-change beg 'invisible nil end))
(unless (invisible-p beg)
(setq str (concat str (buffer-substring beg next))))
(setq beg next))
(prog1 str
(when delete (delete-region beg end)))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-cell-code ()
"Return the code of the current cell."
(filter-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)
2018-01-08 22:31:33 -06:00
(error "Not in code of cell"))
2018-05-15 16:20:53 -05:00
(1+ (- (point) (jupyter-repl-cell-code-beginning-position))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-finalize-cell (req)
2018-01-08 18:11:08 -06:00
"Finalize the current cell.
REQ is the `jupyter-request' to associate with the current cell."
(let ((beg (jupyter-repl-cell-beginning-position))
(count (jupyter-repl-cell-count)))
(goto-char (point-max))
(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))
2017-12-27 21:25:29 -06:00
;; font-lock-multiline to avoid improper syntactic elements from
;; spilling over to the rest of the buffer.
(add-text-properties beg (point) '(read-only t font-lock-multiline t))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-replace-cell-code (new-code)
"Replace the current cell code with NEW-CODE."
2018-01-13 23:18:15 -06:00
(goto-char (jupyter-repl-cell-code-beginning-position))
(delete-region (point) (jupyter-repl-cell-code-end-position))
(jupyter-repl-insert :inherit t :read-only nil new-code))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-truncate-buffer ()
2018-01-08 18:11:08 -06:00
"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
2018-01-08 18:11:08 -06:00
`jupyter-repl-maximum-size' lines."
(save-excursion
(when (= (forward-line (- jupyter-repl-maximum-size)) 0)
(jupyter-repl-next-cell)
(delete-region (point-min) (point)))))
2017-12-23 15:34:28 -06:00
2017-12-27 21:55:58 -06:00
;;; Handlers
2017-12-31 10:16:41 -06:00
(defun jupyter-repl-history-add-input (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))
2017-12-31 10:16:41 -06:00
(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)
(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-input 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))
req))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl--handle-payload (payload)
"Do the client actions in PAYLOAD."
(cl-loop
for pl across payload
do (pcase (plist-get pl :source)
("page"
(let ((text (plist-get (plist-get pl :data) :text/plain))
(line (or (plist-get pl :start) 0)))
(jupyter-with-doc-buffer "pager"
(jupyter-repl-insert-ansi-coded-text text)
(goto-char (point-min))
(forward-line line)
(display-buffer (current-buffer)))))
((or "edit" "edit_magic")
(with-current-buffer (find-file-other-window
(plist-get pl :filename))
(goto-char (point-min))
(forward-line (plist-get pl :line_number))
(set-window-start (selected-window) (point))))
("set_next_input"
(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
_status
execution-count
2018-01-22 20:22:33 -06:00
_user-expressions
payload)
(oset client execution-count (1+ execution-count))
(jupyter-with-repl-buffer client
(save-excursion
(jupyter-repl-goto-cell req)
(jupyter-repl-cell-unmark-busy))
(when payload
(jupyter-repl--handle-payload payload))))
2017-12-23 15:34:28 -06:00
(cl-defmethod jupyter-handle-execute-input ((client jupyter-repl-client)
2018-01-22 20:22:33 -06:00
_req
_code
2017-12-23 15:34:28 -06:00
execution-count)
(oset client execution-count (1+ execution-count)))
(cl-defmethod jupyter-handle-execute-result ((client jupyter-repl-client)
req
2018-01-22 20:22:33 -06:00
_execution-count
2017-12-23 15:34:28 -06:00
data
metadata)
;; Only handle our results
(when req
(jupyter-repl-append-output client req
(jupyter-repl-insert-prompt 'out)
(jupyter-repl-insert-data data metadata))))
2018-05-26 17:45:41 -05:00
(defun jupyter-repl-next-display-with-id (id)
"Go to the start of the next display matching ID.
2018-05-26 17:45:41 -05:00
Return non-nil if successful. If no display with ID is found,
return nil without moving `point'."
(let ((pos (next-single-property-change (point) 'jupyter-display)))
(while (and pos (not (eq (get-text-property pos 'jupyter-display) id)))
(setq pos (next-single-property-change pos 'jupyter-display)))
(and pos (goto-char pos))))
(defun jupyter-repl-update-display (id data metadata)
"Update the display with ID using DATA.
DATA and METADATA have the same meaning as in a `:display-data'
message."
;; Updating a display involves finding and clearing the data that is
;; currently associated with the ID and inserting DATA at the same location.
;; If multiple locations have the same display ID, all of them are updated.
;; Raise an error if no display with ID could be found.
2018-05-26 17:45:41 -05:00
(save-excursion
(goto-char (point-min))
(let (str)
(while (jupyter-repl-next-display-with-id id)
(or str (setq str (with-temp-buffer
(jupyter-repl-insert-data data metadata)
(put-text-property
(point-min) (point-max) 'jupyter-display id)
(buffer-string))))
(delete-region (point) (next-single-property-change
(point) 'jupyter-display))
(let ((beg (point)) ov)
(insert str)
(setq ov (make-overlay (1+ beg) (point)))
(overlay-put ov 'face 'secondary-selection)
(run-at-time 0.3 nil (lambda () (delete-overlay ov)))))
(when (= (point) (point-min))
(error "No display matching id (%s)" id)))))
;; NOTE: Info on display_id
;; https://github.com/jupyter/jupyter_client/issues/209
2017-12-31 09:37:56 -06:00
(cl-defmethod jupyter-handle-display-data ((client jupyter-repl-client)
2018-01-22 20:22:33 -06:00
req
data
metadata
transient)
2018-05-22 21:58:33 -05:00
(let ((clear (prog1 (oref client wait-to-clear)
(oset client wait-to-clear nil)))
(req (if (eq (jupyter-message-parent-type
(jupyter-request-last-message req))
: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
(cl-destructuring-bind (&key display_id &allow-other-keys)
transient
(if display_id
(jupyter-repl-insert-data-with-id display_id data metadata)
(let ((inhibit-redisplay t))
(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-repl-insert-data data metadata)))))))
2018-05-22 21:58:33 -05:00
2018-05-26 17:45:41 -05:00
(cl-defmethod jupyter-handle-update-display-data ((client jupyter-repl-client)
_req
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
2018-05-26 17:45:41 -05:00
(let ((id (gethash display_id jupyter-repl-display-ids)))
(unless id
(error "Display ID not found (%s)" id))
(jupyter-repl-update-display id data metadata)))))
2018-05-22 21:58:33 -05:00
(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
2018-05-22 21:58:33 -05:00
(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
wait)
(unless (oset client wait-to-clear (eq wait t))
(cond
((eq (jupyter-message-parent-type
2018-05-22 21:58:33 -05:00
(jupyter-request-last-message req))
:comm-msg)
(with-current-buffer (get-buffer-create "*jupyter-repl-output*")
(erase-buffer)))
(t
(jupyter-repl-clear-last-cell-output client)))))
2017-12-23 15:34:28 -06:00
(cl-defmethod jupyter-handle-status ((client jupyter-repl-client) _req execution-state)
(oset client execution-state execution-state))
(defvar jupyter-repl--output-marker nil)
(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)))
(inhibit-read-only t)
(stream-buffer
(concat (substring bname 0 (1- (length bname)))
"-" stream "*")))
(with-current-buffer (get-buffer-create stream-buffer)
(unless jupyter-repl--output-marker
(setq-local jupyter-repl--output-marker (set-marker (make-marker) (point-max))))
(goto-char jupyter-repl--output-marker)
(let ((pos (point)))
(jupyter-repl-insert-ansi-coded-text text)
(fill-region pos (point)))
(set-marker jupyter-repl--output-marker (point))
(display-buffer (current-buffer) '(display-buffer-pop-up-window
(pop-up-windows . t))))))
2017-12-23 15:34:28 -06:00
(cl-defmethod jupyter-handle-stream ((client jupyter-repl-client) req name text)
2018-05-22 21:58:33 -05:00
(if (null req)
(jupyter-repl-display-other-output client name text)
(cond
((eq (jupyter-message-parent-type
2018-05-22 21:58:33 -05:00
(jupyter-request-last-message req))
:comm-msg)
(with-current-buffer (get-buffer-create "*jupyter-repl-output*")
(let ((inhibit-read-only t))
(jupyter-repl-insert-ansi-coded-text text)
(display-buffer (current-buffer)))))
2018-05-22 21:58:33 -05:00
(t
(jupyter-repl-append-output client req
2018-05-22 21:58:33 -05:00
(jupyter-repl-insert-ansi-coded-text text))))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-fix-python-traceback-spacing (ename)
"Add spacing between the first occurance of ENAME and \"Traceback\".
Do this for the current cell."
(save-excursion
(jupyter-repl-previous-cell)
(when (and (search-forward ename nil t)
(looking-at "Traceback"))
(let ((len (- fill-column
jupyter-repl-prompt-margin-width
(- (point) (line-beginning-position))
(- (line-end-position) (point)))))
(jupyter-repl-insert
(make-string (if (> len 4) len 4) ? ))))))
2017-12-23 15:34:28 -06:00
(cl-defmethod jupyter-handle-error ((client jupyter-repl-client)
2018-08-27 20:46:03 -05:00
req ename _evalue traceback)
(when req
(setq traceback (concat (mapconcat #'identity traceback "\n") "\n"))
(cond
((eq (jupyter-message-parent-type
(jupyter-request-last-message req))
:comm-msg)
(jupyter-with-doc-buffer "traceback"
(jupyter-repl-insert-ansi-coded-text traceback)
(goto-char (point-min))
(pop-to-buffer (current-buffer))))
(t
(jupyter-repl-append-output client req
(jupyter-repl-insert-ansi-coded-text traceback)
(when (equal (jupyter-kernel-language client) "python")
(jupyter-repl-fix-python-traceback-spacing ename)))))))
2017-12-23 15:34:28 -06:00
2018-01-22 20:22:33 -06:00
(cl-defmethod jupyter-handle-input-reply ((client jupyter-repl-client) req prompt _password)
(jupyter-repl-append-output client req
2017-12-23 15:34:28 -06:00
(let ((value (cl-call-next-method)))
(jupyter-repl-insert (concat prompt value))
(jupyter-repl-newline))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-history--next (n)
"Helper function for `jupyter-repl-history-next'.
Rotates `jupyter-repl-history' N times in the forward direction,
towards newer history elements and returns the Nth history
element in that direction relative to the current REPL history.
If the sentinel value is found before rotating N times, return
nil."
(if (> n 0)
(if (eq (ring-ref jupyter-repl-history -1) 'jupyter-repl-history)
nil
(ring-insert jupyter-repl-history
(ring-remove jupyter-repl-history -1))
(jupyter-repl-history--next (1- n)))
(ring-ref jupyter-repl-history 0)))
(defun jupyter-repl-history-next (&optional n)
2018-01-14 00:03:31 -06:00
"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."
2017-12-31 10:16:41 -06:00
(interactive "p")
(or n (setq n 1))
(if (< n 0) (jupyter-repl-history-previous (- n))
(goto-char (point-max))
(let ((elem (jupyter-repl-history--next n)))
(if (and (null elem) (equal (jupyter-repl-cell-code) ""))
(error "End of history")
(if (null elem)
;; 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
(ring-ref jupyter-repl-history 0)))))))
(defun jupyter-repl-history--previous (n)
"Helper function for `jupyter-repl-history-previous'.
Rotates `jupyter-repl-history' N times in the backward direction,
towards older history elements and returns the Nth history
element in that direction relative to the current REPL history.
If the sentinel value is found before rotating N times, return
nil."
(if (> n 0)
(if (eq (ring-ref jupyter-repl-history 1) 'jupyter-repl-history)
nil
(ring-insert-at-beginning
jupyter-repl-history (ring-remove jupyter-repl-history 0))
(jupyter-repl-history--previous (1- n)))
(unless (eq (ring-ref jupyter-repl-history 0) 'jupyter-repl-history)
(ring-ref jupyter-repl-history 0))))
(defun jupyter-repl-history-previous (&optional n)
2018-01-14 00:03:31 -06:00
"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."
2017-12-31 10:16:41 -06:00
(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 ((elem (jupyter-repl-history--previous n)))
(if (null elem)
(error "Beginning of history")
(jupyter-repl-replace-cell-code
(ring-ref jupyter-repl-history 0))))))
2017-12-23 15:34:28 -06:00
2018-01-22 20:22:33 -06:00
(cl-defmethod jupyter-handle-history-reply ((client jupyter-repl-client) _req history)
(jupyter-with-repl-buffer client
(cl-loop for elem across history
for input-output = (aref elem 2)
do (ring-remove+insert+extend jupyter-repl-history input-output))))
2017-12-23 15:34:28 -06:00
(cl-defmethod jupyter-handle-is-complete-reply ((client jupyter-repl-client) _req status indent)
(jupyter-with-repl-buffer client
2017-12-23 15:34:28 -06:00
(pcase status
("complete"
(jupyter-send-execute-request client))
2017-12-23 15:34:28 -06:00
("incomplete"
2017-12-27 22:21:10 -06:00
(jupyter-repl-newline)
(if (= (length indent) 0) (jupyter-repl-indent-line)
(jupyter-repl-insert :read-only nil indent)))
2017-12-23 15:34:28 -06:00
("invalid"
;; Force an execute to produce a traceback
(jupyter-send-execute-request client))
2017-12-23 15:34:28 -06:00
("unknown"))))
2018-01-13 23:10:40 -06:00
(cl-defmethod jupyter-handle-shutdown-reply ((client jupyter-repl-client) _req restart)
(jupyter-with-repl-buffer client
2018-01-13 23:10:40 -06:00
(goto-char (point-max))
(add-text-properties (jupyter-repl-cell-beginning-position)
(jupyter-repl-cell-end-position)
'(read-only t))
(jupyter-repl-without-continuation-prompts
(jupyter-repl-newline)
(jupyter-repl-newline)
;; TODO: Add a slot mentioning that the kernel is shutdown so that we can
;; block sending requests or delay until it has restarted.
(jupyter-repl-insert
(propertize (concat "kernel " (if restart "restart" "shutdown"))
'font-lock-face 'warning))
(jupyter-repl-newline))))
(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."
2017-12-23 15:34:28 -06:00
(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.
(unless (member (oref jupyter-current-client execution-state)
'("starting" "idle"))
(jupyter-repl-sync-execution-state)
(error "Kernel busy"))
(if force (jupyter-send-execute-request jupyter-current-client)
(if (not jupyter-repl-use-builtin-is-complete)
(let* ((jupyter-inhibit-handlers '(:status))
(res (jupyter-wait-until-received :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)))
(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") ""))))))
(beginning-of-buffer
;; No cells in the current buffer, just insert one
(jupyter-repl-insert-prompt 'in))))
2017-12-23 15:34:28 -06:00
(cl-defgeneric jupyter-indent-line ()
(call-interactively #'indent-for-tab-command))
(cl-defmethod jupyter-indent-line (&context (major-mode julia-mode))
(call-interactively #'julia-latexsub-or-indent))
(defun jupyter-repl-indent-line ()
"Indent the line according to the language of the REPL."
(let* ((pos (jupyter-repl-cell-code-position))
(code (jupyter-repl-cell-code))
(replacement
;; TODO: Convert to using indirect buffers if
;; they are faster. We can keep an indirect
;; buffer around with the languages major mode.
;; This way we avoid copying the buffer strings
;; back and forth between buffers.
(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))
(buffer-string))))))
;; 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))))))
2017-12-23 15:34:28 -06:00
2017-12-27 21:55:58 -06:00
;;; Buffer change functions
(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."
(setq bound (set-marker (make-marker) bound))
(set-marker-insertion-type bound t)
(while (and (< (point) bound)
(search-forward "\n" bound 'noerror))
(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 (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)))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-after-buffer-change (beg end len)
2018-01-08 18:11:08 -06:00
"Insert line continuation prompts in `jupyter-repl-mode' buffers.
BEG, END, and LEN have the same meaning as in
`after-change-functions'."
2017-12-23 15:34:28 -06:00
(when (eq major-mode 'jupyter-repl-mode)
(cond
;; Insertions only
((= len 0)
(goto-char beg)
(when (jupyter-repl-cell-line-p)
;; 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)))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-kill-buffer-query-function ()
2018-01-08 18:11:08 -06:00
"Ask before killing a Jupyter REPL buffer.
If the REPL buffer is killed, stop the client. If the REPL client
is connected to a kernel with a `jupyter-kernel-manager', kill
the kernel.
In addition, exit `jupyter-repl-interaction-mode' in all buffers
associated with the REPL. 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))))
;; TODO: Handle case when multiple clients are connected, i.e. do we
;; want to also delete a kernel if this is the last client connected.
;; See `eieio-instance-tracker'.
(prog1 t
(jupyter-stop-channels jupyter-current-client)
(destructor jupyter-current-client)
(when (jupyter-repl-client-has-manager-p)
(jupyter-shutdown-kernel (oref jupyter-current-client manager))
(destructor (oref jupyter-current-client manager)))
(cl-loop
with client = jupyter-current-client
for buffer in (buffer-list)
do (with-current-buffer buffer
(when (eq jupyter-current-client client)
(jupyter-repl-interaction-mode -1)))))))))
2017-12-23 15:34:28 -06:00
(defun jupyter-repl-preserve-window-margins (&optional window)
"Ensure that the margins of a REPL window are present.
This function is added as a hook to `pre-redisplay-functions' to
2018-05-12 14:52:35 -05:00
ensure that a REPL windows margins 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."
2018-05-13 11:40:08 -05:00
;; 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))))
2017-12-23 15:34:28 -06:00
;;; Completion
(defconst jupyter-completion-argument-regexp
(rx
(group "(" (zero-or-more anything) ")")
(one-or-more anything) " "
(group (one-or-more anything)) ?: (group (one-or-more digit)))
"Regular expression to match arguments and file locations.")
;;; Helpers for completion interface
(defun jupyter-completion-symbol-beginning (&optional pos)
"Return the starting position of a completion symbol.
If POS is non-nil return the position of the symbol before POS
otherwise return the position of the symbol before point."
(save-excursion
(and pos (goto-char pos))
2018-10-08 18:52:57 -05:00
(+ (point) (skip-syntax-backward "w_."))))
;; Adapted from `company-grab-symbol-cons'
(defun jupyter-completion-grab-symbol-cons (re &optional max-len)
"Return the current completion prefix before point.
Return either a STRING or a (STRING . t) pair. If RE matches the
beginning of the current symbol before point, return the latter.
Otherwise return the symbol before point. If no completion can be
done at point, return nil.
MAX-LEN is the maximum number of characters to search behind the
begiining of the symbol at point to look for a match of RE."
2018-10-08 18:52:57 -05:00
(let ((symbol (if (or (looking-at "\\>\\|\\_>")
;; Complete operators
(eq (char-syntax (char-before)) ?.))
(buffer-substring-no-properties
2018-10-08 18:52:57 -05:00
(jupyter-completion-symbol-beginning) (point))
(unless (and (char-after)
(memq (char-syntax (char-after)) '(?w ?_)))
""))))
(when symbol
(save-excursion
(forward-char (- (length symbol)))
(if (looking-back re (if max-len
(- (point) max-len)
(line-beginning-position)))
(cons symbol t)
symbol)))))
(defun jupyter-completion-number-p ()
"Return non-nil if the text before `point' may be a floating point number."
(and (char-before)
(or (<= ?0 (char-before) ?9)
(eq (char-before) ?.))
(save-excursion
(skip-syntax-backward "w.")
(looking-at-p "[0-9]+\\.?[0-9]*"))))
;;; Extracting arguments from argument strings
(defun jupyter-completion--arg-extract-1 (pos)
"Helper function for `arg-extract-top'.
2018-10-01 23:13:07 -05:00
Extract the arguments starting at POS, narrowing to the first
SEXP before extraction."
(save-restriction
(goto-char pos)
(narrow-to-region
pos (save-excursion (forward-sexp) (point)))
(jupyter-completion--arg-extract)))
(defun jupyter-completion--arg-extract ()
"Extract arguments from an argument string.
Works for Julia and Python."
(let (arg-info
inner-args ppss depth inner
(start (1+ (point-min)))
(get-sexp
(lambda ()
(buffer-substring-no-properties
(point) (progn (forward-sexp) (point)))))
(get-string
(lambda (start)
(string-trim
(buffer-substring-no-properties
start (1- (point)))))))
(while (re-search-forward ",\\|::" nil t)
(setq ppss (syntax-ppss)
depth (nth 0 ppss)
inner (nth 1 ppss))
(cl-case (char-before)
(?:
(if (eq (char-after) ?{)
(push (jupyter-completion--arg-extract-1 (point)) inner-args)
(push (list (list (funcall get-sexp))) inner-args)))
(?,
(if (/= depth 1)
(push (jupyter-completion--arg-extract-1 inner) inner-args)
(push (cons (funcall get-string start) (pop inner-args))
arg-info)
(setq start (1+ (point)))))))
(goto-char (point-max))
(push (cons (funcall get-string start) (pop inner-args)) arg-info)
(nreverse arg-info)))
(defun jupyter-completion--make-arg-snippet (args)
"Construct a snippet from ARGS."
(cl-loop
with i = 1
for top-args in args
;; TODO: Handle nested arguments
for (arg . inner-args) = top-args
collect (format "${%d:%s}" i arg) into constructs
and do (setq i (1+ i))
finally return
(concat "(" (mapconcat #'identity constructs ", ") ")")))
;;; Getting the completion context
(cl-defmethod jupyter-code-context ((_type (eql inspect))
&context (major-mode jupyter-repl-mode))
(jupyter-line-context (next-single-property-change
(line-beginning-position) 'invisible)))
(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))))
2018-01-13 22:57:50 -06:00
(cl-defgeneric jupyter-completion-prefix (&optional (re string) max-len)
2018-01-13 22:57:50 -06:00
"Return the prefix for the current completion context.
The default method calls `jupyter-completion-grab-symbol-cons'
with RE and MAX-LEN as arguments, RE defaulting to \"\\\\.\". It
also handles argument lists surrounded by parentheses specially
by considering an open parentheses and the symbol before it as a
completion prefix since some kernels will complete argument lists
if given such a prefix.
2018-01-13 22:57:50 -06:00
Note that the prefix returned is not the content sent to the
kernel, but the prefix used by `jupyter-completion-at-point'. See
`jupyter-code-context' for what is actually sent to the kernel."
(or re (setq re "\\."))
(cond
;; Completing argument lists
((and (char-before)
(eq (char-syntax (char-before)) ?\()
(or (not (char-after))
(looking-at-p "\\_>")
(not (memq (char-syntax (char-after)) '(?w ?_)))))
(buffer-substring-no-properties
(jupyter-completion-symbol-beginning (1- (point)))
(point)))
;; FIXME: Needed for cases where all completions are retrieved
;; from Base.| and the prefix turns empty again after
;; Base.REPLCompletions)|
;;
;; Actually the problem stems from stting the prefix length to 0
;; in company in the case Base.| and we have not selected a
;; completion and just pass over it.
((and (looking-at-p "\\_>")
(eq (char-syntax (char-before)) ?\)))
nil)
(t
(unless (jupyter-completion-number-p)
(jupyter-completion-grab-symbol-cons re max-len)))))
(cl-defmethod jupyter-completion-prefix (&context (major-mode jupyter-repl-mode))
(and (not (get-text-property (point) 'read-only))
(cl-call-next-method)))
(cl-defmethod jupyter-completion-prefix (&context (jupyter-lang julia))
(cl-call-next-method "\\\\\\|\\.\\|::\\|->" 2))
(defun jupyter-completion-construct-candidates (matches metadata)
"Construct candidates for completion.
2018-01-13 22:57:50 -06:00
MATCHES are the completion matches returned by the kernel,
METADATA is any extra data associated with MATCHES that was
supplied by the kernel."
(let* ((matches (append matches nil))
(tail matches)
(types (append (plist-get metadata :_jupyter_types_experimental) nil))
(buf))
(save-current-buffer
(unwind-protect
(while tail
(cond
((string-match jupyter-completion-argument-regexp (car tail))
(let* ((str (car tail))
(args-str (match-string 1 str))
(end (match-end 1))
(path (match-string 2 str))
(line (string-to-number (match-string 3 str)))
(snippet (progn
(unless buf
(setq buf (generate-new-buffer " *temp*"))
(set-buffer buf))
(insert args-str)
(goto-char (point-min))
(prog1 (jupyter-completion--make-arg-snippet
(jupyter-completion--arg-extract))
(erase-buffer)))))
(setcar tail (substring (car tail) 0 end))
(put-text-property 0 1 'snippet snippet (car tail))
(put-text-property 0 1 'location (cons path line) (car tail))
(put-text-property 0 1 'docsig (car tail) (car tail))))
;; TODO: This is specific to the results that
;; the python kernel returns, make a support
;; function?
((string-match-p "\\." (car tail))
(setcar tail (car (last (split-string (car tail) "\\."))))))
(setq tail (cdr tail)))
(when buf (kill-buffer buf))))
;; When a type is supplied add it as an annotation
2018-01-13 22:57:50 -06:00
(when types
(let ((max-len (apply #'max (mapcar #'length matches))))
2018-05-13 09:06:55 -05:00
(cl-mapc
2018-01-13 22:57:50 -06:00
(lambda (match meta)
2018-05-20 12:09:00 -05:00
(let* ((prefix (make-string (1+ (- max-len (length match))) ? ))
(annot (concat prefix (plist-get meta :type))))
(put-text-property 0 1 'annot annot match)))
2018-01-13 22:57:50 -06:00
matches types)))
matches))
;;; Completion at point interface
(defvar jupyter-completion-cache nil
"The cache for completion candidates.
A list that can take the following forms
(PREFIX . CANDIDATES)
(fetched PREFIX MESSAGE)
The first form states that the list of CANDIDATES is for the
prefix, PREFIX.
The second form signifies that the CANDIDATES for PREFIX must be
extracted from MESSAGE and converted to the first form.")
(defun jupyter-completion-prefetch-p (prefix)
"Return non-nil if a prefetch for PREFIX should be performed.
Looks at `jupyter-completion-cache' to determine if its
candidates can be used for PREFIX."
(not (and jupyter-completion-cache
(if (eq (car jupyter-completion-cache) 'fetched)
(equal (nth 1 jupyter-completion-cache) prefix)
(or (equal (car jupyter-completion-cache) prefix)
(and (not (string= (car jupyter-completion-cache) ""))
(string-prefix-p (car jupyter-completion-cache) prefix))))
;; Invalidate the cache when completing argument lists
(or (string= prefix "")
(not (eq (aref prefix (1- (length prefix))) ?\())))))
(defun jupyter-completion-prefetch (fun)
"Get completions for the current completion context.
Run FUN when the completions are available."
(cl-destructuring-bind (code pos)
(jupyter-code-context 'completion)
(let ((req (let ((jupyter-inhibit-handlers t))
(jupyter-send-complete-request
jupyter-current-client
:code code :pos pos))))
(prog1 req
(jupyter-add-callback req :complete-reply fun)))))
(defvar jupyter-completion--company-timer nil)
(defun jupyter-completion--company-idle-begin ()
"Trigger an idle completion."
(when jupyter-completion--company-timer
(cancel-timer jupyter-completion--company-timer))
(setq jupyter-completion--company-timer
;; NOTE: When we reach here `company-idle-delay' is `now' since
;; we are already inside a company completion so we can't use
;; it, just use a sensible time value instead.
(run-with-idle-timer
0.1 nil
(lambda ()
(defvar company-minimum-prefix-length)
(let ((company-minimum-prefix-length 0))
(when (company-auto-begin)
(company-input-noop)
(let ((this-command 'company-idle-begin))
(company-post-command))))))))
(defun jupyter-completion-at-point ()
"Function to add to `completion-at-point-functions'."
(when jupyter-current-client
(let ((prefix (jupyter-completion-prefix)) req)
(when prefix
(when (consp prefix)
(setq prefix (car prefix))
(when (and (bound-and-true-p company-mode)
(< (length prefix) company-minimum-prefix-length))
(jupyter-completion--company-idle-begin)))
(when (jupyter-completion-prefetch-p prefix)
(setq jupyter-completion-cache nil
req (jupyter-completion-prefetch
(lambda (msg) (setq jupyter-completion-cache
(list 'fetched prefix msg))))))
(list
(- (point) (length prefix)) (point)
(completion-table-dynamic
(lambda (_)
(when (and req (not (jupyter-request-idle-received-p req))
(not (eq (jupyter-message-type
(jupyter-request-last-message req))
:complete-reply)))
(jupyter-wait-until-received :complete-reply req))
(when (eq (car jupyter-completion-cache) 'fetched)
(cl-destructuring-bind (&key status
matches metadata
&allow-other-keys)
(jupyter-message-content (nth 2 jupyter-completion-cache))
(setq jupyter-completion-cache
(cons (nth 1 jupyter-completion-cache)
(when (equal status "ok")
(jupyter-completion-construct-candidates
matches metadata))))))
(cdr jupyter-completion-cache)))
:exit-function
#'jupyter-completion--post-completion
:company-location
(lambda (arg) (get-text-property 0 'location arg))
:annotation-function
(lambda (arg) (get-text-property 0 'annot arg))
:company-docsig
(lambda (arg) (get-text-property 0 'docsig arg))
:company-doc-buffer
#'jupyter-completion--company-doc-buffer)))))
(defun jupyter-completion--company-doc-buffer (arg)
"Send an inspect request for ARG to the kernel.
Use the `company-doc-buffer' to insert the results."
(let ((buf (company-doc-buffer)))
(jupyter-inspect arg nil buf)
(with-current-buffer buf
(when (> (point-max) (point-min))
(let ((inhibit-read-only t))
(remove-text-properties
(point-min) (point-max) '(read-only))
(current-buffer))))))
(defun jupyter-completion--post-completion (arg status)
"If ARG is a completion with a snippet, expand the snippet.
Do this only if STATUS is sole or finished."
(when (and (memq status '(sole finished))
(get-text-property 0 'snippet arg))
(when (and (require 'yasnippet nil t)
(not yas-minor-mode))
(yas-minor-mode 1))
;; Due to packages like smartparens
(when (eq (char-after) ?\))
(delete-char 1))
(yas-expand-snippet
(get-text-property 0 'snippet arg)
(save-excursion
(forward-sexp -1)
(point))
(point))))
2017-12-23 15:34:28 -06:00
2018-01-16 11:44:13 -06:00
;;; Inspection
;; TODO: How to add hover documentation support
2018-01-16 11:44:13 -06:00
(defun jupyter-inspect (code &optional pos buffer detail)
"Inspect CODE.
Send an `:inspect-request' to the `jupyter-current-client' of the
`current-buffer' and display the results in a BUFFER.
2018-01-22 19:55:14 -06:00
CODE is the code to inspect and POS is your position in the CODE.
If POS is nil, it defaults to the length of CODE.
If BUFFER is nil, display the results in an inspect buffer.
Otherwise insert the results in BUFFER but do not display it.
2018-01-22 19:55:14 -06:00
DETAIL is the detail level to use for the request and defaults to
0."
(let* ((jupyter-inhibit-handlers '(:status))
(msg (jupyter-wait-until-received :inspect-reply
(jupyter-send-inspect-request jupyter-current-client
:code code :pos (or pos (length code)) :detail detail))))
(if msg
(cl-destructuring-bind
(&key status found data metadata &allow-other-keys)
(jupyter-message-content msg)
(if (and (equal status "ok") found)
(let ((client jupyter-current-client)
(display-p
(unless buffer
(setq buffer (jupyter-with-doc-buffer "inspect"
(current-buffer))))))
(with-current-buffer buffer
(setq jupyter-current-client client)
(jupyter-repl-insert-data data metadata)
2018-09-05 09:51:06 -05:00
(goto-char (point-min))
(when display-p
(display-buffer (current-buffer))
(set-window-start (get-buffer-window) (point-min)))))
(message "Nothing found for %s" code)))
(message "Inspect timed out"))))
2018-01-16 11:44:13 -06:00
(defun jupyter-inspect-at-point (&optional buffer detail)
"Inspect the code at point.
Send an `:inspect-request' to the `jupyter-current-client' of the
`current-buffer' and display the results in a BUFFER. If BUFFER
is nil, display the results in an inspect buffer. Otherwise
insert the results in BUFFER but do not display them.
DETAIL is the detail level to use for the request and defaults to
0."
(interactive (list nil 0))
(cl-destructuring-bind (code pos)
(jupyter-code-context 'inspect)
(jupyter-inspect code pos buffer detail)))
;;; Evaluation
(defun jupyter-repl-eval-string (str &optional silently)
"Evaluate STR with the `jupyter-current-client's REPL.
Replaces the contents of the last cell in the REPL buffer with
STR before evaluating.
If the result of evaluation is more than 10 lines long, a buffer
displaying the results is shown. For results less than 10 lines
long, the result is displayed in the minibuffer.
If a prefix argument is given, SILENTLY evaluate STR without any
modification to the REPL buffer. Only the results of evaluation
are displayed."
2018-02-04 18:22:56 -06:00
(interactive (list (read-string "Jupyter Eval: ") current-prefix-arg))
(unless (buffer-local-value
'jupyter-current-client (current-buffer))
(user-error "No `jupyter-current-client' set, see `jupyter-repl-associate-buffer'"))
(jupyter-with-repl-buffer jupyter-current-client
(goto-char (point-max))
(unless (= (save-excursion (jupyter-repl-previous-cell)) 0)
(jupyter-repl-insert-prompt 'in))
(setq str (if silently (string-trim str)
(prog1 nil
(jupyter-repl-replace-cell-code str))))
(let* ((jupyter-inhibit-handlers (or (and silently t) '(:execute-result)))
(req (jupyter-send-execute-request jupyter-current-client
:code str)))
(jupyter-add-callback req
:execute-reply (lambda (msg)
2018-02-04 17:57:18 -06:00
(cl-destructuring-bind (&key status ename evalue
&allow-other-keys)
(jupyter-message-content msg)
(unless (equal status "ok")
(message "jupyter (%s): %s" ename
2018-05-13 09:09:13 -05:00
(ansi-color-apply evalue)))))
:execute-result
(lambda (msg)
(let ((res (jupyter-message-data msg :text/plain))
(inhibit-read-only t))
;; Prioritize the text representation
(if res
(if (and (jupyter-repl-multiline-p res)
(cl-loop
with nlines = 0
for c across res when (eq c ?\n) do (cl-incf nlines)
thereis (> nlines 10)))
(jupyter-with-doc-buffer "result"
(insert res)
(goto-char (point-min))
2018-05-15 17:47:33 -05:00
(display-buffer (current-buffer)))
(if (equal res "") (message "jupyter: eval done")
(message res)))
(with-current-buffer
(get-buffer-create "*jupyter-repl-result*")
(erase-buffer)
(jupyter-repl-insert-data
(jupyter-message-get msg :data)
(jupyter-message-get msg :metadata))
(goto-char (point-min))
(switch-to-buffer-other-window (current-buffer)))))))
req)))
(defun jupyter-repl-eval-file (file)
"Send the contents of FILE using `jupyter-current-client'."
(interactive
(list (read-file-name "File name: " nil nil nil
(file-name-nondirectory
(or (buffer-file-name) "")))))
(message "Evaluating %s..." file)
(setq file (expand-file-name file))
(if (file-exists-p file)
(let* ((buf (find-buffer-visiting file))
(killp (null buf)))
(when (null buf)
(setq buf (delay-mode-hooks (find-file-noselect file))))
(with-current-buffer buf
(jupyter-repl-eval-string (buffer-string) 'silently))
(when killp
(kill-buffer)))
(error "Not a file (%s)" file)))
2018-01-17 20:48:53 -06:00
(defun jupyter-repl-eval-region (beg end &optional silently)
"Evaluate a region with the `jupyter-current-client'.
BEG and END are the beginning and end of the region to evaluate.
2018-01-22 19:55:14 -06:00
SILENTLY has the same meaning as in `jupyter-repl-eval-string'."
2018-02-04 18:22:56 -06:00
(interactive "rP")
(jupyter-repl-eval-string
2018-01-17 20:48:53 -06:00
(buffer-substring-no-properties beg end) silently))
(defun jupyter-repl-eval-line-or-region ()
"Evaluate the current line or region with the `jupyter-current-client'.
If the current region is active send the current region using
`jupyter-repl-eval-region', otherwise send the current line."
(interactive)
(if (use-region-p)
(jupyter-repl-eval-region (region-beginning) (region-end))
(jupyter-repl-eval-region (line-beginning-position) (line-end-position))))
;;; Kernel management
(defun jupyter-repl-on-kernel-restart (client msg)
"Update the REPL buffer after CLIENT restarts.
If MSG is a startup message, insert the banner of the kernel,
synchronize the execution state, and insert a new input prompt."
(prog1 nil
(jupyter-with-repl-buffer client
(when (jupyter-message-status-starting-p msg)
;; FIXME: Don't assume `jupyter-include-other-output' was previously nil
(jupyter-set jupyter-current-client 'jupyter-include-other-output nil)
(jupyter-repl-without-continuation-prompts
(goto-char (point-max))
(jupyter-repl-previous-cell)
(jupyter-repl-finalize-cell nil)
(jupyter-repl-newline)
(jupyter-repl-insert-banner
(plist-get (jupyter-kernel-info client) :banner))
(jupyter-repl-sync-execution-state)
(jupyter-repl-insert-prompt 'in))))))
(defun jupyter-repl-interrupt-kernel ()
"Interrupt the kernel if possible.
A kernel can be interrupted if it was started using a
`jupyter-kernel-manager'. See `jupyter-start-new-kernel'."
(interactive)
(if (not (jupyter-repl-client-has-manager-p))
(user-error "Cannot interrupt non-subprocess kernels")
(message "Interrupting kernel")
(jupyter-interrupt-kernel
(oref jupyter-current-client manager))))
;; TODO: Make timeouts configurable
2018-05-13 09:06:55 -05:00
(defun jupyter-repl-restart-kernel (&optional shutdown)
"Restart the kernel.
With a prefix argument, SHUTDOWN the kernel completely instead."
(interactive "P")
(unless shutdown
;; This may have been set to t due to a non-responsive kernel so make sure
;; that we try again when restarting.
(setq-local jupyter-repl-use-builtin-is-complete nil)
;; When restarting, the startup message is not associated with any request
;; so ensure that we are able to capture it.
(jupyter-set jupyter-current-client 'jupyter-include-other-output t))
(if (jupyter-repl-client-has-manager-p)
(let ((manager (oref jupyter-current-client manager)))
(cond
((jupyter-kernel-alive-p manager)
(message "%s kernel..." (if shutdown "Shutting down"
"Restarting"))
(jupyter-shutdown-kernel manager (not shutdown)))
(t
(message "Starting dead kernel...")
(jupyter-start-kernel manager))))
2018-05-13 09:06:55 -05:00
(unless (jupyter-wait-until-received :shutdown-reply
(jupyter-send-shutdown-request jupyter-current-client
2018-05-13 09:06:55 -05:00
:restart (not shutdown)))
(jupyter-set jupyter-current-client 'jupyter-include-other-output nil)
(message "Kernel did not respond to shutdown request"))))
(defun jupyter-repl-display-kernel-buffer ()
"Display the kernel processes stdout."
(interactive)
(if (jupyter-repl-client-has-manager-p)
(let ((manager (oref jupyter-current-client manager)))
(display-buffer (process-buffer (oref manager kernel))))
(user-error "Kernel not a subprocess")))
;;; Isearch
;; Adapted from isearch in `comint', see `comint-history-isearch-search' for
;; details
(defun jupyter-repl-isearch-setup ()
2018-01-22 19:55:14 -06:00
"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 ()
2018-01-22 19:55:14 -06:00
"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
2018-02-04 18:18:19 -06:00
;; `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 ()
2018-01-22 19:55:14 -06:00
"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."
(if isearch-forward
(jupyter-repl-history--previous (ring-length jupyter-repl-history))
(jupyter-repl-history--next (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 ()
2018-01-22 19:55:14 -06:00
"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
(if isearch-forward
(jupyter-repl-history--next (ring-length jupyter-repl-history))
(jupyter-repl-history--previous (ring-length jupyter-repl-history))))
(while (not (eq (ring-ref jupyter-repl-history 0) elem))
(if isearch-forward
(jupyter-repl-history--previous 1)
(jupyter-repl-history--next 1)))
(jupyter-repl-replace-cell-code (ring-ref jupyter-repl-history 0)))))
(t
(let ((elem code))
(lambda (_cmd)
(jupyter-repl-replace-cell-code elem)))))))
2018-01-13 23:03:22 -06:00
;;; `jupyter-repl-mode'
(defun jupyter-repl-scratch-buffer ()
"Display a scratch buffer associated with the current REPL buffer."
(interactive)
(if (jupyter-repl-connected-p)
(let ((client jupyter-current-client))
(with-current-buffer (get-buffer-create
(concat "*jupyter-scratch*"))
(funcall (jupyter-repl-language-mode client))
(jupyter-repl-associate-buffer client)
(pop-to-buffer (current-buffer))))
(error "Not in a valid REPL buffer")))
2018-01-13 23:03:22 -06:00
(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 "C-n") #'jupyter-repl-history-next)
(define-key map (kbd "C-p") #'jupyter-repl-history-previous)
(define-key map (kbd "M-n") #'jupyter-repl-history-next)
(define-key map (kbd "M-p") #'jupyter-repl-history-previous)
map))
2018-02-04 18:18:19 -06:00
;; TODO: Gaurd against a major mode change
(put 'jupyter-repl-mode 'mode-class 'special)
2017-12-23 15:34:28 -06:00
(define-derived-mode jupyter-repl-mode fundamental-mode
"Jupyter-REPL"
"A Jupyter REPL major mode."
(cl-check-type jupyter-current-client jupyter-repl-client)
2018-05-27 23:29:58 -05:00
;; 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 filter-buffer-substring-function #'jupyter-repl-filter-substring)
;; 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-info (plist-get info :language_info))
(language (plist-get language-info :name)))
(cl-destructuring-bind (mode syntax)
(jupyter-repl-kernel-language-mode-properties language-info)
(setq-local jupyter-repl-lang-mode mode)
(setq-local jupyter-repl-lang-buffer
(get-buffer-create
(format " *jupyter-repl-lang-%s*"
2018-05-15 16:21:08 -05:00
(plist-get language-info :name))))
(set-syntax-table syntax)
(jupyter-with-repl-lang-buffer
(unless (eq major-mode mode)
(funcall mode))))
;; Get history from kernel
(setq-local 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 'after-change-functions 'jupyter-repl-after-buffer-change nil t)
(add-hook 'pre-redisplay-functions 'jupyter-repl-preserve-window-margins nil t)
;; Initialize the REPL
(buffer-disable-undo)
(jupyter-repl-initialize-hooks)
(jupyter-repl-initialize-fontification)
(jupyter-repl-isearch-setup)
(jupyter-repl-sync-execution-state)
(jupyter-repl-interaction-mode)))
(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 if the REPL is python the method signature
would be
(cl-defmethod jupyter-repl-after-init (&context (jupyter-lang python)))"
nil)
(cl-defmethod jupyter-repl-after-init (&context (jupyter-lang javascript)
(jupyter-repl-mode js2-mode))
"If `js2-mode' is used for Javascript kernels, enable syntax highlighting.
`js2-mode' does not use `font-lock-defaults', but their own
custom method."
(add-hook 'after-change-functions
(lambda (_beg _end len)
;; Insertions only
(when (= len 0)
(unless (jupyter-repl-cell-finalized-p)
(let ((cbeg (jupyter-repl-cell-code-beginning-position))
(cend (jupyter-repl-cell-code-end-position)))
(save-restriction
(narrow-to-region cbeg cend)
(js2-parse)
(js2-mode-apply-deferred-properties))))))
t t))
(add-hook 'jupyter-repl-mode-hook 'jupyter-repl-after-init)
(defun jupyter-repl-initialize-hooks ()
"Initialize startup hooks.
When the kernel restarts, insert a new prompt."
;; NOTE: This hook will only run if `jupyter-include-other-output' is non-nil
;; during the restart.
(jupyter-add-hook jupyter-current-client 'jupyter-iopub-message-hook
(apply-partially
#'jupyter-repl-on-kernel-restart jupyter-current-client)))
2018-01-11 03:28:04 -06:00
(defun jupyter-repl-initialize-fontification ()
"Initialize fontification for the current REPL buffer."
(let (fld sff spf)
(jupyter-with-repl-lang-buffer
(setq fld font-lock-defaults
sff font-lock-syntactic-face-function
spf syntax-propertize-function))
;; 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)
fld
(setq font-lock-defaults
(apply #'list kws kws-only case-fold syntax-alist
(append vars
(list
(cons 'font-lock-syntactic-face-function
(lambda (state)
(unless (get-text-property
(nth 8 state) 'font-lock-fontified)
(when sff (funcall sff state)))))))))
(setq-local syntax-propertize-function spf))
(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
(let ((start (point)))
(jupyter-repl-insert banner)
(jupyter-repl-newline)
(add-text-properties start (point) '(font-lock-face shadow fontified t)))))
(defun jupyter-repl-sync-execution-state ()
"Synchronize the `jupyter-current-client's kernel state."
(let* ((client jupyter-current-client)
(req (let ((jupyter-inhibit-handlers t))
(jupyter-send-execute-request client :code "" :silent t))))
(jupyter-add-callback req
:status (lambda (msg)
(oset client execution-state
(jupyter-message-get msg :execution_state)))
2018-01-11 03:28:04 -06:00
:execute-reply (lambda (msg)
(oset client execution-count
(1+ (jupyter-message-get msg :execution_count)))))
2018-05-13 11:40:08 -05:00
;; FIXME: 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 2)))
2017-12-23 15:34:28 -06:00
;;; `jupyter-repl-interaction-mode'
(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)
"Return a list of REPL buffers that are connected to live kernels.
2018-02-04 18:18:19 -06:00
If MODE is non-nil, return all REPL buffers whose
`jupyter-repl-lang-mode' is MODE."
2018-01-18 16:43:04 -06:00
(delq
nil
(mapcar (lambda (b)
(with-current-buffer b
(and (eq major-mode 'jupyter-repl-mode)
(if mode (eq mode jupyter-repl-lang-mode) t)
2018-02-03 21:49:46 -06:00
(jupyter-repl-connected-p)
2018-01-18 16:43:04 -06:00
(buffer-name b))))
(buffer-list))))
;;;###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, enable
`jupyter-repl-interaction-mode'.
CLIENT should be a `jupyter-repl-client' or a subclass thereof.
If CLIENT is a buffer or the name of a buffer, use the
`jupyter-current-client' local to the buffer."
(interactive
(list
(let ((repls (jupyter-repl-available-repl-buffers major-mode)))
(if repls (completing-read "jupyter-repl: " repls nil t)
nil))))
(if (not client)
(when (y-or-n-p "No REPL for `major-mode' exists. Start one? ")
(call-interactively #'run-jupyter-repl))
(unless (eq (jupyter-repl-language-mode client) major-mode)
(error "Cannot associate buffer to REPL. Wrong `major-mode'"))
(setq client (if (or (bufferp client) (stringp client))
(with-current-buffer client
jupyter-current-client)
client))
(cl-check-type client jupyter-repl-client)
(setq-local jupyter-current-client client)
(unless jupyter-repl-interaction-mode
(jupyter-repl-interaction-mode))))
(defvar jupyter-repl-interaction-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'jupyter-repl-eval-line-or-region)
(define-key map (kbd "C-c C-l") #'jupyter-repl-eval-file)
(define-key map (kbd "C-c C-f") #'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))
(defun jupyter-repl-propagate-client (orig-fun buffer-or-name &rest args)
"Propagate the `jupyter-current-client' to other buffers."
(when jupyter-repl-interaction-mode
(let ((client jupyter-current-client)
(buf (get-buffer buffer-or-name))
(mode major-mode))
(when buf
(with-current-buffer buf
(when (and (eq mode major-mode)
(not jupyter-repl-interaction-mode))
(jupyter-repl-associate-buffer client))))))
(apply orig-fun buffer-or-name args))
(advice-add 'switch-to-buffer :around #'jupyter-repl-propagate-client)
(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-map}"
:group 'jupyter-repl
:lighter " JuPy"
:init-value nil
:keymap jupyter-repl-interaction-map
(if jupyter-repl-interaction-mode
(add-hook 'completion-at-point-functions 'jupyter-completion-at-point nil t)
(remove-hook 'completion-at-point-functions 'jupyter-completion-at-point t)
(unless (eq major-mode 'jupyter-repl-mode)
(kill-local-variable 'jupyter-current-client))))
(defun jupyter-repl-kernel-language-mode-properties (language-info)
"Get the `major-mode' info of a kernel's language.
LANGUAGE-INFO should be the plist of the `:language_info' key in
a kernel's kernel-info. The `major-mode' is found by consulting
`auto-mode-alist' using the language's file extension found in
LANGUAGE-INFO. Return a list
(MODE SYNTAX-TABLE)
Where MODE is the `major-mode' to use for syntax highlighting
purposes and SYNTAX-TABLE is the syntax table of MODE."
(cl-destructuring-bind (&key file_extension &allow-other-keys)
language-info
(with-temp-buffer
(let ((buffer-file-name
(concat "jupyter-repl-lang" file_extension)))
(delay-mode-hooks (set-auto-mode)))
(list major-mode (syntax-table)))))
(defun jupyter-repl--new-repl (client)
"Initialize a new REPL buffer based on CLIENT.
2018-05-26 20:04:02 -05:00
CLIENT is a `jupyter-repl-client' already connected to its kernel
and has a non-nil kernel-info slot.
A new REPL buffer communicating with CLIENT's kernel is created
2018-05-26 20:04:02 -05:00
and set as CLIENT's buffer slot. If CLIENT already has a non-nil
buffer slot, raise an error."
(if (oref client buffer) (error "Client already has a REPL 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]*"
(concat language-name " " language-version))))
(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 run-jupyter-repl (kernel-name &optional associate-buffer client-class)
"Run a Jupyter REPL connected to a kernel with name, KERNEL-NAME.
KERNEL-NAME will be passed to `jupyter-find-kernelspecs' and the
first kernel found will be used to start the new kernel.
Optional argument ASSOCIATE-BUFFER, if non-nil, means to enable
`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 subclass of
`jupyter-repl-client', which is the default.
When called interactively, display the new REPL buffer.
Otherwise, in a non-interactive call, return the
`jupyter-repl-client' connect to the kernel."
(interactive (list (car (jupyter-completing-read-kernelspec
nil current-prefix-arg))
t
nil))
(or client-class (setq client-class 'jupyter-repl-client))
(unless (called-interactively-p 'interactive)
(setq kernel-name (caar (jupyter-find-kernelspecs kernel-name))))
(unless kernel-name
(error "No kernel found for prefix (%s)" kernel-name))
(unless (child-of-class-p client-class 'jupyter-repl-client)
(error "Class should be a subclass of `jupyter-repl-client' (`%s')" client-class))
(cl-destructuring-bind (_manager client)
(jupyter-start-new-kernel kernel-name client-class)
(jupyter-repl--new-repl client)
(when (and associate-buffer
2018-02-09 17:21:10 -06:00
(eq major-mode (jupyter-repl-language-mode client)))
(jupyter-repl-associate-buffer client))
(when (called-interactively-p 'interactive)
(pop-to-buffer (oref client buffer)))
client))
2017-12-23 15:34:28 -06:00
;;;###autoload
(defun connect-jupyter-repl (file-or-plist &optional associate-buffer client-class)
"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 `run-jupyter-repl'.
Optional argument CLIENT-CLASS is the class of the client that
will be used to initialize the REPL and should be a subclass of
`jupyter-repl-client', which is the default.
2018-05-13 09:06:55 -05:00
Return the `jupyter-repl-client' connected to the kernel. When
called interactively, display the new REPL buffer as well."
(interactive (list (read-file-name "Connection file: ") t nil))
(or client-class (setq client-class 'jupyter-repl-client))
(unless (child-of-class-p client-class 'jupyter-repl-client)
(error "Class should be a subclass of `jupyter-repl-client' (`%s')" client-class))
(let ((client (make-instance client-class)))
(jupyter-initialize-connection client file-or-plist)
(jupyter-start-channels client)
(jupyter-repl--new-repl client)
(when (and associate-buffer
(eq major-mode (jupyter-repl-language-mode client)))
(jupyter-repl-associate-buffer client))
(when (called-interactively-p 'interactive)
(pop-to-buffer (oref client buffer)))
client))
2018-01-08 21:38:32 -06:00
(provide 'jupyter-repl)
2018-01-08 21:38:32 -06:00
2017-12-23 15:34:28 -06:00
;; Local Variables:
;; byte-compile-warnings: (not free-vars)
;; End:
;;; jupyter-repl.el ends here