;;; jupyter-repl-client.el --- A Jupyter REPL client -*- lexical-binding: t -*- ;; Copyright (C) 2018 Nathaniel Nicandro ;; Author: Nathaniel Nicandro ;; 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: ;; A Jupyter REPL for Emacs. ;; ;; The main entry points are `run-jupyter-repl' and `connect-jupyter-repl'. ;; ;; 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. ;; ;; 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. ;; ;; `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-repl-inspect-at-point' ;; C-c C-r `jupyter-repl-restart-kernel' ;; C-c C-i `jupyter-repl-interrupt-kernel' ;; C-c C-z `jupyter-repl-pop-to-buffer' ;;; Code: (defgroup jupyter-repl nil "A Jupyter REPL client" :group 'jupyter) (require 'jupyter-base) (require 'jupyter-client) (require 'jupyter-widget-client) (require 'jupyter-kernel-manager) (require 'shr) (require 'ring) (require 'ansi-color) (declare-function company-begin-backend "company" (backend &optional callback)) (declare-function company-doc-buffer "company" (&optional string)) (declare-function company-grab-symbol-cons "company" (idle-begin-after-re &optional max-len)) (declare-function org-format-latex "org" (prefix &optional beg end dir overlays msg forbuffer processing-type)) (declare-function markdown-link-at-pos "markdown-mode" (pos)) (declare-function markdown-follow-link-at-point "markdown-mode") ;; TODO: Fallbacks for when the language doesn't have a major mode installed. ;; TODO: Define `jupyter-kernel-manager-after-restart-hook' to update the ;; execution count after a restart. More generally, define more ways to hook ;; into differnt events of the client/kernel interaction. ;;; User variables (defface jupyter-repl-input-prompt '((((class color) (min-colors 88) (background light)) :foreground "darkseagreen2") (((class color) (min-colors 88) (background dark)) :foreground "darkolivegreen")) "Face used for the input prompt." :group 'jupyter-repl) (defface jupyter-repl-output-prompt '((((class color) (min-colors 88) (background light)) :foreground "indianred3") (((class color) (min-colors 88) (background dark)) :foreground "darkred")) "Face used for the output prompt." :group 'jupyter-repl) (defcustom jupyter-repl-maximum-size 1024 "Maximum number of lines before the buffer is truncated." :type 'integer :group 'jupyter-repl) (defcustom jupyter-repl-maximum-is-complete-timeout 2 "Maximum number of seconds to wait for an is-complete reply. When no is-complete reply is received from the kernel within this timeout, the built-in is-complete handler is used." :type 'integer :group 'jupyter-repl) (defcustom jupyter-repl-history-maximum-length 100 "The maximum number of history elements to keep track of." :type 'integer :group 'jupyter-repl) (defcustom jupyter-repl-prompt-margin-width 12 "The width of the margin which displays prompt strings." :type 'integer :group 'jupyter-repl) ;;; Implementation (defclass jupyter-repl-client (jupyter-widget-client) ((buffer :type (or null buffer) :initform nil :documentation "The REPL buffer whose `jupyter-current-client' is this client.") (wait-to-clear :type boolean :initform nil :documentation "Whether or not we should wait to clear the current output of the cell. Set when the kernel sends a `:clear-output' message.") (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'.") (defvar jupyter-repl-lang-mode nil "The `major-mode' corresponding to the kernel's language.") (defvar jupyter-repl-history nil "The history of the current Jupyter REPL.") (defvar jupyter-repl-fontify-buffers nil "An alist of (MODE . BUFFER) pairs used for fontification. See `jupyter-repl-fontify-according-to-mode'.") (defvar-local jupyter-repl-use-builtin-is-complete nil "Whether or not to send `:is-complete-request's to a kernel. If a Jupyter kernel does not respond to an is_complete_request, the buffer local value of this variable is set to t and code in a cell is considered complete if the last line in a code cell is a blank line, i.e. if RET is pressed twice in a row.") (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 `:display-data' message is retrieved from this table and used to find the display in the REPL buffer. See `jupyter-repl-update-display'.") (cl-generic-define-context-rewriter jupyter-repl-mode (mode &rest modes) `(jupyter-repl-lang-mode (derived-mode ,mode ,@modes))) ;;; Macros (defmacro with-jupyter-repl-buffer (client &rest body) "Switch to CLIENT's buffer before running BODY. 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." (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))) `(with-jupyter-repl-buffer ,client (jupyter-repl-without-continuation-prompts (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 with-jupyter-repl-lang-buffer (&rest body) "Run BODY in the `jupyter-repl-lang-buffer' of the `current-buffer'. The contents of `jupyter-repl-lang-buffer' is erased before running BODY." (declare (indent 0) (debug (&rest form))) (let ((client (make-symbol "clientvar"))) `(let ((,client jupyter-current-client)) (with-current-buffer jupyter-repl-lang-buffer (let ((inhibit-read-only t) (jupyter-current-client ,client)) (erase-buffer) ,@body))))) (defmacro with-jupyter-repl-cell (&rest body) "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'." (declare (indent 0) (debug (&rest form))) `(save-excursion (save-restriction (narrow-to-region (jupyter-repl-cell-code-beginning-position) (jupyter-repl-cell-code-end-position)) (goto-char (jupyter-repl-cell-code-beginning-position)) ,@body))) (defun jupyter-repl-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 ). The buffer returned will have a `buffer-name' of \"*jupyter-repl-NAME*\". If a buffer with this name already exists, it is returned." (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 "") #'scroll-up))) buffer)) (defmacro with-jupyter-repl-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 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-repl-get-doc-buffer ,name))) (setq other-window-scroll-buffer ,buffer) (with-current-buffer ,buffer (let ((inhibit-read-only t)) (erase-buffer) ,@body))))) ;;; Convenience functions (defun jupyter-repl-language-mode (client) "Return the `major-mode' of CLIENT's kernel language." (with-jupyter-repl-buffer client jupyter-repl-lang-mode)) ;;; Text insertion (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 `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) (while (/= (setq next (next-property-change beg object end)) end) (when (eq prop 'face) (let ((val (get-text-property beg prop object))) (put-text-property beg next 'font-lock-face (or val 'default) object))) (setq beg next)))) (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." (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)) (defun jupyter-repl-fontify-according-to-mode (mode str) "Fontify a string according to MODE. In addition to fontifying STR, if MODE has a non-default `fill-forward-paragraph-function', STR will be filled using `fill-region'." (with-current-buffer (jupyter-repl-get-fontify-buffer mode) (let ((inhibit-modification-hooks nil)) (erase-buffer) (insert str) (font-lock-ensure)) (jupyter-repl-add-font-lock-properties (point-min) (point-max)) (when (not (memq fill-forward-paragraph-function '(forward-paragraph))) (fill-region (point-min) (point-max) t 'nosqueeze)) (buffer-string))) (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 \"\") - `: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." (let ((arg nil) (read-only t) (properties nil) (insert-fun #'insert)) (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))) (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)))) (defun jupyter-repl-newline () "Insert a read-only newline into the `current-buffer'." (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))) (defun jupyter-repl-insert-html (html) "Parse and insert the HTML string using `shr-insert-document'." (jupyter-repl-insert ;; `save-excursion' is necessary here since it seems that `with-temp-buffer' ;; moves the REPL window's `point' when it is visible (save-excursion (with-temp-buffer (insert html) (let ((xml (libxml-parse-html-region (point-min) (point-max)))) (erase-buffer) (shr-insert-document xml)) (jupyter-repl-fixup-font-lock-properties (point-min) (point-max)) (string-trim (buffer-string)))))) ;; Markdown integration (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))) (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))))) (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))))) (defvar org-format-latex-options) (defvar org-preview-latex-image-directory) (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." ;; 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) (setq beg (point)) (jupyter-repl-insert tex) (setq end (point)) (org-format-latex org-preview-latex-image-directory beg end org-babel-jupyter-resource-directory 'overlays "Creating LaTeX image...%s" 'forbuffer ;; Use the default method for creating image files org-preview-latex-default-process) (goto-char end))) (defun jupyter-repl-insert-ansi-coded-text (text) "Insert TEXT, converting ANSI color codes to font lock faces." (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))) (cond ((memq :application/vnd.jupyter.widget-view+json mimetypes) (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 "^ N 0) (let ((pos (next-single-property-change (point) 'jupyter-cell))) (while (and pos (not (jupyter-repl-cell-beginning-p pos))) (setq pos (next-single-property-change pos 'jupyter-cell))) (unless (when pos (goto-char pos) (setq N (1- N))) (goto-char (point-max)) (throw 'done t))))) N) (defun jupyter-repl-previous-cell (&optional N) "Go to the beginning of the previous cell. Move N times where N defaults to 1. Return the count of cells left to move. Note, if `point' is not at the beginning of the current cell, the first move is to the beginning of the current cell." (or N (setq N 1)) (catch 'done (let ((starting-pos (point))) (while (> N 0) (let ((pos (previous-single-property-change (point) 'jupyter-cell))) (while (and pos (not (jupyter-repl-cell-beginning-p pos))) (setq pos (previous-single-property-change pos 'jupyter-cell))) (unless (when pos (goto-char pos) (setq N (1- N))) (goto-char (point-min)) ;; Handle edge case when the first cell is at the beginning of the ;; buffer. This happens, for example, when erasing the buffer. (when (and (/= (point) starting-pos) (jupyter-repl-cell-beginning-p (point))) (setq N (1- N))) (throw 'done t)))))) N) (defun jupyter-repl-goto-cell (req) "Go to the cell beginning position of REQ. REQ should be a `jupyter-request' that corresponds to one of the `jupyter-send-execute-request's created by a cell in the `current-buffer'. Note that the `current-buffer' is assumed to be a Jupyter REPL buffer." (goto-char (point-max)) (unless (catch 'done (while (= (jupyter-repl-previous-cell) 0) (when (eq (jupyter-repl-cell-request) req) (throw 'done t)))) (error "Cell for request not found"))) (defun jupyter-repl-forward-cell (&optional arg) "Go to the code beginning of the cell after the current one. ARG is the number of cells to move and defaults to 1." (interactive "^p") (or arg (setq arg 1)) (jupyter-repl-next-cell arg) (goto-char (jupyter-repl-cell-code-beginning-position))) (defun jupyter-repl-backward-cell (&optional arg) "Go to the code beginning of the cell before the current one. ARG is the number of cells to move and defaults to 1." (interactive "^p") (or arg (setq arg 1)) ;; Ignore the case when `point' is in the output of a cell, in this case ;; `jupyter-repl-previous-cell' will go to the previous cell. (ignore-errors (goto-char (jupyter-repl-cell-beginning-position))) (jupyter-repl-previous-cell arg) (goto-char (jupyter-repl-cell-code-beginning-position))) ;;; Predicates (defun jupyter-repl-cell-beginning-p (&optional pos) "Is POS the beginning of a cell? POS defaults to `point'." (setq pos (or pos (point))) (eq (nth 0 (get-text-property pos 'jupyter-cell)) 'beginning)) (defun jupyter-repl-cell-end-p (&optional pos) "Is POS the end of a cell? POS defaults to `point'." (setq pos (or pos (point))) (or (= pos (point-max)) (eq (nth 0 (get-text-property pos 'jupyter-cell)) 'end))) (defun jupyter-repl-multiline-p (text) "Is TEXT a multi-line string?" (string-match-p "\n" text)) (defun jupyter-repl-cell-line-p () "Is the current line a cell input line?" (let ((pos (point))) (ignore-errors (save-excursion (unless (= pos (jupyter-repl-cell-beginning-position)) (jupyter-repl-previous-cell)) (<= (jupyter-repl-cell-code-beginning-position) pos (jupyter-repl-cell-code-end-position)))))) (defun jupyter-repl-cell-finalized-p () "Has the current cell been finalized?" (or (not (jupyter-repl-cell-line-p)) (/= (jupyter-repl-cell-end-position) (point-max)))) (defun jupyter-repl-client-has-manager-p () "Does the `jupyter-current-client' have a `jupyter-kernel-manager'?" (and jupyter-current-client (oref jupyter-current-client manager))) (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)))))) ;;; Modifying cell code, truncating REPL buffer (defun jupyter-repl-cell-code () "Return the code of the current cell." (if (= (point-min) (point-max)) "" (let (lines pos) (save-excursion (goto-char (jupyter-repl-cell-code-beginning-position)) (push (buffer-substring-no-properties (point) (point-at-eol)) lines) (while (and (= (forward-line 1) 0) (/= (point) (point-max)) (jupyter-repl-cell-line-p)) (setq pos (next-single-property-change (point) 'invisible nil (point-at-eol))) (when pos (goto-char pos)) (push (buffer-substring-no-properties (point) (point-at-eol)) lines)) (mapconcat #'identity (nreverse lines) "\n"))))) (defun jupyter-repl-cell-code-position () "Return the relative position of `point' with respect to the cell code." (unless (jupyter-repl-cell-line-p) (error "Not in code of cell")) (1+ (- (point) (jupyter-repl-cell-code-beginning-position)))) (defun jupyter-repl-finalize-cell (req) "Finalize the current 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)) ;; 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)))) (defun jupyter-repl-replace-cell-code (new-code) "Replace the current cell code with NEW-CODE." (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)) (defun jupyter-repl-truncate-buffer () "Truncate the `current-buffer' based on `jupyter-repl-maximum-size'. The `current-buffer' is assumed to be a Jupyter REPL buffer. If the `current-buffer' is larger than `jupyter-repl-maximum-size' lines, truncate it to something less than `jupyter-repl-maximum-size' lines." (save-excursion (when (= (forward-line (- jupyter-repl-maximum-size)) 0) (jupyter-repl-next-cell) (delete-region (point-min) (point))))) ;;; Handlers (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)) (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) (with-jupyter-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)))) (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))) (with-jupyter-repl-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 _user-expressions payload) (oset client execution-count (1+ execution-count)) (with-jupyter-repl-buffer client (save-excursion (jupyter-repl-goto-cell req) (jupyter-repl-cell-unmark-busy)) (when payload (jupyter-repl--handle-payload payload)))) (cl-defmethod jupyter-handle-execute-input ((client jupyter-repl-client) _req _code execution-count) (oset client execution-count (1+ execution-count))) (cl-defmethod jupyter-handle-execute-result ((client jupyter-repl-client) req _execution-count 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)))) (defun jupyter-repl-next-display-with-id (id) "Go to the start of the next display matching ID. 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. (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 (cl-defmethod jupyter-handle-display-data ((client jupyter-repl-client) req data metadata transient) (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. (with-jupyter-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))))))) (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")) (with-jupyter-repl-buffer client (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))))) (defun jupyter-repl-clear-last-cell-output (client) "In CLIENT's REPL buffer, clear the output of the last completed cell." (with-jupyter-repl-buffer client (goto-char (point-max)) (jupyter-repl-previous-cell 2) (delete-region (1+ (jupyter-repl-cell-end-position)) (progn (jupyter-repl-next-cell) (point))))) (cl-defmethod jupyter-handle-clear-output ((client jupyter-repl-client) req wait) (unless (oset client wait-to-clear (eq wait t)) (cond ((eq (jupyter-message-parent-type (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))))) (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)))))) (cl-defmethod jupyter-handle-stream ((client jupyter-repl-client) req name text) (if (null req) (jupyter-repl-display-other-output client name text) (cond ((eq (jupyter-message-parent-type (jupyter-request-last-message req)) :comm-msg) (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))))) (t (jupyter-repl-append-output client req (jupyter-repl-insert-ansi-coded-text text)))))) (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) ? )))))) (cl-defmethod jupyter-handle-error ((client jupyter-repl-client) 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) (with-jupyter-repl-doc-buffer "traceback" (jupyter-repl-insert-ansi-coded-text traceback) (goto-char (line-beginning-position)) (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))))))) (cl-defmethod jupyter-handle-input-reply ((client jupyter-repl-client) req prompt _password) (jupyter-repl-append-output client req (let ((value (cl-call-next-method))) (jupyter-repl-insert (concat prompt value)) (jupyter-repl-newline)))) (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) "Go to the next history element. Navigate through the REPL history to the next (newer) history element and insert it as the last code cell. For N positive move forward in history that many times. If N is negative, move to older history elements." (interactive "p") (or n (setq n 1)) (if (< n 0) (jupyter-repl-history-previous (- n)) (goto-char (point-max)) (let ((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))) (ring-ref jupyter-repl-history 0))) (defun jupyter-repl-history-previous (&optional n) "Go to the previous history element. Similar to `jupyter-repl-history-next' but for older history elements. If N is negative in this case, move to newer history elements." (interactive "p") (or n (setq n 1)) (if (< n 0) (jupyter-repl-history-next (- n)) (goto-char (point-max)) (unless (equal (jupyter-repl-cell-code) (ring-ref jupyter-repl-history 0)) (setq n (1- n))) (let ((elem (jupyter-repl-history--previous n))) (if (null elem) (error "Beginning of history") (jupyter-repl-replace-cell-code (ring-ref jupyter-repl-history 0)))))) (cl-defmethod jupyter-handle-history-reply ((client jupyter-repl-client) _req history) (with-jupyter-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)))) (cl-defmethod jupyter-handle-is-complete-reply ((client jupyter-repl-client) _req status indent) (with-jupyter-repl-buffer client (pcase status ("complete" (jupyter-send-execute-request client)) ("incomplete" (jupyter-repl-newline) (if (= (length indent) 0) (jupyter-repl-indent-line) (jupyter-repl-insert :read-only nil indent))) ("invalid" ;; Force an execute to produce a traceback (jupyter-send-execute-request client)) ("unknown")))) (cl-defmethod jupyter-handle-shutdown-reply ((client jupyter-repl-client) _req restart) (with-jupyter-repl-buffer client (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." (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)))) (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* ((spos (jupyter-repl-cell-code-beginning-position)) (pos (jupyter-repl-cell-code-position)) (code (jupyter-repl-cell-code)) (replacement (with-jupyter-repl-lang-buffer (insert code) (goto-char pos) (jupyter-indent-line) (setq pos (point)) (buffer-string)))) ;; Don't modify the buffer when unnecessary, this allows ;; `company-indent-or-complete-common' to work. (unless (equal code replacement) (jupyter-repl-replace-cell-code replacement) (goto-char (+ pos spos))))) ;;; 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))) (defun jupyter-repl-after-buffer-change (beg end len) "Insert line continuation prompts in `jupyter-repl-mode' buffers. BEG, END, and LEN have the same meaning as in `after-change-functions'." (when (eq major-mode 'jupyter-repl-mode) (cond ;; Insertions only ((= len 0) (goto-char beg) (when (jupyter-repl-cell-line-p) (setq end (jupyter-repl-insert-continuation-prompts end)) (jupyter-repl-mark-as-cell-code beg end)) (goto-char end))))) (defun jupyter-repl-kill-buffer-query-function () "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))))))))) (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 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." ;; NOTE: Sometimes the margins will disappear after the window configuration ;; changes which is why `window-configuration-change-hook' is not used. (when (and (eq major-mode 'jupyter-repl-mode) (let ((margins (window-margins window))) (not (and (consp margins) (car margins) (= (car margins) jupyter-repl-prompt-margin-width))))) (set-window-buffer window (current-buffer)))) ;;; Completion (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)) (+ (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." (let ((symbol (if (looking-at "\\>") (buffer-substring-no-properties (point) (jupyter-completion-symbol-beginning)) (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'. Extract the arguments starting at POS and narrowing to the first SEXP." (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)))) (cl-defgeneric jupyter-completion-prefix (&optional (re string) max-len) "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. 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 "\\." 1))) (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. 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 (when (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)))) (setq tail (cdr tail))) (when buf (kill-buffer buf)))) ;; When a type is supplied add it as an annotation (when types (let ((max-len (apply #'max (mapcar #'length matches)))) (cl-mapc (lambda (match meta) (let* ((prefix (make-string (1+ (- max-len (length match))) ? )) (annot (concat prefix (plist-get meta :type)))) (put-text-property 0 1 'annot annot match))) matches types))) matches)) ;;; Completion at point interface (defvar jupyter-completion-last-prefix nil "The last prefix used to fetch candidates.") (defvar jupyter-completion-cache nil "The cache for completion candidates. Can either be a Jupyter message plist or a list of candidates") (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-at-point () "Function to add to `completion-at-point-functions'." (when jupyter-current-client (let ((prefix (jupyter-completion-prefix)) req) (when jupyter-completion--company-timer (cancel-timer jupyter-completion--company-timer)) (when prefix (when (consp prefix) (setq prefix (car prefix)) (when (and (bound-and-true-p company-mode) (not company-candidates) (< (length prefix) company-minimum-prefix-length)) ;; Trigger completion similar to `company' when ;; `jupyter-completion-prefix' returns a cons cell. (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.01 nil (lambda () (let ((company-minimum-prefix-length 0) (this-command 'company-manual-begin)) (unless company-candidates (company-auto-begin)) ;; Only call frontends when there are ;; completions from the kernel for ;; syntax based auto completion (when jupyter-completion-cache (company-post-command)))))))) ;; Prefetch candidates (when (or (not jupyter-completion-last-prefix) (not jupyter-completion-cache) (and ;; FIXME: We would like to manually get ;; completions for empty prefixes because ;; of things like from foo import | ;; Only when there is no whitespace before point since some ;; kernel's would give a list of all completions on every ;; space. ;; (not (eq (char-syntax (char-before)) ? )) (or ;; This case happens when completing things like foo.| (string= jupyter-completion-last-prefix "") ;; The obvious condition... (not (string-prefix-p jupyter-completion-last-prefix prefix)))) ;; Invalidate the cache when completing argument lists (and (not (string= prefix "")) (eq (aref prefix (1- (length prefix))) ?\())) (setq jupyter-completion-last-prefix prefix req (jupyter-completion-prefetch (lambda (msg) (setq jupyter-completion-cache (cons 'fetched 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 (cdr jupyter-completion-cache)) (setq jupyter-completion-cache (when (equal status "ok") (jupyter-completion-construct-candidates matches metadata))))) 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) (let* ((inhibit-read-only t) (buf (jupyter-repl--inspect arg (length arg) nil (company-doc-buffer) company-async-timeout))) (prog1 buf (when buf (with-current-buffer buf (remove-text-properties (point-min) (point-max) '(read-only))))))) (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)) (yas-expand-snippet (get-text-property 0 'snippet arg) (save-excursion (forward-sexp -1) (point)) (point)))) ;;; Inspection (defun jupyter-repl--inspect (code pos &optional detail buffer timeout) "Send an inspect request to a Jupyter kernel. POS is the position of `point' relative to the inspected CODE. DETAIL is the detail level to use for the request and defaults to 0. If BUFFER is provided, the inspection text returned from the kernel is inserted into BUFFER and BUFFER is returned. Otherwise, when BUFFER is nil, the formated inspection string is returned. It the kernel doesn't respond within TIMEOUT seconds, return nil." (let* ((jupyter-inhibit-handlers '(:status)) (msg (jupyter-wait-until-received :inspect-reply (jupyter-send-inspect-request jupyter-current-client :code code :pos pos :detail detail) timeout))) (when msg (cl-destructuring-bind (&key status found data metadata &allow-other-keys) (jupyter-message-content msg) (when (and (equal status "ok") found) (with-current-buffer (or buffer (generate-new-buffer " *temp*")) (unwind-protect (progn (jupyter-repl-insert-data data metadata) (goto-char (point-min)) (or buffer (buffer-string))) (unless buffer (kill-buffer (current-buffer)))))))))) (defun jupyter-repl-inspect-at-point () "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." (interactive) (cl-destructuring-bind (code pos) (jupyter-code-context 'inspect) (let ((buf (current-buffer))) (with-jupyter-repl-doc-buffer "inspect" ;; Set this in the inspect buffer so that ;; `jupyter-repl-markdown-follow-link-at-point' works in the inspect ;; buffer as well. (setq-local jupyter-current-client (buffer-local-value 'jupyter-current-client buf)) ;; FIXME: Better way of inserting documentation into a buffer. ;; Currently the way text is inserted is by inserting in a temp ;; buffer and returning the string, but in cases where overlays may ;; be inserted in the buffer (markdown), this fails. A better way ;; would be to supply the buffer in which to insert text like what is ;; done here, but how to make it more general for all insertion ;; types? (if (not (jupyter-repl--inspect code pos nil (current-buffer))) (message "Inspect timed out") (display-buffer (current-buffer)) (set-window-start (get-buffer-window) (point-min))))))) ;;; 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." (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'")) (with-jupyter-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) (cl-destructuring-bind (&key status ename evalue &allow-other-keys) (jupyter-message-content msg) (unless (equal status "ok") (message "jupyter (%s): %s" ename (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))) (with-jupyter-repl-doc-buffer "result" (insert res) (goto-char (point-min)) (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))) (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. SILENTLY has the same meaning as in `jupyter-repl-eval-string'." (interactive "rP") (jupyter-repl-eval-string (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 (with-jupyter-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 (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)))) (unless (jupyter-wait-until-received :shutdown-reply (jupyter-send-shutdown-request jupyter-current-client :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 () "Setup Isearch to search through the input history." (setq-local isearch-search-fun-function #'jupyter-repl-history-isearch-search) (setq-local isearch-wrap-function #'jupyter-repl-history-isearch-wrap) (setq-local isearch-push-state-function #'jupyter-repl-history-isearch-push-state)) ;; Adapted from `comint-history-isearch-search' (defun jupyter-repl-history-isearch-search () "Return a search function to search through a REPL's input history." (lambda (string bound noerror) (let ((search-fun (isearch-search-fun-default)) found) (setq isearch-lazy-highlight-start-limit (jupyter-repl-cell-beginning-position)) (or ;; 1. First try searching in the initial cell text (funcall search-fun string (or bound (unless isearch-forward (jupyter-repl-cell-code-beginning-position))) noerror) ;; 2. If the above search fails, start putting next/prev history ;; elements in the cell successively, and search the string in them. Do ;; this only when bound is nil (i.e. not while lazy-highlighting search ;; strings in the current cell text). (unless bound (condition-case err (progn (while (not found) (cond (isearch-forward ;; `jupyter-repl-history-next' clears the cell if the ;; last element is the sentinel, prevent that. (if (eq (ring-ref jupyter-repl-history -1) 'jupyter-repl-history) (error "End of history") (jupyter-repl-history-next)) (goto-char (jupyter-repl-cell-code-beginning-position))) (t (jupyter-repl-history-previous) (goto-char (point-max)))) ;; After putting the next/prev history element, search the ;; string in them again, until an error is thrown at the ;; beginning/end of history. (setq found (funcall search-fun string (unless isearch-forward (jupyter-repl-cell-code-beginning-position)) 'noerror))) ;; Return point of the new search result (point)) (error (unless noerror (signal (car err) (cdr err)))))))))) (defun jupyter-repl-history-isearch-wrap () "Wrap the input history search when search fails. Go to the oldest history element for a forward search or to the newest history element for a backward search." (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 () "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))))))) ;;; `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"))) (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)) ;; TODO: Gaurd against a major mode change (put 'jupyter-repl-mode 'mode-class 'special) (define-derived-mode jupyter-repl-mode fundamental-mode "Jupyter-REPL" "A Jupyter REPL major mode." (cl-check-type jupyter-current-client jupyter-repl-client) ;; This is a better setting 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) ;; 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*" (plist-get language-info :name)))) (set-syntax-table syntax) (with-jupyter-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))) (defun jupyter-repl-initialize-fontification () "Initialize fontification for the current REPL buffer." (let (fld sff) (with-jupyter-repl-lang-buffer (setq fld font-lock-defaults sff font-lock-syntactic-face-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 ;; Only fontify syntactically when the text ;; does not have a font-lock-face property (lambda (state) (unless (get-text-property (nth 8 state) 'font-lock-face) (when sff (funcall sff state)))))))))) (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))) :execute-reply (lambda (msg) (oset client execution-count (1+ (jupyter-message-get msg :execution_count))))) ;; 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))) ;;; `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 (with-jupyter-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. If MODE is non-nil, return all REPL buffers whose `jupyter-repl-lang-mode' is MODE." (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) (jupyter-repl-connected-p) (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 (completing-read "jupyter-repl: " (or (jupyter-repl-available-repl-buffers major-mode) (error "No live REPL for `current-buffer's `major-mode'")) nil t))) (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) (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-repl-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. 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 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)))) (with-jupyter-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 (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)) ;;;###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. 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)) (provide 'jupyter-repl) ;; Local Variables: ;; byte-compile-warnings: (not free-vars) ;; End: ;;; jupyter-repl.el ends here