From d709b31a646451e2f1f12b94899b79390300c868 Mon Sep 17 00:00:00 2001 From: Nathaniel Nicandro Date: Mon, 21 Jan 2019 23:18:50 -0600 Subject: [PATCH] Add `jupyter-message-lambda` This simplifies the writing of message callbacks. --- jupyter-client.el | 110 +++++++++++++++++++++---------------------- jupyter-messages.el | 46 ++++++++++++++++-- jupyter-repl.el | 20 ++++---- test/jupyter-test.el | 18 +++++++ 4 files changed, 122 insertions(+), 72 deletions(-) diff --git a/jupyter-client.el b/jupyter-client.el index 5a31e39..bc344b0 100644 --- a/jupyter-client.el +++ b/jupyter-client.el @@ -978,10 +978,9 @@ text/plain representation." (prog1 req (jupyter-add-callback req :execute-reply - (lambda (msg) - (jupyter-with-message-content msg (status evalue) - (unless (equal status "ok") - (error "%s" (ansi-color-apply evalue))))))))))) + (jupyter-message-lambda (status evalue) + (unless (equal status "ok") + (error "%s" (ansi-color-apply evalue)))))))))) (when msg (jupyter-message-data msg (or mime :text/plain))))) @@ -1006,40 +1005,40 @@ to the above explanation." (had-result nil)) (jupyter-add-callback req :execute-reply - (lambda (msg) - (jupyter-with-message-content msg (status ename evalue) - (if (equal status "ok") - (unless had-result - (message "jupyter: eval done")) - (message "%s: %s" - (ansi-color-apply ename) - (ansi-color-apply evalue))))) + (jupyter-message-lambda (status ename evalue) + (if (equal status "ok") + (unless had-result + (message "jupyter: eval done")) + (setq ename (ansi-color-apply ename)) + (setq evalue (ansi-color-apply evalue)) + (if (string-prefix-p ename evalue) + ;; Happens in IJulia + (message evalue) + (message "%s: %s" ename evalue)))) :execute-result (or (and (functionp cb) cb) (lambda (msg) (setq had-result t) (jupyter--display-eval-result msg))) :error - (lambda (msg) - (jupyter-with-message-content msg (traceback) - ;; FIXME: Assumes the error in the - ;; execute-reply is good enough - (when (> (apply '+ (mapcar 'length traceback)) 250) - (jupyter-display-traceback traceback)))) + (jupyter-message-lambda (traceback) + ;; FIXME: Assumes the error in the + ;; execute-reply is good enough + (when (> (apply '+ (mapcar 'length traceback)) 250) + (jupyter-display-traceback traceback))) :stream - (lambda (msg) - (jupyter-with-message-content msg (name text) - (pcase name - ("stdout" - (jupyter-with-display-buffer "output" req - (jupyter-insert-ansi-coded-text text) - (display-buffer (current-buffer) - '(display-buffer-below-selected)))) - ("stderr" - (jupyter-with-display-buffer "error" req - (jupyter-insert-ansi-coded-text text) - (display-buffer (current-buffer) - '(display-buffer-below-selected)))))))) + (jupyter-message-lambda (name text) + (pcase name + ("stdout" + (jupyter-with-display-buffer "output" req + (jupyter-insert-ansi-coded-text text) + (display-buffer (current-buffer) + '(display-buffer-below-selected)))) + ("stderr" + (jupyter-with-display-buffer "error" req + (jupyter-insert-ansi-coded-text text) + (display-buffer (current-buffer) + '(display-buffer-below-selected))))))) req)) (defun jupyter-eval-region (beg end &optional cb) @@ -1050,24 +1049,6 @@ ignored when called interactively." (interactive "r") (jupyter-eval-string (buffer-substring-no-properties beg end) cb)) -(defun jupyter-eval--insert-result (pos region msg) - (jupyter-with-message-data msg ((res text/plain)) - (when res - (setq res (ansi-color-apply res)) - (with-current-buffer (marker-buffer pos) - (save-excursion - (cond - (region - (goto-char (car region)) - (delete-region (car region) (cdr region))) - (t - (goto-char pos) - (end-of-line) - (insert "\n"))) - (set-marker pos nil) - (insert res) - (when region (push-mark))))))) - (defun jupyter-eval-line-or-region (insert) "Evaluate the current line or region with the `jupyter-current-client'. If the current region is active send it using @@ -1076,14 +1057,29 @@ If the current region is active send it using With a prefix argument, evaluate and INSERT the text/plain representation of the results in the current buffer." (interactive "P") - (let ((cb (when insert - (apply-partially - #'jupyter-eval--insert-result - (point-marker) (when (use-region-p) - (car (region-bounds))))))) - (if (use-region-p) - (jupyter-eval-region (region-beginning) (region-end) cb) - (jupyter-eval-region (line-beginning-position) (line-end-position) cb)))) + (let ((region (when (use-region-p) + (car (region-bounds))))) + (funcall #'jupyter-eval-region + (or (car region) (line-beginning-position)) + (or (cdr region) (line-end-position)) + (when insert + (let ((pos (point-marker))) + (jupyter-message-lambda ((res text/plain)) + (when res + (setq res (ansi-color-apply res)) + (with-current-buffer (marker-buffer pos) + (save-excursion + (cond + (region + (goto-char (car region)) + (delete-region (car region) (cdr region))) + (t + (goto-char pos) + (end-of-line) + (insert "\n"))) + (set-marker pos nil) + (insert res) + (when region (push-mark))))))))))) (defun jupyter-load-file (file) "Send the contents of FILE using `jupyter-current-client'." diff --git a/jupyter-messages.el b/jupyter-messages.el index c52430c..080efad 100644 --- a/jupyter-messages.el +++ b/jupyter-messages.el @@ -453,9 +453,11 @@ So to bind the :status key of MSG you would do (jupyter-with-message-content msg (status) BODY)" (declare (indent 2) (debug (form listp body))) - `(cl-destructuring-bind (&key ,@keys &allow-other-keys) - (jupyter-message-content ,msg) - ,@body)) + (if keys + `(cl-destructuring-bind (&key ,@keys &allow-other-keys) + (jupyter-message-content ,msg) + ,@body) + `(progn ,@body))) (defmacro jupyter-with-message-data (msg varlist &rest body) "For MSG, bind the mimetypes in VARLIST and evaluate BODY. @@ -477,8 +479,42 @@ you would do ,m ',(if (keywordp (cadr el)) (cadr el) (intern (concat ":" (symbol-name (cadr el)))))))) varlist))) - `(let* ((,m ,msg) ,@vars) - ,@body))) + (if vars `(let* ((,m ,msg) ,@vars) + ,@body) + `(progn ,@body)))) + +(defmacro jupyter-message-lambda (keys &rest body) + "Return a function binding KEYS to fields of a message then evaluating BODY. +The returned function takes a single argument which is expected +to be a Jupyter message property list. + +The elements of KEYS can either be a symbol, KEY, or a two +element list (VAL MIMETYPE). In the former case, KEY will be +bound to the corresponding value of KEY in the +`jupyter-message-content' of the message argument. In the latter +case, VAL will be bound to the value of the MIMETYPE found in the +`jupyter-message-data' of the message." + (declare (indent defun) (debug ((&rest [&or symbolp (symbolp symbolp)]) body))) + (let ((msg (cl-gensym "msg")) + content-keys + data-keys) + (while (car keys) + (let ((key (pop keys))) + (push key (if (listp key) data-keys content-keys)))) + `(lambda (,msg) + ,(cond + ((and data-keys content-keys) + `(jupyter-with-message-content ,msg ,content-keys + (jupyter-with-message-data ,msg ,data-keys + ,@body))) + (data-keys + `(jupyter-with-message-data ,msg ,data-keys + ,@body)) + (content-keys + `(jupyter-with-message-content ,msg ,content-keys + ,@body)) + (t + `(progn ,@body)))))) (defmacro jupyter--decode-message-part (key msg) "Return a form to decode the value of KEY in MSG. diff --git a/jupyter-repl.el b/jupyter-repl.el index 422c5ca..7686a06 100644 --- a/jupyter-repl.el +++ b/jupyter-repl.el @@ -1533,16 +1533,16 @@ it." (req (let ((jupyter-inhibit-handlers t)) (jupyter-send-execute-request client :code "" :silent t)))) (jupyter-add-callback req - :execute-reply (lambda (msg) - (oset client execution-count - (1+ (jupyter-message-get msg :execution_count))) - (unless (equal (jupyter-execution-state client) "busy") - ;; Set the cell count and update the prompt - (jupyter-with-repl-buffer client - (save-excursion - (goto-char (point-max)) - (jupyter-repl-update-cell-count - (oref client execution-count))))))) + :execute-reply + (jupyter-message-lambda (execution_count) + (oset client execution-count (1+ execution_count)) + (unless (equal (jupyter-execution-state client) "busy") + ;; Set the cell count and update the prompt + (jupyter-with-repl-buffer client + (save-excursion + (goto-char (point-max)) + (jupyter-repl-update-cell-count + (oref client execution-count))))))) ;; Waiting longer here to account for initial startup of the Jupyter ;; kernel. Sometimes the idle message won't be received if another long ;; running execute request is sent right after. diff --git a/test/jupyter-test.el b/test/jupyter-test.el index eec6e1b..90e5dc3 100644 --- a/test/jupyter-test.el +++ b/test/jupyter-test.el @@ -406,6 +406,24 @@ (should (json-plist-p res)) (should (eq (jupyter-message-type res) :shutdown-reply)))))) +(ert-deftest jupyter-message-lambda () + :tags '(messages) + (let ((msg (jupyter-test-message + (jupyter-request) :execute-reply + (list :status "idle" :data (list :text/plain "foo"))))) + (should (equal (funcall (jupyter-message-lambda (status) + status) + msg) + "idle")) + (should (equal (funcall (jupyter-message-lambda ((res text/plain)) + res) + msg) + "foo")) + (should (equal (funcall (jupyter-message-lambda (status (res text/plain)) + (cons status res)) + msg) + (cons "idle" "foo"))))) + ;;; Channels (ert-deftest jupyter-sync-channel ()